diff --git a/Layout/default/VueViews/WarehouseStocktakePWA.php b/Layout/default/VueViews/WarehouseStocktakePWA.php index 3b3ff2036..9c65502bb 100644 --- a/Layout/default/VueViews/WarehouseStocktakePWA.php +++ b/Layout/default/VueViews/WarehouseStocktakePWA.php @@ -74,6 +74,52 @@ 0% { background-color: rgb(34, 197, 94); } 100% { background-color: transparent; } } + + /* Custom numpad styles */ + .numpad-btn { + min-height: 52px; + font-size: 1.25rem; + font-weight: 600; + border-radius: 12px; + transition: all 0.15s ease; + user-select: none; + -webkit-tap-highlight-color: transparent; + } + .numpad-btn:active { + transform: scale(0.95); + } + + /* Warning banner animation - intense without causing overflow */ + @keyframes pulse-warning { + 0%, 100% { + opacity: 1; + box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.7), inset 0 0 0 0 rgba(251, 191, 36, 0.2); + border-color: rgb(251, 191, 36); + background-color: rgb(254, 243, 199); + } + 50% { + opacity: 0.95; + box-shadow: 0 0 20px 5px rgba(251, 191, 36, 0.6), inset 0 0 20px 0 rgba(251, 191, 36, 0.2); + border-color: rgb(245, 158, 11); + background-color: rgb(253, 230, 138); + } + } + .warning-pulse { + animation: pulse-warning 0.8s ease-in-out infinite; + } + .dark .warning-pulse { + animation: pulse-warning-dark 0.8s ease-in-out infinite; + } + @keyframes pulse-warning-dark { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(251, 191, 36, 0.5), inset 0 0 0 0 rgba(251, 191, 36, 0.1); + border-color: rgb(217, 119, 6); + } + 50% { + box-shadow: 0 0 25px 8px rgba(251, 191, 36, 0.4), inset 0 0 15px 0 rgba(251, 191, 36, 0.15); + border-color: rgb(245, 158, 11); + } + }
@@ -91,22 +137,35 @@ const app = createApp({ const selectedStocktake = ref(null); const isLoading = ref(true); const scannerActive = ref(false); + const cameraAvailable = ref(true); const lastScan = ref(null); const recentScans = ref([]); const progress = reactive({ totalScanned: 0, myScanned: 0 }); const theme = ref(localStorage.getItem('theme') || 'system'); + // Categories + const categories = ref([]); + const selectedCategory = ref(null); + const showCategoryBrowser = ref(false); + + // Already scanned warning + const alreadyScannedWarning = reactive({ + show: false, + existingItem: null, + }); + // Form state const manualForm = reactive({ show: false, article: null, - quantity: 1, + quantity: '', rack: '', shelf: '', note: '', searchQuery: '', searchResults: [], searching: false, + showNumpad: false, }); // Scanner instance @@ -122,6 +181,12 @@ const app = createApp({ return window.matchMedia('(prefers-color-scheme: dark)').matches; }); + // Check if mobile device for numpad display + const isMobile = computed(() => { + const ua = navigator.userAgent.toLowerCase(); + return /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(ua); + }); + // === METHODS === const applyTheme = () => { document.documentElement.classList.toggle('dark', isDark.value); @@ -147,11 +212,23 @@ const app = createApp({ } }; + const fetchCategories = async () => { + try { + const res = await api.get('/getCategories'); + if (res.data.success) { + categories.value = res.data.categories; + } + } catch (e) { + console.error('Failed to fetch categories:', e); + } + }; + const selectStocktake = async (stocktake) => { selectedStocktake.value = stocktake; currentScreen.value = 'scanner'; await fetchMyScans(); await fetchProgress(); + await fetchCategories(); await nextTick(); startScanner(); }; @@ -181,9 +258,11 @@ const app = createApp({ onScanFailure ); scannerActive.value = true; + cameraAvailable.value = true; } catch (err) { console.error('Scanner start error:', err); - alert('Kamera konnte nicht gestartet werden. Bitte Berechtigung erteilen.'); + cameraAvailable.value = false; + // Don't show alert - user can use manual search } }; @@ -215,13 +294,7 @@ const app = createApp({ try { const res = await api.get('/getArticle', { params: { code: decodedText } }); if (res.data.success) { - manualForm.article = res.data.article; - manualForm.quantity = 1; - manualForm.rack = ''; - manualForm.shelf = ''; - manualForm.note = ''; - manualForm.show = true; - await stopScanner(); + await handleArticleSelected(res.data.article); } else { showToast(res.data.message || 'Artikel nicht gefunden', 'error'); } @@ -234,21 +307,63 @@ const app = createApp({ // Ignore - continuous scanning }; - const submitScan = async () => { - if (!manualForm.article || manualForm.quantity <= 0) { + const handleArticleSelected = async (article) => { + await stopScanner(); + + // Reset form fields + manualForm.article = article; + manualForm.quantity = ''; + manualForm.rack = ''; + manualForm.shelf = ''; + manualForm.note = ''; + manualForm.showNumpad = true; + + // Check if already scanned + try { + const checkRes = await api.get('/checkAlreadyScanned', { + params: { + stocktakeId: selectedStocktake.value.id, + articleId: article.id + } + }); + + if (checkRes.data.success && checkRes.data.alreadyScanned) { + alreadyScannedWarning.show = true; + alreadyScannedWarning.existingItem = checkRes.data.existingItem; + } else { + alreadyScannedWarning.show = false; + alreadyScannedWarning.existingItem = null; + } + } catch (e) { + console.error('Check already scanned error:', e); + } + + manualForm.show = true; + }; + + const submitScan = async (overwrite = false) => { + const qty = parseFloat(manualForm.quantity) || 0; + if (!manualForm.article || qty <= 0) { showToast('Bitte Menge angeben', 'error'); return; } try { - const res = await api.post('/submitScan', { + const payload = { stocktakeId: selectedStocktake.value.id, articleId: manualForm.article.id, - quantity: manualForm.quantity, + quantity: qty, rack: manualForm.rack || null, shelf: manualForm.shelf || null, note: manualForm.note || null, - }); + }; + + if (overwrite && alreadyScannedWarning.existingItem) { + payload.overwrite = true; + payload.overwriteItemId = alreadyScannedWarning.existingItem.id; + } + + const res = await api.post('/submitScan', payload); if (res.data.success) { showToast(res.data.message, 'success'); @@ -264,14 +379,12 @@ const app = createApp({ } // Update progress - progress.totalScanned++; - progress.myScanned++; + if (!overwrite) { + progress.totalScanned++; + progress.myScanned++; + } - // Close form and restart scanner - manualForm.show = false; - manualForm.article = null; - await nextTick(); - startScanner(); + closeForm(); } else { showToast(res.data.message || 'Fehler beim Speichern', 'error'); } @@ -280,30 +393,86 @@ const app = createApp({ } }; - const cancelScan = async () => { + const closeForm = async () => { manualForm.show = false; manualForm.article = null; + manualForm.quantity = ''; + manualForm.searchQuery = ''; + manualForm.searchResults = []; + manualForm.showNumpad = false; + alreadyScannedWarning.show = false; + alreadyScannedWarning.existingItem = null; + selectedCategory.value = null; + showCategoryBrowser.value = false; await nextTick(); - startScanner(); + if (cameraAvailable.value) { + startScanner(); + } }; const openManualEntry = async () => { await stopScanner(); + // Reset form fields manualForm.show = true; manualForm.article = null; + manualForm.quantity = ''; + manualForm.rack = ''; + manualForm.shelf = ''; + manualForm.note = ''; manualForm.searchQuery = ''; manualForm.searchResults = []; + manualForm.showNumpad = false; + alreadyScannedWarning.show = false; + selectedCategory.value = null; + showCategoryBrowser.value = false; + }; + + const openCategoryBrowser = () => { + showCategoryBrowser.value = true; + selectedCategory.value = null; + manualForm.searchResults = []; + }; + + const selectCategory = async (category) => { + selectedCategory.value = category; + showCategoryBrowser.value = false; + manualForm.searching = true; + + try { + const res = await api.get('/searchArticles', { + params: { categoryId: category.id, query: manualForm.searchQuery || '' } + }); + if (res.data.success) { + manualForm.searchResults = res.data.articles; + } + } catch (e) { + console.error('Category search error:', e); + } finally { + manualForm.searching = false; + } + }; + + const clearCategoryFilter = () => { + selectedCategory.value = null; + manualForm.searchResults = []; + if (manualForm.searchQuery.length >= 2) { + searchArticles(); + } }; const searchArticles = async () => { - if (manualForm.searchQuery.length < 2) { + if (manualForm.searchQuery.length < 2 && !selectedCategory.value) { manualForm.searchResults = []; return; } manualForm.searching = true; try { - const res = await api.get('/searchArticles', { params: { query: manualForm.searchQuery } }); + const params = { query: manualForm.searchQuery }; + if (selectedCategory.value) { + params.categoryId = selectedCategory.value.id; + } + const res = await api.get('/searchArticles', { params }); if (res.data.success) { manualForm.searchResults = res.data.articles; } @@ -314,11 +483,8 @@ const app = createApp({ } }; - const selectSearchResult = (article) => { - manualForm.article = article; - manualForm.quantity = 1; - manualForm.searchQuery = ''; - manualForm.searchResults = []; + const selectSearchResult = async (article) => { + await handleArticleSelected(article); }; const fetchMyScans = async () => { @@ -346,6 +512,29 @@ const app = createApp({ } }; + // Numpad functions + const numpadInput = (val) => { + if (val === 'clear') { + manualForm.quantity = ''; + } else if (val === 'backspace') { + manualForm.quantity = String(manualForm.quantity).slice(0, -1); + } else if (val === '+') { + const current = parseFloat(manualForm.quantity) || 0; + manualForm.quantity = String(current + 1); + } else if (val === '-') { + const current = parseFloat(manualForm.quantity) || 0; + if (current > 1) { + manualForm.quantity = String(current - 1); + } + } else if (val === '.') { + if (!String(manualForm.quantity).includes('.')) { + manualForm.quantity = (manualForm.quantity || '0') + '.'; + } + } else { + manualForm.quantity = (manualForm.quantity || '') + val; + } + }; + // Toast notification const toast = reactive({ show: false, message: '', type: 'success' }); let toastTimeout = null; @@ -381,10 +570,12 @@ const app = createApp({ }); return { - currentScreen, stocktakes, selectedStocktake, isLoading, scannerActive, - recentScans, progress, theme, manualForm, toast, - selectStocktake, backToList, submitScan, cancelScan, + currentScreen, stocktakes, selectedStocktake, isLoading, scannerActive, cameraAvailable, + recentScans, progress, theme, manualForm, toast, categories, selectedCategory, + showCategoryBrowser, alreadyScannedWarning, isMobile, + selectStocktake, backToList, submitScan, closeForm, openManualEntry, selectSearchResult, setTheme, logout, + openCategoryBrowser, selectCategory, clearCategoryFilter, numpadInput, }; }, template: ` @@ -476,7 +667,7 @@ const app = createApp({Kamera nicht verfügbar
+Verwenden Sie die manuelle Suche unten.
+