Merge branch 'Workorder/improve-pwa' into 'master'

fixed bugs

See merge request fronk/thetool!1728
This commit is contained in:
Luca Haid
2025-09-08 14:15:41 +00:00
2 changed files with 222 additions and 134 deletions

View File

@@ -9,6 +9,7 @@
<link rel="shortcut icon" href="/assets/images/favicon.ico">
<link rel="manifest" href="/js/pages/WorkorderBase/manifest.json">
<!-- This theme-color will be dynamically updated by the script -->
<meta name="theme-color" content="#005384">
<script src="https://cdn.tailwindcss.com"></script>
@@ -22,6 +23,7 @@
window.TT_CONFIG = <?= json_encode($JSGlobals ?? []) ?>;
moment.locale('de');
tailwind.config = {
darkMode: 'class', // Enable dark mode based on a class
theme: {
extend: {
colors: {
@@ -34,10 +36,18 @@
</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%); }
@@ -69,11 +79,9 @@
to { transform: rotate(360deg); }
}
.spin { animation: spin 1s linear infinite; }
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
</style>
</head>
<body class="bg-slate-100">
<body class="transition-colors duration-300">
<div id="app" class="h-screen w-screen overflow-hidden antialiased"></div>
@@ -104,6 +112,10 @@
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';
@@ -155,51 +167,31 @@
const getStatusRank = (status) => {
switch (status) {
// Priority 0: Active and scheduled, sorted by appointment date
case 'scheduled':
case 'civil_engineering_completed':
return 0;
// Priority 1: New/Assigned tasks, sorted by deadline
case 'civil_engineering_completed': return 0;
case 'assigned':
case 'new':
case 'problem_solved':
return 1;
// Priority 2: Tasks with issues, sorted by deadline
case 'problem_solved': return 1;
case 'intervention_required':
case 'correction_requested':
case 'civil_engineering_required':
return 2;
// Priority 3: Finished tasks, sorted by deadline
case 'civil_engineering_required': return 2;
case 'documented':
case 'completed':
return 3;
// Priority 4: Cancelled tasks
case 'cancelled':
return 4;
default:
return 99; // Fallback for any other status
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;
}
// For the highest priority group, sort by appointment date
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);
}
if (dateA === dateB) return (a.deadlineDate || Infinity) - (b.deadlineDate || Infinity);
return dateA - dateB;
}
// For all other groups, sort by deadline
return (a.deadlineDate || Infinity) - (b.deadlineDate || Infinity);
});
});
@@ -242,6 +234,28 @@
// --- 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' },
@@ -364,13 +378,14 @@
for (const type of sortedInterventions) {
let text = type.text;
if (text.includes('X')) {
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.replace('X', detail);
text = text.includes('X') ? text.replace('X', detail) : `${text}: ${detail}`;
}
journalParts.push(text);
}
@@ -389,6 +404,8 @@
const handleCompleteClick = () => {
if (isChecklistComplete.value) {
// Using a custom modal/confirm is better, but for now `confirm` is used.
// In a real PWA, you'd build a custom component to avoid blocking.
if (confirm("Möchten Sie diesen Auftrag wirklich abschließen?")) {
completeWorkorder();
}
@@ -415,81 +432,97 @@
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) {
if (isOpen) {
nextTick(() => {
fcpInputRef.value?.focus();
});
} else {
fcpSearchTerm.value = '';
}
});
// Lock body scroll when any modal is open
watch([isDetailsPanelOpen, isFcpSelectOpen, uploadModal, problemModal, fullscreenViewer, installModal], ([details, fcp, upload, problem, viewer, install]) => {
const isAnyModalOpen = details || fcp || upload.show || problem.show || viewer.show || install.show;
if (isAnyModalOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
watch([isDetailsPanelOpen, isFcpSelectOpen, isSettingsOpen, uploadModal, problemModal, fullscreenViewer, installModal, showThemePicker], (modals) => {
const isAnyModalOpen = modals.some(modal => (typeof modal === 'boolean' && modal) || (typeof modal === 'object' && modal.show));
document.body.style.overflow = isAnyModalOpen ? 'hidden' : '';
}, { deep: true });
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, fetchWorkorders,
openDetails, closeDetails, getStatusInfo, formatDate, googleMapsLink, startEditInfo, saveAdditionalInfo,
handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick, selectFcp
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" @click="closeDetails(); isFcpSelectOpen = false" class="overlay"></div>
<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 overflow-hidden">
<header class="bg-white shadow p-4 flex-shrink-0 z-10">
<div class="flex justify-between items-center">
<img src="/assets/images/xinon-full.png" alt="Logo" class="h-8 w-auto">
<div class="flex items-center space-x-4">
<button v-if="!isStandalone" @click="installModal.show = true" class="text-sm text-primary font-medium hover:underline">
App installieren
</button>
<button @click="fetchWorkorders" class="p-2 rounded-full hover:bg-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-600" :class="{'spin': isLoading}" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<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-300" :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.png" alt="Logo" class="h-8 w-auto">
</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-300" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-1.007 1.11-1.226.554-.22 1.197-.24 1.752-.07C13.06 2.83 13.5 3.326 13.5 3.94v.293c.312.08.614.195.912.338.298.144.58.32.843.533.264.214.504.46.717.734.213.273.392.58.52.922.128.341.19.71.19 1.093v.293c.338.298.636.623.896.98.26.357.46.754.593 1.178.133.424.2 1.86.2 3.371v.293c-.08.312-.195.614-.338.912a5.992 5.992 0 01-.533.843c-.214.264-.46.504-.734.717-.273.213-.58.392-.922.52-.341.128-.71.19-1.093.19h-.293c-.298.338-.623.636-.98.896-.357.26-.754.46-1.178.593-.424.133-1.86.2-3.371.2h-.293c-.312-.08-.614-.195-.912-.338a5.992 5.992 0 01-.843-.533c-.264-.214-.504-.46-.717-.734a6.01 6.01 0 01-.52-.922c-.128-.341-.19-.71-.19-1.093v-.293c-.338-.298-.636-.623-.896-.98-.26-.357-.46-.754-.593-1.178-.133-.424-.2-1.86-.2-3.371v-.293c.08-.312.195-.614.338-.912.144-.298.32-.58.533-.843.214-.264.46-.504.734-.717.273-.213.58-.392.922-.52.341-.128.71-.19 1.093-.19V3.94zM12 6.375a3.625 3.625 0 100 7.25 3.625 3.625 0 000-7.25z" />
</svg>
</button>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-2">
<input type="text" v-model="searchTerm" placeholder="Suche..." class="w-full p-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition">
<button @click="isFcpSelectOpen = true" class="w-full p-3 border border-slate-300 rounded-lg bg-white text-left flex justify-between items-center text-sm">
<span class="truncate pr-2">{{ selectedFcpText }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<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-400">
<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-200">{{ selectedFcpText }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-400 dark:text-slate-500 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="text-center p-10"><p class="text-slate-500">Lade Aufträge...</p></div>
<div v-else-if="filteredWorkorders.length === 0" class="text-center p-10"><p class="text-slate-500">Keine Aufträge gefunden.</p></div>
<div v-if="isLoading" class="text-center p-10"><p class="text-slate-500 dark:text-slate-400">Lade Aufträge...</p></div>
<div v-else-if="filteredWorkorders.length === 0" class="text-center p-10"><p class="text-slate-500 dark:text-slate-400">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 p-4 rounded-lg shadow-md cursor-pointer transition active:scale-[0.98]">
<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 break-words">#{{ wo.id }} | {{ wo.customerName || 'N/A' }}</p>
<p class="text-sm text-slate-500 break-words">{{ wo.street }} {{ wo.hausnummer }}, {{ wo.plz }} {{ wo.city }}</p>
<div class="items-center text-xs text-slate-400 mt-1">
<p class="font-bold text-slate-800 dark:text-slate-100 break-words">#{{ wo.id }} | {{ wo.customerName || 'N/A' }}</p>
<p class="text-sm text-slate-500 dark:text-slate-400 break-words">{{ wo.street }} {{ wo.hausnummer }}, {{ wo.plz }} {{ wo.city }}</p>
<div class="items-center text-xs text-slate-400 dark:text-slate-500 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">{{ formatDate(wo.appointmentDate, 'DD.MM HH:mm') }}</p>
<p class="text-sm font-semibold text-slate-600 dark:text-slate-300">{{ formatDate(wo.appointmentDate, 'DD.MM HH:mm') }}</p>
<p class="text-xs text-red-500">Frist: {{ formatDate(wo.deadlineDate) }}</p>
</div>
</div>
@@ -499,87 +532,90 @@
</div>
<transition name="slide">
<div v-if="isDetailsPanelOpen && selectedWorkorder" class="fixed inset-0 bg-slate-50 z-20 flex flex-col shadow-2xl">
<header class="bg-white p-4 flex justify-between items-center border-b border-slate-200 flex-shrink-0">
<h2 class="text-xl font-bold text-primary truncate pr-2">Auftrag #{{ selectedWorkorder.id }}</h2>
<button @click="closeDetails" class="p-2 rounded-full hover:bg-slate-200 flex-shrink-0"><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>
<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">
<img src="/assets/images/xinon-full.png" alt="Logo" class="h-6 w-auto mr-4">
<h2 class="text-xl font-bold text-primary 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-300" 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 p-4 rounded-lg border border-slate-200 space-y-3 text-sm">
<div class="flex items-center text-base font-bold text-slate-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-500" 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>
<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-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-500 dark:text-slate-400" 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 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 pt-3 mt-3 grid grid-cols-2 gap-2 text-sm">
<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 font-semibold">OAID</p>
<p class="text-slate-800">{{ selectedWorkorder.oaid || 'N/A' }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400 font-semibold">OAID</p>
<p class="text-slate-800 dark:text-slate-200">{{ selectedWorkorder.oaid || 'N/A' }}</p>
</div>
<div>
<p class="text-xs text-slate-500 font-semibold">FCP</p>
<p class="text-slate-800">{{ selectedWorkorder.rimo_fcp_name || 'N/A' }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400 font-semibold">FCP</p>
<p class="text-slate-800 dark:text-slate-200">{{ selectedWorkorder.rimo_fcp_name || 'N/A' }}</p>
</div>
</div>
<div class="border-t pt-3 space-y-2">
<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 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 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 p-4 rounded-lg border border-slate-200">
<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">Notiz</h3>
<button v-if="!isEditingInfo" @click="startEditInfo" class="flex items-center text-sm font-medium text-primary bg-slate-100 hover:bg-slate-200 px-3 py-1.5 rounded-md">
<h3 class="font-bold text-slate-700 dark:text-slate-200">Notiz</h3>
<button v-if="!isEditingInfo" @click="startEditInfo" class="flex items-center text-sm font-medium text-primary bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 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" rows="4"></textarea>
<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 rounded-md text-sm font-medium">Abbrechen</button>
<button @click="isEditingInfo = false" class="px-3 py-1.5 bg-slate-200 dark:bg-slate-700 dark:text-slate-100 rounded-md text-sm font-medium">Abbrechen</button>
<button @click="saveAdditionalInfo" class="px-3 py-1.5 bg-primary text-white rounded-md text-sm font-medium">Speichern</button>
</div>
</div>
<p v-else class="text-sm whitespace-pre-wrap">{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}</p>
<p v-else class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-300">{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}</p>
</div>
<div class="bg-white p-4 rounded-lg border border-slate-200">
<h3 class="font-bold text-slate-700 mb-3">Checkliste</h3>
<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-slate-200 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 mr-2"></div>
<div class="h-4 w-3/4 rounded bg-slate-200"></div>
<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" 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 line-through': item.completed}">{{ item.text }}</span>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-slate-400 dark:text-slate-500" 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-400 line-through': item.completed, 'text-slate-800 dark:text-slate-200': !item.completed}">{{ item.text }}</span>
</li>
</ul>
<p v-else class="text-sm text-slate-500">Keine Checklisten-Einträge vorhanden.</p>
<p v-else class="text-sm text-slate-500 dark:text-slate-400">Keine Checklisten-Einträge vorhanden.</p>
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-slate-200">
<h3 class="font-bold text-slate-700 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 text-sm font-medium rounded-md text-slate-700 bg-slate-50 hover:bg-slate-100 cursor-pointer">
<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-slate-200 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-300 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 rounded-md overflow-hidden cursor-pointer group">
<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 p-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-red-500" 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 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>
@@ -593,35 +629,35 @@
</div>
</div>
<div class="bg-white p-4 rounded-lg border border-slate-200">
<h3 class="font-bold text-slate-700 mb-4">Journal</h3>
<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-slate-200 mb-4">Journal</h3>
<div v-if="isDetailsLoading" class="animate-pulse">
<div class="flex items-start">
<div class="flex-shrink-0 bg-slate-200 h-8 w-8 rounded-full mr-3"></div>
<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"></div>
<div class="h-3 w-1/2 rounded bg-slate-200"></div>
<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">Keine Einträge.</p></div>
<div v-if="filteredJournals.length === 0"><p class="text-sm text-slate-500 dark:text-slate-400">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">{{ entry.text }}</p>
<p class="text-xs text-slate-400 mt-1">{{ entry.createByName }} - {{ formatDate(entry.create, 'DD.MM.YY HH:mm') }}</p>
<p class="text-sm whitespace-pre-wrap text-slate-800 dark:text-slate-200">{{ entry.text }}</p>
<p class="text-xs text-slate-400 dark:text-slate-500 mt-1">{{ entry.createByName }} - {{ formatDate(entry.create, 'DD.MM.YY HH:mm') }}</p>
</div>
</div>
</div>
<div class="mt-4 pt-4 border-t">
<textarea v-model="newJournalEntry" placeholder="Neuer Eintrag..." class="w-full p-2 border rounded-md" rows="3"></textarea>
<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-primary text-white font-semibold rounded-md text-sm">Senden</button>
</div>
</div>
</div>
<footer class="bg-white p-2 border-t border-slate-200 flex-shrink-0 grid grid-cols-2 gap-2 pt-2 px-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
<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>
@@ -641,27 +677,27 @@
<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 rounded-lg p-4 w-full max-w-sm flex flex-col max-h-[80vh]">
<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-200">
<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 text-xl">×</button>
<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" placeholder="FCP suchen..." class="w-full p-2 pl-8 border border-slate-300 rounded-md">
<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 cursor-pointer text-sm font-medium"
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-primary/10 text-primary': 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-primary" 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 p-3">Kein FCP gefunden.</li>
<li v-if="filteredFcpOptions.length === 0" class="text-sm text-slate-500 dark:text-slate-400 p-3">Kein FCP gefunden.</li>
</ul>
</div>
</div>
@@ -669,15 +705,15 @@
<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 rounded-lg p-4 w-full max-w-sm flex flex-col max-h-[80vh]" @click.stop>
<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-200" @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 text-xl">×</button>
<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 cursor-pointer text-sm font-medium"
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-primary/10 text-primary': 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-primary" viewBox="0 0 20 20" fill="currentColor">
@@ -685,46 +721,98 @@
</svg>
</li>
<li v-if="!tenantConfig.documentationTypes || tenantConfig.documentationTypes.length === 0">
<p class="text-sm text-slate-500 p-3">Keine Dokumenttypen konfiguriert.</p>
<p class="text-sm text-slate-500 dark:text-slate-400 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 pt-3">
<button @click="uploadModal.show = false" class="px-4 py-2 bg-slate-200 rounded-md text-sm font-medium">Abbrechen</button>
<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 rounded-lg p-6 w-full max-w-sm flex flex-col max-h-[80vh]">
<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-200">
<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 rounded-lg hover:bg-slate-50 transition cursor-pointer">
<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')"
<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" placeholder="Details hier eingeben...">
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 rounded-md">Abbrechen</button>
<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="installModal.show" class="fixed inset-0 bg-black bg-opacity-50 z-30 flex items-center justify-center p-4">
<div class="bg-white 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">
<h3 class="font-bold text-lg">App installieren</h3>
<button @click="installModal.show = false" class="p-1 rounded-full hover:bg-slate-100">×</button>
<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-200">
<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="overflow-y-auto text-sm text-slate-700 space-y-6">
<div class="p-4 space-y-4">
<div>
<h4 class="text-sm font-semibold mb-2 text-slate-600 dark:text-slate-400">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-400">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-400 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-300 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-400 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-300 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>
@@ -757,7 +845,7 @@
</div>
</transition>
<div v-if="fullscreenViewer.show" @click="fullscreenViewer.show = false" class="fixed inset-0 bg-black bg-opacity-90 z-40 flex items-center justify-center p-2">
<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>

View File

@@ -4,8 +4,8 @@
"description": "A PWA for managing workorders efficiently on mobile devices.",
"start_url": "/WorkorderCompany/mobile",
"display": "standalone",
"background_color": "#f1f5f9",
"theme_color": "#4f46e5",
"background_color": "#0f172a",
"theme_color": "#005384",
"orientation": "portrait-primary",
"icons": [
{