From 6942aa4de1459b2f9e8671cad7fcf75e48baafbd Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Mon, 2 Feb 2026 10:17:52 +0100 Subject: [PATCH] cleanup warehousestocktake progressive web app --- .../default/MobileApp/WarehouseStocktake.php | 10 - .../VueViews/WarehouseStocktakePWA.php | 973 ------------------ .../WarehouseStocktakePWAController.php | 494 --------- public/mobile/warehouse-stocktake/app.css | 168 --- public/mobile/warehouse-stocktake/app.js | 182 ---- .../components/LoginScreen.js | 207 ---- .../warehouse-stocktake/components/Scanner.js | 607 ----------- .../components/StocktakeList.js | 266 ----- .../mobile/warehouse-stocktake/manifest.json | 27 - public/mobile/warehouse-stocktake/sw.js | 109 -- 10 files changed, 3043 deletions(-) delete mode 100644 Layout/default/MobileApp/WarehouseStocktake.php delete mode 100644 Layout/default/VueViews/WarehouseStocktakePWA.php delete mode 100644 application/WarehouseStocktakePWA/WarehouseStocktakePWAController.php delete mode 100644 public/mobile/warehouse-stocktake/app.css delete mode 100644 public/mobile/warehouse-stocktake/app.js delete mode 100644 public/mobile/warehouse-stocktake/components/LoginScreen.js delete mode 100644 public/mobile/warehouse-stocktake/components/Scanner.js delete mode 100644 public/mobile/warehouse-stocktake/components/StocktakeList.js delete mode 100644 public/mobile/warehouse-stocktake/manifest.json delete mode 100644 public/mobile/warehouse-stocktake/sw.js diff --git a/Layout/default/MobileApp/WarehouseStocktake.php b/Layout/default/MobileApp/WarehouseStocktake.php deleted file mode 100644 index 738bab8ed..000000000 --- a/Layout/default/MobileApp/WarehouseStocktake.php +++ /dev/null @@ -1,10 +0,0 @@ - 'Lager Inventur', - 'appName' => 'Inventur', - 'manifestPath' => '/mobile/warehouse-stocktake/manifest.json', - 'appJsPath' => '/mobile/warehouse-stocktake/app.js', - 'swPath' => '/mobile/warehouse-stocktake/sw.js', - 'additionalStylesheets' => ['/mobile/warehouse-stocktake/app.css'], -]; -require __DIR__ . '/Base.php'; diff --git a/Layout/default/VueViews/WarehouseStocktakePWA.php b/Layout/default/VueViews/WarehouseStocktakePWA.php deleted file mode 100644 index 5183c2451..000000000 --- a/Layout/default/VueViews/WarehouseStocktakePWA.php +++ /dev/null @@ -1,973 +0,0 @@ -id) { - $openreplayUserId = !empty($user->email) ? $user->email : (string) $user->id; - $openreplayWorkerId = (string) $user->id; - } -} -?> - - - - - - Inventur Scanner - - - - - - - - - - - - - - - - - - - - -
- - - - diff --git a/application/WarehouseStocktakePWA/WarehouseStocktakePWAController.php b/application/WarehouseStocktakePWA/WarehouseStocktakePWAController.php deleted file mode 100644 index 64a8b2e1b..000000000 --- a/application/WarehouseStocktakePWA/WarehouseStocktakePWAController.php +++ /dev/null @@ -1,494 +0,0 @@ -needlogin = true; - - $me = mfValuecache::singleton()->get("me"); - if (!$me) { - $me = new User(); - $me->loadMe(); - mfValuecache::singleton()->set("me", $me); - } - $this->me = $me; - $this->user = $me; - $this->layout()->set("me", $me); - - // Check permission - if (!$me->can('WarehouseUser')) { - $this->redirect("Dashboard"); - } - } - - /** - * Main PWA View - */ - public function indexAction() { - $this->layout()->setTemplate("VueViews/WarehouseStocktakePWA"); - $this->layout()->set("JSGlobals", [ - 'BASE_PATH' => '/WarehouseStocktakePWA', - 'USER_ID' => $this->user->id, - 'USER_NAME' => $this->user->name, - ]); - } - - /** - * Logout - */ - protected function logoutAction() { - mfLoginController::staticLogout(); - $this->redirect('/WarehouseStocktakePWA'); - } - - /** - * Get active stocktakes that user can participate in - */ - protected function getActiveStocktakesAction() { - $stocktakes = WarehouseStocktakeModel::getAll(['status' => 'in_progress']); - - $result = []; - foreach ($stocktakes as $stocktake) { - $location = $stocktake->getLocation(); - $result[] = [ - 'id' => $stocktake->id, - 'stocktakeNumber' => $stocktake->stocktakeNumber, - 'title' => $stocktake->title, - 'locationName' => $location ? $location->title : 'Unbekannt', - 'totalScannedItems' => $stocktake->totalScannedItems, - 'startedAt' => $stocktake->startedAt ? date('d.m.Y H:i', $stocktake->startedAt) : null, - ]; - } - - self::returnJson(['success' => true, 'stocktakes' => $result]); - } - - /** - * Get stocktake details - */ - protected function getStocktakeAction() { - $id = intval($this->request->id); - if (!$id) { - self::returnJson(['success' => false, 'message' => 'Keine Inventur-ID angegeben']); - return; - } - - $stocktake = WarehouseStocktakeModel::get($id); - if (!$stocktake) { - self::returnJson(['success' => false, 'message' => 'Inventur nicht gefunden']); - return; - } - - $location = $stocktake->getLocation(); - - self::returnJson([ - 'success' => true, - 'stocktake' => [ - 'id' => $stocktake->id, - 'stocktakeNumber' => $stocktake->stocktakeNumber, - 'title' => $stocktake->title, - 'status' => $stocktake->status, - 'locationId' => $stocktake->warehouseLocationId, - 'locationName' => $location ? $location->title : 'Unbekannt', - 'totalScannedItems' => $stocktake->totalScannedItems, - 'startedAt' => $stocktake->startedAt ? date('d.m.Y H:i', $stocktake->startedAt) : null, - ] - ]); - } - - /** - * Get article by QR code or article number - */ - protected function getArticleAction() { - $code = $this->request->code; - - if (!$code) { - self::returnJson(['success' => false, 'message' => 'Kein Code angegeben']); - return; - } - - $articleId = null; - - // Try to parse QR code format: WA:articleId:articleNumber (Warehouse Article) - // Also accept WH: for backwards compatibility - if (preg_match('/^(?:WA|WH):(\d+):/', $code, $matches)) { - $articleId = intval($matches[1]); - } else { - // Try to find by article number - $article = WarehouseArticleModel::getFirst(['articleNumber' => $code]); - if ($article) { - $articleId = $article->id; - } - } - - if (!$articleId) { - self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']); - return; - } - - $article = WarehouseArticleModel::get($articleId); - if (!$article) { - self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']); - return; - } - - // Get category name - $category = WarehouseCategory::get($article->category_id); - - self::returnJson([ - 'success' => true, - 'article' => [ - 'id' => $article->id, - 'articleNumber' => $article->articleNumber, - 'title' => $article->title, - 'description' => $article->description ?? '', - 'unit' => $article->unit ?? 'Stk.', - 'categoryName' => $category ? $category->name : '', - ] - ]); - } - - /** - * Search articles by text with optional category filter - */ - protected function searchArticlesAction() { - $query = $this->request->query ?? ''; - $categoryId = intval($this->request->categoryId ?? 0); - - $db = FronkDB::singleton(); - $conditions = ["(isEndOfLife IS NULL OR isEndOfLife = 0)"]; - - if ($query && strlen($query) >= 2) { - $escapedQuery = $db->escape($query); - $conditions[] = "(articleNumber LIKE '%{$escapedQuery}%' OR title LIKE '%{$escapedQuery}%' OR description LIKE '%{$escapedQuery}%')"; - } - - if ($categoryId > 0) { - $conditions[] = "category_id = {$categoryId}"; - } - - if (count($conditions) === 1 && !$categoryId) { - self::returnJson(['success' => true, 'articles' => []]); - return; - } - - $whereClause = implode(' AND ', $conditions); - $result = $db->query("SELECT id, articleNumber, title, unit, category_id - FROM WarehouseArticle - WHERE {$whereClause} - ORDER BY title ASC - LIMIT 50"); - - $articles = []; - while ($row = $result->fetch_assoc()) { - $articles[] = [ - 'id' => intval($row['id']), - 'articleNumber' => $row['articleNumber'], - 'title' => $row['title'], - 'unit' => $row['unit'] ?? 'Stk.', - 'categoryId' => intval($row['category_id'] ?? 0), - ]; - } - - self::returnJson(['success' => true, 'articles' => $articles]); - } - - /** - * Get all categories for browsing - */ - protected function getCategoriesAction() { - $db = FronkDB::singleton(); - $res = $db->query("SELECT id, name FROM WarehouseCategory ORDER BY name ASC"); - - $categories = []; - while ($row = $res->fetch_assoc()) { - $categories[] = [ - 'id' => intval($row['id']), - 'name' => $row['name'], - ]; - } - self::returnJson(['success' => true, 'categories' => $categories]); - } - - /** - * Check if article is already scanned in stocktake - */ - protected function checkAlreadyScannedAction() { - $stocktakeId = intval($this->request->stocktakeId); - $articleId = intval($this->request->articleId); - - if (!$stocktakeId || !$articleId) { - self::returnJson(['success' => false, 'message' => 'Fehlende Parameter']); - return; - } - - $existing = WarehouseStocktakeItemModel::getFirst([ - 'stocktakeId' => $stocktakeId, - 'articleId' => $articleId, - 'overwrittenById' => null - ]); - - if ($existing) { - $db = FronkDB::singleton(); - $scannedByResult = $db->query("SELECT name FROM Worker WHERE id = {$existing->scannedBy}"); - $scannedByRow = $scannedByResult->fetch_assoc(); - - self::returnJson([ - 'success' => true, - 'alreadyScanned' => true, - 'existingItem' => [ - 'id' => $existing->id, - 'countedQuantity' => $existing->countedQuantity, - 'scannedAt' => $existing->scannedAt ? date('d.m.Y H:i', $existing->scannedAt) : null, - 'scannedBy' => $scannedByRow ? $scannedByRow['name'] : 'Unbekannt', - ] - ]); - } else { - self::returnJson(['success' => true, 'alreadyScanned' => false]); - } - } - - /** - * Submit a scanned item - */ - protected function submitScanAction() { - $postData = json_decode(file_get_contents('php://input'), true) ?? []; - - $stocktakeId = intval($postData['stocktakeId'] ?? 0); - $articleId = intval($postData['articleId'] ?? 0); - $quantity = floatval($postData['quantity'] ?? 0); - $rack = $postData['rack'] ?? null; - $shelf = $postData['shelf'] ?? null; - $note = $postData['note'] ?? null; - $overwrite = boolval($postData['overwrite'] ?? false); - $overwriteItemId = intval($postData['overwriteItemId'] ?? 0); - - if (!$stocktakeId || !$articleId) { - self::returnJson(['success' => false, 'message' => 'Fehlende Parameter']); - return; - } - - if ($quantity <= 0) { - self::returnJson(['success' => false, 'message' => 'Menge muss größer als 0 sein']); - return; - } - - // Verify stocktake exists and is in progress - $stocktake = WarehouseStocktakeModel::get($stocktakeId); - if (!$stocktake) { - self::returnJson(['success' => false, 'message' => 'Inventur nicht gefunden']); - return; - } - - if ($stocktake->status !== 'in_progress') { - self::returnJson(['success' => false, 'message' => 'Inventur ist nicht aktiv']); - return; - } - - // Verify article exists - $article = WarehouseArticleModel::get($articleId); - if (!$article) { - self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']); - return; - } - - $db = FronkDB::singleton(); - - // If overwrite mode is enabled, mark existing item as overwritten - if ($overwrite && $overwriteItemId) { - // Create new entry - $db->query("INSERT INTO WarehouseStocktakeItem - (stocktakeId, articleId, countedQuantity, rack, shelf, note, scannedAt, scannedBy, createBy, `create`) - VALUES ({$stocktakeId}, {$articleId}, {$quantity}, - " . ($rack ? "'{$db->escape($rack)}'" : "NULL") . ", - " . ($shelf ? "'{$db->escape($shelf)}'" : "NULL") . ", - " . ($note ? "'{$db->escape($note)}'" : "NULL") . ", - " . time() . ", {$this->user->id}, {$this->user->id}, " . time() . ")"); - - $itemId = $db->insert_id; - - // Mark old item as overwritten by new item - $db->query("UPDATE WarehouseStocktakeItem SET overwrittenById = {$itemId} WHERE id = {$overwriteItemId}"); - - $finalQuantity = $quantity; - $isOverwrite = true; - - // Log the overwrite - WarehouseStocktakeLogModel::log($stocktakeId, 'overwritten', $itemId, [ - 'articleId' => $articleId, - 'articleNumber' => $article->articleNumber, - 'articleTitle' => $article->title, - 'quantity' => $quantity, - 'overwrittenItemId' => $overwriteItemId, - ]); - - // Update stocktake progress (don't increase count since we're replacing) - $stocktake->updateProgress(); - - self::returnJson([ - 'success' => true, - 'message' => "'{$article->title}' überschrieben ({$quantity} {$article->unit})", - 'item' => [ - 'id' => $itemId, - 'articleId' => $articleId, - 'articleNumber' => $article->articleNumber, - 'articleTitle' => $article->title, - 'countedQuantity' => $finalQuantity, - 'unit' => $article->unit ?? 'Stk.', - 'rack' => $rack, - 'shelf' => $shelf, - 'isOverwrite' => true, - ] - ]); - return; - } - - // Check if this article was already scanned in this stocktake (non-overwritten) - $existing = WarehouseStocktakeItemModel::getFirst([ - 'stocktakeId' => $stocktakeId, - 'articleId' => $articleId, - 'overwrittenById' => null - ]); - - if ($existing) { - // Update existing entry - add to quantity - $newQuantity = $existing->countedQuantity + $quantity; - $db->query("UPDATE WarehouseStocktakeItem SET - countedQuantity = {$newQuantity}, - rack = " . ($rack ? "'{$db->escape($rack)}'" : "rack") . ", - shelf = " . ($shelf ? "'{$db->escape($shelf)}'" : "shelf") . ", - scannedAt = " . time() . ", - scannedBy = {$this->user->id} - WHERE id = {$existing->id}"); - - $itemId = $existing->id; - $finalQuantity = $newQuantity; - $isUpdate = true; - } else { - // Create new entry - $db->query("INSERT INTO WarehouseStocktakeItem - (stocktakeId, articleId, countedQuantity, rack, shelf, note, scannedAt, scannedBy, createBy, `create`) - VALUES ({$stocktakeId}, {$articleId}, {$quantity}, - " . ($rack ? "'{$db->escape($rack)}'" : "NULL") . ", - " . ($shelf ? "'{$db->escape($shelf)}'" : "NULL") . ", - " . ($note ? "'{$db->escape($note)}'" : "NULL") . ", - " . time() . ", {$this->user->id}, {$this->user->id}, " . time() . ")"); - - $itemId = $db->insert_id; - $finalQuantity = $quantity; - $isUpdate = false; - } - - // Update stocktake progress - $stocktake->updateProgress(); - - // Log the scan - WarehouseStocktakeLogModel::log($stocktakeId, 'scanned', $itemId, [ - 'articleId' => $articleId, - 'articleNumber' => $article->articleNumber, - 'articleTitle' => $article->title, - 'quantity' => $quantity, - 'totalQuantity' => $finalQuantity, - 'rack' => $rack, - 'shelf' => $shelf, - 'isUpdate' => $isUpdate, - ]); - - self::returnJson([ - 'success' => true, - 'message' => $isUpdate - ? "Menge für '{$article->title}' erhöht auf {$finalQuantity}" - : "'{$article->title}' hinzugefügt ({$quantity} {$article->unit})", - 'item' => [ - 'id' => $itemId, - 'articleId' => $articleId, - 'articleNumber' => $article->articleNumber, - 'articleTitle' => $article->title, - 'countedQuantity' => $finalQuantity, - 'unit' => $article->unit ?? 'Stk.', - 'rack' => $rack, - 'shelf' => $shelf, - 'isUpdate' => $isUpdate, - ] - ]); - } - - /** - * Get recent scans for current user in a stocktake - */ - protected function getMyScansAction() { - $stocktakeId = intval($this->request->stocktakeId); - - if (!$stocktakeId) { - self::returnJson(['success' => false, 'message' => 'Keine Inventur-ID angegeben']); - return; - } - - $db = FronkDB::singleton(); - $result = $db->query("SELECT si.*, wa.articleNumber, wa.title as articleTitle, wa.unit - FROM WarehouseStocktakeItem si - JOIN WarehouseArticle wa ON wa.id = si.articleId - WHERE si.stocktakeId = {$stocktakeId} - AND si.scannedBy = {$this->user->id} - ORDER BY si.scannedAt DESC - LIMIT 50"); - - $items = []; - while ($row = $result->fetch_assoc()) { - $items[] = [ - 'id' => intval($row['id']), - 'articleId' => intval($row['articleId']), - 'articleNumber' => $row['articleNumber'], - 'articleTitle' => $row['articleTitle'], - 'countedQuantity' => floatval($row['countedQuantity']), - 'unit' => $row['unit'] ?? 'Stk.', - 'rack' => $row['rack'], - 'shelf' => $row['shelf'], - 'scannedAt' => $row['scannedAt'] ? date('H:i', $row['scannedAt']) : null, - ]; - } - - self::returnJson(['success' => true, 'items' => $items]); - } - - /** - * Get progress stats - */ - protected function getProgressAction() { - $stocktakeId = intval($this->request->stocktakeId); - - if (!$stocktakeId) { - self::returnJson(['success' => false, 'message' => 'Keine Inventur-ID angegeben']); - return; - } - - $stocktake = WarehouseStocktakeModel::get($stocktakeId); - if (!$stocktake) { - self::returnJson(['success' => false, 'message' => 'Inventur nicht gefunden']); - return; - } - - $db = FronkDB::singleton(); - - // Total scanned items - $totalResult = $db->query("SELECT COUNT(*) as count FROM WarehouseStocktakeItem WHERE stocktakeId = {$stocktakeId}"); - $totalRow = $totalResult->fetch_assoc(); - $totalScanned = intval($totalRow['count']); - - // My scanned items - $myResult = $db->query("SELECT COUNT(*) as count FROM WarehouseStocktakeItem WHERE stocktakeId = {$stocktakeId} AND scannedBy = {$this->user->id}"); - $myRow = $myResult->fetch_assoc(); - $myScanned = intval($myRow['count']); - - self::returnJson([ - 'success' => true, - 'progress' => [ - 'totalScanned' => $totalScanned, - 'myScanned' => $myScanned, - 'status' => $stocktake->status, - ] - ]); - } -} diff --git a/public/mobile/warehouse-stocktake/app.css b/public/mobile/warehouse-stocktake/app.css deleted file mode 100644 index 7aa7294d7..000000000 --- a/public/mobile/warehouse-stocktake/app.css +++ /dev/null @@ -1,168 +0,0 @@ -/** - * Warehouse Stocktake PWA - App-Specific Styles - */ - -/* QR Scanner Container */ -#qr-reader { - width: 100%; - max-width: 400px; - margin: 0 auto; - border-radius: 0.5rem; - overflow: hidden; -} - -#qr-reader video { - border-radius: 0.5rem; -} - -/* Hide default html5-qrcode UI elements we don't need */ -#qr-reader__status_span, -#qr-reader__dashboard_section_csr, -#qr-reader__dashboard_section_swaplink { - display: none !important; -} - -/* Scanner frame styling */ -#qr-reader__scan_region { - background: transparent !important; -} - -#qr-reader__scan_region img { - opacity: 0.3; -} - -/* Keypad styling */ -.keypad-button { - min-height: 60px; -} - -/* Numeric display */ -.quantity-display { - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; -} - -/* Tab indicator animation */ -.tab-indicator { - transition: transform 0.3s ease, width 0.3s ease; -} - -/* Card hover effect */ -.stocktake-card:active { - transform: scale(0.98); -} - -/* Search results scrolling */ -.search-results { - max-height: calc(100vh - 280px); - overflow-y: auto; -} - -/* History list */ -.history-list { - max-height: calc(100vh - 200px); - overflow-y: auto; -} - -/* Already scanned badge pulse */ -@keyframes pulse-amber { - 0%, 100% { - box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); - } - 50% { - box-shadow: 0 0 0 8px rgba(245, 158, 11, 0); - } -} - -.already-scanned-pulse { - animation: pulse-amber 2s ease-in-out infinite; -} - -/* Toast slide up animation (complementing base.css) */ -.toast-enter-active { - animation: toast-slide-up 0.3s ease-out; -} - -.toast-leave-active { - animation: toast-slide-down 0.3s ease-in; -} - -@keyframes toast-slide-up { - from { - transform: translateY(100%); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -@keyframes toast-slide-down { - from { - transform: translateY(0); - opacity: 1; - } - to { - transform: translateY(100%); - opacity: 0; - } -} - -/* Settings panel slide */ -.settings-panel { - transform: translateX(100%); - transition: transform 0.3s ease-out; -} - -.settings-panel.open { - transform: translateX(0); -} - -/* Loading spinner */ -.spinner { - border: 3px solid rgba(0, 83, 132, 0.1); - border-top-color: #005384; - border-radius: 50%; - width: 40px; - height: 40px; - animation: spin 1s linear infinite; -} - -/* Dark mode specific overrides */ -.dark .spinner { - border-color: rgba(250, 196, 27, 0.1); - border-top-color: #fac41b; -} - -/* Scan success flash */ -@keyframes scan-flash { - 0% { - background-color: rgba(34, 197, 94, 0.3); - } - 100% { - background-color: transparent; - } -} - -.scan-success-flash { - animation: scan-flash 0.5s ease-out; -} - -/* Custom scrollbar for webkit browsers */ -.custom-scrollbar::-webkit-scrollbar { - width: 6px; -} - -.custom-scrollbar::-webkit-scrollbar-track { - background: transparent; -} - -.custom-scrollbar::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.2); - border-radius: 3px; -} - -.dark .custom-scrollbar::-webkit-scrollbar-thumb { - background-color: rgba(255, 255, 255, 0.2); -} diff --git a/public/mobile/warehouse-stocktake/app.js b/public/mobile/warehouse-stocktake/app.js deleted file mode 100644 index b7b09ab45..000000000 --- a/public/mobile/warehouse-stocktake/app.js +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Warehouse Stocktake PWA - Main Vue Application - * - * This is the entry point for the Warehouse Stocktake PWA. - * It manages authentication state and routes between views. - */ - -// Import shared modules -import { api, authState, checkAuth, login, logout } from '/mobile/shared/auth.js'; - -// Import components -import LoginScreen from './components/LoginScreen.js'; -import StocktakeList from './components/StocktakeList.js'; -import Scanner from './components/Scanner.js'; - -const { createApp, ref, computed, onMounted, watch, nextTick } = Vue; - -const App = { - components: { - LoginScreen, - StocktakeList, - Scanner - }, - - setup() { - // ==================== STATE ==================== - const currentView = ref('loading'); // 'loading', 'login', 'list', 'scanner' - const user = ref(null); - const selectedStocktake = ref(null); - const toast = ref({ show: false, message: '', type: 'success' }); - const theme = ref('system'); - - // ==================== THEME ==================== - const applyTheme = () => { - const isDark = localStorage.theme === 'dark' || - (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches); - document.documentElement.classList.toggle('dark', isDark); - - const metaThemeColor = document.querySelector('meta[name="theme-color"]'); - if (metaThemeColor) { - metaThemeColor.setAttribute('content', isDark ? '#0f172a' : '#005384'); - } - }; - - const setTheme = (newTheme) => { - theme.value = newTheme; - if (newTheme === 'system') { - localStorage.removeItem('theme'); - } else { - localStorage.setItem('theme', newTheme); - } - applyTheme(); - }; - - // ==================== AUTH ==================== - const handleLogin = async (credentials) => { - const result = await login(credentials); - if (result.success) { - user.value = result.user; - currentView.value = 'list'; - showToast('Erfolgreich angemeldet', 'success'); - } - return result; - }; - - const handleLogout = async () => { - await logout(); - user.value = null; - selectedStocktake.value = null; - currentView.value = 'login'; - showToast('Abgemeldet', 'success'); - }; - - // ==================== NAVIGATION ==================== - const openScanner = (stocktake) => { - selectedStocktake.value = stocktake; - currentView.value = 'scanner'; - }; - - const closeScanner = () => { - selectedStocktake.value = null; - currentView.value = 'list'; - }; - - // ==================== TOAST ==================== - const showToast = (message, type = 'success') => { - toast.value = { show: true, message, type }; - setTimeout(() => { - toast.value.show = false; - }, 3000); - }; - - // ==================== LIFECYCLE ==================== - onMounted(async () => { - // Initialize theme - const savedTheme = localStorage.getItem('theme'); - if (savedTheme) { - theme.value = savedTheme; - } - applyTheme(); - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); - - // Check authentication - const result = await checkAuth(); - if (result.authenticated) { - user.value = result.user; - currentView.value = 'list'; - } else { - currentView.value = 'login'; - } - }); - - return { - // State - currentView, - user, - selectedStocktake, - toast, - theme, - - // Methods - handleLogin, - handleLogout, - openScanner, - closeScanner, - showToast, - setTheme, - }; - }, - - template: ` -
- -
-
- - -
Lädt...
-
-
- - - - - - - - - - - - -
-
- {{ toast.message }} -
-
-
-
- ` -}; - -// Mount the app -createApp(App).mount('#app'); diff --git a/public/mobile/warehouse-stocktake/components/LoginScreen.js b/public/mobile/warehouse-stocktake/components/LoginScreen.js deleted file mode 100644 index b7b5382d9..000000000 --- a/public/mobile/warehouse-stocktake/components/LoginScreen.js +++ /dev/null @@ -1,207 +0,0 @@ -/** - * LoginScreen Component - * - * Displays the login form for the PWA. - * Handles username/password authentication with remember me option. - */ - -export default { - name: 'LoginScreen', - emits: ['login', 'set-theme'], - props: { - theme: { - type: String, - default: 'system' - } - }, - - setup(props, { emit }) { - const { ref } = Vue; - - // Form state - const username = ref(''); - const password = ref(''); - const rememberMe = ref(true); - const error = ref(''); - const loading = ref(false); - const showPassword = ref(false); - - // Theme picker (shown on first visit) - const showThemePicker = ref(!localStorage.getItem('theme')); - - const handleSubmit = async () => { - if (!username.value || !password.value) { - error.value = 'Bitte Benutzername und Passwort eingeben'; - return; - } - - loading.value = true; - error.value = ''; - - try { - const result = await new Promise((resolve) => { - // Emit returns undefined, we need to wait for parent to call back - const loginPromise = emit('login', { - username: username.value, - password: password.value, - rememberMe: rememberMe.value - }); - - // The parent will return the result - resolve(loginPromise); - }); - - if (result && !result.success) { - error.value = result.message || 'Login fehlgeschlagen'; - if (result.requires2FA) { - error.value = 'Zwei-Faktor-Authentifizierung wird derzeit nicht unterstützt.'; - } - } - } catch (e) { - error.value = 'Ein Fehler ist aufgetreten'; - } finally { - loading.value = false; - } - }; - - const selectTheme = (newTheme) => { - emit('set-theme', newTheme); - showThemePicker.value = false; - }; - - return { - username, - password, - rememberMe, - error, - loading, - showPassword, - showThemePicker, - handleSubmit, - selectTheme - }; - }, - - template: ` -
- - -
-
-

