375 lines
13 KiB
PHP
375 lines
13 KiB
PHP
<?php
|
|
|
|
class WarehouseStocktakePWAController extends mfBaseController {
|
|
|
|
protected $user;
|
|
|
|
protected function init() {
|
|
$this->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: WH:articleId:articleNumber
|
|
if (preg_match('/^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 = WarehouseCategoryModel::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
|
|
*/
|
|
protected function searchArticlesAction() {
|
|
$query = $this->request->query;
|
|
|
|
if (!$query || strlen($query) < 2) {
|
|
self::returnJson(['success' => true, 'articles' => []]);
|
|
return;
|
|
}
|
|
|
|
$db = FronkDB::singleton();
|
|
$escapedQuery = $db->escape($query);
|
|
|
|
$result = $db->query("SELECT id, articleNumber, title, unit
|
|
FROM WarehouseArticle
|
|
WHERE (articleNumber LIKE '%{$escapedQuery}%' OR title LIKE '%{$escapedQuery}%')
|
|
AND (isEndOfLife IS NULL OR isEndOfLife = 0)
|
|
LIMIT 20");
|
|
|
|
$articles = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$articles[] = [
|
|
'id' => intval($row['id']),
|
|
'articleNumber' => $row['articleNumber'],
|
|
'title' => $row['title'],
|
|
'unit' => $row['unit'] ?? 'Stk.',
|
|
];
|
|
}
|
|
|
|
self::returnJson(['success' => true, 'articles' => $articles]);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
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;
|
|
}
|
|
|
|
// 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->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,
|
|
]
|
|
]);
|
|
}
|
|
}
|