added improved version of pwa app
This commit is contained in:
@@ -1,28 +1,22 @@
|
||||
<?php
|
||||
// This file acts as the main layout for the Vue 3 PWA.
|
||||
// It includes all necessary CDN links and the main Vue app structure.
|
||||
?>
|
||||
<!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">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Workorders</title>
|
||||
|
||||
<!-- PWA Manifest and Theme Color -->
|
||||
<link rel="manifest" href="/js/pages/WorkorderBase/manifest.json">
|
||||
<meta name="theme-color" content="#005384">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Vue 3 and Axios -->
|
||||
<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>
|
||||
|
||||
<!-- Global Config from PHP -->
|
||||
<script>
|
||||
window.TT_CONFIG = <?= json_encode($JSGlobals ?? []) ?>;
|
||||
moment.locale('de');
|
||||
@@ -95,6 +89,9 @@
|
||||
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 API_BASE_URL = window.TT_CONFIG.BASE_PATH || '/WorkorderCompany';
|
||||
const api = axios.create({ baseURL: API_BASE_URL });
|
||||
@@ -119,7 +116,7 @@
|
||||
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}`;
|
||||
return `https://maps.google.com/?q=${address}`;
|
||||
});
|
||||
|
||||
const checklist = computed(() => {
|
||||
@@ -316,30 +313,39 @@
|
||||
};
|
||||
|
||||
|
||||
onMounted(fetchWorkorders);
|
||||
onMounted(() => {
|
||||
fetchWorkorders();
|
||||
isStandalone.value = window.matchMedia('(display-mode: standalone)').matches;
|
||||
});
|
||||
|
||||
return {
|
||||
isLoading, filteredWorkorders, searchTerm, isDetailsPanelOpen, selectedWorkorder, documentation, tenantConfig,
|
||||
tempAdditionalInfo, isEditingInfo, newJournalEntry, uploadModal, problemModal, isUploading, isChecklistComplete,
|
||||
checklist, fullscreenViewer, missingTasksPopover, translatedDocs, filteredJournals,
|
||||
checklist, fullscreenViewer, missingTasksPopover, translatedDocs, filteredJournals, installModal, isStandalone,
|
||||
openDetails, closeDetails, getStatusInfo, formatDate, googleMapsLink, startEditInfo, saveAdditionalInfo,
|
||||
handleFileSelect, executeUpload, addJournalEntry, submitProblem, handleCompleteClick
|
||||
};
|
||||
},
|
||||
template: `
|
||||
<!-- Main List View -->
|
||||
<div class="relative h-full w-full">
|
||||
<transition name="overlay">
|
||||
<div v-if="isDetailsPanelOpen" @click="closeDetails" class="overlay"></div>
|
||||
<div v-if="isDetailsPanelOpen || installModal.show" @click="closeDetails" class="overlay"></div>
|
||||
</transition>
|
||||
|
||||
<div :class="{'panel-open': isDetailsPanelOpen}" class="list-container flex flex-col h-full bg-slate-100">
|
||||
<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">
|
||||
<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" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h5M20 20v-5h-5M4 4l16 16"/></svg>
|
||||
</button>
|
||||
<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">
|
||||
Wie installiere ich die App?
|
||||
</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" 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>
|
||||
<div class="mt-4"><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"></div>
|
||||
</header>
|
||||
@@ -349,9 +355,9 @@
|
||||
<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 class="flex justify-between items-start">
|
||||
<div class="flex-grow pr-2">
|
||||
<p class="font-bold text-slate-800 truncate">{{ wo.customerName || 'N/A' }}</p>
|
||||
<p class="text-sm text-slate-500 truncate">{{ wo.street }} {{ wo.hausnummer }}, {{ wo.plz }} {{ wo.city }}</p>
|
||||
<div class="flex-grow pr-2 min-w-0">
|
||||
<p class="font-bold text-slate-800 break-words">{{ wo.customerName || 'N/A' }}</p>
|
||||
<p class="text-sm text-slate-500 break-words">{{ wo.street }} {{ wo.hausnummer }}, {{ wo.plz }} {{ wo.city }}</p>
|
||||
<p class="text-xs text-slate-400 mt-1">ID: {{ wo.id }}</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0 ml-2 text-right space-y-1">
|
||||
@@ -365,7 +371,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Details Panel -->
|
||||
<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">
|
||||
@@ -374,7 +379,6 @@
|
||||
</header>
|
||||
|
||||
<div class="overflow-y-auto p-4 flex-grow space-y-4">
|
||||
<!-- Customer & Address -->
|
||||
<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>
|
||||
@@ -390,7 +394,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notiz -->
|
||||
<div class="bg-white p-4 rounded-lg border border-slate-200">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="font-bold text-slate-700">Notiz</h3>
|
||||
@@ -408,7 +411,6 @@
|
||||
<p v-else class="text-sm whitespace-pre-wrap">{{ selectedWorkorder.additionalInfo || 'Keine Notiz.' }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Checklist -->
|
||||
<div class="bg-white p-4 rounded-lg border border-slate-200">
|
||||
<h3 class="font-bold text-slate-700 mb-3">Checkliste</h3>
|
||||
<ul v-if="checklist.length > 0" class="space-y-2">
|
||||
@@ -421,7 +423,6 @@
|
||||
<p v-else class="text-sm text-slate-500">Keine Checklisten-Einträge vorhanden.</p>
|
||||
</div>
|
||||
|
||||
<!-- Docs -->
|
||||
<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">
|
||||
@@ -447,7 +448,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Journal -->
|
||||
<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="space-y-4 max-h-60 overflow-y-auto pr-2 journal-container">
|
||||
@@ -467,23 +467,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="bg-white p-2 border-t border-slate-200 flex-shrink-0 grid grid-cols-2 gap-2 relative">
|
||||
<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))]">
|
||||
<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>
|
||||
<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 left-1/2 -translate-x-1/2 mb-2 w-72 bg-slate-800 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">
|
||||
<li v-for="task in missingTasksPopover.tasks" :key="task">{{ task }}</li>
|
||||
</ul>
|
||||
<div class="absolute bottom-[-5px] left-1/2 -translate-x-1/2 w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-slate-800"></div>
|
||||
</div>
|
||||
</transition>
|
||||
<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 left-1/2 -translate-x-1/2 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] left-1/2 -translate-x-1/2 w-0 h-0 border-x-8 border-x-transparent border-t-8 border-t-red-700"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div v-if="uploadModal.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">
|
||||
<h3 class="font-bold text-lg mb-4">Dokumenttyp wählen</h3>
|
||||
@@ -498,29 +499,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Problem Modal -->
|
||||
<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">
|
||||
<h3 class="font-bold text-lg mb-4">Problem melden</h3>
|
||||
<div class="space-y-2 mb-4 overflow-y-auto" style="max-height: 30vh;">
|
||||
<div class="bg-white rounded-lg p-6 w-full max-w-sm flex flex-col max-h-[80vh]">
|
||||
<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-2 border rounded-md">
|
||||
<input type="checkbox" :value="type" v-model="problemModal.selectedInterventions" class="h-4 w-4 rounded text-primary focus:ring-primary">
|
||||
<span class="ml-2 text-sm">{{ type.text.replace('X', '...') }}</span>
|
||||
<label class="flex items-center p-3 border rounded-lg hover:bg-slate-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')"
|
||||
v-model="problemModal.details[type.value]"
|
||||
type="text" class="w-full p-2 border rounded-md mt-1 text-sm" placeholder="Details für dieses Problem...">
|
||||
type="text" class="w-full p-2 border rounded-md mt-1 text-sm focus:ring-primary focus:border-primary" placeholder="Details hier eingeben...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2 mt-auto">
|
||||
<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="submitProblem" class="px-4 py-2 bg-red-600 text-white rounded-md">Senden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen Viewer -->
|
||||
<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>
|
||||
<div class="overflow-y-auto text-sm text-slate-700 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-40 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">
|
||||
@@ -537,5 +576,4 @@
|
||||
</script>
|
||||
<script src="/js/pages/WorkorderBase/WorkorderServiceWorker.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -9,13 +9,13 @@
|
||||
"orientation": "portrait-primary",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://placehold.co/192x192/4f46e5/ffffff?text=WO",
|
||||
"src": "https://thetool.xinon.at/assets/images/xinon-sm-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "https://placehold.co/512x512/4f46e5/ffffff?text=WO",
|
||||
"src": "https://thetool.xinon.at/assets/images/xinon-sm-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any maskable"
|
||||
|
||||
@@ -3,6 +3,13 @@ Vue.component('workorder-company', {
|
||||
template: `
|
||||
<div>
|
||||
<tt-card>
|
||||
|
||||
<div v-if="/Android/.test(navigator.userAgent) && /Chrome/.test(navigator.userAgent) || /iPhone/.test(navigator.userAgent) && /Safari/.test(navigator.userAgent)" class="mb-3">
|
||||
<a :href="window.TT_CONFIG.BASE_PATH + '/WorkorderCompany/Mobile'" class="btn btn-primary">
|
||||
<i class="fas fa-phone-alt"></i> Mobile Ansicht
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<tt-table-crud
|
||||
ref="table"
|
||||
:crud-config="crudConfig"
|
||||
@@ -90,6 +97,7 @@ Vue.component('workorder-company', {
|
||||
data() {
|
||||
return {
|
||||
window,
|
||||
navigator,
|
||||
rescheduleModalData: null,
|
||||
editingAdditionalInfoId: null,
|
||||
tempAdditionalInfo: '',
|
||||
|
||||
Reference in New Issue
Block a user