Tul xxx Tul
User / IP
:
216.73.217.21
Host / Server
:
45.84.207.204 / aircan.me
System
:
Linux lt-bnk-web1726.main-hosting.eu 5.14.0-611.36.1.el9_7.x86_64 #1 SMP PREEMPT_DYNAMIC Tue Mar 3 11:23:52 EST 2026 x86_64
Command
|
Upload
|
Create
Mass Deface
|
Jumping
|
Symlink
|
Reverse Shell
Ping
|
Port Scan
|
DNS Lookup
|
Whois
|
Header
|
cURL
:
/
home
/
u931257429
/
domains
/
aircan.me
/
public_html
/
chorizon
/
admin
/
Viewing: inventory.php
<?php include '../components/connect.php'; session_start(); $admin_id = $_SESSION['admin_id'] ?? null; if(!$admin_id){ header('location:admin_login.php'); exit(); } if (!isset($message) || !is_array($message)) { $message = []; } function getIngredientStock(PDO $conn, int $ingredientId): float { $stmt = $conn->prepare("SELECT COALESCE(SUM(CASE movement_type WHEN 'in' THEN quantity WHEN 'out' THEN -quantity ELSE quantity END), 0) FROM inventory_movements WHERE ingredient_id = ?"); $stmt->execute([$ingredientId]); return (float)$stmt->fetchColumn(); } $errors = []; $lastAction = ''; $ingredientForm = null; $movementForm = null; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = (string)($_POST['action'] ?? ''); $lastAction = $action; if ($action === 'cleanup_orphan_movements') { $deleted = 0; try { $conn->beginTransaction(); $stmt = $conn->query("SELECT DISTINCT notes FROM `inventory_movements` WHERE movement_type = 'out' AND (notes LIKE '%Comanda #%' OR notes LIKE '%Pedido domicilio #%')"); $candidateNotes = $stmt ? $stmt->fetchAll(PDO::FETCH_COLUMN, 0) : []; $dineIds = []; $deliveryIds = []; $dineNoteById = []; $deliveryNoteById = []; foreach ($candidateNotes as $note) { $note = (string)$note; if ($note === '') { continue; } if (preg_match('/\bComanda\s*#\s*(\d+)\b/i', $note, $m)) { $id = (int)$m[1]; if ($id > 0) { $dineIds[$id] = $id; $dineNoteById[$id] = $note; } continue; } if (preg_match('/\bPedido\s*domicilio\s*#\s*(\d+)\b/i', $note, $m)) { $id = (int)$m[1]; if ($id > 0) { $deliveryIds[$id] = $id; $deliveryNoteById[$id] = $note; } continue; } } $existingDine = []; if (!empty($dineIds)) { $ids = array_values($dineIds); $placeholders = implode(',', array_fill(0, count($ids), '?')); $q = $conn->prepare("SELECT id FROM `dine_in_orders` WHERE id IN ($placeholders)"); $q->execute($ids); foreach ($q->fetchAll(PDO::FETCH_COLUMN, 0) as $id) { $existingDine[(int)$id] = true; } } $existingDelivery = []; if (!empty($deliveryIds)) { $ids = array_values($deliveryIds); $placeholders = implode(',', array_fill(0, count($ids), '?')); $q = $conn->prepare("SELECT id FROM `delivery_orders` WHERE id IN ($placeholders)"); $q->execute($ids); foreach ($q->fetchAll(PDO::FETCH_COLUMN, 0) as $id) { $existingDelivery[(int)$id] = true; } } $orphanNotes = []; foreach ($dineIds as $id) { if (!isset($existingDine[(int)$id])) { $note = $dineNoteById[(int)$id] ?? ''; if ($note !== '') { $orphanNotes[$note] = $note; } } } foreach ($deliveryIds as $id) { if (!isset($existingDelivery[(int)$id])) { $note = $deliveryNoteById[(int)$id] ?? ''; if ($note !== '') { $orphanNotes[$note] = $note; } } } if (!empty($orphanNotes)) { $chunkSize = 400; $notes = array_values($orphanNotes); for ($i = 0; $i < count($notes); $i += $chunkSize) { $chunk = array_slice($notes, $i, $chunkSize); $placeholders = implode(',', array_fill(0, count($chunk), '?')); $del = $conn->prepare("DELETE FROM `inventory_movements` WHERE movement_type = 'out' AND notes IN ($placeholders)"); $del->execute($chunk); $deleted += (int)$del->rowCount(); } } $conn->commit(); } catch (Throwable $e) { if ($conn->inTransaction()) { $conn->rollBack(); } } if ($deleted > 0) { $message[] = 'Se eliminaron ' . $deleted . ' movimientos huérfanos.'; } else { $message[] = 'No se encontraron movimientos huérfanos.'; } header('location:inventory.php'); exit(); } if ($action === 'delete_movement') { $movementId = isset($_POST['movement_id']) ? (int)$_POST['movement_id'] : 0; if ($movementId <= 0) { $errors[] = 'Movimiento inválido.'; } $movementRow = null; if (empty($errors)) { $stmt = $conn->prepare("SELECT m.id, m.ingredient_id, m.movement_type, m.quantity, m.notes, i.name AS ingredient_name FROM `inventory_movements` m INNER JOIN `ingredients` i ON i.id = m.ingredient_id WHERE m.id = ?"); $stmt->execute([$movementId]); $movementRow = $stmt->fetch(PDO::FETCH_ASSOC); if (!$movementRow) { $errors[] = 'El movimiento no existe.'; } } if (empty($errors)) { $notes = (string)($movementRow['notes'] ?? ''); $isOrderMovement = $notes !== '' && ( preg_match('/\bComanda\s*#\s*(\d+)\b/i', $notes) || preg_match('/\bPedido\s*domicilio\s*#\s*(\d+)\b/i', $notes) ); if ($isOrderMovement) { $errors[] = 'No se puede eliminar un movimiento generado por comandas o pedidos.'; } } if (empty($errors)) { $ingredientId = (int)($movementRow['ingredient_id'] ?? 0); $movementType = (string)($movementRow['movement_type'] ?? ''); $qty = (float)($movementRow['quantity'] ?? 0); $effect = 0.0; if ($movementType === 'in') { $effect = $qty; } elseif ($movementType === 'out') { $effect = -$qty; } elseif ($movementType === 'adjust') { $effect = $qty; } $currentStock = getIngredientStock($conn, $ingredientId); $stockAfterDelete = $currentStock - $effect; if ($stockAfterDelete < -0.0000001) { $errors[] = 'No se puede eliminar este movimiento porque dejaría el stock en negativo.'; } } if (empty($errors)) { try { $conn->beginTransaction(); $del = $conn->prepare('DELETE FROM `inventory_movements` WHERE id = ?'); $del->execute([$movementId]); $conn->commit(); $message[] = 'Movimiento eliminado y stock revertido.'; header('location:inventory.php'); exit(); } catch (Throwable $e) { if ($conn->inTransaction()) { $conn->rollBack(); } $errors[] = 'No se pudo eliminar el movimiento.'; } } } if ($action === 'add_ingredient') { $name = trim((string)($_POST['name'] ?? '')); $name = filter_var($name, FILTER_SANITIZE_STRING); $description = trim((string)($_POST['description'] ?? '')); $description = filter_var($description, FILTER_SANITIZE_STRING); $unit = trim((string)($_POST['unit'] ?? 'u')); $unit = filter_var($unit, FILTER_SANITIZE_STRING); if ($unit === '') { $unit = 'u'; } $stockMinRaw = (string)($_POST['stock_min'] ?? '0'); $stockMin = is_numeric($stockMinRaw) ? (float)$stockMinRaw : 0.0; if ($stockMin < 0) { $stockMin = 0.0; } $stockQtyRaw = (string)($_POST['stock_qty'] ?? ''); $stockQtyTrim = trim($stockQtyRaw); if ($stockQtyTrim === '') { $stockQty = 0.0; } else { $normalized = str_replace(',', '.', $stockQtyTrim); if (!is_numeric($normalized)) { $errors[] = 'La cantidad debe ser un número válido.'; $stockQty = 0.0; } else { $stockQty = (float)$normalized; } } if ($stockQty < 0) { $errors[] = 'La cantidad no puede ser negativa.'; } $ingredientForm = [ 'name' => $name, 'description' => $description, 'unit' => $unit, 'stock_min' => $stockMinRaw, 'stock_qty' => $stockQtyRaw, ]; if ($name === '') { $errors[] = 'El nombre del insumo es obligatorio.'; } if (empty($errors)) { $check = $conn->prepare('SELECT id FROM `ingredients` WHERE name = ? LIMIT 1'); $check->execute([$name]); if ($check->fetchColumn()) { $errors[] = 'Ya existe un insumo con ese nombre.'; } } if (empty($errors)) { $insert = $conn->prepare('INSERT INTO `ingredients` (name, description, unit, stock_min, is_active) VALUES (?, ?, ?, ?, 1)'); $insert->execute([$name, $description !== '' ? $description : null, $unit, $stockMin]); $newIngredientId = (int)$conn->lastInsertId(); if ($newIngredientId > 0 && $stockQty > 0) { $insertMovement = $conn->prepare('INSERT INTO `inventory_movements` (ingredient_id, movement_type, quantity, notes, admin_id) VALUES (?, ?, ?, ?, ?)'); $insertMovement->execute([$newIngredientId, 'in', $stockQty, 'Stock inicial', (int)$admin_id]); } $message[] = 'Insumo agregado.'; header('location:inventory.php'); exit(); } } if ($action === 'update_ingredient') { $ingredientId = isset($_POST['ingredient_id']) ? (int)$_POST['ingredient_id'] : 0; $name = trim((string)($_POST['name'] ?? '')); $name = filter_var($name, FILTER_SANITIZE_STRING); $description = trim((string)($_POST['description'] ?? '')); $description = filter_var($description, FILTER_SANITIZE_STRING); $unit = trim((string)($_POST['unit'] ?? 'u')); $unit = filter_var($unit, FILTER_SANITIZE_STRING); if ($unit === '') { $unit = 'u'; } $stockMinRaw = (string)($_POST['stock_min'] ?? '0'); $stockMin = is_numeric($stockMinRaw) ? (float)$stockMinRaw : 0.0; if ($stockMin < 0) { $stockMin = 0.0; } $stockQtyRaw = (string)($_POST['stock_qty'] ?? ''); $stockQtyTrim = trim($stockQtyRaw); $stockQtyDesired = null; if ($stockQtyTrim !== '') { $normalized = str_replace(',', '.', $stockQtyTrim); if (!is_numeric($normalized)) { $errors[] = 'La cantidad debe ser un número válido.'; } else { $stockQtyDesired = (float)$normalized; if ($stockQtyDesired < 0) { $errors[] = 'La cantidad no puede ser negativa.'; } } } $ingredientForm = [ 'name' => $name, 'description' => $description, 'unit' => $unit, 'stock_min' => $stockMinRaw, 'stock_qty' => $stockQtyRaw, ]; if ($ingredientId <= 0) { $errors[] = 'Insumo inválido.'; } if ($name === '') { $errors[] = 'El nombre del insumo es obligatorio.'; } if (empty($errors)) { $check = $conn->prepare('SELECT id FROM `ingredients` WHERE name = ? AND id != ? LIMIT 1'); $check->execute([$name, $ingredientId]); if ($check->fetchColumn()) { $errors[] = 'Ya existe otro insumo con ese nombre.'; } } if (empty($errors)) { $currentStock = getIngredientStock($conn, $ingredientId); $update = $conn->prepare('UPDATE `ingredients` SET name = ?, description = ?, unit = ?, stock_min = ? WHERE id = ?'); $update->execute([$name, $description !== '' ? $description : null, $unit, $stockMin, $ingredientId]); if ($stockQtyDesired !== null) { $delta = $stockQtyDesired - $currentStock; if (abs($delta) > 0.0000001) { $insertMovement = $conn->prepare('INSERT INTO `inventory_movements` (ingredient_id, movement_type, quantity, notes, admin_id) VALUES (?, ?, ?, ?, ?)'); $insertMovement->execute([$ingredientId, 'adjust', $delta, 'Ajuste por edición de insumo', (int)$admin_id]); } } $message[] = 'Insumo actualizado.'; header('location:inventory.php'); exit(); } } if ($action === 'toggle_ingredient') { $ingredientId = isset($_POST['ingredient_id']) ? (int)$_POST['ingredient_id'] : 0; $newStatus = isset($_POST['new_status']) && $_POST['new_status'] === '1' ? 1 : 0; if ($ingredientId > 0) { $update = $conn->prepare('UPDATE `ingredients` SET is_active = ? WHERE id = ?'); $update->execute([$newStatus, $ingredientId]); $message[] = $newStatus ? 'Insumo activado.' : 'Insumo desactivado.'; } header('location:inventory.php'); exit(); } if ($action === 'delete_ingredient') { $ingredientId = isset($_POST['ingredient_id']) ? (int)$_POST['ingredient_id'] : 0; if ($ingredientId <= 0) { $errors[] = 'Insumo inválido.'; } else { $check = $conn->prepare('SELECT id, name FROM `ingredients` WHERE id = ? LIMIT 1'); $check->execute([$ingredientId]); $ingredientRow = $check->fetch(PDO::FETCH_ASSOC); if (!$ingredientRow) { $errors[] = 'El insumo no existe.'; } } $usedByProducts = []; if (empty($errors)) { $usedStmt = $conn->prepare('SELECT DISTINCT p.name FROM `product_ingredients` pi INNER JOIN `products` p ON p.id = pi.product_id WHERE pi.ingredient_id = ? ORDER BY p.name'); $usedStmt->execute([$ingredientId]); $usedByProducts = $usedStmt->fetchAll(PDO::FETCH_COLUMN, 0); if (!empty($usedByProducts)) { $label = (string)($ingredientRow['name'] ?? ''); $list = implode(', ', array_map(static fn($n) => (string)$n, $usedByProducts)); $errors[] = 'No se puede eliminar el insumo "' . $label . '" porque está usado en las recetas de: ' . $list . '. Elimínalo de esas recetas para poder eliminarlo del inventario.'; } } if (empty($errors)) { try { $conn->beginTransaction(); $deleteMovements = $conn->prepare('DELETE FROM `inventory_movements` WHERE ingredient_id = ?'); $deleteMovements->execute([$ingredientId]); $deleteIngredient = $conn->prepare('DELETE FROM `ingredients` WHERE id = ?'); $deleteIngredient->execute([$ingredientId]); $conn->commit(); $message[] = 'Insumo eliminado.'; header('location:inventory.php'); exit(); } catch (Throwable $e) { if ($conn->inTransaction()) { $conn->rollBack(); } $errors[] = 'No se pudo eliminar el insumo.'; } } } if ($action === 'add_movement') { $ingredientId = isset($_POST['ingredient_id']) ? (int)$_POST['ingredient_id'] : 0; $movementType = (string)($_POST['movement_type'] ?? 'in'); $quantityRaw = (string)($_POST['quantity'] ?? '0'); $quantity = is_numeric($quantityRaw) ? (float)$quantityRaw : 0.0; $notes = trim((string)($_POST['notes'] ?? '')); $notes = filter_var($notes, FILTER_SANITIZE_STRING); if ($notes === '') { $notes = null; } $movementForm = [ 'ingredient_id' => $ingredientId, 'movement_type' => $movementType, 'quantity' => (string)($_POST['quantity'] ?? ''), 'notes' => $notes ?? '', ]; if ($ingredientId <= 0) { $errors[] = 'Selecciona un insumo.'; } if (!in_array($movementType, ['in', 'out', 'adjust'], true)) { $errors[] = 'Tipo de movimiento inválido.'; } if ($movementType === 'in' || $movementType === 'out') { if ($quantity <= 0) { $errors[] = 'La cantidad debe ser mayor a 0.'; } } else { if ($quantity == 0.0) { $errors[] = 'El ajuste no puede ser 0.'; } } if (empty($errors)) { $ingredientStmt = $conn->prepare('SELECT id, is_active FROM `ingredients` WHERE id = ? LIMIT 1'); $ingredientStmt->execute([$ingredientId]); $ingredientRow = $ingredientStmt->fetch(PDO::FETCH_ASSOC); if (!$ingredientRow) { $errors[] = 'El insumo seleccionado no existe.'; } elseif (empty($ingredientRow['is_active'])) { $errors[] = 'El insumo seleccionado está inactivo.'; } } if (empty($errors) && $movementType === 'out') { $currentStock = getIngredientStock($conn, $ingredientId); if ($quantity > $currentStock) { $errors[] = 'Stock insuficiente para registrar la salida.'; } } if (empty($errors)) { $insert = $conn->prepare('INSERT INTO `inventory_movements` (ingredient_id, movement_type, quantity, notes, admin_id) VALUES (?, ?, ?, ?, ?)'); $insert->execute([$ingredientId, $movementType, $quantity, $notes, (int)$admin_id]); $message[] = 'Movimiento registrado.'; header('location:inventory.php'); exit(); } } } $editId = isset($_GET['edit']) ? (int)$_GET['edit'] : 0; $editIngredient = null; if ($editId > 0) { $stmt = $conn->prepare('SELECT * FROM `ingredients` WHERE id = ? LIMIT 1'); $stmt->execute([$editId]); $editIngredient = $stmt->fetch(PDO::FETCH_ASSOC); if (!$editIngredient) { $editId = 0; } } $ingredientsStmt = $conn->query( "SELECT i.*,\n COALESCE(SUM(CASE m.movement_type WHEN 'in' THEN m.quantity WHEN 'out' THEN -m.quantity ELSE m.quantity END), 0) AS stock,\n GROUP_CONCAT(DISTINCT p.name ORDER BY p.name SEPARATOR '||') AS used_products\n FROM `ingredients` i\n LEFT JOIN `inventory_movements` m ON m.ingredient_id = i.id\n LEFT JOIN `product_ingredients` pi ON pi.ingredient_id = i.id\n LEFT JOIN `products` p ON p.id = pi.product_id\n GROUP BY i.id\n ORDER BY i.name ASC" ); $ingredients = $ingredientsStmt ? $ingredientsStmt->fetchAll(PDO::FETCH_ASSOC) : []; $historyStmt = $conn->query( "SELECT m.id, m.ingredient_id, m.movement_type, m.quantity, m.notes, m.created_at, i.name AS ingredient_name, a.name AS admin_name\n FROM `inventory_movements` m\n INNER JOIN `ingredients` i ON i.id = m.ingredient_id\n LEFT JOIN `admin` a ON a.id = m.admin_id\n ORDER BY m.created_at DESC, m.id DESC\n LIMIT 80" ); $history = $historyStmt ? $historyStmt->fetchAll(PDO::FETCH_ASSOC) : []; $dineOrderIds = []; $deliveryOrderIds = []; foreach ($history as $row) { $notes = (string)($row['notes'] ?? ''); if ($notes === '') { continue; } if (preg_match('/\bComanda\s*#\s*(\d+)\b/i', $notes, $m)) { $dineOrderIds[(int)$m[1]] = true; continue; } if (preg_match('/\bPedido\s*domicilio\s*#\s*(\d+)\b/i', $notes, $m)) { $deliveryOrderIds[(int)$m[1]] = true; continue; } } $dineOrderNumberById = []; if (!empty($dineOrderIds)) { $ids = array_keys($dineOrderIds); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $conn->prepare("SELECT id, order_number FROM `dine_in_orders` WHERE id IN ($placeholders)"); $stmt->execute($ids); while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { $id = (int)($r['id'] ?? 0); $num = (int)($r['order_number'] ?? 0); if ($id > 0 && $num > 0) { $dineOrderNumberById[$id] = $num; } } } $deliveryOrderNumberById = []; if (!empty($deliveryOrderIds)) { $ids = array_keys($deliveryOrderIds); $placeholders = implode(',', array_fill(0, count($ids), '?')); $stmt = $conn->prepare("SELECT id, order_number FROM `delivery_orders` WHERE id IN ($placeholders)"); $stmt->execute($ids); while ($r = $stmt->fetch(PDO::FETCH_ASSOC)) { $id = (int)($r['id'] ?? 0); $num = (int)($r['order_number'] ?? 0); if ($id > 0 && $num > 0) { $deliveryOrderNumberById[$id] = $num; } } } $businessName = getBusinessName($conn); $businessLogoVersion = getBusinessLogoVersion($conn); $iconHref = '../icon.php?size=64' . ($businessLogoVersion !== '' ? '&v=' . rawurlencode($businessLogoVersion) : ''); $editIngredientStock = 0.0; if ($editId > 0) { $editIngredientStock = getIngredientStock($conn, (int)$editId); } $editIngredientStockLabel = rtrim(rtrim(number_format((float)$editIngredientStock, 3, '.', ''), '0'), '.'); $openIngredientModal = ($editId > 0) || in_array($lastAction, ['add_ingredient', 'update_ingredient'], true); $openMovementModal = ($lastAction === 'add_movement'); $ingredientValues = []; if ($editId > 0) { $ingredientValues = $editIngredient ?? []; if (is_array($ingredientForm)) { $ingredientValues = array_merge($ingredientValues, $ingredientForm); } } elseif (is_array($ingredientForm)) { $ingredientValues = $ingredientForm; } ?> <!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Inventario | <?= htmlspecialchars($businessName); ?></title> <link rel="icon" href="<?= htmlspecialchars($iconHref); ?>" type="image/png"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="../css/admin_style.css"> <style> body.inventory-page { background: linear-gradient(135deg, #f5f9ff 0%, #fff7f3 100%); min-height: 100vh; } .inventory-page .wrap { max-width: 1200px; margin: 0 auto; padding: 2rem; } .inventory-page .grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; } @media (min-width: 992px) { .inventory-page .grid { grid-template-columns: 1fr 1fr; } } .inventory-page .card { background: #fff; border-radius: 16px; border: 1px solid rgba(226, 232, 240, 0.85); box-shadow: 0 18px 42px rgba(17, 24, 39, 0.10); padding: 1.6rem 1.7rem; } .inventory-page .card h3 { margin: 0 0 1.2rem; font-size: 1.6rem; font-weight: 800; color: #111827; } .inventory-page .field { margin-bottom: 1rem; } .inventory-page .field label { display: block; font-weight: 700; margin-bottom: .5rem; color: #1f2937; font-size: 1.2rem; } .inventory-page input.box, .inventory-page textarea.box, .inventory-page select.box { width: 100%; border-radius: 12px; border: 1px solid #cbd5f5; padding: .9rem 1rem; font-size: 1.3rem; background: #fff; } .inventory-page textarea.box { min-height: 88px; resize: vertical; } .inventory-page .row { display: grid; grid-template-columns: 1fr; gap: 1rem; } @media (min-width: 720px) { .inventory-page .row.cols-2 { grid-template-columns: 1fr 1fr; } } .inventory-page .actions { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1.2rem; } .inventory-page .actions .btn, .inventory-page .actions .option-btn { width: auto; min-width: 160px; } .inventory-page .table-card { margin-top: 1.5rem; } .inventory-page table { width: 100%; border-collapse: collapse; font-size: 1.25rem; } .inventory-page th, .inventory-page td { padding: .85rem .8rem; border-bottom: 1px solid #e2e8f0; text-align: left; vertical-align: top; } .inventory-page th { background: #f8fafc; font-weight: 800; color: #0f172a; } .inventory-page .tag { display: inline-flex; align-items: center; gap: .5rem; padding: .25rem .55rem; border-radius: 999px; font-size: 1.1rem; font-weight: 800; border: 1px solid rgba(148, 163, 184, 0.55); background: rgba(248, 250, 252, 0.8); } .inventory-page .tag.low { border-color: rgba(244, 63, 94, 0.35); background: rgba(254, 242, 242, 0.9); color: #b91c1c; } .inventory-page .tag.ok { border-color: rgba(16, 185, 129, 0.25); background: rgba(236, 253, 245, 0.9); color: #047857; } .inventory-page .small { color: #64748b; font-size: 1.1rem; margin-top: .35rem; line-height: 1.35; } .inventory-page .inline-form { display: flex; gap: .6rem; flex-wrap: wrap; } @media (max-width: 640px) { .inventory-page table .inline-form { flex-wrap: nowrap; gap: .45rem; } .inventory-page table .inline-form .btn, .inventory-page table .inline-form .option-btn, .inventory-page table .inline-form .delete-btn, .inventory-page table .inline-form .success-btn { min-width: 0; padding: .75rem .9rem; font-size: 0; } .inventory-page table .inline-form .btn i, .inventory-page table .inline-form .option-btn i, .inventory-page table .inline-form .delete-btn i, .inventory-page table .inline-form .success-btn i { font-size: 1.25rem; } } .inventory-page .inline-form .option-btn, .inventory-page .inline-form .delete-btn { width: auto; padding: .75rem 1.1rem; font-size: 1.2rem; margin-top: 0; } .inventory-page .errors { background: #fef2f2; border: 1px solid #fca5a5; color: #b91c1c; padding: 1rem 1.2rem; border-radius: 14px; margin: 1.5rem 0; font-size: 1.25rem; } .inventory-page .action-buttons { display: flex; gap: 1rem; flex-wrap: wrap; margin: 0 0 1.4rem; } .inventory-page .action-buttons .btn, .inventory-page .action-buttons .option-btn { width: auto; min-width: 220px; } @media (max-width: 640px) { .inventory-page .action-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: .9rem; } .inventory-page .action-buttons .btn, .inventory-page .action-buttons .option-btn { width: 100%; min-width: 0; } } .inventory-page .btn, .inventory-page .option-btn, .inventory-page .delete-btn, .inventory-page .success-btn { display: inline-flex; align-items: center; justify-content: center; gap: .55rem; padding: .85rem 1.2rem; border-radius: 14px; font-weight: 900; letter-spacing: .2px; font-size: 1.22rem; line-height: 1; border: 1px solid transparent; cursor: pointer; text-decoration: none; user-select: none; -webkit-tap-highlight-color: transparent; transition: transform .08s ease, box-shadow .2s ease, filter .2s ease, border-color .2s ease; } .inventory-page .btn:active, .inventory-page .option-btn:active, .inventory-page .delete-btn:active, .inventory-page .success-btn:active { transform: translateY(1px); } .inventory-page .btn:focus-visible, .inventory-page .option-btn:focus-visible, .inventory-page .delete-btn:focus-visible, .inventory-page .success-btn:focus-visible { outline: 3px solid rgba(14, 165, 233, 0.35); outline-offset: 2px; } .inventory-page .btn { color: #fff; background: linear-gradient(135deg, #0ea5e9, #2563eb); box-shadow: 0 12px 26px rgba(37, 99, 235, 0.22); } .inventory-page .btn:hover { filter: brightness(1.02); box-shadow: 0 14px 30px rgba(37, 99, 235, 0.26); } .inventory-page .option-btn { color: #0f172a; background: rgba(255, 255, 255, 0.85); border-color: rgba(148, 163, 184, 0.55); box-shadow: 0 10px 24px rgba(15, 23, 42, 0.10); backdrop-filter: blur(6px); } .inventory-page .option-btn:hover { border-color: rgba(148, 163, 184, 0.75); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.12); } .inventory-page .delete-btn { color: #fff; background: linear-gradient(135deg, #ef4444, #b91c1c); box-shadow: 0 12px 26px rgba(239, 68, 68, 0.18); } .inventory-page .delete-btn:hover { filter: brightness(1.02); box-shadow: 0 14px 30px rgba(239, 68, 68, 0.22); } .inventory-page .success-btn { color: #fff; background: linear-gradient(135deg, #22c55e, #16a34a); box-shadow: 0 12px 26px rgba(34, 197, 94, 0.18); } .inventory-page .success-btn:hover { filter: brightness(1.02); box-shadow: 0 14px 30px rgba(34, 197, 94, 0.22); } .inventory-page .btn i, .inventory-page .option-btn i, .inventory-page .delete-btn i, .inventory-page .success-btn i { font-size: 1.1em; } .inventory-page .modal-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.55); backdrop-filter: blur(3px); display: none; align-items: center; justify-content: center; padding: 1.5rem; z-index: 2500; } .inventory-page .modal-overlay.show { display: flex; } .inventory-page .modal { width: 100%; max-width: 760px; background: #fff; border-radius: 18px; border: 1px solid rgba(226, 232, 240, 0.95); box-shadow: 0 25px 70px rgba(0, 0, 0, 0.22); overflow: hidden; } .inventory-page .modal-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1.2rem 1.35rem; background: linear-gradient(135deg, rgba(179,0,0,0.06), rgba(14,165,233,0.06)); border-bottom: 1px solid rgba(226, 232, 240, 0.95); } .inventory-page .modal-header h3 { margin: 0; font-size: 1.45rem; font-weight: 900; color: #0f172a; } .inventory-page .modal-close { border: 0; background: rgba(15, 23, 42, 0.08); color: #0f172a; width: 42px; height: 42px; border-radius: 12px; cursor: pointer; font-size: 1.6rem; font-weight: 900; line-height: 1; } .inventory-page .modal-body { padding: 1.4rem 1.7rem 1.7rem; max-height: 78vh; overflow: auto; } .inventory-page .details-grid { display: grid; grid-template-columns: 1fr; gap: .9rem; } @media (min-width: 720px) { .inventory-page .details-grid { grid-template-columns: 1fr 1fr; } } .inventory-page .details-item { border: 1px solid rgba(226, 232, 240, 0.95); border-radius: 14px; padding: .9rem 1rem; background: rgba(248, 250, 252, 0.75); } .inventory-page .details-item strong { display: block; font-size: 1.05rem; color: #0f172a; margin-bottom: .25rem; } .inventory-page .details-item div { font-size: 1.18rem; color: #111827; word-break: break-word; } </style> </head> <body class="inventory-page"> <?php include '../components/admin_header.php' ?> <section class="page-heading"> <h1 class="section-title">Inventario</h1> </section> <div class="wrap"> <?php if (!empty($errors)): ?> <div class="errors"> <?php foreach ($errors as $err): ?> <div><?= htmlspecialchars($err); ?></div> <?php endforeach; ?> </div> <?php endif; ?> <div class="action-buttons"> <button type="button" class="btn" id="openIngredientModalBtn"><i class="fa-solid fa-plus"></i> Agregar insumo</button> <button type="button" class="option-btn" id="openMovementModalBtn"><i class="fa-solid fa-right-left"></i> Registrar movimiento</button> </div> <div class="modal-overlay" id="ingredientModal" aria-hidden="true"> <div class="modal" role="dialog" aria-modal="true" aria-label="Formulario de insumo"> <div class="modal-header"> <h3><?= $editId > 0 ? 'Editar insumo' : 'Agregar insumo'; ?></h3> <button type="button" class="modal-close" data-close="ingredient">×</button> </div> <div class="modal-body"> <form method="POST" action=""> <input type="hidden" name="action" value="<?= $editId > 0 ? 'update_ingredient' : 'add_ingredient'; ?>"> <?php if ($editId > 0): ?> <input type="hidden" name="ingredient_id" value="<?= (int)$editId; ?>"> <?php endif; ?> <div class="field"> <label for="ingName">Nombre</label> <input id="ingName" class="box" type="text" name="name" required maxlength="100" value="<?= htmlspecialchars((string)($ingredientValues['name'] ?? '')); ?>" placeholder="Ej: Queso mozzarella"> </div> <div class="field"> <label for="ingDesc">Descripción (opcional)</label> <textarea id="ingDesc" class="box" name="description" maxlength="1000" placeholder="Notas internas, proveedor, presentación..."><?= htmlspecialchars((string)($ingredientValues['description'] ?? '')); ?></textarea> </div> <div class="row cols-2"> <div class="field"> <label for="ingMin">Stock mínimo</label> <input id="ingMin" class="box" type="number" name="stock_min" min="0" step="0.001" value="<?= htmlspecialchars((string)($ingredientValues['stock_min'] ?? ($editIngredient['stock_min'] ?? '0'))); ?>"> <div class="small">Si el stock actual cae por debajo, se marcará como bajo.</div> </div> <div class="field"> <label for="ingQty">Cantidad</label> <?php $qtyValue = (string)($ingredientValues['stock_qty'] ?? ''); if ($qtyValue === '' && $editId > 0) { $qtyValue = $editIngredientStockLabel; } ?> <input id="ingQty" class="box" type="number" name="stock_qty" min="0" step="0.001" value="<?= htmlspecialchars($qtyValue); ?>" placeholder="Ej: 10"> </div> </div> <div class="field"> <label for="ingUnit">Unidad</label> <input id="ingUnit" class="box" type="text" name="unit" maxlength="20" value="<?= htmlspecialchars((string)($ingredientValues['unit'] ?? 'u')); ?>" placeholder="Ej: kg, g, ml, u"> <div class="small">Se usa para mostrar el stock. Ej: kg, g, ml, u.</div> </div> <div class="actions"> <button type="submit" class="btn"><i class="fa-solid fa-floppy-disk"></i> Guardar</button> <?php if ($editId > 0): ?> <a href="inventory.php" class="option-btn"><i class="fa-solid fa-xmark"></i> Cancelar</a> <?php endif; ?> </div> </form> </div> </div> </div> <div class="modal-overlay" id="detailsModal" aria-hidden="true"> <div class="modal" role="dialog" aria-modal="true" aria-label="Detalles"> <div class="modal-header"> <h3 id="detailsTitle">Detalles</h3> <button type="button" class="modal-close" data-close="details">×</button> </div> <div class="modal-body"> <div id="detailsContent"></div> </div> </div> </div> <div class="modal-overlay" id="movementModal" aria-hidden="true"> <div class="modal" role="dialog" aria-modal="true" aria-label="Formulario de movimiento"> <div class="modal-header"> <h3>Registrar movimiento</h3> <button type="button" class="modal-close" data-close="movement">×</button> </div> <div class="modal-body"> <form method="POST" action=""> <input type="hidden" name="action" value="add_movement"> <div class="field"> <label for="movementIngredient">Insumo</label> <select id="movementIngredient" class="box" name="ingredient_id" required> <option value="" selected disabled>Selecciona un insumo</option> <?php foreach ($ingredients as $ing): ?> <?php $selected = (int)($movementForm['ingredient_id'] ?? 0) === (int)$ing['id']; ?> <option value="<?= (int)$ing['id']; ?>" <?= $selected ? 'selected' : ''; ?>><?= htmlspecialchars((string)$ing['name']); ?></option> <?php endforeach; ?> </select> </div> <div class="row cols-2"> <div class="field"> <label for="movementType">Tipo</label> <?php $currentMovementType = (string)($movementForm['movement_type'] ?? 'in'); ?> <select id="movementType" class="box" name="movement_type" required> <option value="in" <?= $currentMovementType === 'in' ? 'selected' : ''; ?>>Entrada (+)</option> <option value="out" <?= $currentMovementType === 'out' ? 'selected' : ''; ?>>Salida (-)</option> <option value="adjust" <?= $currentMovementType === 'adjust' ? 'selected' : ''; ?>>Ajuste (+/-)</option> </select> </div> <div class="field"> <label for="movementQty">Cantidad</label> <input id="movementQty" class="box" type="number" name="quantity" step="0.001" required placeholder="Ej: 1" value="<?= htmlspecialchars((string)($movementForm['quantity'] ?? '')); ?>"> <div class="small">En salida, no permite bajar de 0. En ajuste puedes usar negativo.</div> </div> </div> <div class="field"> <label for="movementNotes">Notas (opcional)</label> <input id="movementNotes" class="box" type="text" name="notes" maxlength="255" placeholder="Ej: Compra proveedor / Merma / Corrección" value="<?= htmlspecialchars((string)($movementForm['notes'] ?? '')); ?>"> </div> <div class="actions"> <button type="submit" class="btn"><i class="fa-solid fa-check"></i> Registrar</button> </div> </form> </div> </div> </div> <div class="card table-card"> <h3>Stock actual</h3> <div style="overflow:auto;"> <table> <thead> <tr> <th>Insumo</th> <th>Stock</th> <th>Mínimo</th> <th>Estado</th> <th>Acciones</th> </tr> </thead> <tbody> <?php if (empty($ingredients)): ?> <tr> <td colspan="5">No hay insumos registrados.</td> </tr> <?php else: ?> <?php foreach ($ingredients as $ing): ?> <?php $stock = (float)($ing['stock'] ?? 0); $min = (float)($ing['stock_min'] ?? 0); $unit = (string)($ing['unit'] ?? 'u'); $active = !empty($ing['is_active']); $isLow = $min > 0 && $stock < $min; $usedProductsRaw = (string)($ing['used_products'] ?? ''); ?> <tr> <td> <div style="font-weight:800; color:#0f172a; font-size:1.35rem;"> <?= htmlspecialchars((string)$ing['name']); ?> </div> </td> <td> <span class="tag <?= $isLow ? 'low' : 'ok'; ?>"> <?= rtrim(rtrim(number_format($stock, 3, '.', ''), '0'), '.'); ?> <?= htmlspecialchars($unit); ?> </span> </td> <td><?= rtrim(rtrim(number_format($min, 3, '.', ''), '0'), '.'); ?> <?= htmlspecialchars($unit); ?></td> <td> <span class="tag <?= $active ? 'ok' : 'low'; ?>"> <?= $active ? 'Activo' : 'Inactivo'; ?> </span> </td> <td> <div class="inline-form"> <button type="button" class="option-btn" title="Ver" aria-label="Ver" data-view-ingredient="1" data-name="<?= htmlspecialchars((string)$ing['name'], ENT_QUOTES, 'UTF-8'); ?>" data-desc="<?= htmlspecialchars((string)($ing['description'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>" data-unit="<?= htmlspecialchars($unit, ENT_QUOTES, 'UTF-8'); ?>" data-stock="<?= htmlspecialchars(rtrim(rtrim(number_format($stock, 3, '.', ''), '0'), '.'), ENT_QUOTES, 'UTF-8'); ?>" data-min="<?= htmlspecialchars(rtrim(rtrim(number_format($min, 3, '.', ''), '0'), '.'), ENT_QUOTES, 'UTF-8'); ?>" data-active="<?= $active ? '1' : '0'; ?>" data-used="<?= htmlspecialchars($usedProductsRaw, ENT_QUOTES, 'UTF-8'); ?>"> <i class="fa-solid fa-eye"></i> Ver </button> <a class="option-btn" title="Editar" aria-label="Editar" href="inventory.php?edit=<?= (int)$ing['id']; ?>"><i class="fa-solid fa-pen"></i> Editar</a> <form method="POST" action="" style="margin:0;" data-toggle-ingredient="1" data-active="<?= $active ? '1' : '0'; ?>"> <input type="hidden" name="action" value="toggle_ingredient"> <input type="hidden" name="ingredient_id" value="<?= (int)$ing['id']; ?>"> <input type="hidden" name="new_status" value="<?= $active ? '0' : '1'; ?>"> <button type="submit" title="<?= $active ? 'Desactivar' : 'Activar'; ?>" aria-label="<?= $active ? 'Desactivar' : 'Activar'; ?>" class="<?= $active ? 'delete-btn' : 'success-btn'; ?>"> <i class="fa-solid <?= $active ? 'fa-ban' : 'fa-check'; ?>"></i> <?= $active ? 'Desactivar' : 'Activar'; ?> </button> </form> <form method="POST" action="" style="margin:0;" data-used-products="<?= htmlspecialchars($usedProductsRaw, ENT_QUOTES, 'UTF-8'); ?>"> <input type="hidden" name="action" value="delete_ingredient"> <input type="hidden" name="ingredient_id" value="<?= (int)$ing['id']; ?>"> <button type="submit" title="Eliminar" aria-label="Eliminar" class="delete-btn"><i class="fa-solid fa-trash"></i> Eliminar</button> </form> </div> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> <div class="card table-card"> <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;"> <h3 style="margin:0;">Historial de movimientos</h3> <form method="POST" action="" style="margin:0;" data-clean-orphans="1"> <input type="hidden" name="action" value="cleanup_orphan_movements"> <button type="submit" class="option-btn"><i class="fa-solid fa-broom"></i> Limpiar huérfanos</button> </form> </div> <div style="overflow:auto;"> <table> <thead> <tr> <th>Fecha</th> <th>Insumo</th> <th>Tipo</th> <th>Cantidad</th> <th>Notas</th> <th>Acciones</th> </tr> </thead> <tbody> <?php if (empty($history)): ?> <tr> <td colspan="6">Aún no hay movimientos.</td> </tr> <?php else: ?> <?php foreach ($history as $row): ?> <?php $type = (string)$row['movement_type']; $qty = (float)$row['quantity']; $typeLabel = $type === 'in' ? 'Entrada' : ($type === 'out' ? 'Salida' : 'Ajuste'); $qtyLabel = rtrim(rtrim(number_format($qty, 3, '.', ''), '0'), '.'); $notesLabel = (string)($row['notes'] ?? ''); $notesRawForDelete = (string)($row['notes'] ?? ''); $isOrderMovement = $notesRawForDelete !== '' && ( preg_match('/\bComanda\s*#\s*(\d+)\b/i', $notesRawForDelete) || preg_match('/\bPedido\s*domicilio\s*#\s*(\d+)\b/i', $notesRawForDelete) ); $isSystemNote = $notesRawForDelete !== '' && ( $isOrderMovement || in_array($notesRawForDelete, ['Stock inicial', 'Ajuste por edición de insumo'], true) ); $canDeleteMovement = !$isOrderMovement; if ($notesLabel !== '') { if (preg_match('/\bComanda\s*#\s*(\d+)\b/i', $notesLabel, $m)) { $internalId = (int)$m[1]; $orderNumber = $dineOrderNumberById[$internalId] ?? 0; if ($orderNumber > 0) { $notesLabel = preg_replace('/\bComanda\s*#\s*' . preg_quote((string)$internalId, '/') . '\b/i', 'Comanda #' . $orderNumber, $notesLabel, 1); } } elseif (preg_match('/\bPedido\s*domicilio\s*#\s*(\d+)\b/i', $notesLabel, $m)) { $internalId = (int)$m[1]; $orderNumber = $deliveryOrderNumberById[$internalId] ?? 0; if ($orderNumber > 0) { $notesLabel = preg_replace('/\bPedido\s*domicilio\s*#\s*' . preg_quote((string)$internalId, '/') . '\b/i', 'Pedido domicilio #' . $orderNumber, $notesLabel, 1); } } } $userLabel = trim((string)($row['admin_name'] ?? '')); if ($userLabel === '') { $userLabel = 'Sistema'; } $createdRaw = (string)($row['created_at'] ?? ''); $createdLabel = $createdRaw; if ($createdRaw !== '') { try { $dt = new DateTime($createdRaw); $createdLabel = $dt->format('Y-m-d h:i A'); } catch (Throwable $e) { $createdLabel = $createdRaw; } } ?> <tr> <td><?= htmlspecialchars($createdLabel); ?></td> <td><?= htmlspecialchars((string)$row['ingredient_name']); ?></td> <td><?= htmlspecialchars($typeLabel); ?></td> <td><?= htmlspecialchars($qtyLabel); ?></td> <td><?= $notesLabel !== '' ? htmlspecialchars($notesLabel) : '—'; ?></td> <td> <div class="inline-form"> <button type="button" class="option-btn" title="Ver" aria-label="Ver" data-view-movement="1" data-date="<?= htmlspecialchars($createdLabel, ENT_QUOTES, 'UTF-8'); ?>" data-ingredient="<?= htmlspecialchars((string)$row['ingredient_name'], ENT_QUOTES, 'UTF-8'); ?>" data-type="<?= htmlspecialchars($typeLabel, ENT_QUOTES, 'UTF-8'); ?>" data-qty="<?= htmlspecialchars($qtyLabel, ENT_QUOTES, 'UTF-8'); ?>" data-user="<?= htmlspecialchars($userLabel, ENT_QUOTES, 'UTF-8'); ?>" data-notes="<?= htmlspecialchars($notesLabel, ENT_QUOTES, 'UTF-8'); ?>"> <i class="fa-solid fa-eye"></i> Ver </button> <?php if ($canDeleteMovement): ?> <form method="POST" action="" style="margin:0;" data-delete-movement="1" data-ingredient="<?= htmlspecialchars((string)$row['ingredient_name'], ENT_QUOTES, 'UTF-8'); ?>" data-type="<?= htmlspecialchars($typeLabel, ENT_QUOTES, 'UTF-8'); ?>" data-qty="<?= htmlspecialchars($qtyLabel, ENT_QUOTES, 'UTF-8'); ?>"> <input type="hidden" name="action" value="delete_movement"> <input type="hidden" name="movement_id" value="<?= (int)$row['id']; ?>"> <button type="submit" title="Eliminar" aria-label="Eliminar" class="delete-btn"><i class="fa-solid fa-trash"></i> Eliminar</button> </form> <?php endif; ?> </div> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script> const ingredientModal = document.getElementById('ingredientModal'); const movementModal = document.getElementById('movementModal'); const detailsModal = document.getElementById('detailsModal'); const detailsTitle = document.getElementById('detailsTitle'); const detailsContent = document.getElementById('detailsContent'); const openIngredientModalBtn = document.getElementById('openIngredientModalBtn'); const openMovementModalBtn = document.getElementById('openMovementModalBtn'); function escapeHtml(text){ const str = String(text ?? ''); return str .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function canUseSwal(){ return typeof window.Swal !== 'undefined' && typeof window.Swal.fire === 'function'; } function openModal(el){ if(!el) return; el.classList.add('show'); el.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; const focusable = el.querySelector('input, select, textarea, button'); focusable?.focus(); } function closeModal(el){ if(!el) return; el.classList.remove('show'); el.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; } const isEditingIngredient = <?= $editId > 0 ? 'true' : 'false'; ?>; function closeIngredientModalWithReset(){ closeModal(ingredientModal); if(isEditingIngredient){ window.location.href = 'inventory.php'; } } openIngredientModalBtn?.addEventListener('click', () => openModal(ingredientModal)); openMovementModalBtn?.addEventListener('click', () => openModal(movementModal)); document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', () => { const which = btn.getAttribute('data-close'); if(which === 'ingredient') closeIngredientModalWithReset(); if(which === 'movement') closeModal(movementModal); if(which === 'details') closeModal(detailsModal); }); }); [ingredientModal, movementModal, detailsModal].forEach(el => { el?.addEventListener('click', (e) => { if(e.target === el) { if(el === ingredientModal) { closeIngredientModalWithReset(); } else { closeModal(el); } } }); }); document.addEventListener('keydown', (e) => { if(e.key === 'Escape'){ if(ingredientModal?.classList.contains('show')) closeIngredientModalWithReset(); if(movementModal?.classList.contains('show')) closeModal(movementModal); if(detailsModal?.classList.contains('show')) closeModal(detailsModal); } }); const shouldOpenIngredient = <?= $openIngredientModal ? 'true' : 'false'; ?>; const shouldOpenMovement = <?= $openMovementModal ? 'true' : 'false'; ?>; if(shouldOpenMovement){ openModal(movementModal); } else if(shouldOpenIngredient){ openModal(ingredientModal); } function renderDetails(items){ const safeItems = Array.isArray(items) ? items : []; const html = safeItems.map(({label, value}) => { const v = (value ?? '') === '' ? '—' : String(value); return ` <div class="details-item"> <strong>${escapeHtml(label)}</strong> <div>${escapeHtml(v)}</div> </div> `; }).join(''); return `<div class="details-grid">${html}</div>`; } document.querySelectorAll('button[data-view-ingredient]').forEach(btn => { btn.addEventListener('click', () => { const name = (btn.getAttribute('data-name') || '').trim(); const desc = (btn.getAttribute('data-desc') || '').trim(); const unit = (btn.getAttribute('data-unit') || '').trim(); const stock = (btn.getAttribute('data-stock') || '').trim(); const min = (btn.getAttribute('data-min') || '').trim(); const active = (btn.getAttribute('data-active') || '').trim() === '1'; const usedRaw = (btn.getAttribute('data-used') || '').trim(); const used = usedRaw ? usedRaw.split('||').map(s => s.trim()).filter(Boolean).join(', ') : ''; if(detailsTitle) detailsTitle.textContent = name ? `Detalle de ${name}` : 'Detalle de insumo'; if(detailsContent){ detailsContent.innerHTML = renderDetails([ { label: 'Insumo', value: name }, { label: 'Estado', value: active ? 'Activo' : 'Inactivo' }, { label: 'Stock actual', value: stock && unit ? `${stock} ${unit}` : stock }, { label: 'Stock mínimo', value: min && unit ? `${min} ${unit}` : min }, { label: 'Unidad', value: unit }, { label: 'Descripción', value: desc }, { label: 'Usado en recetas', value: used } ]); } openModal(detailsModal); }); }); document.querySelectorAll('button[data-view-movement]').forEach(btn => { btn.addEventListener('click', () => { const date = (btn.getAttribute('data-date') || '').trim(); const ingredient = (btn.getAttribute('data-ingredient') || '').trim(); const type = (btn.getAttribute('data-type') || '').trim(); const qty = (btn.getAttribute('data-qty') || '').trim(); const user = (btn.getAttribute('data-user') || '').trim(); const notes = (btn.getAttribute('data-notes') || '').trim(); if(detailsTitle) detailsTitle.textContent = 'Detalle de movimiento'; if(detailsContent){ detailsContent.innerHTML = renderDetails([ { label: 'Fecha', value: date }, { label: 'Insumo', value: ingredient }, { label: 'Tipo', value: type }, { label: 'Cantidad', value: qty }, { label: 'Usuario', value: user }, { label: 'Notas', value: notes } ]); } openModal(detailsModal); }); }); document.querySelectorAll('form[data-used-products]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); const raw = (form.getAttribute('data-used-products') || '').trim(); if(raw !== ''){ const names = raw.split('||').map(s => s.trim()).filter(Boolean); const show = names.slice(0, 12); const moreCount = Math.max(0, names.length - show.length); if(!canUseSwal()){ const more = moreCount > 0 ? `\n\nY ${moreCount} más...` : ''; alert(`No se puede eliminar este insumo porque está usado en las recetas de:\n\n${show.join('\n')}${more}\n\nElimínalo de esas recetas para poder eliminarlo del inventario.`); return; } const listItems = show.map(n => `<li style="margin:.2rem 0;">${escapeHtml(n)}</li>`).join(''); const moreLine = moreCount > 0 ? `<div style="margin-top:.6rem;color:#64748b;font-size:.95rem;">Y ${moreCount} más...</div>` : ''; await Swal.fire({ icon: 'info', title: 'No se puede eliminar', html: ` <div style="text-align:left;line-height:1.35;"> <div style="margin-bottom:.6rem;">Este insumo está usado en las recetas de:</div> <ul style="margin:.2rem 0 .6rem 1.1rem;padding:0;">${listItems}</ul> ${moreLine} <div style="margin-top:.8rem;">Elimínalo de esas recetas para poder eliminarlo del inventario.</div> </div> `, confirmButtonText: 'Entendido', confirmButtonColor: '#0ea5e9' }); return; } if(!canUseSwal()){ if(confirm('Seguro que deseas eliminar este insumo? Esta acción no se puede deshacer.')){ form.submit(); } return; } const result = await Swal.fire({ icon: 'warning', title: 'Eliminar insumo', text: 'Esta acción no se puede deshacer.', showCancelButton: true, confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar', confirmButtonColor: '#ef4444', cancelButtonColor: '#64748b' }); if(result.isConfirmed){ form.submit(); } }); }); document.querySelectorAll('form[data-toggle-ingredient]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); const willActivate = (form.querySelector('input[name="new_status"]')?.value || '') === '1'; const actionLabel = willActivate ? 'Activar' : 'Desactivar'; const color = willActivate ? '#22c55e' : '#ef4444'; if(!canUseSwal()){ if(confirm(`Seguro que deseas ${actionLabel.toLowerCase()} este insumo?`)){ form.submit(); } return; } const result = await Swal.fire({ icon: 'question', title: `${actionLabel} insumo`, text: `Seguro que deseas ${actionLabel.toLowerCase()} este insumo?`, showCancelButton: true, confirmButtonText: `Sí, ${actionLabel.toLowerCase()}`, cancelButtonText: 'Cancelar', confirmButtonColor: color, cancelButtonColor: '#64748b' }); if(result.isConfirmed){ form.submit(); } }); }); document.querySelectorAll('form[data-delete-movement]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); const ingredient = (form.getAttribute('data-ingredient') || '').trim(); const type = (form.getAttribute('data-type') || '').trim(); const qty = (form.getAttribute('data-qty') || '').trim(); const text = ingredient && type && qty ? `Se eliminará el movimiento (${type} ${qty}) de ${ingredient} y se revertirá el stock automáticamente.` : 'Se eliminará el movimiento y se revertirá el stock automáticamente.'; if(!canUseSwal()){ if(confirm('Seguro que deseas eliminar este movimiento? Esta acción no se puede deshacer.')){ form.submit(); } return; } const result = await Swal.fire({ icon: 'warning', title: 'Eliminar movimiento', text, showCancelButton: true, confirmButtonText: 'Sí, eliminar', cancelButtonText: 'Cancelar', confirmButtonColor: '#ef4444', cancelButtonColor: '#64748b' }); if(result.isConfirmed){ form.submit(); } }); }); document.querySelectorAll('form[data-clean-orphans]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); if(!canUseSwal()){ if(confirm('Esto eliminará movimientos de inventario que pertenecían a comandas/pedidos ya eliminados. ¿Deseas continuar?')){ form.submit(); } return; } const result = await Swal.fire({ icon: 'warning', title: 'Limpiar movimientos huérfanos', text: 'Esto eliminará del historial los movimientos que pertenecían a comandas/pedidos ya eliminados y corregirá el stock automáticamente.', showCancelButton: true, confirmButtonText: 'Sí, limpiar', cancelButtonText: 'Cancelar', confirmButtonColor: '#ef4444', cancelButtonColor: '#64748b' }); if(result.isConfirmed){ form.submit(); } }); }); </script> </body> </html>
Coded With 💗 by
0x6ick