Willkommen!

-

Wähle dein bevorzugtes Farbschema.

-
- - - -
-
-
-
- - -
- -
- Logo - -
- - -

- Lager Inventur -

- - -
- -
- - -
- - -
- -
- - -
-
- - - - - -
-

{{ error }}

-
- - - -
- - -
-

- powered by XINON GmbH -

-
-
-
- ` -}; diff --git a/public/mobile/warehouse-stocktake/components/Scanner.js b/public/mobile/warehouse-stocktake/components/Scanner.js deleted file mode 100644 index 64dc52088..000000000 --- a/public/mobile/warehouse-stocktake/components/Scanner.js +++ /dev/null @@ -1,607 +0,0 @@ -/** - * Scanner Component - * - * The main scanning interface for the stocktake. - * Features: - * - QR code scanning via camera - * - Manual article search - * - Quantity input with custom keypad - * - Recent scans list - */ - -import { api } from '/mobile/shared/auth.js'; - -export default { - name: 'Scanner', - emits: ['close', 'toast'], - props: { - stocktake: { - type: Object, - required: true - }, - user: { - type: Object, - required: true - } - }, - - setup(props, { emit }) { - const { ref, onMounted, onUnmounted, nextTick, computed } = Vue; - - // ==================== STATE ==================== - const currentTab = ref('scan'); // 'scan', 'search', 'history' - const isLoading = ref(false); - - // Scanner state - const scanner = ref(null); - const isScannerActive = ref(false); - const scannerError = ref(''); - - // Article state - const scannedArticle = ref(null); - const quantity = ref('1'); - const rack = ref(''); - const shelf = ref(''); - - // Search state - const searchQuery = ref(''); - const searchResults = ref([]); - const categories = ref([]); - const selectedCategory = ref(0); - const isSearching = ref(false); - - // History state - const recentScans = ref([]); - const isLoadingHistory = ref(false); - - // Already scanned warning - const alreadyScannedWarning = ref(null); - - // Custom keypad - const showKeypad = ref(false); - - // ==================== COMPUTED ==================== - const canSubmit = computed(() => { - return scannedArticle.value && - parseFloat(quantity.value) > 0 && - !isLoading.value; - }); - - // ==================== SCANNER ==================== - const startScanner = async () => { - scannerError.value = ''; - - try { - // Initialize scanner - scanner.value = new Html5Qrcode('qr-reader'); - - await scanner.value.start( - { facingMode: 'environment' }, - { - fps: 10, - qrbox: { width: 250, height: 250 }, - aspectRatio: 1.0 - }, - onScanSuccess, - onScanError - ); - - isScannerActive.value = true; - } catch (err) { - console.error('Scanner start error:', err); - scannerError.value = 'Kamera konnte nicht gestartet werden. Bitte Berechtigungen prüfen.'; - } - }; - - const stopScanner = async () => { - if (scanner.value && isScannerActive.value) { - try { - await scanner.value.stop(); - } catch (e) { - console.error('Scanner stop error:', e); - } - isScannerActive.value = false; - } - }; - - const onScanSuccess = async (decodedText) => { - // Stop scanner temporarily - await stopScanner(); - - // Look up article - await lookupArticle(decodedText); - }; - - const onScanError = (errorMessage) => { - // Silent - this fires constantly when no QR code is detected - }; - - // ==================== ARTICLE LOOKUP ==================== - const lookupArticle = async (code) => { - isLoading.value = true; - alreadyScannedWarning.value = null; - - try { - const result = await api.get(`WarehouseStocktake/getArticle?code=${encodeURIComponent(code)}`); - - if (result.success) { - scannedArticle.value = result.article; - - // Check if already scanned - const checkResult = await api.get( - `WarehouseStocktake/checkAlreadyScanned?stocktakeId=${props.stocktake.id}&articleId=${result.article.id}` - ); - - if (checkResult.success && checkResult.alreadyScanned) { - alreadyScannedWarning.value = checkResult.existingItem; - } - - // Reset quantity - quantity.value = '1'; - } else { - emit('toast', result.message || 'Artikel nicht gefunden', 'error'); - // Restart scanner - await startScanner(); - } - } catch (e) { - emit('toast', 'Fehler beim Laden des Artikels', 'error'); - await startScanner(); - } finally { - isLoading.value = false; - } - }; - - // ==================== SUBMIT SCAN ==================== - const submitScan = async (overwrite = false) => { - if (!canSubmit.value) return; - - isLoading.value = true; - - try { - const payload = { - stocktakeId: props.stocktake.id, - articleId: scannedArticle.value.id, - quantity: parseFloat(quantity.value), - rack: rack.value || null, - shelf: shelf.value || null, - overwrite: overwrite, - overwriteItemId: overwrite && alreadyScannedWarning.value ? alreadyScannedWarning.value.id : 0 - }; - - const result = await api.post('WarehouseStocktake/submitScan', payload); - - if (result.success) { - emit('toast', result.message, 'success'); - - // Reset state - scannedArticle.value = null; - quantity.value = '1'; - rack.value = ''; - shelf.value = ''; - alreadyScannedWarning.value = null; - - // Restart scanner - await startScanner(); - } else { - emit('toast', result.message || 'Fehler beim Speichern', 'error'); - } - } catch (e) { - emit('toast', 'Netzwerkfehler', 'error'); - } finally { - isLoading.value = false; - } - }; - - // ==================== SEARCH ==================== - const loadCategories = async () => { - const result = await api.get('WarehouseStocktake/getCategories'); - if (result.success) { - categories.value = result.categories; - } - }; - - const searchArticles = async () => { - if (searchQuery.value.length < 2 && !selectedCategory.value) { - searchResults.value = []; - return; - } - - isSearching.value = true; - - try { - const params = new URLSearchParams(); - if (searchQuery.value) params.set('query', searchQuery.value); - if (selectedCategory.value) params.set('categoryId', selectedCategory.value); - - const result = await api.get(`WarehouseStocktake/searchArticles?${params}`); - - if (result.success) { - searchResults.value = result.articles; - } - } catch (e) { - console.error('Search error:', e); - } finally { - isSearching.value = false; - } - }; - - const selectSearchResult = async (article) => { - await stopScanner(); - scannedArticle.value = article; - quantity.value = '1'; - currentTab.value = 'scan'; - - // Check if already scanned - const checkResult = await api.get( - `WarehouseStocktake/checkAlreadyScanned?stocktakeId=${props.stocktake.id}&articleId=${article.id}` - ); - - if (checkResult.success && checkResult.alreadyScanned) { - alreadyScannedWarning.value = checkResult.existingItem; - } - }; - - // ==================== HISTORY ==================== - const loadHistory = async () => { - isLoadingHistory.value = true; - - try { - const result = await api.get(`WarehouseStocktake/getMyScans?stocktakeId=${props.stocktake.id}`); - - if (result.success) { - recentScans.value = result.items; - } - } catch (e) { - console.error('History load error:', e); - } finally { - isLoadingHistory.value = false; - } - }; - - // ==================== KEYPAD ==================== - const appendDigit = (digit) => { - if (digit === '.' && quantity.value.includes('.')) return; - if (quantity.value === '0' && digit !== '.') { - quantity.value = digit; - } else { - quantity.value += digit; - } - }; - - const deleteDigit = () => { - if (quantity.value.length > 1) { - quantity.value = quantity.value.slice(0, -1); - } else { - quantity.value = '0'; - } - }; - - const clearQuantity = () => { - quantity.value = '0'; - }; - - // ==================== LIFECYCLE ==================== - const handleClose = async () => { - await stopScanner(); - emit('close'); - }; - - const switchTab = async (tab) => { - currentTab.value = tab; - - if (tab === 'scan' && !scannedArticle.value) { - await nextTick(); - await startScanner(); - } else if (tab === 'search') { - await stopScanner(); - await loadCategories(); - } else if (tab === 'history') { - await stopScanner(); - await loadHistory(); - } - }; - - const cancelScan = async () => { - scannedArticle.value = null; - alreadyScannedWarning.value = null; - quantity.value = '1'; - await startScanner(); - }; - - onMounted(async () => { - await startScanner(); - }); - - onUnmounted(async () => { - await stopScanner(); - }); - - return { - // State - currentTab, - isLoading, - isScannerActive, - scannerError, - scannedArticle, - quantity, - rack, - shelf, - searchQuery, - searchResults, - categories, - selectedCategory, - isSearching, - recentScans, - isLoadingHistory, - alreadyScannedWarning, - showKeypad, - canSubmit, - - // Methods - startScanner, - stopScanner, - submitScan, - searchArticles, - selectSearchResult, - loadHistory, - appendDigit, - deleteDigit, - clearQuantity, - handleClose, - switchTab, - cancelScan - }; - }, - - template: ` -
- -
-
- -

