Files
thetool/public/mobile/modules/workorder/WorkorderModule.js
2026-01-17 12:48:08 +00:00

1374 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Workorder Module (Aufträge)
*
* Main module for workorder management in MobileApp.
* Provides list view and full-screen detail view with collapsible cards.
*/
export default {
name: 'WorkorderModule',
emits: ['navigate', 'toast'],
props: {
user: Object,
submodule: String
},
setup(props, { emit }) {
const { ref, computed, watch, onMounted, nextTick } = Vue;
// =====================
// STATE
// =====================
const isLoading = ref(true);
const workorders = ref([]);
const searchTerm = ref('');
const selectedFcp = ref('all');
const showFcpFilter = ref(false);
// Detail view state
const selectedWorkorder = ref(null);
const isDetailLoading = ref(false);
const documentation = ref({ docs: [], journals: [] });
const tenantConfig = ref(null);
const checklist = ref([]);
// Expanded cards state
const expandedCards = ref({
customer: true,
checklist: true,
documentation: false,
notes: false,
journal: false,
cableData: false
});
// Edit states
const isEditingNotes = ref(false);
const tempNotes = ref('');
const newJournalText = ref('');
const cableDataForm = ref({ cableLength: '', cableType: '' });
// Bottom sheets
const showDocUploadSheet = ref(false);
const showProblemSheet = ref(false);
const showCompleteSheet = ref(false);
// Upload state
const uploadDocType = ref('');
const isUploading = ref(false);
const fileInputRef = ref(null);
const pendingChecklistUpload = ref(null); // For auto-advance camera
// Problem form
const problemType = ref('');
const problemComment = ref('');
// Swipe state for list cards
const swipeStartX = ref(0);
const swipeCardId = ref(null);
const swipeOffset = ref({}); // { [workorderId]: offsetX }
// =====================
// COMPUTED
// =====================
const fcpOptions = computed(() => {
if (!workorders.value.length) return [{ value: 'all', text: 'Alle FCPs' }];
const fcps = [...new Set(workorders.value.map(wo => wo.fcpName).filter(Boolean))].sort();
return [{ value: 'all', text: 'Alle FCPs' }, ...fcps.map(fcp => ({ value: fcp, text: fcp }))];
});
const filteredWorkorders = computed(() => {
let filtered = workorders.value;
// FCP filter
if (selectedFcp.value !== 'all') {
filtered = filtered.filter(wo => wo.fcpName === selectedFcp.value);
}
// Search filter
if (searchTerm.value.length > 2) {
const term = searchTerm.value.toLowerCase();
filtered = filtered.filter(wo =>
wo.id.toString().includes(term) ||
(wo.oaid && wo.oaid.toLowerCase().includes(term)) ||
(wo.fcpName && wo.fcpName.toLowerCase().includes(term)) ||
(wo.customerName && wo.customerName.toLowerCase().includes(term)) ||
(wo.customerAddress && wo.customerAddress.toLowerCase().includes(term))
);
}
// Sort by status priority
const getStatusRank = (status) => {
switch (status) {
case 'scheduled':
case 'civil_engineering_completed': return 0;
case 'assigned':
case 'new':
case 'problem_solved': return 1;
case 'in_progress': return 2;
case 'intervention_required':
case 'correction_requested': return 3;
case 'documented':
case 'completed': 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;
return (a.appointmentDate || Infinity) - (b.appointmentDate || Infinity);
});
});
const checklistProgress = computed(() => {
if (!checklist.value.length) return { completed: 0, total: 0, text: '0/0' };
const completed = checklist.value.filter(c => c.completed).length;
const total = checklist.value.length;
return { completed, total, text: `${completed}/${total}` };
});
const isChecklistComplete = computed(() => {
// Check required items are completed (if any are marked as required)
const requiredItems = checklist.value.filter(c => c.required);
if (requiredItems.length > 0) {
const allRequiredComplete = requiredItems.every(c => c.completed);
if (!allRequiredComplete) return false;
} else if (checklist.value.length > 0) {
// If no items are marked as required, check if at least some items are completed
const hasAnyCompleted = checklist.value.some(c => c.completed);
if (!hasAnyCompleted) return false;
}
// Check cable data if required
if (tenantConfig.value?.requireCableLength && !cableDataForm.value.cableLength?.trim()) {
return false;
}
if (tenantConfig.value?.requireCableType && !cableDataForm.value.cableType?.trim()) {
return false;
}
return true;
});
const incompleteItems = computed(() => {
const items = [];
checklist.value.filter(c => !c.completed).forEach(c => {
items.push(c.text);
});
if (tenantConfig.value?.requireCableLength && !cableDataForm.value.cableLength?.trim()) {
items.push('Kabellänge');
}
if (tenantConfig.value?.requireCableType && !cableDataForm.value.cableType?.trim()) {
items.push('Kabeltyp');
}
return items;
});
const googleMapsLink = computed(() => {
if (!selectedWorkorder.value?.customer) return '#';
const c = selectedWorkorder.value.customer;
const address = encodeURIComponent(`${c.street}, ${c.zip} ${c.city}`);
return `https://maps.google.com/maps?q=${address}`;
});
// =====================
// METHODS
// =====================
const fetchWorkorders = async () => {
isLoading.value = true;
try {
const response = await fetch('/MobileApp/Workorder/Workorder/get', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ pagination: { page: 1, per_page: 500 } })
});
const data = await response.json();
if (data.success) {
workorders.value = data.workorders;
}
} catch (error) {
console.error('Error fetching workorders:', error);
emit('toast', 'Fehler beim Laden der Aufträge', 'error');
} finally {
isLoading.value = false;
}
};
const openDetail = async (workorder) => {
selectedWorkorder.value = workorder;
isDetailLoading.value = true;
expandedCards.value = { customer: true, checklist: true, documentation: false, notes: false, journal: false, cableData: false };
try {
// Fetch all workorder details in a single request
const response = await fetch(`/MobileApp/Workorder/Workorder/getWorkorderDetail?id=${workorder.id}`, { credentials: 'include' });
const data = await response.json();
if (data.success) {
selectedWorkorder.value = data.workorder;
cableDataForm.value = {
cableLength: data.workorder.cableLength || '',
cableType: data.workorder.cableType || ''
};
documentation.value = { docs: data.docs, journals: data.journals };
tenantConfig.value = data.tenantConfig;
checklist.value = data.checklist;
} else {
emit('toast', data.message || 'Fehler beim Laden', 'error');
}
} catch (error) {
console.error('Error loading detail:', error);
emit('toast', 'Fehler beim Laden der Details', 'error');
} finally {
isDetailLoading.value = false;
}
};
const closeDetail = () => {
selectedWorkorder.value = null;
documentation.value = { docs: [], journals: [] };
tenantConfig.value = null;
checklist.value = [];
isEditingNotes.value = false;
};
const toggleCard = (cardId) => {
expandedCards.value[cardId] = !expandedCards.value[cardId];
};
// Notes editing
const startEditNotes = () => {
tempNotes.value = selectedWorkorder.value.additionalInfo || '';
isEditingNotes.value = true;
};
const cancelEditNotes = () => {
isEditingNotes.value = false;
tempNotes.value = '';
};
const saveNotes = async () => {
try {
const response = await fetch('/MobileApp/Workorder/Workorder/updateAdditionalInfo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
workorderId: selectedWorkorder.value.id,
additionalInfo: tempNotes.value
})
});
const data = await response.json();
if (data.success) {
selectedWorkorder.value.additionalInfo = data.newInfo;
isEditingNotes.value = false;
emit('toast', 'Notiz gespeichert', 'success');
// Refresh journals
const docRes = await fetch(`/MobileApp/Workorder/Workorder/getDocumentation?workorderId=${selectedWorkorder.value.id}`, { credentials: 'include' });
const docData = await docRes.json();
if (docData.success) documentation.value.journals = docData.journals;
}
} catch (error) {
emit('toast', 'Fehler beim Speichern', 'error');
}
};
// Journal
const addJournalEntry = async () => {
if (!newJournalText.value.trim()) return;
try {
const response = await fetch('/MobileApp/Workorder/Workorder/addJournal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
workorderId: selectedWorkorder.value.id,
text: newJournalText.value
})
});
const data = await response.json();
if (data.success) {
documentation.value.journals = data.journals;
newJournalText.value = '';
emit('toast', 'Eintrag hinzugefügt', 'success');
}
} catch (error) {
emit('toast', 'Fehler beim Hinzufügen', 'error');
}
};
// Cable data
const saveCableData = async () => {
try {
const response = await fetch('/MobileApp/Workorder/Workorder/updateWorkorderData', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
workorderId: selectedWorkorder.value.id,
cableLength: cableDataForm.value.cableLength,
cableType: cableDataForm.value.cableType
})
});
const data = await response.json();
if (data.success) {
emit('toast', 'Daten gespeichert', 'success');
}
} catch (error) {
emit('toast', 'Fehler beim Speichern', 'error');
}
};
// Documentation upload
const openDocUpload = () => {
if (tenantConfig.value?.documentationTypes?.length) {
uploadDocType.value = tenantConfig.value.documentationTypes[0].value;
}
showDocUploadSheet.value = true;
};
const triggerFileInput = () => {
if (fileInputRef.value) {
fileInputRef.value.click();
}
};
const handleFileSelect = async (event) => {
const files = event.target.files;
if (!files || !files.length) return;
isUploading.value = true;
const wasFromChecklist = pendingChecklistUpload.value !== null;
const formData = new FormData();
formData.append('workorderId', selectedWorkorder.value.id);
formData.append('documentType', uploadDocType.value);
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
try {
const response = await fetch('/MobileApp/Workorder/Workorder/uploadDocumentation', {
method: 'POST',
credentials: 'include',
body: formData
});
const data = await response.json();
if (data.success) {
triggerHaptic('light');
emit('toast', 'Dokument hochgeladen', 'success');
showDocUploadSheet.value = false;
// Refresh documentation and checklist
const [docRes, checkRes] = await Promise.all([
fetch(`/MobileApp/Workorder/Workorder/getDocumentation?workorderId=${selectedWorkorder.value.id}`, { credentials: 'include' }),
fetch(`/MobileApp/Workorder/Workorder/getChecklist?workorderId=${selectedWorkorder.value.id}`, { credentials: 'include' })
]);
const docData = await docRes.json();
const checkData = await checkRes.json();
if (docData.success) documentation.value = { docs: docData.docs, journals: docData.journals };
if (checkData.success) checklist.value = checkData.checklist;
// Auto-advance: If upload was from checklist, open camera for next item
if (wasFromChecklist) {
pendingChecklistUpload.value = null;
await nextTick();
const nextItem = getNextUncompletedItem();
if (nextItem) {
// Short delay so user sees the check mark
setTimeout(() => {
quickUploadForItem(nextItem);
}, 500);
}
}
}
} catch (error) {
emit('toast', 'Upload fehlgeschlagen', 'error');
} finally {
isUploading.value = false;
if (fileInputRef.value) fileInputRef.value.value = '';
}
};
// Problem reporting
const openProblemSheet = () => {
problemType.value = tenantConfig.value?.interventionTypes?.[0]?.value || '';
problemComment.value = '';
showProblemSheet.value = true;
};
const submitProblem = async () => {
if (!problemType.value && !problemComment.value) {
emit('toast', 'Bitte Grund angeben', 'error');
return;
}
const typeText = tenantConfig.value?.interventionTypes?.find(t => t.value === problemType.value)?.text || problemType.value;
try {
const response = await fetch('/MobileApp/Workorder/Workorder/requestIntervention', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
workorderId: selectedWorkorder.value.id,
interventionType: typeText,
journalText: problemComment.value || typeText
})
});
const data = await response.json();
if (data.success) {
emit('toast', 'Problem gemeldet', 'success');
showProblemSheet.value = false;
closeDetail();
fetchWorkorders();
} else {
emit('toast', data.message || 'Fehler', 'error');
}
} catch (error) {
emit('toast', 'Fehler beim Melden', 'error');
}
};
// Complete workorder
const openCompleteSheet = () => {
showCompleteSheet.value = true;
};
const submitComplete = async () => {
try {
const response = await fetch('/MobileApp/Workorder/Workorder/completeWorkorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
workorderId: selectedWorkorder.value.id
})
});
const data = await response.json();
if (data.success) {
triggerHaptic('success');
emit('toast', 'Auftrag abgeschlossen', 'success');
showCompleteSheet.value = false;
closeDetail();
fetchWorkorders();
} else {
emit('toast', data.message || 'Fehler', 'error');
}
} catch (error) {
emit('toast', 'Fehler beim Abschließen', 'error');
}
};
const formatDate = (timestamp, format = 'date') => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
if (format === 'datetime') {
return date.toLocaleDateString('de-DE') + ' ' + date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleDateString('de-DE');
};
const getStatusColor = (color) => {
const colors = {
primary: 'bg-blue-500',
info: 'bg-sky-500',
warning: 'bg-amber-500',
danger: 'bg-red-500',
success: 'bg-green-500',
secondary: 'bg-slate-500',
orange: 'bg-orange-500',
purple: 'bg-purple-500',
muted: 'bg-gray-400'
};
return colors[color] || 'bg-gray-500';
};
// Get left border color for status indicator on cards
const getStatusBorderColor = (status) => {
const borderColors = {
'new': 'border-l-blue-500',
'assigned': 'border-l-sky-500',
'scheduled': 'border-l-amber-500',
'in_progress': 'border-l-amber-500',
'correction_requested': 'border-l-red-500',
'intervention_required': 'border-l-red-500',
'civil_engineering_required': 'border-l-orange-500',
'civil_engineering_completed': 'border-l-green-500',
'problem_solved': 'border-l-green-500',
'documented': 'border-l-green-500',
'completed': 'border-l-slate-500',
'charged': 'border-l-purple-500',
'cancelled': 'border-l-red-500',
'archived': 'border-l-gray-400'
};
return borderColors[status] || 'border-l-gray-400';
};
// Quick camera upload for checklist item
const quickUploadForItem = (item) => {
uploadDocType.value = item.type;
pendingChecklistUpload.value = item.type;
nextTick(() => {
if (fileInputRef.value) {
fileInputRef.value.click();
}
});
};
// Get next uncompleted checklist item
const getNextUncompletedItem = () => {
return checklist.value.find(item => !item.completed);
};
// Haptic feedback
const triggerHaptic = (type = 'success') => {
if ('vibrate' in navigator) {
if (type === 'success') {
navigator.vibrate([50, 50, 100]);
} else {
navigator.vibrate(50);
}
}
};
// Swipe handlers for list cards
const handleTouchStart = (e, wo) => {
swipeStartX.value = e.touches[0].clientX;
swipeCardId.value = wo.id;
};
const handleTouchMove = (e, wo) => {
if (swipeCardId.value !== wo.id) return;
const currentX = e.touches[0].clientX;
const diff = swipeStartX.value - currentX;
// Only allow left swipe, max 100px
if (diff > 0) {
swipeOffset.value = { ...swipeOffset.value, [wo.id]: Math.min(diff, 100) };
} else {
swipeOffset.value = { ...swipeOffset.value, [wo.id]: 0 };
}
};
const handleTouchEnd = (e, wo) => {
if (swipeCardId.value !== wo.id) return;
const offset = swipeOffset.value[wo.id] || 0;
// If swiped more than 60px, trigger navigation
if (offset > 60 && wo.customerAddress) {
triggerHaptic('light');
const address = encodeURIComponent(wo.customerAddress);
window.open(`https://maps.google.com/maps?q=${address}`, '_blank');
}
// Reset with animation
swipeOffset.value = { ...swipeOffset.value, [wo.id]: 0 };
swipeCardId.value = null;
};
const getSwipeStyle = (woId) => {
const offset = swipeOffset.value[woId] || 0;
return {
transform: `translateX(-${offset}px)`,
transition: swipeCardId.value === woId ? 'none' : 'transform 0.3s ease-out'
};
};
// Open navigation directly
const openNavigation = () => {
if (selectedWorkorder.value?.customer) {
const c = selectedWorkorder.value.customer;
const address = encodeURIComponent(`${c.street}, ${c.zip} ${c.city}`);
window.open(`https://maps.google.com/maps?q=${address}`, '_blank');
}
};
// Call customer directly
const callCustomer = () => {
if (selectedWorkorder.value?.customer?.phone) {
window.location.href = `tel:${selectedWorkorder.value.customer.phone}`;
}
};
// Smart complete - show confirmation sheet
const handleComplete = () => {
if (isChecklistComplete.value) {
showCompleteSheet.value = true;
}
// Button is disabled when not complete, so this won't be called
};
// Initialize
onMounted(() => {
fetchWorkorders();
});
return {
// State
isLoading,
workorders,
searchTerm,
selectedFcp,
showFcpFilter,
fcpOptions,
filteredWorkorders,
selectedWorkorder,
isDetailLoading,
documentation,
tenantConfig,
checklist,
expandedCards,
isEditingNotes,
tempNotes,
newJournalText,
cableDataForm,
showDocUploadSheet,
showProblemSheet,
showCompleteSheet,
uploadDocType,
isUploading,
fileInputRef,
problemType,
problemComment,
checklistProgress,
isChecklistComplete,
incompleteItems,
googleMapsLink,
// Methods
fetchWorkorders,
openDetail,
closeDetail,
toggleCard,
startEditNotes,
cancelEditNotes,
saveNotes,
addJournalEntry,
saveCableData,
openDocUpload,
triggerFileInput,
handleFileSelect,
openProblemSheet,
submitProblem,
openCompleteSheet,
submitComplete,
formatDate,
getStatusColor,
getStatusBorderColor,
quickUploadForItem,
openNavigation,
callCustomer,
handleComplete,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
getSwipeStyle,
swipeOffset
};
},
template: `
<div class="h-full flex flex-col">
<!-- LIST VIEW -->
<template v-if="!selectedWorkorder">
<!-- Search & Filter Bar -->
<div class="p-3 space-y-2 flex-shrink-0">
<div class="flex gap-2">
<!-- Search Input -->
<div class="flex-1 relative">
<svg xmlns="http://www.w3.org/2000/svg" class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" 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>
<input
v-model="searchTerm"
type="text"
placeholder="Suchen..."
class="w-full pl-10 pr-4 py-2.5 bg-white dark:bg-slate-800 rounded-xl text-slate-800 dark:text-white placeholder-slate-400 border border-slate-200 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-primary"
>
</div>
<!-- FCP Filter Button -->
<button
@click="showFcpFilter = !showFcpFilter"
class="px-4 py-2.5 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 flex items-center"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
<span class="hidden sm:inline">FCP</span>
</button>
</div>
<!-- FCP Dropdown -->
<div v-if="showFcpFilter" class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-2 max-h-48 overflow-y-auto">
<button
v-for="opt in fcpOptions"
:key="opt.value"
@click="selectedFcp = opt.value; showFcpFilter = false"
:class="[
'w-full text-left px-3 py-2 rounded-lg transition',
selectedFcp === opt.value
? 'bg-primary/10 text-primary font-medium'
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700'
]"
>
{{ opt.text }}
</button>
</div>
</div>
<!-- Workorder List -->
<div class="flex-1 overflow-y-auto px-3 pb-3">
<!-- Loading -->
<div v-if="isLoading" class="space-y-3">
<div v-for="i in 4" :key="i" class="bg-white dark:bg-slate-800 p-4 rounded-xl animate-pulse">
<div class="flex justify-between">
<div class="flex-1">
<div class="h-5 bg-slate-200 dark:bg-slate-700 rounded w-3/4 mb-2"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-full mb-2"></div>
<div class="h-3 bg-slate-200 dark:bg-slate-700 rounded w-1/2"></div>
</div>
<div class="ml-3 text-right">
<div class="h-5 w-20 bg-slate-200 dark:bg-slate-700 rounded-full mb-2"></div>
<div class="h-4 w-16 bg-slate-200 dark:bg-slate-700 rounded"></div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else-if="filteredWorkorders.length === 0" class="text-center py-12">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-slate-300 dark:text-slate-600 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
<p class="text-slate-500 dark:text-slate-400">Keine Aufträge gefunden</p>
</div>
<!-- Workorder Cards -->
<div v-else class="space-y-3">
<div
v-for="wo in filteredWorkorders"
:key="wo.id"
class="relative overflow-hidden rounded-xl"
>
<!-- Background action (revealed on swipe) -->
<div class="absolute inset-y-0 right-0 w-24 bg-primary flex items-center justify-center rounded-r-xl">
<div class="text-white text-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span class="text-xs font-medium mt-1">Navigation</span>
</div>
</div>
<!-- Card content (slides on swipe) -->
<div
@click="openDetail(wo)"
@touchstart="handleTouchStart($event, wo)"
@touchmove="handleTouchMove($event, wo)"
@touchend="handleTouchEnd($event, wo)"
:style="getSwipeStyle(wo.id)"
:class="[
'relative w-full bg-white dark:bg-slate-800 p-4 rounded-xl shadow-sm text-left cursor-pointer',
'border-l-4',
getStatusBorderColor(wo.status)
]"
>
<div class="flex justify-between items-start">
<div class="flex-1 min-w-0 pr-3">
<div class="flex items-center gap-2 mb-1">
<span v-if="wo.oaid" class="font-bold text-primary dark:text-sky-400">{{ wo.oaid }}</span>
<span v-else class="font-bold text-primary dark:text-sky-400">#{{ wo.id }}</span>
<span v-if="wo.fcpName" class="text-xs bg-slate-100 dark:bg-slate-700 px-1.5 py-0.5 rounded text-slate-600 dark:text-slate-400">{{ wo.fcpName }}</span>
</div>
<p class="font-medium text-slate-800 dark:text-white truncate">{{ wo.customerName || 'Unbekannt' }}</p>
<p class="text-sm text-slate-500 dark:text-slate-400 truncate">{{ wo.customerAddress }}</p>
</div>
<div class="text-right flex-shrink-0">
<span :class="[getStatusColor(wo.statusColor), 'inline-block px-2 py-0.5 rounded-full text-xs font-medium text-white']">
{{ wo.statusText }}
</span>
<p v-if="wo.appointmentFormatted" class="text-sm font-medium text-slate-600 dark:text-slate-300 mt-1">
{{ wo.appointmentFormatted }}
</p>
<p v-if="wo.deadlineFormatted" class="text-xs text-red-500 mt-0.5">
Frist: {{ wo.deadlineFormatted }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- DETAIL VIEW -->
<template v-else>
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 bg-white dark:bg-slate-800 border-b border-slate-100 dark:border-slate-700 flex-shrink-0">
<button @click="closeDetail" class="flex items-center text-slate-600 dark:text-slate-300">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Zurück
</button>
<div class="flex items-center gap-2">
<span v-if="selectedWorkorder.oaid" class="font-bold text-primary dark:text-sky-400">{{ selectedWorkorder.oaid }}</span>
<span v-else class="font-bold text-primary dark:text-sky-400">#{{ selectedWorkorder.id }}</span>
<span v-if="selectedWorkorder.fcpName" class="text-xs bg-slate-100 dark:bg-slate-700 px-1.5 py-0.5 rounded text-slate-600 dark:text-slate-400">{{ selectedWorkorder.fcpName }}</span>
</div>
<span :class="[getStatusColor(selectedWorkorder.statusColor), 'px-2 py-0.5 rounded-full text-xs font-medium text-white']">
{{ selectedWorkorder.statusText }}
</span>
</div>
<!-- Detail Content -->
<div class="flex-1 overflow-y-auto p-3 pb-24 space-y-3">
<!-- Loading -->
<div v-if="isDetailLoading" class="space-y-3 animate-pulse">
<div class="bg-white dark:bg-slate-800 p-4 rounded-xl">
<div class="h-5 bg-slate-200 dark:bg-slate-700 rounded w-1/2 mb-3"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-full mb-2"></div>
<div class="h-4 bg-slate-200 dark:bg-slate-700 rounded w-3/4"></div>
</div>
</div>
<template v-else>
<!-- Customer Card (Expanded by default) -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<button
@click="toggleCard('customer')"
class="w-full flex items-center justify-between p-4 text-left"
>
<div class="flex items-center">
<div class="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<span class="font-semibold text-slate-800 dark:text-white">Kunde</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" :class="['h-5 w-5 text-slate-400 transition-transform', expandedCards.customer ? 'rotate-180' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-if="expandedCards.customer && selectedWorkorder.customer" class="px-4 pb-4 space-y-3">
<p class="font-medium text-slate-800 dark:text-white text-lg">{{ selectedWorkorder.customer.name }}</p>
<p class="text-slate-600 dark:text-slate-400">
{{ selectedWorkorder.customer.street }}, {{ selectedWorkorder.customer.zip }} {{ selectedWorkorder.customer.city }}
</p>
<!-- Prominent Action Buttons -->
<div class="flex gap-3 pt-2">
<button
@click="openNavigation"
class="flex-1 py-3 bg-primary text-white rounded-xl font-medium flex items-center justify-center gap-2 active:scale-95 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Navigation
</button>
<button
v-if="selectedWorkorder.customer.phone"
@click="callCustomer"
class="flex-1 py-3 bg-green-500 text-white rounded-xl font-medium flex items-center justify-center gap-2 active:scale-95 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
Anrufen
</button>
</div>
<!-- Email (smaller, secondary) -->
<div v-if="selectedWorkorder.customer.email" class="flex items-center pt-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<a :href="'mailto:' + selectedWorkorder.customer.email" class="text-sm text-primary dark:text-sky-400 truncate">{{ selectedWorkorder.customer.email }}</a>
</div>
</div>
</div>
<!-- Checklist Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<button
@click="toggleCard('checklist')"
class="w-full flex items-center justify-between p-4 text-left"
>
<div class="flex items-center">
<div class="w-8 h-8 bg-amber-500/10 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
</div>
<span class="font-semibold text-slate-800 dark:text-white">Checkliste</span>
<span :class="[
'ml-2 px-2 py-0.5 rounded-full text-xs',
checklistProgress.completed === checklistProgress.total && checklistProgress.total > 0
? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
: 'bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300'
]">
{{ checklistProgress.text }}
</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" :class="['h-5 w-5 text-slate-400 transition-transform', expandedCards.checklist ? 'rotate-180' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-if="expandedCards.checklist" class="px-4 pb-4">
<div v-if="checklist.length === 0" class="text-center py-4 text-slate-500 dark:text-slate-400">
Keine Checkliste vorhanden
</div>
<ul v-else class="space-y-2">
<li v-for="item in checklist" :key="item.type" class="flex items-center justify-between">
<div class="flex items-center flex-1 min-w-0">
<svg v-if="item.completed" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" 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 text-slate-400 mr-2 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<circle cx="12" cy="12" r="10" stroke-width="2" />
</svg>
<span :class="[item.completed ? 'text-slate-500 dark:text-slate-400 line-through' : 'text-slate-800 dark:text-white', 'truncate']">
{{ item.text }}
</span>
</div>
<!-- Quick Camera Button for uncompleted items -->
<button
v-if="!item.completed"
@click="quickUploadForItem(item)"
class="ml-2 p-2 bg-primary/10 hover:bg-primary/20 rounded-lg text-primary active:scale-95 transition flex-shrink-0"
:disabled="isUploading"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<!-- Check mark for completed items -->
<div v-else class="ml-2 p-2 text-green-500 flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
</div>
</li>
</ul>
</div>
</div>
<!-- Documentation Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<button
@click="toggleCard('documentation')"
class="w-full flex items-center justify-between p-4 text-left"
>
<div class="flex items-center">
<div class="w-8 h-8 bg-green-500/10 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<span class="font-semibold text-slate-800 dark:text-white">Dokumentation</span>
<span class="ml-2 px-2 py-0.5 bg-slate-100 dark:bg-slate-700 rounded-full text-xs text-slate-600 dark:text-slate-300">
{{ documentation.docs.length }}
</span>
</div>
<div class="flex items-center">
<button
@click.stop="openDocUpload"
class="mr-2 p-1.5 bg-primary/10 rounded-lg text-primary"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
<svg xmlns="http://www.w3.org/2000/svg" :class="['h-5 w-5 text-slate-400 transition-transform', expandedCards.documentation ? 'rotate-180' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
<div v-if="expandedCards.documentation" class="px-4 pb-4">
<div v-if="documentation.docs.length === 0" class="text-center py-4 text-slate-500 dark:text-slate-400">
Keine Dokumente vorhanden
</div>
<div v-else class="space-y-2">
<a
v-for="doc in documentation.docs"
:key="doc.id"
:href="doc.previewUrl"
target="_blank"
class="flex items-center p-2 bg-slate-50 dark:bg-slate-700/50 rounded-lg"
>
<div class="w-10 h-10 bg-slate-200 dark:bg-slate-600 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-500 dark:text-slate-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-slate-800 dark:text-white truncate">{{ doc.fileName }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ doc.createFormatted }} - {{ doc.userName }}</p>
</div>
</a>
</div>
</div>
</div>
<!-- Notes Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<button
@click="toggleCard('notes')"
class="w-full flex items-center justify-between p-4 text-left"
>
<div class="flex items-center">
<div class="w-8 h-8 bg-purple-500/10 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<span class="font-semibold text-slate-800 dark:text-white">Notizen</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" :class="['h-5 w-5 text-slate-400 transition-transform', expandedCards.notes ? 'rotate-180' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-if="expandedCards.notes" class="px-4 pb-4">
<div v-if="isEditingNotes">
<textarea
v-model="tempNotes"
rows="4"
class="w-full p-3 bg-slate-50 dark:bg-slate-700 rounded-lg text-slate-800 dark:text-white border-0 focus:ring-2 focus:ring-primary"
placeholder="Notiz eingeben..."
></textarea>
<div class="flex justify-end gap-2 mt-2">
<button @click="cancelEditNotes" class="px-4 py-2 text-sm text-slate-600 dark:text-slate-300">
Abbrechen
</button>
<button @click="saveNotes" class="px-4 py-2 bg-primary text-white text-sm rounded-lg">
Speichern
</button>
</div>
</div>
<div v-else>
<p class="text-slate-700 dark:text-slate-300 whitespace-pre-wrap">{{ selectedWorkorder.additionalInfo || 'Keine Notiz vorhanden.' }}</p>
<button @click="startEditNotes" class="mt-3 text-sm text-primary dark:text-sky-400 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
Bearbeiten
</button>
</div>
</div>
</div>
<!-- Journal Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<button
@click="toggleCard('journal')"
class="w-full flex items-center justify-between p-4 text-left"
>
<div class="flex items-center">
<div class="w-8 h-8 bg-sky-500/10 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-sky-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<span class="font-semibold text-slate-800 dark:text-white">Journal</span>
<span class="ml-2 px-2 py-0.5 bg-slate-100 dark:bg-slate-700 rounded-full text-xs text-slate-600 dark:text-slate-300">
{{ documentation.journals.length }}
</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" :class="['h-5 w-5 text-slate-400 transition-transform', expandedCards.journal ? 'rotate-180' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-if="expandedCards.journal" class="px-4 pb-4 space-y-3">
<!-- Add Entry -->
<div class="flex gap-2">
<input
v-model="newJournalText"
type="text"
placeholder="Neuer Eintrag..."
class="flex-1 px-3 py-2 bg-slate-50 dark:bg-slate-700 rounded-lg text-slate-800 dark:text-white border-0"
@keyup.enter="addJournalEntry"
>
<button @click="addJournalEntry" class="px-4 py-2 bg-primary text-white rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</button>
</div>
<!-- Entries -->
<div v-if="documentation.journals.length === 0" class="text-center py-2 text-slate-500 dark:text-slate-400 text-sm">
Keine Einträge
</div>
<div v-else class="space-y-2 max-h-48 overflow-y-auto">
<div v-for="entry in documentation.journals" :key="entry.id" class="p-2 bg-slate-50 dark:bg-slate-700/50 rounded-lg">
<p class="text-sm text-slate-800 dark:text-white whitespace-pre-wrap">{{ entry.text }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">{{ entry.createFormatted }} - {{ entry.createByName }}</p>
</div>
</div>
</div>
</div>
<!-- Cable Data Card (only if required) -->
<div v-if="tenantConfig && (tenantConfig.requireCableLength || tenantConfig.requireCableType)" class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<button
@click="toggleCard('cableData')"
class="w-full flex items-center justify-between p-4 text-left"
>
<div class="flex items-center">
<div class="w-8 h-8 bg-orange-500/10 rounded-lg flex items-center justify-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-orange-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
</svg>
</div>
<span class="font-semibold text-slate-800 dark:text-white">Kabeldaten</span>
</div>
<svg xmlns="http://www.w3.org/2000/svg" :class="['h-5 w-5 text-slate-400 transition-transform', expandedCards.cableData ? 'rotate-180' : '']" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div v-if="expandedCards.cableData" class="px-4 pb-4 space-y-3">
<div v-if="tenantConfig.requireCableLength">
<label class="text-sm text-slate-600 dark:text-slate-400 mb-1 block">Kabellänge (m)</label>
<input
v-model="cableDataForm.cableLength"
type="text"
placeholder="z.B. 20"
class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 rounded-lg text-slate-800 dark:text-white border-0"
>
</div>
<div v-if="tenantConfig.requireCableType">
<label class="text-sm text-slate-600 dark:text-slate-400 mb-1 block">Kabeltyp</label>
<input
v-model="cableDataForm.cableType"
type="text"
placeholder="z.B. LWL 4F"
class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 rounded-lg text-slate-800 dark:text-white border-0"
>
</div>
<button @click="saveCableData" class="w-full py-2 bg-primary text-white rounded-lg text-sm font-medium">
Speichern
</button>
</div>
</div>
</template>
</div>
<!-- Bottom Action Bar -->
<div class="absolute bottom-0 left-0 right-0 p-3 bg-white dark:bg-slate-800 border-t border-slate-100 dark:border-slate-700 flex gap-3">
<button
@click="openProblemSheet"
class="flex-1 py-3 bg-amber-500 text-white rounded-xl font-medium flex items-center justify-center active:scale-95 transition"
>
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
Problem
</button>
<button
@click="handleComplete"
:disabled="!isChecklistComplete"
:class="[
'flex-1 py-3 rounded-xl font-medium flex items-center justify-center active:scale-95 transition',
isChecklistComplete
? 'bg-green-500 text-white'
: 'bg-slate-300 dark:bg-slate-600 text-slate-500 dark:text-slate-400 cursor-not-allowed'
]"
>
<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="M5 13l4 4L19 7" />
</svg>
<span v-if="checklist.length > 0">
Abschließen ({{ checklistProgress.text }})
</span>
<span v-else>Abschließen</span>
</button>
</div>
</template>
<!-- BOTTOM SHEETS -->
<!-- Doc Upload Sheet -->
<teleport to="body">
<transition name="fade">
<div v-if="showDocUploadSheet" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" @click="showDocUploadSheet = false"></div>
<transition name="slide-up-sheet">
<div v-if="showDocUploadSheet" class="absolute bottom-0 left-0 right-0 bg-white dark:bg-slate-800 rounded-t-2xl shadow-xl max-h-[70vh] overflow-hidden">
<div class="flex justify-center pt-2 pb-1">
<div class="w-10 h-1 bg-slate-300 dark:bg-slate-600 rounded-full"></div>
</div>
<div class="px-4 pb-3 border-b border-slate-100 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-800 dark:text-white text-center">Dokumentation hochladen</h3>
</div>
<div class="p-4 space-y-4">
<!-- Doc Type Selection -->
<div v-if="tenantConfig?.documentationTypes?.length">
<label class="text-sm text-slate-600 dark:text-slate-400 mb-2 block">Dokumenttyp</label>
<div class="space-y-2">
<button
v-for="type in tenantConfig.documentationTypes"
:key="type.value"
@click="uploadDocType = type.value"
:class="[
'w-full p-3 rounded-lg text-left transition',
uploadDocType === type.value
? 'bg-primary/10 border-2 border-primary text-primary'
: 'bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
]"
>
{{ type.text }}
</button>
</div>
</div>
<!-- Upload Buttons -->
<div class="space-y-2">
<input
ref="fileInputRef"
type="file"
accept="image/*,application/pdf"
multiple
class="hidden"
@change="handleFileSelect"
capture="environment"
>
<button
@click="triggerFileInput"
:disabled="isUploading"
class="w-full py-3 bg-primary text-white rounded-xl font-medium flex items-center justify-center disabled:opacity-50"
>
<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="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{{ isUploading ? 'Wird hochgeladen...' : 'Foto aufnehmen / auswählen' }}
</button>
</div>
</div>
<div class="h-6"></div>
</div>
</transition>
</div>
</transition>
</teleport>
<!-- Problem Sheet -->
<teleport to="body">
<transition name="fade">
<div v-if="showProblemSheet" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" @click="showProblemSheet = false"></div>
<transition name="slide-up-sheet">
<div v-if="showProblemSheet" class="absolute bottom-0 left-0 right-0 bg-white dark:bg-slate-800 rounded-t-2xl shadow-xl max-h-[80vh] overflow-hidden flex flex-col">
<div class="flex justify-center pt-2 pb-1 flex-shrink-0">
<div class="w-10 h-1 bg-slate-300 dark:bg-slate-600 rounded-full"></div>
</div>
<div class="px-4 pb-3 border-b border-slate-100 dark:border-slate-700 flex-shrink-0">
<h3 class="text-lg font-semibold text-slate-800 dark:text-white text-center">Problem melden</h3>
</div>
<div class="p-4 space-y-4 overflow-y-auto flex-1">
<!-- Problem Type Selection -->
<div v-if="tenantConfig?.interventionTypes?.length">
<label class="text-sm text-slate-600 dark:text-slate-400 mb-2 block">Grund</label>
<div class="space-y-2">
<button
v-for="type in tenantConfig.interventionTypes"
:key="type.value"
@click="problemType = type.value"
:class="[
'w-full p-3 rounded-lg text-left transition',
problemType === type.value
? 'bg-amber-500/10 border-2 border-amber-500 text-amber-600 dark:text-amber-400'
: 'bg-slate-50 dark:bg-slate-700 text-slate-700 dark:text-slate-300'
]"
>
{{ type.text }}
</button>
</div>
</div>
<!-- Comment -->
<div>
<label class="text-sm text-slate-600 dark:text-slate-400 mb-2 block">Kommentar (optional)</label>
<textarea
v-model="problemComment"
rows="3"
placeholder="Weitere Details..."
class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 rounded-lg text-slate-800 dark:text-white border-0"
></textarea>
</div>
<!-- Submit -->
<button
@click="submitProblem"
class="w-full py-3 bg-amber-500 text-white rounded-xl font-medium"
>
Problem melden
</button>
</div>
<div class="h-6 flex-shrink-0"></div>
</div>
</transition>
</div>
</transition>
</teleport>
<!-- Complete Sheet (only shown when complete - for confirmation) -->
<teleport to="body">
<transition name="fade">
<div v-if="showCompleteSheet" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" @click="showCompleteSheet = false"></div>
<transition name="slide-up-sheet">
<div v-if="showCompleteSheet" class="absolute bottom-0 left-0 right-0 bg-white dark:bg-slate-800 rounded-t-2xl shadow-xl max-h-[70vh] overflow-hidden">
<div class="flex justify-center pt-2 pb-1">
<div class="w-10 h-1 bg-slate-300 dark:bg-slate-600 rounded-full"></div>
</div>
<div class="px-4 pb-3 border-b border-slate-100 dark:border-slate-700">
<h3 class="text-lg font-semibold text-slate-800 dark:text-white text-center">Auftrag abschließen</h3>
</div>
<div class="p-4 space-y-4">
<!-- Success message -->
<div class="p-4 bg-green-50 dark:bg-green-900/20 rounded-xl">
<div class="flex items-center justify-center mb-3">
<div class="w-12 h-12 bg-green-100 dark:bg-green-900/50 rounded-full flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<p class="text-center text-green-700 dark:text-green-400 font-medium">Alle Anforderungen erfüllt!</p>
<p class="text-center text-green-600 dark:text-green-500 text-sm mt-1">Der Auftrag kann abgeschlossen werden.</p>
</div>
<!-- Buttons -->
<div class="flex gap-3">
<button
@click="showCompleteSheet = false"
class="flex-1 py-3 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-xl font-medium"
>
Abbrechen
</button>
<button
@click="submitComplete"
class="flex-1 py-3 bg-green-500 text-white rounded-xl font-medium flex items-center justify-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Abschließen
</button>
</div>
</div>
<div class="h-6"></div>
</div>
</transition>
</div>
</transition>
</teleport>
</div>
`
};