fixed stocktake and warehouselocation
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-slate-100 dark:bg-slate-900 transition-colors duration-300">
|
||||
@@ -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({
|
||||
|
||||
<!-- Scanner View -->
|
||||
<div v-if="!manualForm.show" class="p-4">
|
||||
<div class="relative bg-black rounded-xl overflow-hidden mb-4">
|
||||
<div v-if="cameraAvailable" class="relative bg-black rounded-xl overflow-hidden mb-4">
|
||||
<div id="qr-reader" class="w-full"></div>
|
||||
<div v-if="!scannerActive" class="absolute inset-0 flex items-center justify-center bg-slate-900/80">
|
||||
<div class="text-center text-white">
|
||||
@@ -489,6 +680,18 @@ const app = createApp({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-amber-500 mr-2 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-amber-800 dark:text-amber-200 font-medium">Kamera nicht verfügbar</p>
|
||||
<p class="text-amber-700 dark:text-amber-300 text-sm">Verwenden Sie die manuelle Suche unten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="openManualEntry"
|
||||
class="w-full py-3 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-200 rounded-xl font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -503,6 +706,19 @@ const app = createApp({
|
||||
<div v-if="manualForm.show" class="flex-1 bg-white dark:bg-slate-800 p-4 overflow-auto">
|
||||
<!-- Search (if no article selected) -->
|
||||
<div v-if="!manualForm.article" class="space-y-4">
|
||||
<!-- Category Filter -->
|
||||
<div v-if="selectedCategory" class="flex items-center bg-primary/10 dark:bg-primary/20 rounded-lg p-2 mb-2">
|
||||
<span class="text-sm text-primary dark:text-secondary font-medium flex-1">
|
||||
Kategorie: {{ selectedCategory.name }}
|
||||
</span>
|
||||
<button @click="clearCategoryFilter" class="p-1 text-primary dark:text-secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Input -->
|
||||
<div class="relative">
|
||||
<input v-model="manualForm.searchQuery" type="text" inputmode="search"
|
||||
placeholder="Artikelnummer oder Name..."
|
||||
@@ -515,6 +731,39 @@ const app = createApp({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Browser Button -->
|
||||
<button @click="openCategoryBrowser"
|
||||
class="w-full py-3 bg-primary/10 dark:bg-primary/20 text-primary dark:text-secondary rounded-xl font-medium flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
Nach Kategorie durchsuchen
|
||||
</button>
|
||||
|
||||
<!-- Category Browser Modal -->
|
||||
<div v-if="showCategoryBrowser" class="fixed inset-0 bg-black/50 z-50 flex items-end">
|
||||
<div class="bg-white dark:bg-slate-800 w-full rounded-t-2xl max-h-[70vh] flex flex-col">
|
||||
<div class="p-4 border-b dark:border-slate-700 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg dark:text-white">Kategorie wählen</h3>
|
||||
<button @click="showCategoryBrowser = false" class="p-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 p-4">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-for="cat in categories" :key="cat.id"
|
||||
@click="selectCategory(cat)"
|
||||
class="p-3 bg-slate-50 dark:bg-slate-700 rounded-lg cursor-pointer active:bg-slate-100 dark:active:bg-slate-600 text-center">
|
||||
<p class="font-medium text-slate-800 dark:text-white text-sm">{{ cat.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Results -->
|
||||
<div v-if="manualForm.searchResults.length" class="space-y-2 max-h-64 overflow-auto">
|
||||
<div v-for="article in manualForm.searchResults" :key="article.id"
|
||||
@click="selectSearchResult(article)"
|
||||
@@ -524,25 +773,84 @@ const app = createApp({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button @click="cancelScan" class="w-full py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-xl">
|
||||
<button @click="closeForm" class="w-full py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-xl">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Article Form (if article selected) -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- Already Scanned Warning -->
|
||||
<div v-if="alreadyScannedWarning.show" class="bg-amber-100 dark:bg-amber-900/30 border-2 border-amber-400 dark:border-amber-600 rounded-xl p-4 warning-pulse">
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-amber-600 dark:text-amber-400 mr-3 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p class="font-bold text-amber-800 dark:text-amber-200">Bereits gescannt!</p>
|
||||
<p class="text-sm text-amber-700 dark:text-amber-300 mt-1">
|
||||
Dieser Artikel wurde bereits erfasst:
|
||||
<br><strong>{{ alreadyScannedWarning.existingItem.countedQuantity }} Stk.</strong>
|
||||
von {{ alreadyScannedWarning.existingItem.scannedBy }}
|
||||
({{ alreadyScannedWarning.existingItem.scannedAt }})
|
||||
</p>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mt-2">
|
||||
Geben Sie die neue Menge ein und klicken Sie auf "Überschreiben".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 dark:bg-slate-700 rounded-xl p-4">
|
||||
<p class="font-bold text-lg text-slate-800 dark:text-white">{{ manualForm.article.title }}</p>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">{{ manualForm.article.articleNumber }}</p>
|
||||
<p v-if="manualForm.article.categoryName" class="text-xs text-slate-400 mt-1">{{ manualForm.article.categoryName }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Quantity with Custom Numpad -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||
Menge ({{ manualForm.article.unit }}) *
|
||||
</label>
|
||||
<input v-model.number="manualForm.quantity" type="number" inputmode="decimal" min="0.01" step="0.01"
|
||||
class="w-full px-4 py-3 text-xl font-bold text-center rounded-xl border dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-primary">
|
||||
<div class="text-center bg-slate-100 dark:bg-slate-700 rounded-xl p-4 mb-3">
|
||||
<span class="text-4xl font-bold text-primary dark:text-secondary">
|
||||
{{ manualForm.quantity || '0' }}
|
||||
</span>
|
||||
<span class="text-xl text-slate-500 ml-2">{{ manualForm.article.unit }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: regular input field -->
|
||||
<div v-if="!isMobile" class="mt-2">
|
||||
<input v-model="manualForm.quantity" type="number" inputmode="decimal" min="0.01" step="0.01"
|
||||
placeholder="Menge eingeben..."
|
||||
class="w-full px-4 py-3 text-xl font-bold text-center rounded-xl border dark:border-slate-600 dark:bg-slate-700 dark:text-white focus:ring-2 focus:ring-primary">
|
||||
</div>
|
||||
|
||||
<!-- Numpad (only on mobile devices) -->
|
||||
<div v-if="manualForm.showNumpad && isMobile" class="grid grid-cols-4 gap-2">
|
||||
<button @click="numpadInput('1')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">1</button>
|
||||
<button @click="numpadInput('2')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">2</button>
|
||||
<button @click="numpadInput('3')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">3</button>
|
||||
<button @click="numpadInput('+')" class="numpad-btn bg-green-500 text-white">+</button>
|
||||
|
||||
<button @click="numpadInput('4')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">4</button>
|
||||
<button @click="numpadInput('5')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">5</button>
|
||||
<button @click="numpadInput('6')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">6</button>
|
||||
<button @click="numpadInput('-')" class="numpad-btn bg-red-500 text-white">-</button>
|
||||
|
||||
<button @click="numpadInput('7')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">7</button>
|
||||
<button @click="numpadInput('8')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">8</button>
|
||||
<button @click="numpadInput('9')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">9</button>
|
||||
<button @click="numpadInput('backspace')" class="numpad-btn bg-slate-300 dark:bg-slate-500 text-slate-800 dark:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2M3 12l6.414 6.414a2 2 0 001.414.586H19a2 2 0 002-2V7a2 2 0 00-2-2h-8.172a2 2 0 00-1.414.586L3 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button @click="numpadInput('.')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">.</button>
|
||||
<button @click="numpadInput('0')" class="numpad-btn bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white">0</button>
|
||||
<button @click="numpadInput('clear')" class="numpad-btn bg-slate-300 dark:bg-slate-500 text-slate-800 dark:text-white col-span-2">C</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@@ -558,13 +866,25 @@ const app = createApp({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3 pt-4">
|
||||
<button @click="cancelScan" class="flex-1 py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-xl font-medium">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button @click="submitScan" class="flex-1 py-3 bg-green-600 text-white rounded-xl font-bold">
|
||||
Speichern
|
||||
</button>
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-2 pt-4">
|
||||
<div class="flex space-x-3">
|
||||
<button @click="closeForm" class="flex-1 py-3 bg-slate-200 dark:bg-slate-600 text-slate-700 dark:text-slate-200 rounded-xl font-medium">
|
||||
Abbrechen
|
||||
</button>
|
||||
<!-- Show "Speichern" only when NOT already scanned -->
|
||||
<button v-if="!alreadyScannedWarning.show"
|
||||
@click="submitScan(false)"
|
||||
class="flex-1 py-3 bg-green-600 text-white rounded-xl font-bold">
|
||||
Speichern
|
||||
</button>
|
||||
<!-- Show "Überschreiben" only when already scanned -->
|
||||
<button v-else
|
||||
@click="submitScan(true)"
|
||||
class="flex-1 py-3 bg-amber-500 text-white rounded-xl font-bold">
|
||||
Überschreiben
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user