Files
thetool/public/mobile/app.js
2026-01-27 10:35:15 +01:00

851 lines
44 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';
import OfflineIndicator from '/mobile/components/OfflineIndicator.js';
import SyncStatus from '/mobile/components/SyncStatus.js';
import { initDatabase, clearAllData, getStorageEstimate } from '/mobile/shared/db.js';
import offlineSettings from '/mobile/shared/offlineSettings.js';
import SyncManager from '/mobile/shared/syncManager.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,
OfflineIndicator,
SyncStatus
},
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);
// Offline mode state
const offlineModeEnabled = ref(false);
const offlineAutoSync = ref(true);
const offlinePendingCount = ref(0);
const offlinePendingOps = ref(0);
const offlinePendingPhotos = ref(0);
const offlineFailedCount = ref(0);
const offlineIsSyncing = ref(false);
const offlineLastSyncText = ref('Nie synchronisiert');
const offlineFreshness = ref('unknown');
const offlineSyncProgress = ref(null);
const offlineStorageUsed = ref(0);
const isOnline = ref(navigator.onLine);
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) {}
};
// Offline mode functions
const loadOfflineSettings = () => {
const settings = offlineSettings.load();
offlineModeEnabled.value = settings.enabled;
offlineAutoSync.value = settings.autoSync;
offlineLastSyncText.value = offlineSettings.getLastSyncText();
offlineFreshness.value = offlineSettings.getFreshness();
};
const toggleOfflineMode = async () => {
if (offlineModeEnabled.value) {
offlineSettings.disable();
offlineModeEnabled.value = false;
} else {
offlineSettings.enable();
offlineModeEnabled.value = true;
// Initialize database
try {
await initDatabase();
SyncManager.init();
} catch (error) {
console.error('Failed to initialize offline mode:', error);
showToast('Offline-Modus konnte nicht aktiviert werden', 'error');
offlineSettings.disable();
offlineModeEnabled.value = false;
}
}
};
const setOfflineAutoSync = (value) => {
offlineAutoSync.value = value;
offlineSettings.setAutoSync(value);
};
const triggerManualSync = async () => {
if (!navigator.onLine) {
showToast('Keine Internetverbindung', 'error');
return;
}
const result = await SyncManager.sync();
if (result.success) {
showToast('Synchronisation abgeschlossen', 'success');
} else {
showToast(result.error || 'Synchronisation fehlgeschlagen', 'error');
}
};
const clearOfflineData = async () => {
try {
await clearAllData();
offlineSettings.clear();
offlineModeEnabled.value = false;
offlinePendingCount.value = 0;
showToast('Offline-Daten gelöscht', 'success');
} catch (error) {
showToast('Fehler beim Löschen', 'error');
}
};
const updateOfflineStatus = async () => {
if (offlineModeEnabled.value) {
const summary = await SyncManager.getPendingSummary();
offlinePendingCount.value = summary.total;
offlinePendingOps.value = summary.operations;
offlinePendingPhotos.value = summary.photos;
offlineFailedCount.value = summary.failed;
offlineLastSyncText.value = offlineSettings.getLastSyncText();
offlineFreshness.value = offlineSettings.getFreshness();
const storage = await getStorageEstimate();
offlineStorageUsed.value = storage.usage;
}
};
const handleSyncEvent = (event, data) => {
switch (event) {
case 'sync-start':
offlineIsSyncing.value = true;
offlineSyncProgress.value = null;
break;
case 'sync-progress':
offlineSyncProgress.value = data;
break;
case 'sync-complete':
offlineIsSyncing.value = false;
offlineSyncProgress.value = null;
updateOfflineStatus();
if (data.reassigned?.length > 0) {
for (const wo of data.reassigned) {
showToast(`Arbeitsauftrag #${wo.id} wurde neu zugewiesen`, 'warning');
}
}
break;
case 'sync-error':
offlineIsSyncing.value = false;
offlineSyncProgress.value = null;
break;
}
};
const handleOnlineStatusChange = () => {
isOnline.value = navigator.onLine;
};
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);
window.addEventListener('online', handleOnlineStatusChange);
window.addEventListener('offline', handleOnlineStatusChange);
loadLagerSettings();
loadOfflineSettings();
// Initialize offline mode if enabled
if (offlineModeEnabled.value) {
try {
await initDatabase();
SyncManager.init();
SyncManager.subscribe(handleSyncEvent);
await updateOfflineStatus();
} catch (error) {
console.error('Failed to initialize offline mode:', error);
}
}
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);
window.removeEventListener('online', handleOnlineStatusChange);
window.removeEventListener('offline', handleOnlineStatusChange);
if (offlineModeEnabled.value) {
SyncManager.destroy();
}
});
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,
// Offline mode
offlineModeEnabled,
offlineAutoSync,
offlinePendingCount,
offlinePendingOps,
offlinePendingPhotos,
offlineFailedCount,
offlineIsSyncing,
offlineLastSyncText,
offlineFreshness,
offlineSyncProgress,
offlineStorageUsed,
isOnline,
toggleOfflineMode,
setOfflineAutoSync,
triggerManualSync,
clearOfflineData,
};
},
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 items-center justify-center gap-2">
<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">
<OfflineIndicator
:offline-mode-enabled="offlineModeEnabled"
:pending-count="offlinePendingCount"
:is-syncing="offlineIsSyncing"
:freshness="offlineFreshness"
@sync-click="triggerManualSync"
/>
</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 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">Offline-Modus (Workorder)</p>
<div class="flex items-center justify-between mb-3">
<div>
<p class="font-medium text-slate-800 dark:text-white text-sm">Offline-Modus</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Arbeitsaufträge offline verfügbar</p>
</div>
<button
@click="toggleOfflineMode"
:class="[
'relative w-11 h-6 rounded-full transition-colors',
offlineModeEnabled ? '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',
offlineModeEnabled ? 'left-5' : 'left-0.5'
]"></span>
</button>
</div>
<template v-if="offlineModeEnabled">
<div class="flex items-center justify-between mb-3">
<div>
<p class="font-medium text-slate-800 dark:text-white text-sm">Auto-Sync</p>
<p class="text-xs text-slate-500 dark:text-slate-400">Automatisch synchronisieren</p>
</div>
<button
@click="setOfflineAutoSync(!offlineAutoSync)"
:class="[
'relative w-11 h-6 rounded-full transition-colors',
offlineAutoSync ? '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',
offlineAutoSync ? 'left-5' : 'left-0.5'
]"></span>
</button>
</div>
<SyncStatus
:pending-operations="offlinePendingOps"
:pending-photos="offlinePendingPhotos"
:failed-count="offlineFailedCount"
:last-sync-text="offlineLastSyncText"
:is-syncing="offlineIsSyncing"
:is-online="isOnline"
:sync-progress="offlineSyncProgress"
@sync="triggerManualSync"
/>
<button
@click="clearOfflineData"
class="w-full mt-3 py-2 px-4 text-sm text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition"
>
Offline-Daten löschen
</button>
</template>
</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');