From e99b0d4658ea04370a4c56162aeeb1bd4a559c63 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Sun, 18 Jan 2026 13:25:14 +0100 Subject: [PATCH] improved some bugs --- .../ShippingNote/ShippingNoteHandler.php | 60 +++++++++++++ public/mobile/app.js | 26 +++++- .../lager/shippingnote/ShippingNoteForm.js | 50 +++-------- .../modules/workorder/WorkorderModule.js | 90 +++++++++++-------- public/mobile/shared/base.css | 9 ++ 5 files changed, 158 insertions(+), 77 deletions(-) diff --git a/application/MobileApp/Modules/Lager/ShippingNote/ShippingNoteHandler.php b/application/MobileApp/Modules/Lager/ShippingNote/ShippingNoteHandler.php index 2e7d004e6..4464462e9 100644 --- a/application/MobileApp/Modules/Lager/ShippingNote/ShippingNoteHandler.php +++ b/application/MobileApp/Modules/Lager/ShippingNote/ShippingNoteHandler.php @@ -16,6 +16,66 @@ class ShippingNoteHandler extends MobileAppBaseHandler { const OFFICE_LAT = 46.99552810791587; const OFFICE_LNG = 15.7751923956463; + public function initializeAction() { + $db = $this->db(); + $userId = $this->user->id; + + $userCar = null; + $sql = "SELECT id, number_plate, brand, model + FROM TimerecordingCar + WHERE user_id = {$userId} + AND (retired IS NULL OR retired = 0) + LIMIT 1"; + $result = $db->query($sql); + if ($result && $row = $result->fetch_assoc()) { + $carName = trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? '')); + if (!$carName) $carName = $row['number_plate']; + $userCar = [ + 'id' => intval($row['id']), + 'name' => $carName, + 'plate' => $row['number_plate'], + ]; + } + + $allCars = []; + $sql = "SELECT id, number_plate, brand, model + FROM TimerecordingCar + WHERE (retired IS NULL OR retired = 0) + ORDER BY brand, model ASC"; + $result = $db->query($sql); + while ($row = $result->fetch_assoc()) { + $carName = trim(($row['brand'] ?? '') . ' ' . ($row['model'] ?? '')); + if (!$carName) $carName = $row['number_plate']; + $allCars[] = [ + 'id' => intval($row['id']), + 'name' => $carName, + 'plate' => $row['number_plate'], + ]; + } + + $hourTypes = [ + ['id' => '', 'name' => 'Normal'], + ['id' => '50', 'name' => '+50%'], + ['id' => '100', 'name' => '+100%'], + ['id' => 'regie', 'name' => 'Regie'], + ]; + + $currentUser = [ + 'id' => $this->user->id, + 'name' => $this->user->name, + 'firstname' => $this->user->firstname ?? '', + 'lastname' => $this->user->lastname ?? '', + ]; + + self::returnJson([ + 'success' => true, + 'userCar' => $userCar, + 'allCars' => $allCars, + 'hourTypes' => $hourTypes, + 'currentUser' => $currentUser, + ]); + } + /** * Get customer by GPS location (nearest within radius) * GET /MobileApp/Lager/ShippingNote/getCustomerByLocation?lat=X&lng=Y diff --git a/public/mobile/app.js b/public/mobile/app.js index a71b22eac..1373cc5d6 100644 --- a/public/mobile/app.js +++ b/public/mobile/app.js @@ -51,7 +51,9 @@ const App = { const deferredInstallPrompt = ref(null); const isIOS = ref(/iPad|iPhone|iPod/.test(navigator.userAgent)); const isAndroid = ref(/Android/.test(navigator.userAgent)); - const canGoBack = computed(() => currentModule.value !== null); + const canGoBack = computed(() => currentModule.value !== null || workorderDetailOpen.value); + const workorderDetailOpen = ref(false); + const workorderRef = ref(null); const applyTheme = () => { const isDark = localStorage.theme === 'dark' || @@ -157,6 +159,10 @@ const App = { }; const goBack = () => { + if (workorderDetailOpen.value && workorderRef.value?.closeDetail) { + workorderRef.value.closeDetail(); + return; + } if (currentSubmodule.value) { navigate(currentModule.value, null); } else if (currentModule.value) { @@ -164,7 +170,19 @@ const App = { } }; + const handleWorkorderDetailOpen = (workorderId) => { + workorderDetailOpen.value = true; + }; + + const handleWorkorderDetailClose = () => { + workorderDetailOpen.value = false; + }; + const handlePopstate = (event) => { + if (workorderDetailOpen.value && workorderRef.value?.closeDetail) { + workorderRef.value.closeDetail(); + return; + } if (event.state) { currentModule.value = event.state.module; currentSubmodule.value = event.state.submodule; @@ -301,6 +319,9 @@ const App = { showContinuePrompt, continueLastWorkflow, dismissContinuePrompt, + workorderRef, + handleWorkorderDetailOpen, + handleWorkorderDetailClose, }; }, @@ -500,9 +521,12 @@ const App = { diff --git a/public/mobile/modules/lager/shippingnote/ShippingNoteForm.js b/public/mobile/modules/lager/shippingnote/ShippingNoteForm.js index de39a3b91..ad884ca5f 100644 --- a/public/mobile/modules/lager/shippingnote/ShippingNoteForm.js +++ b/public/mobile/modules/lager/shippingnote/ShippingNoteForm.js @@ -169,53 +169,27 @@ export default { return hoursEntries.value.map(e => e.userId).filter(id => id !== null); }); - // Initialize onMounted(async () => { - await Promise.all([ - loadUserCar(), - loadAllCars(), - loadHourTypes() - ]); + await loadInitializationData(); detectGPS(); }); - // Load user's assigned car - const loadUserCar = async () => { + const loadInitializationData = async () => { try { - const data = await shippingNoteApi.get(`getUserCar?userId=${props.user?.id}`); - if (data.success && data.car) { - userCar.value = data.car; - if (hoursEntries.value[0]) { - hoursEntries.value[0].carId = data.car.id; - hoursEntries.value[0].carName = data.car.name; + const data = await shippingNoteApi.get('initialize'); + if (data.success) { + if (data.userCar) { + userCar.value = data.userCar; + if (hoursEntries.value[0]) { + hoursEntries.value[0].carId = data.userCar.id; + hoursEntries.value[0].carName = data.userCar.name; + } } - } - } catch (e) { - console.error('Failed to load user car:', e); - } - }; - - // Load all available cars - const loadAllCars = async () => { - try { - const data = await shippingNoteApi.get('getAllCars'); - if (data.success) { - allCars.value = data.cars || []; - } - } catch (e) { - console.error('Failed to load cars:', e); - } - }; - - // Load hour types - const loadHourTypes = async () => { - try { - const data = await shippingNoteApi.get('getHourTypes'); - if (data.success) { + allCars.value = data.allCars || []; hourTypes.value = data.hourTypes || []; } } catch (e) { - console.error('Failed to load hour types:', e); + console.error('Failed to load initialization data:', e); } }; diff --git a/public/mobile/modules/workorder/WorkorderModule.js b/public/mobile/modules/workorder/WorkorderModule.js index cdd16a81b..1324c1da9 100644 --- a/public/mobile/modules/workorder/WorkorderModule.js +++ b/public/mobile/modules/workorder/WorkorderModule.js @@ -7,7 +7,7 @@ export default { name: 'WorkorderModule', - emits: ['navigate', 'toast'], + emits: ['navigate', 'toast', 'detail-open', 'detail-close'], props: { user: Object, submodule: String @@ -63,10 +63,10 @@ export default { const problemType = ref(''); const problemComment = ref(''); - // Swipe state for list cards const swipeStartX = ref(0); const swipeCardId = ref(null); - const swipeOffset = ref({}); // { [workorderId]: offsetX } + const swipeOffset = ref({}); + const swipeTriggered = ref(false); // ===================== // COMPUTED @@ -136,12 +136,10 @@ export default { 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; + const allCompleted = checklist.value.every(c => c.completed); + if (!allCompleted) return false; } - // Check cable data if required if (tenantConfig.value?.requireCableLength && !cableDataForm.value.cableLength?.trim()) { return false; } @@ -201,6 +199,7 @@ export default { selectedWorkorder.value = workorder; isDetailLoading.value = true; expandedCards.value = { customer: true, checklist: true, documentation: false, notes: false, journal: false, cableData: false }; + emit('detail-open', workorder.id); try { // Fetch all workorder details in a single request @@ -233,6 +232,7 @@ export default { tenantConfig.value = null; checklist.value = []; isEditingNotes.value = false; + emit('detail-close'); }; const toggleCard = (cardId) => { @@ -535,10 +535,16 @@ export default { } }; - // Swipe handlers for list cards + const scrollIntoViewOnFocus = (e) => { + setTimeout(() => { + e.target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 300); + }; + const handleTouchStart = (e, wo) => { swipeStartX.value = e.touches[0].clientX; swipeCardId.value = wo.id; + swipeTriggered.value = false; }; const handleTouchMove = (e, wo) => { @@ -546,9 +552,11 @@ export default { 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) }; + if (diff > 10) { + swipeTriggered.value = true; + } } else { swipeOffset.value = { ...swipeOffset.value, [wo.id]: 0 }; } @@ -558,18 +566,25 @@ export default { 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) { + swipeTriggered.value = true; 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 handleCardClick = (wo) => { + if (swipeTriggered.value) { + swipeTriggered.value = false; + return; + } + openDetail(wo); + }; + const getSwipeStyle = (woId) => { const offset = swipeOffset.value[woId] || 0; return { @@ -666,8 +681,10 @@ export default { handleTouchStart, handleTouchMove, handleTouchEnd, + handleCardClick, getSwipeStyle, - swipeOffset + swipeOffset, + scrollIntoViewOnFocus }; }, @@ -767,13 +784,13 @@ export default {