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 {
-
-
{{ selectedWorkorder.oaid }}
#{{ selectedWorkorder.id }}
@@ -839,7 +849,7 @@ export default {
-
+