cleanup warehousestocktake progressive web app
This commit is contained in:
@@ -1,494 +0,0 @@
|
||||
<?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: 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,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user