Files
thetool/application/MobileApp/Modules/Lager/Inventur/InventurHandler.php
2026-01-13 12:44:45 +01:00

444 lines
16 KiB
PHP

<?php
require_once APPDIR . 'MobileApp/Shared/MobileAppBaseHandler.php';
/**
* Inventur (Stocktake) Handler
*
* Handles all endpoints for the Lager > Inventur module.
* API Base: /MobileApp/Lager/Inventur/{action}
*/
class InventurHandler extends MobileAppBaseHandler {
protected $requiredPermission = 'WarehouseUser';
/**
* Get active stocktakes
* GET /MobileApp/Lager/Inventur/getActiveStocktakes
*/
public 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
*/
public 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
*/
public function getArticleAction() {
$code = $this->request->code;
if (!$code) {
self::returnJson(['success' => false, 'message' => 'Kein Code angegeben']);
return;
}
$articleId = null;
if (preg_match('/^(?:WA|WH):(\d+):/', $code, $matches)) {
$articleId = intval($matches[1]);
} else {
$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;
}
$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
*/
public function searchArticlesAction() {
$query = $this->request->query ?? '';
$categoryId = intval($this->request->categoryId ?? 0);
$db = $this->db();
$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 categories
*/
public function getCategoriesAction() {
$db = $this->db();
$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 already scanned
*/
public 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 = $this->db();
$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 scan
*/
public function submitScanAction() {
$postData = $this->getPostData();
$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;
}
$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;
}
$article = WarehouseArticleModel::get($articleId);
if (!$article) {
self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']);
return;
}
$db = $this->db();
if ($overwrite && $overwriteItemId) {
$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;
$db->query("UPDATE WarehouseStocktakeItem SET overwrittenById = {$itemId} WHERE id = {$overwriteItemId}");
$finalQuantity = $quantity;
WarehouseStocktakeLogModel::log($stocktakeId, 'overwritten', $itemId, [
'articleId' => $articleId,
'articleNumber' => $article->articleNumber,
'articleTitle' => $article->title,
'quantity' => $quantity,
'overwrittenItemId' => $overwriteItemId,
]);
$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;
}
$existing = WarehouseStocktakeItemModel::getFirst([
'stocktakeId' => $stocktakeId,
'articleId' => $articleId,
'overwrittenById' => null
]);
if ($existing) {
$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 {
$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;
}
$stocktake->updateProgress();
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 my scans
*/
public function getMyScansAction() {
$stocktakeId = intval($this->request->stocktakeId);
if (!$stocktakeId) {
self::returnJson(['success' => false, 'message' => 'Keine Inventur-ID angegeben']);
return;
}
$db = $this->db();
$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
*/
public 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 = $this->db();
$totalResult = $db->query("SELECT COUNT(*) as count FROM WarehouseStocktakeItem WHERE stocktakeId = {$stocktakeId}");
$totalRow = $totalResult->fetch_assoc();
$totalScanned = intval($totalRow['count']);
$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,
]
]);
}
}