'articleId', 'text' => 'Artikel', 'required' => true, 'modal' => ['type' => 'autocomplete', 'apiUrl' => '/WarehouseArticle/autocomplete'], 'table' => ['priority' => 10]], ['key' => 'countedQuantity', 'text' => 'Menge', 'required' => true, 'modal' => ['type' => 'number'], 'table' => ['priority' => 9]], ['key' => 'rack', 'text' => 'Regal', 'required' => false, 'modal' => ['type' => 'text'], 'table' => ['priority' => 8]], ['key' => 'shelf', 'text' => 'Fach', 'required' => false, 'modal' => ['type' => 'text'], 'table' => ['priority' => 7]], ['key' => 'note', 'text' => 'Notiz', 'required' => false, 'modal' => ['type' => 'textarea'], 'table' => ['priority' => 6]], ['key' => 'scannedAt', 'text' => 'Gescannt am', 'required' => false, 'modal' => false, 'table' => ['priority' => 5]], ['key' => 'actions', 'text' => 'Aktionen', 'required' => false, 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]], ]; protected array $permissionCheck = ['WarehouseUser']; protected function formatRow($row) { // Format article if ($row['articleId']) { $article = WarehouseArticleModel::get($row['articleId']); $row['articleId'] = $article ? "[{$article->articleNumber}] {$article->title}" : 'Unbekannt'; } // Format scannedAt if ($row['scannedAt']) { $row['scannedAt'] = date('d.m.Y H:i', $row['scannedAt']); } else { $row['scannedAt'] = '-'; } return $row; } /** * Add item via scan (used by PWA) */ protected function scanItemAction() { $stocktakeId = intval($this->request->stocktakeId); $articleId = intval($this->request->articleId); $quantity = floatval($this->request->quantity); $rack = $this->request->rack ?? null; $shelf = $this->request->shelf ?? null; $note = $this->request->note ?? null; if (!$stocktakeId || !$articleId) { self::returnJson(['success' => false, 'message' => 'Fehlende Parameter']); 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; } // Check if this article was already scanned in this stocktake $existing = WarehouseStocktakeItemModel::getFirst([ 'stocktakeId' => $stocktakeId, 'articleId' => $articleId ]); $db = FronkDB::singleton(); 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->me->id} WHERE id = {$existing->id}"); $itemId = $existing->id; $message = "Artikel aktualisiert: {$article->title} (Neue Menge: {$newQuantity})"; } 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->me->id}, {$this->me->id}, " . time() . ")"); $itemId = $db->insert_id; $message = "Artikel hinzugefügt: {$article->title} (Menge: {$quantity})"; } // Update stocktake progress $stocktake->updateProgress(); // Log the scan WarehouseStocktakeLogModel::log($stocktakeId, 'scanned', $itemId, [ 'articleId' => $articleId, 'articleNumber' => $article->articleNumber, 'articleTitle' => $article->title, 'quantity' => $quantity, 'rack' => $rack, 'shelf' => $shelf, ]); self::returnJson([ 'success' => true, 'message' => $message, 'item' => [ 'id' => $itemId, 'articleId' => $articleId, 'articleNumber' => $article->articleNumber, 'articleTitle' => $article->title, 'countedQuantity' => $existing ? ($existing->countedQuantity + $quantity) : $quantity, 'rack' => $rack, 'shelf' => $shelf, ], 'totalScanned' => $stocktake->totalScannedItems + 1, ]); } /** * Get article info by QR code or article number */ protected function getArticleByCodeAction() { $code = $this->request->code; if (!$code) { self::returnJson(['success' => false, 'message' => 'Kein Code angegeben']); return; } // Try to parse QR code format: WA:articleId:articleNumber (Warehouse Article) // Also accept WH: for backwards compatibility $articleId = null; 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; } self::returnJson([ 'success' => true, 'article' => [ 'id' => $article->id, 'articleNumber' => $article->articleNumber, 'title' => $article->title, 'description' => $article->description ?? '', 'unit' => $article->unit ?? 'Stk.', ] ]); } }