Xinon mobile/improve

This commit is contained in:
Luca Haid
2026-01-17 12:48:08 +00:00
parent 51c9c5ae7e
commit 1426d769d2
37 changed files with 7502 additions and 1042 deletions
@@ -2,20 +2,10 @@
require_once APPDIR . 'MobileApp/Shared/MobileAppBaseHandler.php';
/**
* Movement (Stock Movement) Handler
*
* Handles all endpoints for the Lager > Movement module.
* API Base: /MobileApp/Lager/Movement/{action}
*/
class MovementHandler extends MobileAppBaseHandler {
protected $requiredPermission = 'WarehouseUser';
/**
* Get available locations (Office + Außenlager only)
* GET /MobileApp/Lager/Movement/getLocations
*/
public function getLocationsAction() {
$allLocations = WarehouseLocationModel::getAll();
$locations = [];
@@ -33,10 +23,6 @@ class MovementHandler extends MobileAppBaseHandler {
self::returnJson(['success' => true, 'locations' => $locations]);
}
/**
* Get article by QR code or article number
* GET /MobileApp/Lager/Movement/getArticle?code=X
*/
public function getArticleAction() {
$code = $this->request->code;
@@ -84,10 +70,6 @@ class MovementHandler extends MobileAppBaseHandler {
]);
}
/**
* Search articles
* GET /MobileApp/Lager/Movement/searchArticles?query=X
*/
public function searchArticlesAction() {
$query = $this->request->query ?? '';
@@ -122,10 +104,6 @@ class MovementHandler extends MobileAppBaseHandler {
self::returnJson(['success' => true, 'articles' => $articles]);
}
/**
* Get reason categories for a movement type
* GET /MobileApp/Lager/Movement/getReasonCategories?type=IN|OUT|ADJUSTMENT
*/
public function getReasonCategoriesAction() {
$type = $this->request->type ?? null;
@@ -142,10 +120,6 @@ class MovementHandler extends MobileAppBaseHandler {
}
}
/**
* Get current stock for an article at a location
* GET /MobileApp/Lager/Movement/getCurrentStock?articleId=X&locationId=X
*/
public function getCurrentStockAction() {
$articleId = intval($this->request->articleId ?? 0);
$locationId = intval($this->request->locationId ?? 0);
@@ -165,10 +139,6 @@ class MovementHandler extends MobileAppBaseHandler {
self::returnJson(['success' => true, 'currentStock' => $currentStock]);
}
/**
* Submit a stock movement
* POST /MobileApp/Lager/Movement/submitMovement
*/
public function submitMovementAction() {
$postData = $this->getPostData();
@@ -284,10 +254,6 @@ class MovementHandler extends MobileAppBaseHandler {
]);
}
/**
* Get recent movements by current user
* GET /MobileApp/Lager/Movement/getMyMovements
*/
public function getMyMovementsAction() {
$locationId = intval($this->request->locationId ?? 0);
$limit = intval($this->request->limit ?? 20);
@@ -330,10 +296,6 @@ class MovementHandler extends MobileAppBaseHandler {
self::returnJson(['success' => true, 'movements' => $movements]);
}
/**
* Get movement types with labels
* GET /MobileApp/Lager/Movement/getMovementTypes
*/
public function getMovementTypesAction() {
$types = [
['value' => 'IN', 'text' => 'Einbuchung', 'icon' => 'plus-circle', 'color' => 'green'],
@@ -343,4 +305,277 @@ class MovementHandler extends MobileAppBaseHandler {
self::returnJson(['success' => true, 'types' => $types]);
}
public function getPendingOrdersAction() {
$db = $this->db();
$result = $db->query("SELECT wo.*, wd.name as distributorName
FROM WarehouseOrder wo
LEFT JOIN WarehouseDistributor wd ON wd.id = wo.distributorId
WHERE wo.status IN ('sent', 'partiallyDelivered')
ORDER BY wo.`create` DESC");
$orders = [];
while ($row = $result->fetch_assoc()) {
$positions = json_decode($row['positions'], true) ?: [];
$totalItems = array_sum(array_column($positions, 'amount'));
// Calculate days since sent
$daysSinceSent = 0;
if (!empty($row['create'])) {
$daysSinceSent = floor((time() - intval($row['create'])) / 86400);
}
$orders[] = [
'id' => intval($row['id']),
'orderNumber' => $row['orderNumber'],
'distributorName' => $row['distributorName'] ?? 'Unbekannt',
'status' => $row['status'],
'statusLabel' => $row['status'] === 'sent' ? 'Versendet' : 'Teilweise geliefert',
'totalItems' => $totalItems,
'positionCount' => count($positions),
'daysSinceSent' => $daysSinceSent,
'create' => date('d.m.Y', $row['create']),
];
}
self::returnJson(['success' => true, 'orders' => $orders]);
}
public function getOrderForReceivingAction() {
$orderId = intval($this->request->orderId ?? 0);
if ($orderId <= 0) {
self::returnJson(['success' => false, 'message' => 'Keine Bestellung angegeben']);
return;
}
$order = WarehouseOrderModel::get($orderId);
if (!$order) {
self::returnJson(['success' => false, 'message' => 'Bestellung nicht gefunden']);
return;
}
if (!in_array($order->status, ['sent', 'partiallyDelivered'])) {
self::returnJson(['success' => false, 'message' => 'Bestellung ist nicht im Status Versendet oder Teilweise geliefert']);
return;
}
$distributor = WarehouseDistributorModel::get($order->distributorId);
$positions = json_decode($order->positions, true) ?: [];
// Get already delivered quantities from linked movements
$linkedMovementIds = $order->linkedMovementIds ? json_decode($order->linkedMovementIds, true) : [];
$deliveredByArticle = [];
foreach ($linkedMovementIds as $movementId) {
$movement = WarehouseMovementModel::get($movementId);
if ($movement && $movement->movementType === 'IN') {
if (!isset($deliveredByArticle[$movement->articleId])) {
$deliveredByArticle[$movement->articleId] = 0;
}
$deliveredByArticle[$movement->articleId] += $movement->quantity;
}
}
// Enrich positions with article details and delivered quantities
$enrichedPositions = [];
foreach ($positions as $index => $pos) {
$articleId = intval($pos['article']);
$article = WarehouseArticleModel::get($articleId);
$orderedQty = floatval($pos['amount']);
$deliveredQty = $deliveredByArticle[$articleId] ?? 0;
$remainingQty = max(0, $orderedQty - $deliveredQty);
$enrichedPositions[] = [
'index' => $index,
'articleId' => $articleId,
'articleNumber' => $article ? $article->articleNumber : '',
'articleTitle' => $article ? $article->title : ($pos['article_text'] ?? 'Unbekannt'),
'unit' => $article ? ($article->unit ?? 'Stk.') : 'Stk.',
'orderedQty' => $orderedQty,
'deliveredQty' => $deliveredQty,
'remainingQty' => $remainingQty,
'receivingQty' => $remainingQty, // Default to remaining
];
}
self::returnJson([
'success' => true,
'order' => [
'id' => $order->id,
'orderNumber' => $order->orderNumber,
'distributorName' => $distributor ? $distributor->name : 'Unbekannt',
'status' => $order->status,
'note' => $order->note,
'create' => date('d.m.Y H:i', $order->create),
],
'positions' => $enrichedPositions
]);
}
public function submitOrderReceivingAction() {
$postData = $this->getPostData();
$orderId = intval($postData['orderId'] ?? 0);
$locationId = intval($postData['locationId'] ?? 0);
$positions = $postData['positions'] ?? [];
$deliveryNoteFileId = $postData['deliveryNoteFileId'] ?? null;
$note = $postData['note'] ?? null;
// Validation
if ($orderId <= 0) {
self::returnJson(['success' => false, 'message' => 'Keine Bestellung angegeben']);
return;
}
if ($locationId <= 0) {
self::returnJson(['success' => false, 'message' => 'Kein Lagerort ausgewählt']);
return;
}
if (empty($positions)) {
self::returnJson(['success' => false, 'message' => 'Keine Positionen angegeben']);
return;
}
$order = WarehouseOrderModel::get($orderId);
if (!$order) {
self::returnJson(['success' => false, 'message' => 'Bestellung nicht gefunden']);
return;
}
if (!in_array($order->status, ['sent', 'partiallyDelivered'])) {
self::returnJson(['success' => false, 'message' => 'Bestellung ist nicht im Status Versendet oder Teilweise geliefert']);
return;
}
$db = $this->db();
$createdMovementIds = [];
$totalReceived = 0;
// Create movements for each position with quantity > 0
foreach ($positions as $pos) {
$articleId = intval($pos['articleId'] ?? 0);
$quantity = floatval($pos['quantity'] ?? 0);
if ($articleId <= 0 || $quantity <= 0) {
continue;
}
// Find or create WarehouseItem
$existingItems = WarehouseItemModel::getAll([
'articleId' => $articleId,
'warehouseLocationId' => $locationId
]);
$warehouseItem = count($existingItems) > 0 ? $existingItems[0] : null;
$currentQty = $warehouseItem ? floatval($warehouseItem->quantity) : 0;
$newQty = $currentQty + $quantity;
// Update or create WarehouseItem
if ($warehouseItem) {
$db->query("UPDATE WarehouseItem SET quantity = {$newQty} WHERE id = {$warehouseItem->id}");
$warehouseItemId = $warehouseItem->id;
} else {
$db->query("INSERT INTO WarehouseItem (articleId, warehouseLocationId, quantity, createBy, `create`)
VALUES ({$articleId}, {$locationId}, {$newQty}, {$this->user->id}, " . time() . ")");
$warehouseItemId = $db->insert_id;
}
// Create movement record
$movementNote = "Lagereingang aus Bestellung {$order->orderNumber}";
if ($note) {
$movementNote .= " - " . $note;
}
$noteEscaped = "'" . $db->escape($movementNote) . "'";
$db->query("INSERT INTO WarehouseMovement
(movementType, articleId, warehouseLocationId, warehouseItemId, quantity, quantityBefore, quantityAfter, reasonCategory, linkedOrderId, note, userId, createBy, `create`)
VALUES ('IN', {$articleId}, {$locationId}, {$warehouseItemId}, {$quantity}, {$currentQty}, {$newQty}, 'Warenlieferung', {$orderId}, {$noteEscaped}, {$this->user->id}, {$this->user->id}, " . time() . ")");
$movementId = $db->insert_id;
// Generate movement number
$movementNumber = WarehouseMovementModel::generateMovementNumber();
$db->query("UPDATE WarehouseMovement SET movementNumber = '{$movementNumber}' WHERE id = {$movementId}");
$createdMovementIds[] = $movementId;
$totalReceived += $quantity;
}
if (empty($createdMovementIds)) {
self::returnJson(['success' => false, 'message' => 'Keine Mengen eingegeben']);
return;
}
// Update order with linked movement IDs
$existingMovementIds = $order->linkedMovementIds ? json_decode($order->linkedMovementIds, true) : [];
$allMovementIds = array_merge($existingMovementIds, $createdMovementIds);
// Update delivery note file IDs if provided
$existingFileIds = $order->deliveryNoteFileIds ? json_decode($order->deliveryNoteFileIds, true) : [];
if ($deliveryNoteFileId) {
$existingFileIds[] = $deliveryNoteFileId;
}
// Determine new status - check if all items are now fully delivered
$orderPositions = json_decode($order->positions, true) ?: [];
$allFullyDelivered = true;
// Get all delivered quantities including new ones
$deliveredByArticle = [];
foreach ($allMovementIds as $movementId) {
$movement = WarehouseMovementModel::get($movementId);
if ($movement && $movement->movementType === 'IN') {
if (!isset($deliveredByArticle[$movement->articleId])) {
$deliveredByArticle[$movement->articleId] = 0;
}
$deliveredByArticle[$movement->articleId] += $movement->quantity;
}
}
foreach ($orderPositions as $pos) {
$articleId = intval($pos['article']);
$orderedQty = floatval($pos['amount']);
$deliveredQty = $deliveredByArticle[$articleId] ?? 0;
if ($deliveredQty < $orderedQty) {
$allFullyDelivered = false;
break;
}
}
$newStatus = $allFullyDelivered ? 'fullyDelivered' : 'partiallyDelivered';
// Update order
$orderAsArray = (array)$order;
$orderAsArray['linkedMovementIds'] = json_encode($allMovementIds);
$orderAsArray['deliveryNoteFileIds'] = json_encode($existingFileIds);
$orderAsArray['status'] = $newStatus;
WarehouseOrderModel::update($orderAsArray);
// Create log entry
$logMessage = count($createdMovementIds) . " Lagerbewegung(en) erstellt via Mobile App.";
if ($note) {
$logMessage .= "\n" . $note;
}
WarehouseLogModel::create([
'table' => 'WarehouseOrder',
'rowId' => $orderId,
'type' => 'statusChange',
'message' => "Status geändert auf " . ($newStatus === 'fullyDelivered' ? 'Geliefert' : 'Teilweise geliefert') . ".\n" . $logMessage,
'createBy' => $this->user->id,
'create' => time()
]);
self::returnJson([
'success' => true,
'message' => "{$totalReceived} Artikel empfangen. " . count($createdMovementIds) . " Lagerbewegung(en) erstellt.",
'newStatus' => $newStatus,
'createdMovementIds' => $createdMovementIds
]);
}
}