- {{ stocktake.title || 'Inventur #' + stocktake.stocktakeNumber }} -

-
-
- - -
- - - -
-
- - -
- -
- -
- -
- - -
-

{{ scannerError }}

- -
- -

- QR-Code scannen oder Artikel suchen -

-
- - -
- -
-
- - - -
-

Bereits gescannt

-

- Menge: {{ alreadyScannedWarning.countedQuantity }}
- Von: {{ alreadyScannedWarning.scannedBy }}
- Am: {{ alreadyScannedWarning.scannedAt }} -

-
-
-
- - -
-

- {{ scannedArticle.title }} -

-

- Art.-Nr.: {{ scannedArticle.articleNumber }} -

-

- Kategorie: {{ scannedArticle.categoryName }} -

-
- - -
- -
- {{ quantity }} -
-
- - -
-
- - -
-
- - -
-
- - -
- - - - -
-
-
- - -
-
- - -
- -
-
-
- -
-

- {{ searchQuery.length < 2 ? 'Mindestens 2 Zeichen eingeben' : 'Keine Artikel gefunden' }} -

-
- -
-
-

{{ article.title }}

-

{{ article.articleNumber }}

-
-
-
- - -
-
-
-
-
-
-
- -
-

Noch keine Scans

-
- -
-
-
-
-

