title); if ($title === 'k1 fladnitz 150' || $title === 'aussenlager-extern') { $locations[] = [ 'id' => $location->id, 'title' => $location->title, ]; } } self::returnJson(['success' => true, 'locations' => $locations]); } public function getArticleAction() { $code = $this->request->code; if (!$code) { self::returnJson(['success' => false, 'message' => 'Kein Code angegeben']); return; } $articleId = null; // Check for QR code format WA:ID: or WH:ID: 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; } $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 : '', ] ]); } public function searchArticlesAction() { $query = $this->request->query ?? ''; $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}%')"; } else { 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.', ]; } self::returnJson(['success' => true, 'articles' => $articles]); } public function getReasonCategoriesAction() { $type = $this->request->type ?? null; $categories = WarehouseMovementModel::getReasonCategories($type); if ($type && is_array($categories)) { $items = []; foreach ($categories as $key => $label) { $items[] = ['value' => $key, 'text' => $label]; } self::returnJson(['success' => true, 'categories' => $items]); } else { self::returnJson(['success' => true, 'categories' => $categories]); } } public function getCurrentStockAction() { $articleId = intval($this->request->articleId ?? 0); $locationId = intval($this->request->locationId ?? 0); if (!$articleId || !$locationId) { self::returnJson(['success' => true, 'currentStock' => 0]); return; } $existingItems = WarehouseItemModel::getAll([ 'articleId' => $articleId, 'warehouseLocationId' => $locationId ]); $currentStock = count($existingItems) > 0 ? floatval($existingItems[0]->quantity) : 0; self::returnJson(['success' => true, 'currentStock' => $currentStock]); } public function submitMovementAction() { $postData = $this->getPostData(); $movementType = $postData['movementType'] ?? ''; $articleId = intval($postData['articleId'] ?? 0); $locationId = intval($postData['locationId'] ?? 0); $quantity = floatval($postData['quantity'] ?? 0); $reasonCategory = $postData['reasonCategory'] ?? ''; $note = $postData['note'] ?? null; // Validate required fields if (!in_array($movementType, ['IN', 'OUT', 'ADJUSTMENT'])) { self::returnJson(['success' => false, 'message' => 'Ungültiger Bewegungstyp']); return; } if ($articleId <= 0) { self::returnJson(['success' => false, 'message' => 'Kein Artikel ausgewählt']); return; } if ($locationId <= 0) { self::returnJson(['success' => false, 'message' => 'Kein Lagerort ausgewählt']); return; } if ($quantity <= 0) { self::returnJson(['success' => false, 'message' => 'Menge muss größer als 0 sein']); return; } if (empty($reasonCategory)) { self::returnJson(['success' => false, 'message' => 'Bitte Grund auswählen']); return; } // Get article info $article = WarehouseArticleModel::get($articleId); if (!$article) { self::returnJson(['success' => false, 'message' => 'Artikel nicht gefunden']); return; } $db = $this->db(); // Find or create WarehouseItem for this article at this location $existingItems = WarehouseItemModel::getAll([ 'articleId' => $articleId, 'warehouseLocationId' => $locationId ]); $warehouseItem = count($existingItems) > 0 ? $existingItems[0] : null; $currentQty = $warehouseItem ? floatval($warehouseItem->quantity) : 0; // Calculate new quantity based on movement type // Note: Negative stock is allowed (items can be taken out even if stock is 0) switch ($movementType) { case 'IN': $newQty = $currentQty + $quantity; break; case 'OUT': $newQty = $currentQty - $quantity; // Negative stock is allowed - no validation needed break; case 'ADJUSTMENT': // For adjustment, quantity is the new absolute value $newQty = $quantity; break; default: $newQty = $currentQty; } // Update or create WarehouseItem $warehouseItemId = null; 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 the movement record $noteEscaped = $note ? "'" . $db->escape($note) . "'" : "NULL"; $db->query("INSERT INTO WarehouseMovement (movementType, articleId, warehouseLocationId, warehouseItemId, quantity, quantityBefore, quantityAfter, reasonCategory, note, userId, createBy, `create`) VALUES ('{$movementType}', {$articleId}, {$locationId}, {$warehouseItemId}, {$quantity}, {$currentQty}, {$newQty}, '{$db->escape($reasonCategory)}', {$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}"); // Get type label for message $typeLabels = ['IN' => 'Einbuchung', 'OUT' => 'Ausbuchung', 'ADJUSTMENT' => 'Korrektur']; $typeLabel = $typeLabels[$movementType] ?? $movementType; self::returnJson([ 'success' => true, 'message' => "{$typeLabel} erfolgreich: {$quantity} x {$article->title}", 'movement' => [ 'id' => $movementId, 'movementNumber' => $movementNumber, 'movementType' => $movementType, 'articleId' => $articleId, 'articleTitle' => $article->title, 'quantity' => $quantity, 'quantityBefore' => $currentQty, 'quantityAfter' => $newQty, ] ]); } public function getMyMovementsAction() { $locationId = intval($this->request->locationId ?? 0); $limit = intval($this->request->limit ?? 20); $db = $this->db(); $whereClause = "m.userId = {$this->user->id}"; if ($locationId > 0) { $whereClause .= " AND m.warehouseLocationId = {$locationId}"; } $result = $db->query("SELECT m.*, wa.articleNumber, wa.title as articleTitle, wa.unit, wl.title as locationTitle FROM WarehouseMovement m LEFT JOIN WarehouseArticle wa ON wa.id = m.articleId LEFT JOIN WarehouseLocation wl ON wl.id = m.warehouseLocationId WHERE {$whereClause} ORDER BY m.`create` DESC LIMIT {$limit}"); $movements = []; while ($row = $result->fetch_assoc()) { $movements[] = [ 'id' => intval($row['id']), 'movementNumber' => $row['movementNumber'], 'movementType' => $row['movementType'], 'articleId' => intval($row['articleId']), 'articleNumber' => $row['articleNumber'], 'articleTitle' => $row['articleTitle'], 'unit' => $row['unit'] ?? 'Stk.', 'locationTitle' => $row['locationTitle'], 'quantity' => floatval($row['quantity']), 'quantityBefore' => floatval($row['quantityBefore']), 'quantityAfter' => floatval($row['quantityAfter']), 'reasonCategory' => $row['reasonCategory'], 'note' => $row['note'], 'create' => date('d.m.Y H:i', $row['create']), ]; } self::returnJson(['success' => true, 'movements' => $movements]); } public function getMovementTypesAction() { $types = [ ['value' => 'IN', 'text' => 'Einbuchung', 'icon' => 'plus-circle', 'color' => 'green'], ['value' => 'OUT', 'text' => 'Ausbuchung', 'icon' => 'minus-circle', 'color' => 'red'], ['value' => 'ADJUSTMENT', 'text' => 'Korrektur', 'icon' => 'edit', 'color' => 'yellow'], ]; 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 ]); } }