Files
thetool/Layout/default/VueViews/WorkorderCompanyPWA.php
2025-09-08 19:57:46 +02:00

876 lines
58 KiB
PHP
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.
<?php
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Workorders</title>
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<link rel="manifest" href="/js/pages/WorkorderBase/manifest.json">
<meta name="theme-color" content="#005384">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.27/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.7.2/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.30.1/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.30.1/locale/de.js"></script>
<script>
window.TT_CONFIG = <?= json_encode($JSGlobals ?? []) ?>;
moment.locale('de');
tailwind.config = {
darkMode: 'class', // Enable dark mode based on a class
theme: {
extend: {
colors: {
'primary': '#005384', // Dark Blue
'secondary': '#fac41b', // Yellow/Gold
},
}
}
}
</script>
<style>
html, body {
/* Prevents the rubber-band scroll effect on iOS and pull-to-refresh on Android */
overscroll-behavior: none;
}
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
main {
/* Prevents scrolling within the main container from affecting the body */
overscroll-behavior-y: contain;
}
.slide-enter-active, .slide-leave-active { transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1); }
.slide-enter-from, .slide-leave-to { transform: translateX(100%); }
.list-container.panel-open {
transform: scale(0.95);
filter: blur(4px);
opacity: 0.7;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), filter 0.35s, opacity 0.35s;
}
.list-container {
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), filter 0.35s, opacity 0.35s;
}
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0,0,0,0.4);
transition: opacity 0.35s ease;
z-index: 15;
}
.overlay-enter-from, .overlay-leave-to { opacity: 0; }
.overlay-enter-to, .overlay-leave-from { opacity: 1; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease-in-out; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin { animation: spin 1.5s ease-in-out infinite; }
</style>
</head>
<body class="transition-colors duration-300 overflow-hidden">
<div id="app" class="h-screen w-screen overflow-hidden antialiased"></div>
<script>
const { createApp, ref, reactive, computed, onMounted, watch, nextTick } = Vue;
const app = createApp({
setup() {
// --- STATE ---
const workorders = ref([]);
const selectedWorkorder = ref(null);
const isLoading = ref(true);
const isDetailsLoading = ref(false);
const isDetailsPanelOpen = ref(false);
const searchTerm = ref('');
const documentation = reactive({ docs: [], journals: [] });
const tenantConfig = ref(null);
const tempAdditionalInfo = ref('');
const isEditingInfo = ref(false);
const newJournalEntry = ref('');
const isUploading = ref(false);
const uploadModal = reactive({ show: false, files: null, documentType: '' });
const problemModal = reactive({ show: false, selectedInterventions: [], details: {} });
const fullscreenViewer = reactive({ show: false, item: null });
const missingTasksPopover = reactive({ show: false, tasks: [] });
const installModal = reactive({ show: false });
const isStandalone = ref(false);
const selectedFcp = ref('all');
const isFcpSelectOpen = ref(false);
const fcpSearchTerm = ref('');
const fcpInputRef = ref(null); // For autofocusing FCP search
const isSettingsOpen = ref(false);
const theme = ref('system'); // 'light', 'dark', 'system'
const showThemePicker = ref(false);
const API_BASE_URL = window.TT_CONFIG.BASE_PATH || '/WorkorderCompany';
const api = axios.create({ baseURL: API_BASE_URL });
// --- COMPUTED ---
const fcpOptions = computed(() => {
if (!workorders.value || workorders.value.length === 0) {
return [{ value: 'all', text: 'Alle FCPs' }];
}
const fcps = [...new Set(workorders.value.map(wo => wo.rimo_fcp_name).filter(Boolean))].sort();
const options = fcps.map(fcp => ({ value: fcp, text: fcp }));
return [{ value: 'all', text: 'Alle FCPs' }, ...options];
});
const filteredFcpOptions = computed(() => {
if (!fcpSearchTerm.value) {
return fcpOptions.value;
}
const lowerCaseSearch = fcpSearchTerm.value.toLowerCase();
return fcpOptions.value.filter(option =>
option.text.toLowerCase().includes(lowerCaseSearch)
);
});
const selectedFcpText = computed(() => {
const selectedOption = fcpOptions.value.find(opt => opt.value === selectedFcp.value);
return selectedOption ? selectedOption.text : 'Alle FCPs';
});
const filteredWorkorders = computed(() => {
let filtered = workorders.value;
if (selectedFcp.value !== 'all') {
filtered = filtered.filter(wo => wo.rimo_fcp_name === selectedFcp.value);
}
if (searchTerm.value.length > 2) {
const lowerSearch = searchTerm.value.toLowerCase();
filtered = filtered.filter(wo =>
wo.id.toString().includes(lowerSearch) ||
(wo.customerName && wo.customerName.toLowerCase().includes(lowerSearch)) ||
(wo.street && wo.street.toLowerCase().includes(lowerSearch)) ||
(wo.city && wo.city.toLowerCase().includes(lowerSearch)) ||
(wo.oaid && wo.oaid.toLowerCase().includes(lowerSearch)) ||
(wo.rimo_fcp_name && wo.rimo_fcp_name.toLowerCase().includes(lowerSearch))
);
}
const getStatusRank = (status) => {
switch (status) {
case 'scheduled':
case 'civil_engineering_completed': return 0;
case 'assigned':
case 'new':
case 'problem_solved': return 1;
case 'intervention_required':
case 'correction_requested':
case 'civil_engineering_required': return 2;
case 'documented':
case 'completed': return 3;
case 'cancelled': return 4;
default: return 99;
}
};
return filtered.sort((a, b) => {
const rankA = getStatusRank(a.status);
const rankB = getStatusRank(b.status);
if (rankA !== rankB) return rankA - rankB;
if (rankA === 0) {
const dateA = a.appointmentDate || Infinity;
const dateB = b.appointmentDate || Infinity;
if (dateA === dateB) return (a.deadlineDate || Infinity) - (b.deadlineDate || Infinity);
return dateA - dateB;
}
return (a.deadlineDate || Infinity) - (b.deadlineDate || Infinity);
});
});
const googleMapsLink = computed(() => {
if (!selectedWorkorder.value) return '#';
const { street, hausnummer, plz, city } = selectedWorkorder.value;
const address = encodeURIComponent(`${street} ${hausnummer}, ${plz} ${city}`);
return `https://www.google.com/maps/search/?api=1&query=${address}`;
});
const checklist = computed(() => {
if (!tenantConfig.value?.documentationTypes || !Array.isArray(tenantConfig.value.documentationTypes)) return [];
return tenantConfig.value.documentationTypes.map(reqType => {
const isCompleted = documentation.docs.some(doc => doc.documentType === reqType.value);
return { ...reqType, completed: isCompleted };
});
});
const isChecklistComplete = computed(() => {
if (checklist.value.length === 0) return true;
return checklist.value.every(item => item.completed);
});
const translatedDocs = computed(() => {
if (!documentation.docs.length || !tenantConfig.value?.documentationTypes) {
return documentation.docs;
}
const typeMap = new Map(tenantConfig.value.documentationTypes.map(t => [t.value, t.text]));
return documentation.docs.map(doc => ({
...doc,
translatedName: typeMap.get(doc.documentType) || doc.documentType,
}));
});
const filteredJournals = computed(() => {
return documentation.journals.filter(j => !j.text.toLowerCase().includes('wurde zugewiesen.'));
});
// --- METHODS ---
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) => {
if (!['light', 'dark', 'system'].includes(newTheme)) return;
theme.value = newTheme;
if (newTheme === 'system') {
localStorage.removeItem('theme');
} else {
localStorage.setItem('theme', newTheme);
}
applyTheme();
isSettingsOpen.value = false;
if (showThemePicker.value) showThemePicker.value = false;
};
const getStatusInfo = (status) => {
const statuses = {
'new': { text: 'Neu', color: 'bg-blue-500' }, 'assigned': { text: 'Zugewiesen', color: 'bg-sky-500' },
'scheduled': { text: 'Geplant', color: 'bg-amber-500' }, 'correction_requested': { text: 'Korrektur', color: 'bg-red-500' },
'intervention_required': { text: 'Eingriff', color: 'bg-red-700' }, 'civil_engineering_required': { text: 'Tiefbau', color: 'bg-orange-500' },
'civil_engineering_completed': { text: 'Tiefbau OK', color: 'bg-green-500' }, 'problem_solved': { text: 'Problem gelöst', color: 'bg-teal-500' },
'documented': { text: 'Dokumentiert', color: 'bg-indigo-500' }, 'completed': { text: 'Abgeschlossen', color: 'bg-slate-500' },
'cancelled': { text: 'Storniert', color: 'bg-gray-600' }, 'default': { text: 'Unbekannt', color: 'bg-gray-400' }
};
return statuses[status] || statuses.default;
};
const formatDate = (timestamp, format = 'DD.MM.YYYY') => {
if (!timestamp) return '';
return moment.unix(timestamp).format(format);
};
const fetchWorkorders = async () => {
isLoading.value = true;
try {
const response = await api.post(`/get`, { pagination: { page: 1, per_page: 500 } });
workorders.value = response.data.rows;
} catch (error) { console.error("Failed to fetch workorders:", error); }
finally { isLoading.value = false; }
};
const fetchDetails = async (workorderId) => {
isDetailsLoading.value = true;
try {
const [docRes, configRes] = await Promise.all([
api.get(`/getDocumentation?workorderId=${workorderId}`),
api.get(`/getTenantConfig?workorderId=${workorderId}`)
]);
documentation.docs = docRes.data.docs.map(d => ({...d, isPdf: d.mimetype === 'application/pdf'}));
documentation.journals = docRes.data.journals;
if (configRes.data.success) {
tenantConfig.value = configRes.data;
}
} catch (e) { console.error("Could not load details", e); }
finally { isDetailsLoading.value = false; }
};
const openDetails = (workorder) => {
selectedWorkorder.value = workorder;
isDetailsPanelOpen.value = true;
fetchDetails(workorder.id);
};
const closeDetails = () => {
isDetailsPanelOpen.value = false;
setTimeout(() => {
selectedWorkorder.value = null;
documentation.docs = []; documentation.journals = [];
tenantConfig.value = null; isEditingInfo.value = false;
}, 350);
};
const startEditInfo = () => {
tempAdditionalInfo.value = selectedWorkorder.value.additionalInfo || '';
isEditingInfo.value = true;
};
const saveAdditionalInfo = async () => {
const newInfo = tempAdditionalInfo.value;
try {
await api.post('/updateAdditionalInfo', { workorderId: selectedWorkorder.value.id, additionalInfo: newInfo });
selectedWorkorder.value.additionalInfo = newInfo;
const woInList = workorders.value.find(w => w.id === selectedWorkorder.value.id);
if(woInList) woInList.additionalInfo = newInfo;
await fetchDetails(selectedWorkorder.value.id); // to refresh journal
} catch(e) { console.error("Failed to save info", e); }
finally { isEditingInfo.value = false; }
};
const addJournalEntry = async () => {
if (!newJournalEntry.value.trim()) return;
try {
const response = await api.post('/addJournal', { workorderId: selectedWorkorder.value.id, text: newJournalEntry.value });
documentation.journals = response.data.journals;
newJournalEntry.value = '';
await nextTick(() => {
const journalContainer = document.querySelector('.journal-container');
if(journalContainer) journalContainer.scrollTop = journalContainer.scrollHeight;
});
} catch(e) { console.error("Failed to add journal entry", e); }
};
const handleFileSelect = (event) => {
if (!event.target.files.length) return;
uploadModal.files = event.target.files;
uploadModal.documentType = tenantConfig.value?.documentationTypes?.[0]?.value || 'general';
uploadModal.show = true;
};
const executeUpload = async () => {
if (!uploadModal.files) return;
isUploading.value = true;
const formData = new FormData();
formData.append('workorderId', selectedWorkorder.value.id);
formData.append('documentType', uploadModal.documentType);
for (let i = 0; i < uploadModal.files.length; i++) {
formData.append('files[]', uploadModal.files[i]);
}
try {
await api.post(`/uploadDocumentation`, formData, { headers: { 'Content-Type': 'multipart/form-data' } });
await fetchDetails(selectedWorkorder.value.id);
} catch (error) { console.error('Upload failed:', error); }
finally {
isUploading.value = false;
uploadModal.show = false;
uploadModal.files = null;
}
};
const submitProblem = async () => {
if (problemModal.selectedInterventions.length === 0) return;
let journalParts = [];
const sortedInterventions = [...problemModal.selectedInterventions].sort((a, b) => a.value.localeCompare(b.value));
for (const type of sortedInterventions) {
let text = type.text;
const needsDetail = type.text.includes('X') || type.text.toLowerCase().includes('sonstiges');
if (needsDetail) {
const detail = problemModal.details[type.value] || '';
if (!detail) {
alert(`Bitte geben Sie Details für "${type.text}" an.`);
return;
}
text = text.includes('X') ? text.replace('X', detail) : `${text}: ${detail}`;
}
journalParts.push(text);
}
const combinedText = journalParts.join('\n');
try {
await api.post('/requestIntervention', {
workorderId: selectedWorkorder.value.id,
journalText: combinedText
});
await fetchWorkorders();
closeDetails();
} catch(e) { console.error("Failed to report problem", e); }
finally { problemModal.show = false; problemModal.selectedInterventions = []; problemModal.details = {}; }
};
const handleCompleteClick = () => {
if (isChecklistComplete.value) {
if (confirm("Möchten Sie diesen Auftrag wirklich abschließen?")) {
completeWorkorder();
}
} else {
missingTasksPopover.tasks = checklist.value.filter(t => !t.completed).map(t => t.text);
missingTasksPopover.show = true;
setTimeout(() => missingTasksPopover.show = false, 4000);
}
};
const completeWorkorder = async () => {
try {
await api.post('/completeWorkorder', { workorderId: selectedWorkorder.value.id });
await fetchWorkorders();
closeDetails();
} catch(e) { console.error("Failed to complete workorder", e); }
};
const selectFcp = (fcpValue) => {
selectedFcp.value = fcpValue;
isFcpSelectOpen.value = false;
};
onMounted(() => {
fetchWorkorders();
isStandalone.value = window.matchMedia('(display-mode: standalone)').matches;
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
theme.value = savedTheme;
} else {
showThemePicker.value = true;
}
applyTheme();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
});
watch(isFcpSelectOpen, (isOpen) => {
if (isOpen) {
nextTick(() => {
fcpInputRef.value?.focus();
});
} else {
fcpSearchTerm.value = '';
}
});
return {
isLoading, isDetailsLoading, filteredWorkorders, searchTerm, isDetailsPanelOpen, selectedWorkorder, documentation, tenantConfig,
tempAdditionalInfo, isEditingInfo, newJournalEntry, uploadModal, problemModal, isUploading, isChecklistComplete,
checklist, fullscreenViewer, missingTasksPopover, translatedDocs, filteredJournals, installModal, isStandalone,
selectedFcp, isFcpSelectOpen, fcpOptions, selectedFcpText, fcpSearchTerm, filteredFcpOptions, fcpInputRef,
isSettingsOpen, theme, showThemePicker,
fetchWorkorders, openDetails, closeDetails, getStatusInfo, formatDate, googleMapsLink, startEditInfo, saveAdditionalInfo,
handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp, setTheme
};
},
template: `
<div class="relative h-full w-full">
<transition name="overlay">
<div v-if="isDetailsPanelOpen || installModal.show || isFcpSelectOpen || isSettingsOpen" @click="closeDetails(); isFcpSelectOpen = false; isSettingsOpen = false;" class="overlay"></div>
</transition>
<div :class="{'panel-open': isDetailsPanelOpen}" class="list-container flex flex-col h-full bg-slate-100 dark:bg-slate-900 overflow-hidden transition-colors duration-300">
<header class="bg-white dark:bg-slate-800 shadow dark:shadow-md p-4 flex-shrink-0 z-10">
<div class="grid grid-cols-3 items-center">
<div class="justify-self-start">
<button @click="fetchWorkorders" class="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-600 dark:text-slate-200" :class="{'spin': isLoading}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0011.667 0l3.181-3.183m-4.991-2.695h-4.992v.001M21.015 4.356v4.992m0 0h-4.992m4.992 0l-3.181-3.183a8.25 8.25 0 00-11.667 0L3.985 9.348" />
</svg>
</button>
</div>
<div class="justify-self-center">
<img src="/assets/images/xinon-full-transparent.png" alt="Logo" class="h-8 w-auto block dark:hidden">
<img src="/assets/images/xinon-full-transparent-white.png" alt="Logo" class="h-8 w-auto hidden dark:block">
</div>
<div class="justify-self-end">
<button @click="isSettingsOpen = true" class="p-2 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-600 dark:text-slate-200" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h3m-6.75 3h13.5M3.75 12h16.5m-16.5 3H12m-8.25 3h13.5" />
</svg>
</button>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-2">
<input type="text" v-model="searchTerm" placeholder="Suche..." inputmode="search" class="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:placeholder-slate-300">
<button @click="isFcpSelectOpen = true" class="w-full p-3 border border-slate-300 rounded-lg bg-white dark:bg-slate-700 dark:border-slate-600 text-left flex justify-between items-center text-sm">
<span class="truncate pr-2 text-slate-800 dark:text-slate-100">{{ selectedFcpText }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400 dark:text-slate-400 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</header>
<main class="flex-grow overflow-y-auto p-2 pb-16">
<div v-if="isLoading" class="space-y-3 p-2 animate-pulse">
<div v-for="i in 4" :key="i" class="bg-white dark:bg-slate-800 p-4 rounded-lg shadow-md">
<div class="flex justify-between items-start">
<div class="flex-grow pr-2 min-w-0 space-y-2">
<div class="h-5 bg-slate-200 dark:bg-slate-700 rounded w-3/4"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-full"></div>
<div class="pt-2">
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded w-1/2"></div>
</div>
</div>
<div class="flex-shrink-0 ml-2 text-right space-y-2">
<div class="h-5 w-24 bg-slate-200 dark:bg-slate-700 rounded-full ml-auto"></div>
<div class="h-4 w-28 bg-slate-200 dark:bg-slate-700 rounded ml-auto"></div>
<div class="h-3 w-20 bg-slate-200 dark:bg-slate-700 rounded ml-auto"></div>
</div>
</div>
</div>
</div>
<div v-else-if="filteredWorkorders.length === 0" class="text-center p-10"><p class="text-slate-500 dark:text-slate-300">Keine Aufträge gefunden.</p></div>
<div v-else class="space-y-3">
<div v-for="wo in filteredWorkorders" :key="wo.id" @click="openDetails(wo)" class="bg-white dark:bg-slate-800 p-4 rounded-lg shadow-md dark:shadow-lg cursor-pointer transition active:scale-[0.98]">
<div class="flex justify-between items-start">
<div class="flex-grow pr-2 min-w-0">
<p class="font-bold text-slate-800 dark:text-slate-50 break-words"><span class="dark:text-secondary">#{{ wo.id }}</span> | {{ wo.customerName || 'N/A' }}</p>
<p class="text-sm text-slate-500 dark:text-slate-300 break-words">{{ wo.street }} {{ wo.hausnummer }}, {{ wo.plz }} {{ wo.city }}</p>
<div class="items-center text-xs text-slate-400 dark:text-slate-400 mt-1">
<span class="mr-2">OAID: {{ wo.oaid || 'N/A' }}</span><br>
<span class="truncate">FCP: {{ wo.rimo_fcp_name || 'N/A' }}</span>
</div>
</div>
<div class="flex-shrink-0 ml-2 text-right space-y-1">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium text-white" :class="getStatusInfo(wo.status).color">{{ getStatusInfo(wo.status).text }}</span>
<p class="text-sm font-semibold text-slate-600 dark:text-slate-200">{{ formatDate(wo.appointmentDate, 'DD.MM HH:mm') }}</p>
<p class="text-xs text-red-500">Frist: {{ formatDate(wo.deadlineDate) }}</p>
</div>
</div>
</div>
</div>
</main>
</div>
<transition name="slide">
<div v-if="isDetailsPanelOpen && selectedWorkorder" class="fixed inset-0 bg-slate-50 dark:bg-slate-950 z-20 flex flex-col shadow-2xl">
<header class="bg-white dark:bg-slate-900 p-4 flex justify-between items-center border-b border-slate-200 dark:border-slate-800 flex-shrink-0">
<div class="flex items-center min-w-0">
<div class="h-6 w-auto mr-4">
<img src="/assets/images/xinon-full-transparent.png" alt="Logo" class="h-6 w-auto block dark:hidden">
<img src="/assets/images/xinon-full-transparent-white.png" alt="Logo" class="h-6 w-auto hidden dark:block">
</div>
<h2 class="text-xl font-bold text-primary dark:text-secondary truncate">Auftrag #{{ selectedWorkorder.id }}</h2>
</div>
<button @click="closeDetails" class="p-2 rounded-full hover:bg-slate-200 dark:hover:bg-slate-700 flex-shrink-0"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-600 dark:text-slate-200" 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>
</header>
<div class="overflow-y-auto p-4 flex-grow space-y-4">
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800 space-y-3 text-sm">
<div class="flex items-center text-base font-bold text-slate-800 dark:text-slate-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-500 dark:text-slate-300" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" /></svg>
<span>{{ selectedWorkorder.customerCompany || selectedWorkorder.customerName }}</span>
</div>
<a :href="googleMapsLink" target="_blank" class="flex items-center text-primary dark:text-secondary hover:underline">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd" /></svg>
<span>{{ selectedWorkorder.street }} {{ selectedWorkorder.hausnummer }}, {{ selectedWorkorder.plz }} {{ selectedWorkorder.city }}</span>
</a>
<div class="border-t border-slate-200 dark:border-slate-800 pt-3 mt-3 grid grid-cols-2 gap-2 text-sm">
<div>
<p class="text-xs text-slate-500 dark:text-slate-300 font-semibold">OAID</p>
<p class="text-slate-800 dark:text-slate-100">{{ selectedWorkorder.oaid || 'N/A' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 dark:text-slate-300 font-semibold">FCP</p>
<p class="text-slate-800 dark:text-slate-100">{{ selectedWorkorder.rimo_fcp_name || 'N/A' }}</p>
</div>
</div>
<div class="border-t border-slate-200 dark:border-slate-800 pt-3 space-y-2">
<a :href="'mailto:' + selectedWorkorder.email" class="flex items-center text-primary dark:text-secondary hover:underline"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" /><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" /></svg><span>{{ selectedWorkorder.email }}</span></a>
<a :href="'tel:' + selectedWorkorder.phone" class="flex items-center text-primary dark:text-secondary hover:underline"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" /></svg><span>{{ selectedWorkorder.phone }}</span></a>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<div class="flex justify-between items-center mb-2">
<h3 class="font-bold text-slate-700 dark:text-secondary">Notiz</h3>
<button v-if="!isEditingInfo" @click="startEditInfo" class="flex items-center text-sm font-medium text-primary dark:text-primary bg-slate-100 hover:bg-slate-200 dark:bg-secondary dark:hover:bg-yellow-400 px-3 py-1.5 rounded-md">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor"><path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" /><path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" /></svg> Bearbeiten
</button>
</div>
<div v-if="isEditingInfo">
<textarea v-model="tempAdditionalInfo" class="w-full p-2 border rounded-md dark:bg-slate-800 dark:border-slate-700 dark:text-white" rows="4"></textarea>
<div class="flex justify-end space-x-2 mt-2">
<button @click="isEditingInfo = false" class="px-3 py-1.5 bg-slate-200 dark:bg-slate-600 dark:text-slate-100 rounded-md text-sm font-medium">Abbrechen</button>
<button @click="saveAdditionalInfo" class="px-3 py-1.5 bg-secondary text-primary font-bold rounded-md text-sm">Speichern</button>
</div>
</div>
<p v-else class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-200">{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}</p>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-3">Checkliste</h3>
<div v-if="isDetailsLoading" class="space-y-3 animate-pulse">
<div v-for="i in 4" :key="i" class="flex items-center">
<div class="h-5 w-5 rounded-full bg-slate-200 dark:bg-slate-700 mr-2"></div>
<div class="h-4 w-3/4 rounded bg-slate-200 dark:bg-slate-700"></div>
</div>
</div>
<div v-else>
<ul v-if="checklist.length > 0" class="space-y-2">
<li v-for="item in checklist" :key="item.value" class="flex items-center text-sm">
<svg v-if="item.completed" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-green-500" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-400 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10" /></svg>
<span :class="{'text-slate-500 dark:text-slate-300 line-through': item.completed, 'text-slate-800 dark:text-slate-100': !item.completed}">{{ item.text }}</span>
</li>
</ul>
<p v-else class="text-sm text-slate-500 dark:text-slate-300">Keine Checklisten-Einträge vorhanden.</p>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-2">Dokumentation</h3>
<label for="file-upload" class="w-full inline-flex items-center justify-center px-4 py-2 border border-dashed border-slate-300 dark:border-slate-700 text-sm font-medium rounded-md text-slate-700 dark:text-slate-200 bg-slate-50 dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer">
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /></svg>
<span>Foto/Dokument hinzufügen</span>
</label>
<input id="file-upload" type="file" class="hidden" @change="handleFileSelect" multiple accept="image/*,application/pdf">
<div v-if="translatedDocs.length > 0" class="grid grid-cols-3 sm:grid-cols-4 gap-2 mt-4">
<div v-for="doc in translatedDocs" :key="doc.id" @click="fullscreenViewer.show = true; fullscreenViewer.item = doc" class="relative aspect-square bg-slate-100 dark:bg-slate-800 rounded-md overflow-hidden cursor-pointer group">
<template v-if="doc.isPdf">
<div class="h-full w-full flex items-center justify-center bg-red-50 dark:bg-red-900/20 p-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-red-500 dark:text-red-400" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 2a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V4a2 2 0 00-2-2H4zm3 4a1 1 0 000 2h6a1 1 0 100-2H7zm0 4a1 1 0 100 2h6a1 1 0 100-2H7zm0 4a1 1 0 100 2h4a1 1 0 100-2H7z" clip-rule="evenodd" /></svg>
</div>
</template>
<template v-else>
<img :src="'/File/show?id=' + doc.fileId + '&size=small'" class="h-full w-full object-cover">
</template>
<div class="absolute inset-x-0 bottom-0 p-1 bg-black bg-opacity-50">
<p class="text-white text-xs truncate">{{ doc.translatedName }}</p>
</div>
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-40 transition flex items-center justify-center"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-white opacity-0 group-hover:opacity-100 transition" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg></div>
</div>
</div>
</div>
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800">
<h3 class="font-bold text-slate-700 dark:text-secondary mb-4">Journal</h3>
<div v-if="isDetailsLoading" class="animate-pulse">
<div class="flex items-start">
<div class="flex-shrink-0 bg-slate-200 dark:bg-slate-700 h-8 w-8 rounded-full mr-3"></div>
<div class="flex-grow space-y-2">
<div class="h-4 w-full rounded bg-slate-200 dark:bg-slate-700"></div>
<div class="h-3 w-1/2 rounded bg-slate-200 dark:bg-slate-700"></div>
</div>
</div>
</div>
<div v-else class="space-y-4 max-h-60 overflow-y-auto pr-2 journal-container">
<div v-if="filteredJournals.length === 0"><p class="text-sm text-slate-500 dark:text-slate-300">Keine Einträge.</p></div>
<div v-for="entry in filteredJournals" :key="entry.id" class="flex items-start">
<div class="flex-shrink-0 bg-secondary h-8 w-8 rounded-full flex items-center justify-center mr-3"><svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /></svg></div>
<div class="flex-grow">
<p class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-100">{{ entry.text }}</p>
<p class="text-xs text-slate-400 dark:text-slate-400 mt-1">{{ entry.createByName }} - {{ formatDate(entry.create, 'DD.MM.YY HH:mm') }}</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-800">
<textarea v-model="newJournalEntry" placeholder="Neuer Eintrag..." class="w-full p-2 border rounded-md dark:bg-slate-800 dark:border-slate-700 dark:text-white" rows="3"></textarea>
<button @click="addJournalEntry" class="mt-2 w-full px-4 py-2 bg-secondary text-primary font-bold rounded-md text-sm">Senden</button>
</div>
</div>
</div>
<footer class="bg-white dark:bg-slate-900 p-2 border-t border-slate-200 dark:border-slate-800 flex-shrink-0 grid grid-cols-2 gap-2 pt-2 px-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
<button @click="problemModal.show = true" class="w-full px-4 py-3 bg-red-600 text-white font-bold rounded-md text-center">Problem melden</button>
<div class="relative">
<button @click="handleCompleteClick" class="w-full px-4 py-3 bg-green-600 text-white font-bold rounded-md text-center disabled:bg-slate-300">Abschließen</button>
<transition name="fade">
<div v-if="missingTasksPopover.show" class="absolute bottom-full right-0 mb-2 w-72 bg-red-700 text-white text-sm rounded-lg shadow-lg p-3">
<h4 class="font-bold mb-1">Fehlende Checklisten-Punkte:</h4>
<ul class="list-disc list-inside space-y-1">
<li v-for="task in missingTasksPopover.tasks" :key="task">{{ task }}</li>
</ul>
<div class="absolute bottom-[-5px] right-[calc(6rem-8px)] w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-red-700"></div>
</div>
</transition>
</div>
</footer>
</div>
</transition>
<transition name="fade">
<div v-if="isFcpSelectOpen" class="fixed inset-0 z-30 flex items-start justify-center p-4 pt-20" @click.self="isFcpSelectOpen = false">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 w-full max-w-sm flex flex-col max-h-[80vh] text-slate-800 dark:text-slate-100">
<div class="flex justify-between items-center mb-2 flex-shrink-0">
<h3 class="font-bold text-lg">FCP auswählen</h3>
<button @click="isFcpSelectOpen = false" class="flex items-center justify-center h-7 w-7 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-xl">×</button>
</div>
<div class="relative mb-2 flex-shrink-0">
<input type="text" v-model="fcpSearchTerm" ref="fcpInputRef" inputmode="search" placeholder="FCP suchen..." class="w-full p-2 pl-8 border border-slate-300 rounded-md dark:bg-slate-700 dark:border-slate-600">
<svg class="absolute left-2 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</div>
<ul class="flex-grow overflow-y-auto -mr-2 pr-2">
<li v-for="option in filteredFcpOptions" :key="option.value" @click="selectFcp(option.value)"
class="flex justify-between items-center p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer text-sm font-medium"
:class="{'bg-secondary/20 text-secondary': selectedFcp === option.value}">
<span>{{ option.text }}</span>
<svg v-if="selectedFcp === option.value" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-secondary" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</li>
<li v-if="filteredFcpOptions.length === 0" class="text-sm text-slate-500 dark:text-slate-300 p-3">Kein FCP gefunden.</li>
</ul>
</div>
</div>
</transition>
<div v-if="uploadModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-30 flex items-start justify-center p-4 pt-20" @click.self="uploadModal.show = false">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 w-full max-w-sm flex flex-col max-h-[80vh] text-slate-800 dark:text-slate-100" @click.stop>
<div class="flex justify-between items-center mb-4 flex-shrink-0">
<h3 class="font-bold text-lg">Dokumenttyp wählen</h3>
<button @click="uploadModal.show = false" class="flex items-center justify-center h-7 w-7 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-xl">×</button>
</div>
<ul class="flex-grow overflow-y-auto -mr-2 pr-2 space-y-1 mb-4">
<li v-for="type in tenantConfig.documentationTypes" :key="type.value" @click="uploadModal.documentType = type.value"
class="flex justify-between items-center p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 cursor-pointer text-sm font-medium"
:class="{'bg-secondary/20 text-secondary': uploadModal.documentType === type.value}">
<span>{{ type.text }}</span>
<svg v-if="uploadModal.documentType === type.value" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-secondary" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</li>
<li v-if="!tenantConfig.documentationTypes || tenantConfig.documentationTypes.length === 0">
<p class="text-sm text-slate-500 dark:text-slate-300 p-3">Keine Dokumenttypen konfiguriert.</p>
</li>
</ul>
<div class="flex justify-end space-x-2 mt-auto flex-shrink-0 border-t border-slate-200 dark:border-slate-700 pt-3">
<button @click="uploadModal.show = false" class="px-4 py-2 bg-slate-200 dark:bg-slate-600 dark:text-slate-100 rounded-md text-sm font-medium">Abbrechen</button>
<button @click="executeUpload" :disabled="isUploading" class="px-4 py-2 bg-primary text-white rounded-md disabled:bg-slate-400 text-sm font-medium">{{ isUploading ? 'Lade...' : 'Hochladen' }}</button>
</div>
</div>
</div>
<div v-if="problemModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-30 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-sm flex flex-col max-h-[80vh] text-slate-800 dark:text-slate-100">
<h3 class="font-bold text-lg mb-4 flex-shrink-0">Problem melden</h3>
<div class="flex-grow overflow-y-auto pr-2 space-y-2 mb-4">
<div v-for="type in tenantConfig.interventionTypes" :key="type.value">
<label class="flex items-center p-3 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition cursor-pointer">
<input type="checkbox" :value="type" v-model="problemModal.selectedInterventions" class="h-5 w-5 rounded text-primary focus:ring-primary focus:ring-2 focus:ring-offset-1">
<span class="ml-3 text-sm font-medium">{{ type.text.replace('X', '...') }}</span>
</label>
<input v-if="problemModal.selectedInterventions.some(i => i.value === type.value) && (type.text.includes('X') || type.text.toLowerCase().includes('sonstiges'))"
v-model="problemModal.details[type.value]"
type="text" class="w-full p-2 border rounded-md mt-1 text-sm focus:ring-primary focus:border-primary dark:bg-slate-700 dark:border-slate-600" placeholder="Details hier eingeben...">
</div>
</div>
<div class="flex justify-end space-x-2 mt-auto flex-shrink-0">
<button @click="problemModal.show = false; problemModal.selectedInterventions = []; problemModal.details = {}" class="px-4 py-2 bg-slate-200 dark:bg-slate-600 dark:text-slate-100 rounded-md">Abbrechen</button>
<button @click="submitProblem" class="px-4 py-2 bg-red-600 text-white rounded-md">Senden</button>
</div>
</div>
</div>
<transition name="fade">
<div v-if="isSettingsOpen" class="fixed inset-0 z-30 flex items-start justify-center p-4 pt-20" @click.self="isSettingsOpen = false">
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl w-full max-w-sm flex flex-col text-slate-800 dark:text-slate-100">
<div class="p-4 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center">
<h3 class="font-bold text-lg">Einstellungen</h3>
<button @click="isSettingsOpen = false" class="flex items-center justify-center h-7 w-7 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700 text-xl">×</button>
</div>
<div class="p-4 space-y-4">
<div>
<h4 class="text-sm font-semibold mb-2 text-slate-600 dark:text-slate-300">Farbschema</h4>
<div class="grid grid-cols-3 gap-2">
<button @click="setTheme('light')" :class="{'bg-primary text-white': theme === 'light'}" class="p-2 text-sm font-medium rounded-md border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700">Hell</button>
<button @click="setTheme('dark')" :class="{'bg-primary text-white': theme === 'dark'}" class="p-2 text-sm font-medium rounded-md border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700">Dunkel</button>
<button @click="setTheme('system')" :class="{'bg-primary text-white': theme === 'system'}" class="p-2 text-sm font-medium rounded-md border border-slate-300 dark:border-slate-600 hover:bg-slate-100 dark:hover:bg-slate-700">System</button>
</div>
</div>
<div v-if="!isStandalone">
<h4 class="text-sm font-semibold mb-2 text-slate-600 dark:text-slate-300">App</h4>
<button @click="installModal.show = true; isSettingsOpen = false" class="w-full text-left p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 text-sm font-medium">App installieren</button>
</div>
</div>
<div class="p-4 border-t border-slate-200 dark:border-slate-700">
<a href="https://thetool.xinon.at/Dashboard/logout" class="w-full text-left p-3 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 text-sm font-medium flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 102 0V4a1 1 0 00-1-1zm10.293 9.293a1 1 0 001.414 1.414l3-3a1 1 0 000-1.414l-3-3a1 1 0 10-1.414 1.414L14.586 9H7a1 1 0 100 2h7.586l-1.293 1.293z" clip-rule="evenodd" /></svg>
Logout
</a>
</div>
<footer class="p-4 mt-2 text-center text-xs text-slate-500 dark:text-slate-300 space-y-2">
<img src="/assets/images/xinon-sm.png" class="h-10 w-10 mx-auto" alt="XINON Logo">
<p>
powered by XINON GmbH<br>
<a href="https://xinon.at/impressum/" target="_blank" class="hover:underline">Impressum</a>
</p>
</footer>
</div>
</div>
</transition>
<transition name="fade">
<div v-if="showThemePicker" class="fixed inset-0 bg-black bg-opacity-60 z-40 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-xs text-center shadow-2xl">
<h3 class="font-bold text-lg mb-2 dark:text-white">Willkommen!</h3>
<p class="text-sm text-slate-600 dark:text-slate-200 mb-6">Wähle dein bevorzugtes Farbschema.</p>
<div class="flex flex-col space-y-3">
<button @click="setTheme('light')" class="w-full px-4 py-3 bg-slate-200 text-slate-800 font-bold rounded-md">Hell</button>
<button @click="setTheme('dark')" class="w-full px-4 py-3 bg-slate-700 text-white font-bold rounded-md">Dunkel</button>
<button @click="setTheme('system')" class="w-full mt-2 text-sm text-slate-500 dark:text-slate-300 hover:underline">Systemstandard</button>
</div>
</div>
</div>
</transition>
<transition name="fade">
<div v-if="installModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 w-full max-w-md max-h-[80vh] flex flex-col">
<div class="flex justify-between items-center mb-4 flex-shrink-0 dark:text-white">
<h3 class="font-bold text-lg">App installieren</h3>
<button @click="installModal.show = false" class="p-1 rounded-full hover:bg-slate-100 dark:hover:bg-slate-700">×</button>
</div>
<div class="overflow-y-auto text-sm text-slate-700 dark:text-slate-200 space-y-6">
<div>
<h4 class="font-bold text-base mb-2 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
iPhone & iPad (mit Safari)
</h4>
<ol class="list-decimal list-inside space-y-1 pl-2">
<li>Öffnen Sie diese Webseite im <strong>Safari</strong>-Browser.</li>
<li>Tippen Sie auf das "Teilen"-Symbol (das Quadrat mit dem Pfeil nach oben).</li>
<li>Scrollen Sie nach unten und wählen Sie <strong>"Zum Home-Bildschirm"</strong>.</li>
<li>Bestätigen Sie mit "Hinzufügen". Die App erscheint nun auf Ihrem Startbildschirm.</li>
</ol>
</div>
<div>
<h4 class="font-bold text-base mb-2 flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V6zM3.5 9h17M3.5 15h17"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.5v15"></path></svg>
Android (mit Chrome)
</h4>
<ol class="list-decimal list-inside space-y-1 pl-2">
<li>Öffnen Sie diese Webseite im <strong>Chrome</strong>-Browser.</li>
<li>Tippen Sie auf die drei Punkte oben rechts, um das Menü zu öffnen.</li>
<li>Wählen Sie <strong>"App installieren"</strong> oder <strong>"Zum Startbildschirm hinzufügen"</strong>.</li>
<li>Bestätigen Sie die Installation. Die App erscheint nun auf Ihrem Startbildschirm.</li>
</ol>
</div>
</div>
<div class="mt-6 text-right flex-shrink-0">
<button @click="installModal.show = false" class="px-4 py-2 bg-primary text-white rounded-md">Verstanden</button>
</div>
</div>
</div>
</transition>
<div v-if="fullscreenViewer.show" @click="fullscreenViewer.show = false" class="fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-2">
<button @click="fullscreenViewer.show = false" class="absolute top-2 right-2 p-2 bg-white/20 rounded-full text-white"><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="M6 18L18 6M6 6l12 12" /></svg></button>
<template v-if="fullscreenViewer.item.isPdf">
<iframe :src="'/File/show?id=' + fullscreenViewer.item.fileId" class="w-full h-full border-0"></iframe>
</template>
<template v-else>
<img :src="'/File/show?id=' + fullscreenViewer.item.fileId" class="max-w-full max-h-full object-contain">
</template>
</div>
</div>
`
});
app.mount('#app');
</script>
<script src="/js/pages/WorkorderBase/WorkorderServiceWorker.js"></script>
</body>
</html>