{{ scan.articleTitle }}

-

{{ scan.articleNumber }}

-
-
-

{{ scan.countedQuantity }} {{ scan.unit }}

-

{{ scan.scannedAt }}

-
-
-
-
-
-
- - - -
-
-
- -
{{ quantity }}
- -
-
- - -
-
-
-
-
- ` -}; diff --git a/public/mobile/warehouse-stocktake/components/StocktakeList.js b/public/mobile/warehouse-stocktake/components/StocktakeList.js deleted file mode 100644 index d9b63f49a..000000000 --- a/public/mobile/warehouse-stocktake/components/StocktakeList.js +++ /dev/null @@ -1,266 +0,0 @@ -/** - * StocktakeList Component - * - * Displays a list of active stocktakes that the user can participate in. - * Includes settings menu with theme toggle and logout. - */ - -import { api } from '/mobile/shared/auth.js'; - -export default { - name: 'StocktakeList', - emits: ['select', 'logout', 'set-theme'], - props: { - user: { - type: Object, - default: null - }, - theme: { - type: String, - default: 'system' - } - }, - - setup(props, { emit }) { - const { ref, onMounted } = Vue; - - // State - const stocktakes = ref([]); - const isLoading = ref(true); - const error = ref(''); - const isSettingsOpen = ref(false); - - // Fetch stocktakes - const fetchStocktakes = async () => { - isLoading.value = true; - error.value = ''; - - try { - const result = await api.get('WarehouseStocktake/getActiveStocktakes'); - - if (result.success) { - stocktakes.value = result.stocktakes; - } else { - error.value = result.error || 'Fehler beim Laden'; - } - } catch (e) { - error.value = 'Netzwerkfehler'; - } finally { - isLoading.value = false; - } - }; - - const selectStocktake = (stocktake) => { - emit('select', stocktake); - }; - - const handleLogout = () => { - isSettingsOpen.value = false; - emit('logout'); - }; - - const setTheme = (newTheme) => { - emit('set-theme', newTheme); - }; - - onMounted(() => { - fetchStocktakes(); - }); - - return { - stocktakes, - isLoading, - error, - isSettingsOpen, - fetchStocktakes, - selectStocktake, - handleLogout, - setTheme - }; - }, - - template: ` -
- - -
-
- - -
-
- - - - -
- Logo - -
- - - -
- - -

