Xinon mobile/improve
This commit is contained in:
@@ -1,19 +1,6 @@
|
||||
/**
|
||||
* Scanner Component (Inventur)
|
||||
*
|
||||
* The main scanning interface for stocktakes.
|
||||
* Uses the new API path: /MobileApp/Lager/Inventur/{action}
|
||||
*/
|
||||
import { createModuleApi, debounce } from '/mobile/shared/api.js';
|
||||
|
||||
// Inventur-specific API
|
||||
const inventurApi = {
|
||||
get: (endpoint) => fetch(`/MobileApp/Lager/Inventur/${endpoint}`).then(r => r.json()),
|
||||
post: (endpoint, data) => fetch(`/MobileApp/Lager/Inventur/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(r => r.json())
|
||||
};
|
||||
const inventurApi = createModuleApi('Lager/Inventur');
|
||||
|
||||
export default {
|
||||
name: 'Scanner',
|
||||
@@ -26,44 +13,29 @@ export default {
|
||||
setup(props, { emit }) {
|
||||
const { ref, onMounted, onUnmounted, nextTick, computed } = Vue;
|
||||
|
||||
// State
|
||||
const currentTab = ref('scan');
|
||||
const isLoading = ref(false);
|
||||
|
||||
// Scanner
|
||||
const scanner = ref(null);
|
||||
const isScannerActive = ref(false);
|
||||
const scannerError = ref('');
|
||||
|
||||
// Article
|
||||
const scannedArticle = ref(null);
|
||||
const quantity = ref('1');
|
||||
const rack = ref('');
|
||||
const shelf = ref('');
|
||||
|
||||
// Search
|
||||
const searchQuery = ref('');
|
||||
const searchResults = ref([]);
|
||||
const categories = ref([]);
|
||||
const selectedCategory = ref(0);
|
||||
const isSearching = ref(false);
|
||||
|
||||
// History
|
||||
const recentScans = ref([]);
|
||||
const isLoadingHistory = ref(false);
|
||||
|
||||
// Warning
|
||||
const alreadyScannedWarning = ref(null);
|
||||
|
||||
// Keypad
|
||||
const showKeypad = ref(false);
|
||||
|
||||
// Computed
|
||||
const canSubmit = computed(() => {
|
||||
return scannedArticle.value && parseFloat(quantity.value) > 0 && !isLoading.value;
|
||||
});
|
||||
|
||||
// Scanner functions
|
||||
const startScanner = async () => {
|
||||
scannerError.value = '';
|
||||
try {
|
||||
@@ -92,7 +64,6 @@ export default {
|
||||
await lookupArticle(decodedText);
|
||||
};
|
||||
|
||||
// Article lookup
|
||||
const lookupArticle = async (code) => {
|
||||
isLoading.value = true;
|
||||
alreadyScannedWarning.value = null;
|
||||
@@ -121,7 +92,6 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// Submit
|
||||
const submitScan = async (overwrite = false) => {
|
||||
if (!canSubmit.value) return;
|
||||
isLoading.value = true;
|
||||
@@ -140,6 +110,7 @@ export default {
|
||||
const result = await inventurApi.post('submitScan', payload);
|
||||
|
||||
if (result.success) {
|
||||
navigator.vibrate?.([100]);
|
||||
emit('toast', result.message, 'success');
|
||||
scannedArticle.value = null;
|
||||
quantity.value = '1';
|
||||
@@ -157,13 +128,12 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// Search
|
||||
const loadCategories = async () => {
|
||||
const result = await inventurApi.get('getCategories');
|
||||
if (result.success) categories.value = result.categories;
|
||||
};
|
||||
|
||||
const searchArticles = async () => {
|
||||
const doSearch = async () => {
|
||||
if (searchQuery.value.length < 2 && !selectedCategory.value) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
@@ -180,6 +150,8 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const searchArticles = debounce(doSearch, 300);
|
||||
|
||||
const selectSearchResult = async (article) => {
|
||||
await stopScanner();
|
||||
scannedArticle.value = article;
|
||||
@@ -194,7 +166,6 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// History
|
||||
const loadHistory = async () => {
|
||||
isLoadingHistory.value = true;
|
||||
try {
|
||||
@@ -205,7 +176,6 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
// Keypad
|
||||
const appendDigit = (digit) => {
|
||||
if (digit === '.' && quantity.value.includes('.')) return;
|
||||
if (quantity.value === '0' && digit !== '.') {
|
||||
@@ -221,7 +191,6 @@ export default {
|
||||
|
||||
const clearQuantity = () => { quantity.value = '0'; };
|
||||
|
||||
// Navigation
|
||||
const handleClose = async () => {
|
||||
await stopScanner();
|
||||
emit('close');
|
||||
@@ -265,7 +234,6 @@ export default {
|
||||
|
||||
template: `
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Title bar with close -->
|
||||
<div class="flex items-center justify-between px-3 py-2 bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-slate-300 truncate flex-1">{{ stocktake.title || 'Inventur #' + stocktake.stocktakeNumber }}</span>
|
||||
<button @click="handleClose" class="ml-2 p-1.5 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-700 transition">
|
||||
@@ -275,18 +243,14 @@ export default {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700 px-3 py-2">
|
||||
<button @click="switchTab('scan')" :class="[currentTab === 'scan' ? 'bg-white dark:bg-slate-800 shadow-sm' : 'text-slate-500 dark:text-slate-400']" class="flex-1 py-2 text-sm font-medium rounded-lg transition">Scannen</button>
|
||||
<button @click="switchTab('search')" :class="[currentTab === 'search' ? 'bg-white dark:bg-slate-800 shadow-sm' : 'text-slate-500 dark:text-slate-400']" class="flex-1 py-2 text-sm font-medium rounded-lg transition mx-1">Suche</button>
|
||||
<button @click="switchTab('history')" :class="[currentTab === 'history' ? 'bg-white dark:bg-slate-800 shadow-sm' : 'text-slate-500 dark:text-slate-400']" class="flex-1 py-2 text-sm font-medium rounded-lg transition">Verlauf</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="flex-grow overflow-y-auto bg-slate-50 dark:bg-slate-900">
|
||||
<!-- SCAN TAB -->
|
||||
<div v-if="currentTab === 'scan'" class="p-4 space-y-4">
|
||||
<!-- Scanner -->
|
||||
<div v-if="!scannedArticle" class="space-y-4">
|
||||
<div id="qr-reader" class="w-full rounded-lg overflow-hidden bg-black"></div>
|
||||
<div v-if="scannerError" class="p-4 bg-red-50 dark:bg-red-900/30 rounded-lg">
|
||||
@@ -296,9 +260,7 @@ export default {
|
||||
<p class="text-center text-sm text-slate-500 dark:text-slate-400">QR-Code scannen oder Artikel suchen</p>
|
||||
</div>
|
||||
|
||||
<!-- Scanned Article -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- Warning -->
|
||||
<div v-if="alreadyScannedWarning" class="p-4 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-amber-500 mr-2 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
|
||||
@@ -315,24 +277,31 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Info -->
|
||||
<div class="bg-white dark:bg-slate-800 p-4 rounded-lg shadow-sm">
|
||||
<h3 class="font-bold text-lg text-slate-800 dark:text-white">{{ scannedArticle.title }}</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">Art.-Nr.: {{ scannedArticle.articleNumber }}</p>
|
||||
<p v-if="scannedArticle.categoryName" class="text-sm text-slate-500 dark:text-slate-400">Kategorie: {{ scannedArticle.categoryName }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Quantity -->
|
||||
<div class="bg-white dark:bg-slate-800 p-4 rounded-lg shadow-sm">
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
Menge ({{ scannedArticle.unit || 'Stk.' }})
|
||||
</label>
|
||||
<div @click="showKeypad = true" class="w-full p-4 text-3xl font-bold text-center border-2 border-slate-300 dark:border-slate-600 rounded-lg cursor-pointer hover:border-primary transition text-slate-800 dark:text-white">
|
||||
{{ quantity }}
|
||||
<div class="flex gap-2 mb-3">
|
||||
<button @click="quantity = '1'" :class="['flex-1 py-2 rounded-lg font-bold transition', quantity === '1' ? 'bg-primary text-white' : 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300']">1</button>
|
||||
<button @click="quantity = '5'" :class="['flex-1 py-2 rounded-lg font-bold transition', quantity === '5' ? 'bg-primary text-white' : 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300']">5</button>
|
||||
<button @click="quantity = '10'" :class="['flex-1 py-2 rounded-lg font-bold transition', quantity === '10' ? 'bg-primary text-white' : 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300']">10</button>
|
||||
<button @click="quantity = '20'" :class="['flex-1 py-2 rounded-lg font-bold transition', quantity === '20' ? 'bg-primary text-white' : 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300']">20</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="quantity = String(Math.max(1, parseFloat(quantity) - 1))" class="w-14 h-14 bg-slate-100 dark:bg-slate-700 rounded-lg text-2xl font-bold text-slate-700 dark:text-slate-300 active:bg-slate-200 dark:active:bg-slate-600">-</button>
|
||||
<div @click="showKeypad = true" class="flex-1 p-3 text-3xl font-bold text-center border-2 border-slate-300 dark:border-slate-600 rounded-lg cursor-pointer hover:border-primary transition text-slate-800 dark:text-white">
|
||||
{{ quantity }}
|
||||
</div>
|
||||
<button @click="quantity = String(parseFloat(quantity) + 1)" class="w-14 h-14 bg-slate-100 dark:bg-slate-700 rounded-lg text-2xl font-bold text-slate-700 dark:text-slate-300 active:bg-slate-200 dark:active:bg-slate-600">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rack/Shelf -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Regal</label>
|
||||
@@ -344,19 +313,17 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="space-y-2">
|
||||
<button v-if="alreadyScannedWarning" @click="submitScan(false)" :disabled="!canSubmit" class="w-full py-4 bg-green-600 text-white font-bold rounded-lg disabled:opacity-50">Zur Menge addieren</button>
|
||||
<button v-if="alreadyScannedWarning" @click="submitScan(true)" :disabled="!canSubmit" class="w-full py-4 bg-amber-600 text-white font-bold rounded-lg disabled:opacity-50">Überschreiben</button>
|
||||
<button v-if="!alreadyScannedWarning" @click="submitScan(false)" :disabled="!canSubmit" class="w-full py-4 bg-green-600 text-white font-bold rounded-lg disabled:opacity-50">
|
||||
<button v-if="alreadyScannedWarning" @click="submitScan(false)" :disabled="!canSubmit" class="w-full py-5 bg-green-600 text-white text-lg font-bold rounded-xl disabled:opacity-50 active:scale-[0.98] transition">Zur Menge addieren</button>
|
||||
<button v-if="alreadyScannedWarning" @click="submitScan(true)" :disabled="!canSubmit" class="w-full py-4 bg-amber-600 text-white font-bold rounded-xl disabled:opacity-50 active:scale-[0.98] transition">Überschreiben</button>
|
||||
<button v-if="!alreadyScannedWarning" @click="submitScan(false)" :disabled="!canSubmit" class="w-full py-5 bg-green-600 text-white text-lg font-bold rounded-xl disabled:opacity-50 active:scale-[0.98] transition">
|
||||
{{ isLoading ? 'Speichert...' : 'Speichern' }}
|
||||
</button>
|
||||
<button @click="cancelScan" class="w-full py-3 text-slate-600 dark:text-slate-400 font-medium">Abbrechen</button>
|
||||
<button @click="cancelScan" class="w-full py-2 text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEARCH TAB -->
|
||||
<div v-else-if="currentTab === 'search'" class="p-4 space-y-4">
|
||||
<div class="sticky top-0 bg-slate-100 dark:bg-slate-900 pb-2 space-y-3">
|
||||
<input v-model="searchQuery" @input="searchArticles" type="search" placeholder="Artikel suchen..." class="w-full p-3 border border-slate-300 dark:border-slate-600 rounded-lg dark:bg-slate-800 dark:text-white">
|
||||
@@ -382,7 +349,6 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HISTORY TAB -->
|
||||
<div v-else-if="currentTab === 'history'" class="p-4">
|
||||
<div v-if="isLoadingHistory" class="space-y-3">
|
||||
<div v-for="i in 5" :key="i" class="bg-white dark:bg-slate-800 p-3 rounded-lg shadow-sm animate-pulse">
|
||||
@@ -412,7 +378,6 @@ export default {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Keypad -->
|
||||
<transition name="slide-up">
|
||||
<div v-if="showKeypad" class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-end">
|
||||
<div class="bg-white dark:bg-slate-800 w-full rounded-t-2xl p-4 safe-area-bottom">
|
||||
|
||||
@@ -1,21 +1,6 @@
|
||||
/**
|
||||
* StocktakeList Component (Inventur)
|
||||
*
|
||||
* Displays a list of active stocktakes.
|
||||
* Uses the new API path: /MobileApp/Lager/Inventur/{action}
|
||||
*/
|
||||
import { createModuleApi } from '/mobile/shared/api.js';
|
||||
|
||||
import { api } from '/mobile/shared/auth.js';
|
||||
|
||||
// Override API base for Inventur
|
||||
const inventurApi = {
|
||||
get: (endpoint) => fetch(`/MobileApp/Lager/Inventur/${endpoint}`).then(r => r.json()),
|
||||
post: (endpoint, data) => fetch(`/MobileApp/Lager/Inventur/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
}).then(r => r.json())
|
||||
};
|
||||
const inventurApi = createModuleApi('Lager/Inventur');
|
||||
|
||||
export default {
|
||||
name: 'StocktakeList',
|
||||
|
||||
Reference in New Issue
Block a user