/** * ShippingNote Form Component - Redesigned * * Features: * - GPS-based customer auto-detection with smart collapsed card * - Multi-employee support with individual hours * - Custom date picker * - Reorganized field layout by importance */ import { shippingNoteApi } from '/mobile/modules/lager/shippingnote/ShippingNoteModule.js'; import DatePicker from '/mobile/modules/lager/shippingnote/DatePicker.js'; import EmployeeSelector from '/mobile/modules/lager/shippingnote/EmployeeSelector.js'; export default { name: 'ShippingNoteForm', emits: ['created', 'createAndSign', 'toast'], props: { user: Object }, components: { DatePicker, EmployeeSelector }, setup(props, { emit }) { const { ref, reactive, computed, onMounted, watch } = Vue; // GPS detection state const gpsState = ref('detecting'); // detecting | customer_found | no_customer | manual | error const gpsPosition = ref(null); const detectedCustomer = ref(null); const gpsAddress = ref(null); const gpsDistance = ref(null); // Customer card state - collapsed when customer found const customerCardExpanded = ref(false); // Form data const form = reactive({ customerId: null, customerName: '', billingAddressId: null, deliveryAddressName: '', deliveryAddressLine: '', deliveryAddressPLZ: '', deliveryAddressCity: '', deliveryAddressEMail: '', note: '', type: 'V' }); // Hours entries - supports multiple employees const hoursEntries = ref([createNewHoursEntry()]); // Active entry index for date picker const activeEntryIndex = ref(0); const showDatePicker = ref(false); const showEmployeeSelector = ref(false); // Positions const positions = ref([]); // UI state const loading = ref(false); const showCustomerSearch = ref(false); const customerSearchQuery = ref(''); const customerSearchResults = ref([]); const customerSearchLoading = ref(false); const showArticleSearch = ref(false); const articleSearchQuery = ref(''); const articleSearchResults = ref([]); const articleSearchLoading = ref(false); const showCarSelect = ref(false); const carSelectEntryIndex = ref(0); const allCars = ref([]); const showHourTypeSelect = ref(false); const hourTypeSelectEntryIndex = ref(0); const hourTypes = ref([]); const showPositionsSection = ref(false); // User's car const userCar = ref(null); // Quick work type chips const quickWorkTypes = ['Spleißen', 'Jetten', 'Inbetriebnahme']; const selectedWorkType = ref(null); // Select quick work type const selectWorkType = (type) => { if (selectedWorkType.value === type) { // Deselect if already selected selectedWorkType.value = null; form.note = ''; } else { selectedWorkType.value = type; form.note = type; } navigator.vibrate?.([20]); }; // Watch for manual note changes to clear chip selection watch(() => form.note, (newVal) => { if (newVal && !quickWorkTypes.includes(newVal)) { selectedWorkType.value = null; } }); // Create new hours entry - default to 1h (most common) function createNewHoursEntry(userId = null, userName = '') { return { id: Date.now() + Math.random(), userId: userId || props.user?.id || null, userName: userName || props.user?.name || '', date: new Date().toISOString().split('T')[0], hourCount: 1, hourType: '', hourTypeName: 'Normal', carId: null, carName: '', kilometerCount: 0, comment: '', expanded: true }; } // Validation const isValid = computed(() => { return form.customerName.trim() !== '' && form.deliveryAddressLine.trim() !== '' && form.deliveryAddressCity.trim() !== '' && form.note.trim() !== ''; }); // Format distance for display const formatDistance = (distance) => { if (distance === null || distance === undefined || isNaN(distance)) { return ''; } return Math.round(distance) + 'm'; }; // Format date for display - German style const formatDateDisplay = (dateStr) => { if (!dateStr) return 'Datum wählen'; const date = new Date(dateStr); const weekDays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; const months = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']; const dayName = weekDays[date.getDay()]; const day = date.getDate(); const month = months[date.getMonth()]; const year = date.getFullYear(); return `${dayName}, ${day}. ${month} ${year}`; }; // Format hours display as "1h 15m" const formatHoursValue = (hours) => { const h = Math.floor(hours); const m = Math.round((hours - h) * 60); return { h, m }; }; // Get selected employee IDs (to exclude from selector) const selectedEmployeeIds = computed(() => { return hoursEntries.value.map(e => e.userId).filter(id => id !== null); }); // Initialize onMounted(async () => { await Promise.all([ loadUserCar(), loadAllCars(), loadHourTypes() ]); detectGPS(); }); // Load user's assigned car const loadUserCar = 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; } } } 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) { hourTypes.value = data.hourTypes || []; } } catch (e) { console.error('Failed to load hour types:', e); } }; // GPS Detection const detectGPS = () => { gpsState.value = 'detecting'; if (!navigator.geolocation) { gpsState.value = 'error'; return; } navigator.geolocation.getCurrentPosition( async (position) => { gpsPosition.value = { lat: position.coords.latitude, lng: position.coords.longitude }; await findNearbyCustomer(); await calculateDistance(); }, (error) => { console.error('GPS error:', error); gpsState.value = 'error'; }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 } ); }; // Find nearby customer const findNearbyCustomer = async () => { if (!gpsPosition.value) return; try { const data = await shippingNoteApi.get( `getCustomerByLocation?lat=${gpsPosition.value.lat}&lng=${gpsPosition.value.lng}` ); if (data.success && data.customer) { detectedCustomer.value = data.customer; gpsDistance.value = data.customer.distance; gpsState.value = 'customer_found'; fillFromCustomer(data.customer); customerCardExpanded.value = false; // Collapse card when customer found navigator.vibrate?.([100, 50, 100]); } else { gpsState.value = 'no_customer'; customerCardExpanded.value = true; // Expand card when no customer await reverseGeocode(); } } catch (e) { console.error('Customer detection failed:', e); gpsState.value = 'no_customer'; customerCardExpanded.value = true; await reverseGeocode(); } }; // Reverse geocode GPS position const reverseGeocode = async () => { if (!gpsPosition.value) return; try { const data = await shippingNoteApi.get( `reverseGeocode?lat=${gpsPosition.value.lat}&lng=${gpsPosition.value.lng}` ); if (data.success && data.address) { gpsAddress.value = data.address; form.deliveryAddressLine = data.address.street || ''; form.deliveryAddressPLZ = data.address.zip || ''; form.deliveryAddressCity = data.address.city || ''; } } catch (e) { console.error('Reverse geocode failed:', e); } }; // Calculate distance from office const calculateDistance = async () => { if (!gpsPosition.value) return; try { const data = await shippingNoteApi.get( `calculateDistance?lat=${gpsPosition.value.lat}&lng=${gpsPosition.value.lng}` ); if (data.success && data.distanceRoundTrip) { if (hoursEntries.value[0]) { hoursEntries.value[0].kilometerCount = Math.round(data.distanceRoundTrip); } } } catch (e) { console.error('Distance calculation failed:', e); } }; // Fill form from customer const fillFromCustomer = (customer) => { form.customerId = customer.id; form.customerName = customer.displayName || customer.company || customer.name || ''; form.billingAddressId = customer.id; form.deliveryAddressName = customer.displayName || customer.company || customer.name || ''; form.deliveryAddressLine = customer.street || ''; form.deliveryAddressPLZ = customer.zip || ''; form.deliveryAddressCity = customer.city || ''; form.deliveryAddressEMail = customer.email || ''; }; // Customer search const searchCustomers = async () => { if (customerSearchQuery.value.length < 1) { customerSearchResults.value = []; return; } customerSearchLoading.value = true; try { const data = await shippingNoteApi.get( `searchCustomers?query=${encodeURIComponent(customerSearchQuery.value)}` ); customerSearchResults.value = data.customers || []; } catch (e) { console.error('Customer search failed:', e); } finally { customerSearchLoading.value = false; } }; // Select customer from search const selectCustomer = async (customer) => { fillFromCustomer(customer); gpsState.value = 'manual'; customerCardExpanded.value = false; showCustomerSearch.value = false; customerSearchQuery.value = ''; customerSearchResults.value = []; // Calculate distance if customer has GPS coordinates if (customer.gpsLat && customer.gpsLong) { try { const data = await shippingNoteApi.get( `calculateDistance?lat=${customer.gpsLat}&lng=${customer.gpsLong}` ); if (data.success && data.distanceRoundTrip) { if (hoursEntries.value[0]) { hoursEntries.value[0].kilometerCount = Math.round(data.distanceRoundTrip); } } } catch (e) { console.error('Distance calculation failed:', e); } } }; // Article search const searchArticles = async () => { if (articleSearchQuery.value.length < 1) { articleSearchResults.value = []; return; } articleSearchLoading.value = true; try { const data = await shippingNoteApi.get( `searchArticles?query=${encodeURIComponent(articleSearchQuery.value)}` ); articleSearchResults.value = data.articles || []; } catch (e) { console.error('Article search failed:', e); } finally { articleSearchLoading.value = false; } }; // Add article to positions const addArticle = (article) => { const existing = positions.value.find(p => p.articleId === article.id); if (existing) { existing.amount++; } else { positions.value.push({ articleId: article.id, articleNumber: article.articleNumber, articleName: article.title, amount: 1, isEnergieMaterial: false }); } showArticleSearch.value = false; articleSearchQuery.value = ''; articleSearchResults.value = []; navigator.vibrate?.([50]); }; // Remove position const removePosition = (index) => { positions.value.splice(index, 1); }; // Toggle Beigestelltes Material const toggleEnergieMaterial = (pos) => { pos.isEnergieMaterial = !pos.isEnergieMaterial; navigator.vibrate?.([20]); }; // Hours functions for specific entry const setHours = (entryIndex, hours) => { if (hoursEntries.value[entryIndex]) { hoursEntries.value[entryIndex].hourCount = hours; navigator.vibrate?.([30]); } }; const adjustHours = (entryIndex, delta) => { if (hoursEntries.value[entryIndex]) { const newVal = Math.max(0, hoursEntries.value[entryIndex].hourCount + delta); hoursEntries.value[entryIndex].hourCount = Math.round(newVal * 4) / 4; navigator.vibrate?.([20]); } }; // Date picker const openDatePicker = (entryIndex) => { activeEntryIndex.value = entryIndex; showDatePicker.value = true; }; const updateEntryDate = (dateStr) => { if (hoursEntries.value[activeEntryIndex.value]) { hoursEntries.value[activeEntryIndex.value].date = dateStr; } }; // Employee selector const openEmployeeSelector = () => { showEmployeeSelector.value = true; }; const addEmployee = (employee) => { const newEntry = createNewHoursEntry(employee.id, employee.name); // Copy date and km from first entry if (hoursEntries.value[0]) { newEntry.date = hoursEntries.value[0].date; newEntry.kilometerCount = hoursEntries.value[0].kilometerCount; } hoursEntries.value.push(newEntry); navigator.vibrate?.([50]); }; const removeHoursEntry = (entryIndex) => { if (hoursEntries.value.length > 1) { hoursEntries.value.splice(entryIndex, 1); navigator.vibrate?.([30]); } }; // Select car for specific entry const openCarSelect = (entryIndex) => { carSelectEntryIndex.value = entryIndex; showCarSelect.value = true; }; const selectCar = (car) => { if (hoursEntries.value[carSelectEntryIndex.value]) { hoursEntries.value[carSelectEntryIndex.value].carId = car.id; hoursEntries.value[carSelectEntryIndex.value].carName = car.name; } showCarSelect.value = false; navigator.vibrate?.([30]); }; // Select hour type for specific entry const openHourTypeSelect = (entryIndex) => { hourTypeSelectEntryIndex.value = entryIndex; showHourTypeSelect.value = true; }; const selectHourType = (hourType) => { if (hoursEntries.value[hourTypeSelectEntryIndex.value]) { hoursEntries.value[hourTypeSelectEntryIndex.value].hourType = hourType.id; hoursEntries.value[hourTypeSelectEntryIndex.value].hourTypeName = hourType.name; } showHourTypeSelect.value = false; navigator.vibrate?.([30]); }; // Customer card expand/collapse const toggleCustomerCard = () => { customerCardExpanded.value = !customerCardExpanded.value; }; // Switch to manual mode const switchToManual = () => { gpsState.value = 'manual'; showCustomerSearch.value = true; }; // Refresh GPS const refreshGPS = () => { detectGPS(); }; // Mock customer found (for screenshots/demo) const mockCustomerFound = () => { detectedCustomer.value = { id: 999, displayName: 'Frau im Zentrum GmbH', company: 'Frau im Zentrum GmbH', street: 'Schmiedgasse 14', zip: '8010', city: 'Graz', email: 'office@frauzentrum.at' }; gpsDistance.value = 42; gpsState.value = 'customer_found'; fillFromCustomer(detectedCustomer.value); customerCardExpanded.value = false; if (hoursEntries.value[0]) { hoursEntries.value[0].kilometerCount = 84; } navigator.vibrate?.([100, 50, 100]); }; // Submit form const submit = async (andSign = false) => { if (!isValid.value) { emit('toast', 'Bitte alle Pflichtfelder ausfüllen', 'error'); return; } loading.value = true; try { const payload = { billingAddressId: form.billingAddressId, deliveryAddressName: form.deliveryAddressName, deliveryAddressLine: form.deliveryAddressLine, deliveryAddressPLZ: form.deliveryAddressPLZ, deliveryAddressCity: form.deliveryAddressCity, deliveryAddressEMail: form.deliveryAddressEMail, note: form.note, type: form.type, positions: positions.value.map(p => ({ article: p.articleId, amount: p.amount, isEnergieMaterial: p.isEnergieMaterial })), hoursEntries: hoursEntries.value.filter(h => h.hourCount > 0).map(h => ({ userId: h.userId, date: h.date, hourCount: h.hourCount, hourType: h.hourType, carId: h.carId, kilometerCount: h.kilometerCount, comment: h.comment })) }; const data = await shippingNoteApi.post('create', payload); if (data.success) { if (andSign) { emit('createAndSign', data.shippingNote); } else { emit('created', data.shippingNote); } resetForm(); } else { emit('toast', data.error || 'Fehler beim Erstellen', 'error'); } } catch (e) { console.error('Submit failed:', e); emit('toast', 'Netzwerkfehler', 'error'); } finally { loading.value = false; } }; // Reset form const resetForm = () => { form.customerId = null; form.customerName = ''; form.billingAddressId = null; form.deliveryAddressName = ''; form.deliveryAddressLine = ''; form.deliveryAddressPLZ = ''; form.deliveryAddressCity = ''; form.deliveryAddressEMail = ''; form.note = ''; positions.value = []; hoursEntries.value = [createNewHoursEntry()]; if (userCar.value) { hoursEntries.value[0].carId = userCar.value.id; hoursEntries.value[0].carName = userCar.value.name; } detectGPS(); }; // Debounced search watchers let customerSearchTimeout = null; watch(customerSearchQuery, () => { clearTimeout(customerSearchTimeout); customerSearchTimeout = setTimeout(searchCustomers, 300); }); let articleSearchTimeout = null; watch(articleSearchQuery, () => { clearTimeout(articleSearchTimeout); articleSearchTimeout = setTimeout(searchArticles, 300); }); return { gpsState, gpsPosition, detectedCustomer, gpsAddress, gpsDistance, customerCardExpanded, form, hoursEntries, positions, loading, showCustomerSearch, customerSearchQuery, customerSearchResults, customerSearchLoading, showArticleSearch, articleSearchQuery, articleSearchResults, articleSearchLoading, showCarSelect, carSelectEntryIndex, allCars, showHourTypeSelect, hourTypeSelectEntryIndex, hourTypes, showPositionsSection, showDatePicker, activeEntryIndex, showEmployeeSelector, selectedEmployeeIds, userCar, isValid, quickWorkTypes, selectedWorkType, selectWorkType, formatDistance, formatDateDisplay, formatHoursValue, detectGPS, refreshGPS, mockCustomerFound, switchToManual, selectCustomer, toggleCustomerCard, addArticle, removePosition, toggleEnergieMaterial, setHours, adjustHours, openDatePicker, updateEntryDate, openEmployeeSelector, addEmployee, removeHoursEntry, openCarSelect, selectCar, openHourTypeSelect, selectHourType, submit, resetForm }; }, template: `
Kunde erkannt {{ formatDistance(gpsDistance) }}

{{ form.customerName }}

{{ form.deliveryAddressLine }}, {{ form.deliveryAddressPLZ }} {{ form.deliveryAddressCity }}

Art der Arbeit *

Arbeitszeit

{{ entry.userName.charAt(0).toUpperCase() }}
{{ entry.userName }}
{{ formatHoursValue(entry.hourCount).h }} h {{ formatHoursValue(entry.hourCount).m }} m
km
{{ pos.articleName }}
{{ pos.articleNumber }}
{{ pos.amount }}

Kunde suchen

Suche...
Keine Kunden gefunden

Artikel suchen

Suche...
Keine Artikel gefunden

Fahrzeug wählen

Stundenart wählen

` };