- Aktive Inventuren -

-

- Angemeldet als {{ user.name }} -

-
- - -
- -
-
-
-
-
-
-
- - -
- - - -

{{ error }}

- -
- - -
- - - -

Keine aktiven Inventuren

-
- - -
-
-
-
-

- {{ stocktake.title || 'Inventur #' + stocktake.stocktakeNumber }} -

-

- {{ stocktake.locationName }} -

-
-
- - Aktiv - -
-
-
-
- {{ stocktake.totalScannedItems || 0 }} - Artikel gescannt -
-
- {{ stocktake.startedAt || 'Nicht gestartet' }} -
-
-
-
-
- - - -
-
-

Einstellungen

- -
- -
- -
-

Angemeldet als

-

{{ user.name }}

-
- - -
-

Farbschema

-
- - - -
-
-
- - -
- - -
- XINON -

- powered by XINON GmbH -

-
-
-
-
-
- ` -}; diff --git a/public/mobile/warehouse-stocktake/manifest.json b/public/mobile/warehouse-stocktake/manifest.json deleted file mode 100644 index 415b4b751..000000000 --- a/public/mobile/warehouse-stocktake/manifest.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "Lager Inventur", - "short_name": "Inventur", - "description": "PWA für Lager-Inventur und Artikelerfassung", - "start_url": "/MobileApp/WarehouseStocktake", - "display": "standalone", - "background_color": "#0f172a", - "theme_color": "#005384", - "orientation": "portrait-primary", - "scope": "/MobileApp/", - "icons": [ - { - "src": "/assets/images/xinon-sm-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "any maskable" - }, - { - "src": "/assets/images/xinon-sm-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "any maskable" - } - ], - "categories": ["business", "productivity"], - "lang": "de-DE" -} diff --git a/public/mobile/warehouse-stocktake/sw.js b/public/mobile/warehouse-stocktake/sw.js deleted file mode 100644 index 8c240672d..000000000 --- a/public/mobile/warehouse-stocktake/sw.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Warehouse Stocktake PWA - Service Worker - * - * Provides basic caching for the app shell (offline-first for static assets). - * API calls are always fetched from network. - */ - -const CACHE_NAME = 'warehouse-stocktake-v1'; - -// Static assets to cache for offline use -const ASSETS_TO_CACHE = [ - '/MobileApp/WarehouseStocktake', - '/mobile/warehouse-stocktake/app.js', - '/mobile/warehouse-stocktake/app.css', - '/mobile/warehouse-stocktake/components/LoginScreen.js', - '/mobile/warehouse-stocktake/components/StocktakeList.js', - '/mobile/warehouse-stocktake/components/Scanner.js', - '/mobile/shared/auth.js', - '/mobile/shared/base.css', - '/assets/images/xinon-full-transparent.png', - '/assets/images/xinon-full-transparent-white.png', - '/assets/images/xinon-sm.png', - '/assets/images/favicon.ico' -]; - -// Install event - cache static assets -self.addEventListener('install', (event) => { - event.waitUntil( - caches.open(CACHE_NAME) - .then(cache => { - console.log('[SW] Caching app shell'); - return cache.addAll(ASSETS_TO_CACHE); - }) - .then(() => self.skipWaiting()) - ); -}); - -// Activate event - clean up old caches -self.addEventListener('activate', (event) => { - event.waitUntil( - caches.keys() - .then(cacheNames => { - return Promise.all( - cacheNames - .filter(name => name !== CACHE_NAME) - .map(name => { - console.log('[SW] Deleting old cache:', name); - return caches.delete(name); - }) - ); - }) - .then(() => self.clients.claim()) - ); -}); - -// Fetch event - network first for API, cache first for assets -self.addEventListener('fetch', (event) => { - const { request } = event; - const url = new URL(request.url); - - // Skip non-GET requests - if (request.method !== 'GET') { - return; - } - - // Skip CDN requests (Vue, Tailwind, etc.) - if (url.hostname !== self.location.hostname) { - return; - } - - // API calls - always go to network (no caching) - if (url.pathname.startsWith('/MobileApp/') && - !url.pathname.endsWith('/WarehouseStocktake') && - url.pathname !== '/MobileApp/WarehouseStocktake') { - return; - } - - // Static assets - cache first, fallback to network - event.respondWith( - caches.match(request) - .then(cachedResponse => { - if (cachedResponse) { - // Return cached version, but also update cache in background - event.waitUntil( - fetch(request) - .then(networkResponse => { - if (networkResponse.ok) { - caches.open(CACHE_NAME) - .then(cache => cache.put(request, networkResponse)); - } - }) - .catch(() => {}) - ); - return cachedResponse; - } - - // Not in cache - fetch from network and cache - return fetch(request) - .then(networkResponse => { - if (networkResponse.ok) { - const responseToCache = networkResponse.clone(); - caches.open(CACHE_NAME) - .then(cache => cache.put(request, responseToCache)); - } - return networkResponse; - }); - }) - ); -});