Merge branch 'XinonMobile/improvement' into 'master'

improved some bugs

See merge request fronk/thetool!2026
This commit is contained in:
Luca Haid
2026-01-18 12:25:32 +00:00
5 changed files with 158 additions and 77 deletions

View File

@@ -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

View File

@@ -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 = {
<WorkorderModule
v-else-if="currentModule?.toLowerCase() === 'workorder'"
ref="workorderRef"
:user="user"
@navigate="navigate"
@toast="showToast"
@detail-open="handleWorkorderDetailOpen"
@detail-close="handleWorkorderDetailClose"
/>
</main>
</div>

View File

@@ -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);
}
};

View File

@@ -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 {
<!-- Card content (slides on swipe) -->
<div
@click="openDetail(wo)"
@click="handleCardClick(wo)"
@touchstart="handleTouchStart($event, wo)"
@touchmove="handleTouchMove($event, wo)"
@touchend="handleTouchEnd($event, wo)"
:style="getSwipeStyle(wo.id)"
:style="{ ...getSwipeStyle(wo.id), touchAction: 'pan-y' }"
:class="[
'relative w-full bg-white dark:bg-slate-800 p-4 rounded-xl shadow-sm text-left cursor-pointer',
'relative w-full bg-white dark:bg-slate-800 p-4 rounded-xl shadow-sm text-left cursor-pointer card-contrast',
'border-l-4',
getStatusBorderColor(wo.status)
]"
@@ -808,14 +825,7 @@ export default {
<!-- 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>
@@ -839,7 +849,7 @@ export default {
<template v-else>
<!-- Customer Card (Expanded by default) -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden card-contrast">
<button
@click="toggleCard('customer')"
class="w-full flex items-center justify-between p-4 text-left"
@@ -897,7 +907,7 @@ export default {
</div>
<!-- Checklist Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden card-contrast">
<button
@click="toggleCard('checklist')"
class="w-full flex items-center justify-between p-4 text-left"
@@ -963,7 +973,7 @@ export default {
</div>
<!-- Documentation Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden card-contrast">
<button
@click="toggleCard('documentation')"
class="w-full flex items-center justify-between p-4 text-left"
@@ -1020,7 +1030,7 @@ export default {
</div>
<!-- Notes Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden card-contrast">
<button
@click="toggleCard('notes')"
class="w-full flex items-center justify-between p-4 text-left"
@@ -1044,6 +1054,7 @@ export default {
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..."
@focus="scrollIntoViewOnFocus"
></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">
@@ -1067,7 +1078,7 @@ export default {
</div>
<!-- Journal Card -->
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden">
<div class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden card-contrast">
<button
@click="toggleCard('journal')"
class="w-full flex items-center justify-between p-4 text-left"
@@ -1096,6 +1107,7 @@ export default {
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"
@focus="scrollIntoViewOnFocus"
>
<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">
@@ -1117,7 +1129,7 @@ export default {
</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">
<div v-if="tenantConfig && (tenantConfig.requireCableLength || tenantConfig.requireCableType)" class="bg-white dark:bg-slate-800 rounded-xl overflow-hidden card-contrast">
<button
@click="toggleCard('cableData')"
class="w-full flex items-center justify-between p-4 text-left"
@@ -1162,7 +1174,7 @@ export default {
</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">
<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" style="padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));">
<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"
@@ -1230,15 +1242,6 @@ export default {
</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"
@@ -1300,6 +1303,7 @@ export default {
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"
@focus="scrollIntoViewOnFocus"
></textarea>
</div>
<!-- Submit -->
@@ -1368,6 +1372,16 @@ export default {
</div>
</transition>
</teleport>
<input
ref="fileInputRef"
type="file"
accept="image/*,application/pdf"
multiple
class="hidden"
@change="handleFileSelect"
capture="environment"
>
</div>
`
};

View File

@@ -336,3 +336,12 @@ textarea:focus-visible {
font-variant-numeric: tabular-nums;
letter-spacing: 0.05em;
}
.card-contrast {
border: 1px solid rgba(148, 163, 184, 0.2);
}
.dark .card-contrast {
border-color: transparent;
}