39957-vm/index.php
Flatlogic Bot 3cfcfefee3 v 0.1
2026-05-11 21:40:02 +00:00

608 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
require_once __DIR__ . '/pos_app.php';
pos_handle_post_request();
$projectName = pos_project_name();
$projectDescription = pos_project_description();
$projectImageUrl = pos_project_image_url();
$pageTitle = $projectName . ' | Punto de venta accesible';
$currentUser = pos_current_user();
$flashes = pos_pull_flashes();
$categoryMeta = pos_category_meta();
$products = pos_all_products();
$groupedProducts = [];
foreach ($categoryMeta as $category => $_meta) {
$groupedProducts[$category] = [];
}
foreach ($products as $product) {
$groupedProducts[$product['category']][] = $product;
}
$groupedProducts = array_filter($groupedProducts, static fn (array $items): bool => $items !== []);
$cartSummary = pos_cart_summary();
$recentSales = pos_recent_sales(8);
$metrics = pos_today_metrics();
$lowStockCount = pos_low_stock_count();
$lowStockProducts = pos_low_stock_products(6);
$cssVersion = file_exists(__DIR__ . '/assets/css/custom.css') ? (string)filemtime(__DIR__ . '/assets/css/custom.css') : (string)time();
$jsVersion = file_exists(__DIR__ . '/assets/js/main.js') ? (string)filemtime(__DIR__ . '/assets/js/main.js') : (string)time();
$flashClassMap = [
'success' => 'text-bg-success',
'danger' => 'text-bg-danger',
'warning' => 'text-bg-warning',
'secondary' => 'text-bg-secondary',
'info' => 'text-bg-primary',
];
$toLower = static function (string $value): string {
if (function_exists('mb_strtolower')) {
return mb_strtolower($value, 'UTF-8');
}
return strtolower($value);
};
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($pageTitle) ?></title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta name="robots" content="noindex, nofollow" />
<meta property="og:title" content="<?= htmlspecialchars($pageTitle) ?>" />
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<meta property="twitter:title" content="<?= htmlspecialchars($pageTitle) ?>" />
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php if ($projectImageUrl !== ''): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="assets/css/custom.css?v=<?= htmlspecialchars($cssVersion) ?>" rel="stylesheet">
</head>
<body class="app-body">
<header class="topbar border-bottom">
<nav class="navbar navbar-expand-lg py-3">
<div class="container-xxl align-items-center">
<a class="navbar-brand brand-mark" href="index.php">
<span class="brand-mark__icon"><i class="bi bi-grid-1x2-fill"></i></span>
<span>
<small class="text-uppercase text-muted d-block">MVP de ventas</small>
<strong><?= htmlspecialchars($projectName) ?></strong>
</span>
</a>
<div class="d-flex align-items-center gap-2 ms-auto flex-wrap justify-content-end">
<a class="btn btn-outline-secondary btn-sm" href="healthz.php">Estado</a>
<?php if ($currentUser): ?>
<a class="btn btn-outline-secondary btn-sm" href="#catalogo">Catálogo</a>
<a class="btn btn-outline-secondary btn-sm" href="#carrito">Canasta</a>
<a class="btn btn-outline-secondary btn-sm" href="#ventas">Ventas</a>
<a class="btn btn-outline-secondary btn-sm" href="#stock">Stock</a>
<span class="badge rounded-pill text-bg-dark px-3 py-2"><?= htmlspecialchars($currentUser['role_label']) ?></span>
<span class="user-chip">
<strong><?= htmlspecialchars($currentUser['name']) ?></strong>
<small><?= htmlspecialchars($currentUser['branch']) ?></small>
</span>
<form method="post" class="d-inline-flex">
<input type="hidden" name="action" value="logout">
<button class="btn btn-outline-danger btn-sm" type="submit">Salir</button>
</form>
<?php endif; ?>
</div>
</div>
</nav>
</header>
<div class="toast-container position-fixed top-0 end-0 p-3">
<?php foreach ($flashes as $flash): ?>
<?php $flashClass = $flashClassMap[$flash['type']] ?? 'text-bg-dark'; ?>
<div class="toast align-items-center border-0 js-app-toast <?= htmlspecialchars($flashClass) ?>" role="status" aria-live="polite" aria-atomic="true" data-bs-delay="3600">
<div class="d-flex">
<div class="toast-body"><?= htmlspecialchars($flash['message']) ?></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Cerrar"></button>
</div>
</div>
<?php endforeach; ?>
</div>
<main class="container-xxl py-4 py-lg-5">
<?php if (!$currentUser): ?>
<section class="row g-4 align-items-stretch mb-4">
<div class="col-lg-7">
<div class="panel hero-panel h-100">
<span class="eyebrow">Diseño limpio, alto contraste, flujo corto</span>
<h1 class="display-heading">Vende en tres pasos y con menos errores.</h1>
<p class="lead-copy">
Catálogo por categorías, canasta siempre visible, recibos claros y alertas de stock bajo.
Este primer MVP está listo para conectar con NestJS más adelante sin cambiar la experiencia de caja.
</p>
<div class="instruction-grid mt-4">
<div class="instruction-card">
<span>1</span>
<strong>Selecciona</strong>
<p>Elige una categoría con botones grandes y visibles.</p>
</div>
<div class="instruction-card">
<span>2</span>
<strong>Agrega</strong>
<p>Presiona “Agregar” una sola vez para llevarlo a la canasta.</p>
</div>
<div class="instruction-card">
<span>3</span>
<strong>Confirma</strong>
<p>Registra la venta y genera un recibo simple al instante.</p>
</div>
</div>
<div class="d-flex flex-wrap gap-3 mt-4">
<a class="btn btn-dark btn-lg px-4" href="#login">Iniciar sesión</a>
<a class="btn btn-outline-secondary btn-lg px-4" href="healthz.php">Ver estado del sistema</a>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="panel h-100 status-panel">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<span class="eyebrow">Qué incluye esta entrega</span>
<h2 class="section-title mb-0">Primer flujo real</h2>
</div>
<span class="status-dot"></span>
</div>
<ul class="feature-list mb-0">
<li><i class="bi bi-check2-circle"></i> Login con roles Admin y Cajero</li>
<li><i class="bi bi-check2-circle"></i> Productos agrupados por categoría</li>
<li><i class="bi bi-check2-circle"></i> Canasta con total y anulación rápida</li>
<li><i class="bi bi-check2-circle"></i> Registro de venta con recibo</li>
<li><i class="bi bi-check2-circle"></i> Lista de ventas recientes</li>
<li><i class="bi bi-check2-circle"></i> Control de stock con alertas</li>
</ul>
<div class="integration-note mt-4">
<strong>Escalable:</strong> la UI queda lista para mover autenticación, productos y ventas a endpoints de NestJS en una siguiente iteración.
</div>
</div>
</div>
</section>
<section id="login" class="row g-4 align-items-stretch">
<div class="col-lg-5">
<div class="panel h-100">
<span class="eyebrow">Acceso rápido con PIN</span>
<h2 class="section-title">Entrar al punto de venta</h2>
<p class="section-copy">Usa un código corto y un PIN de cuatro dígitos. Esto es ideal para una caja con operadores frecuentes.</p>
<form method="post" class="stack-form mt-4">
<input type="hidden" name="action" value="login">
<div>
<label class="form-label" for="access_code">Código de acceso</label>
<input class="form-control form-control-lg" id="access_code" name="access_code" placeholder="Ej. CAJA01" autocomplete="username" required>
</div>
<div>
<label class="form-label" for="pin">PIN</label>
<input class="form-control form-control-lg" id="pin" name="pin" type="password" placeholder="4 dígitos" inputmode="numeric" maxlength="4" autocomplete="current-password" required>
</div>
<button class="btn btn-dark btn-lg w-100" type="submit">
<i class="bi bi-box-arrow-in-right me-2"></i>Ingresar ahora
</button>
</form>
</div>
</div>
<div class="col-lg-7">
<div class="panel h-100">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-3">
<div>
<span class="eyebrow">Accesos de prueba</span>
<h2 class="section-title mb-0">Roles listos para probar</h2>
</div>
<span class="text-muted small">Autocompleta el formulario con un toque</span>
</div>
<div class="row g-3">
<?php foreach (pos_demo_users() as $code => $demoUser): ?>
<div class="col-md-4">
<button
class="demo-user-card text-start w-100"
type="button"
data-fill-login
data-code="<?= htmlspecialchars($code) ?>"
data-pin="<?= htmlspecialchars((string)$demoUser['pin']) ?>"
>
<span class="demo-role"><?= htmlspecialchars($demoUser['role_label']) ?></span>
<strong><?= htmlspecialchars($demoUser['name']) ?></strong>
<span class="demo-code">Código: <?= htmlspecialchars($code) ?></span>
<span class="demo-code">PIN: <?= htmlspecialchars((string)$demoUser['pin']) ?></span>
</button>
</div>
<?php endforeach; ?>
</div>
<div class="mini-grid mt-4">
<div class="mini-card">
<i class="bi bi-person-check"></i>
<strong>Admin</strong>
<p>Ajusta stock y supervisa alertas críticas.</p>
</div>
<div class="mini-card">
<i class="bi bi-cart-check"></i>
<strong>Cajero</strong>
<p>Vende rápido sin entrar a pantallas complejas.</p>
</div>
<div class="mini-card">
<i class="bi bi-receipt"></i>
<strong>Recibos</strong>
<p>Consulta la venta al detalle con un clic.</p>
</div>
</div>
</div>
</div>
</section>
<?php else: ?>
<section class="overview-grid mb-4">
<div class="panel overview-panel">
<span class="eyebrow">Caja activa</span>
<h1 class="display-heading mb-3">Hola, <?= htmlspecialchars($currentUser['name']) ?>.</h1>
<p class="lead-copy mb-0">Usa el flujo corto: categoría, agregar a canasta y confirmar venta. Todo lo esencial está visible en la misma pantalla.</p>
<div class="quick-step-row mt-4">
<span class="quick-step"><strong>1.</strong> Elegir categoría</span>
<span class="quick-step"><strong>2.</strong> Agregar productos</span>
<span class="quick-step"><strong>3.</strong> Registrar venta</span>
</div>
</div>
<div class="panel metric-card">
<span class="metric-label">Ventas hoy</span>
<strong><?= htmlspecialchars((string)$metrics['sales_count']) ?></strong>
<small>Operaciones registradas hoy</small>
</div>
<div class="panel metric-card">
<span class="metric-label">Total vendido</span>
<strong><?= htmlspecialchars(pos_format_money((float)$metrics['sales_total'])) ?></strong>
<small>Acumulado del día</small>
</div>
<div class="panel metric-card">
<span class="metric-label">Alertas</span>
<strong><?= htmlspecialchars((string)$lowStockCount) ?></strong>
<small>Productos con stock bajo</small>
</div>
</section>
<?php if ($lowStockCount > 0): ?>
<section class="panel alert-panel mb-4">
<div>
<span class="eyebrow text-warning-emphasis">Atención visible</span>
<h2 class="section-title mb-1">Hay productos con stock por debajo del umbral.</h2>
<p class="section-copy mb-0">Esto ayuda a evitar ventas fallidas y a reponer inventario a tiempo.</p>
</div>
<a class="btn btn-outline-secondary" href="#stock">Revisar stock</a>
</section>
<?php endif; ?>
<section class="row g-4 align-items-start">
<div class="col-xl-8">
<div class="panel mb-4" id="catalogo">
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-center mb-4">
<div>
<span class="eyebrow">Catálogo por categorías</span>
<h2 class="section-title mb-1">Selecciona productos</h2>
<p class="section-copy mb-0">Filtros grandes, categorías claras y botones de acción directos.</p>
</div>
<div class="search-shell">
<label class="form-label visually-hidden" for="product-search">Buscar producto</label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input id="product-search" class="form-control" type="search" placeholder="Buscar producto" aria-label="Buscar producto">
</div>
</div>
</div>
<div class="filter-toolbar mb-4" role="tablist" aria-label="Filtro por categoría">
<button class="btn btn-dark is-active" type="button" data-filter-category="all">Todas</button>
<?php foreach ($groupedProducts as $category => $items): ?>
<?php $meta = $categoryMeta[$category] ?? ['icon' => 'bi-box-seam', 'description' => 'Productos']; ?>
<button class="btn btn-outline-secondary" type="button" data-filter-category="<?= htmlspecialchars($category) ?>">
<i class="bi <?= htmlspecialchars($meta['icon']) ?> me-2"></i><?= htmlspecialchars($category) ?>
</button>
<?php endforeach; ?>
</div>
<div id="catalog-results">
<?php foreach ($groupedProducts as $category => $items): ?>
<?php $meta = $categoryMeta[$category] ?? ['icon' => 'bi-box-seam', 'description' => 'Productos']; ?>
<section class="catalog-section mb-4" data-category-section="<?= htmlspecialchars($category) ?>">
<div class="catalog-section__header">
<div>
<span class="category-pill"><i class="bi <?= htmlspecialchars($meta['icon']) ?>"></i><?= htmlspecialchars($category) ?></span>
<p class="section-copy mb-0"><?= htmlspecialchars($meta['description']) ?></p>
</div>
<span class="text-muted small"><?= htmlspecialchars((string)count($items)) ?> productos</span>
</div>
<div class="row g-3 mt-1">
<?php foreach ($items as $product): ?>
<?php
$stock = (int)$product['stock'];
$isLow = $stock <= (int)$product['low_stock_threshold'];
$searchData = $toLower((string)$product['name'] . ' ' . (string)$product['category']);
?>
<div class="col-sm-6 col-xxl-4 product-col" data-product-card data-search="<?= htmlspecialchars($searchData) ?>" data-category="<?= htmlspecialchars((string)$product['category']) ?>">
<article class="product-card h-100">
<div class="product-card__head">
<span class="icon-surface"><i class="bi <?= htmlspecialchars($meta['icon']) ?>"></i></span>
<span class="stock-badge <?= $isLow ? 'is-low' : '' ?>">
<?= $isLow ? 'Stock bajo' : 'Disponible' ?>
</span>
</div>
<div class="product-card__body">
<h3><?= htmlspecialchars((string)$product['name']) ?></h3>
<p><?= htmlspecialchars((string)$product['unit_label']) ?> · <?= htmlspecialchars((string)$stock) ?> en stock</p>
</div>
<div class="product-card__footer">
<div>
<div class="price-tag"><?= htmlspecialchars(pos_format_money((float)$product['price'])) ?></div>
<small class="text-muted">Categoría: <?= htmlspecialchars((string)$product['category']) ?></small>
</div>
<form method="post">
<input type="hidden" name="action" value="add_to_cart">
<input type="hidden" name="product_id" value="<?= htmlspecialchars((string)$product['id']) ?>">
<button class="btn btn-dark btn-action w-100 mt-3" type="submit" <?= $stock < 1 ? 'disabled' : '' ?>>
<i class="bi bi-plus-lg me-2"></i><?= $stock < 1 ? 'Sin stock' : 'Agregar a canasta' ?>
</button>
</form>
</div>
</article>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endforeach; ?>
</div>
<div class="empty-search-state d-none" id="empty-search-state">
<i class="bi bi-search-heart"></i>
<strong>No encontramos productos con ese filtro.</strong>
<p>Prueba otra categoría o borra la búsqueda.</p>
</div>
</div>
<section class="panel mb-4" id="ventas">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3 mb-3">
<div>
<span class="eyebrow">Historial</span>
<h2 class="section-title mb-1">Ventas recientes</h2>
<p class="section-copy mb-0">Cada venta queda guardada con total, fecha y enlace al recibo.</p>
</div>
<?php if (!empty($_SESSION['pos_last_receipt_id'])): ?>
<a class="btn btn-outline-secondary" href="receipt.php?id=<?= htmlspecialchars((string)$_SESSION['pos_last_receipt_id']) ?>">Ver último recibo</a>
<?php endif; ?>
</div>
<?php if ($recentSales === []): ?>
<div class="empty-state">
<i class="bi bi-receipt-cutoff"></i>
<strong>Todavía no hay ventas registradas.</strong>
<p>Cuando confirmes una venta aparecerá aquí automáticamente.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-borderless align-middle sales-table mb-0">
<thead>
<tr>
<th>Recibo</th>
<th>Fecha</th>
<th>Cajero</th>
<th>Items</th>
<th>Total</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($recentSales as $sale): ?>
<tr>
<td>
<strong><?= htmlspecialchars((string)$sale['receipt_number']) ?></strong>
<div class="text-muted small"><?= htmlspecialchars((string)$sale['cashier_role']) ?></div>
</td>
<td><?= htmlspecialchars(pos_format_datetime((string)$sale['created_at'])) ?></td>
<td><?= htmlspecialchars((string)$sale['cashier_name']) ?></td>
<td><?= htmlspecialchars((string)$sale['item_count']) ?></td>
<td><strong><?= htmlspecialchars(pos_format_money((float)$sale['total'])) ?></strong></td>
<td class="text-end">
<a class="btn btn-outline-secondary btn-sm" href="receipt.php?id=<?= htmlspecialchars((string)$sale['id']) ?>">Ver detalle</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</section>
<section class="panel" id="stock">
<?php if (pos_is_admin()): ?>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3 mb-3">
<div>
<span class="eyebrow">Administración</span>
<h2 class="section-title mb-1">Control de stock</h2>
<p class="section-copy mb-0">Ajusta inventario con botones directos y alertas visibles cuando la cantidad sea crítica.</p>
</div>
<span class="badge text-bg-light border">Solo admin</span>
</div>
<div class="table-responsive">
<table class="table table-borderless align-middle stock-table mb-0">
<thead>
<tr>
<th>Producto</th>
<th>Categoría</th>
<th>Stock actual</th>
<th>Umbral</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<?php $isLow = (int)$product['stock'] <= (int)$product['low_stock_threshold']; ?>
<tr class="<?= $isLow ? 'row-alert' : '' ?>">
<td>
<strong><?= htmlspecialchars((string)$product['name']) ?></strong>
</td>
<td><?= htmlspecialchars((string)$product['category']) ?></td>
<td>
<span class="stock-figure"><?= htmlspecialchars((string)$product['stock']) ?></span>
<?php if ($isLow): ?>
<span class="badge text-bg-warning ms-2">Bajo</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars((string)$product['low_stock_threshold']) ?></td>
<td class="text-end">
<div class="stock-actions justify-content-end">
<?php foreach ([-5, -1, 1, 5] as $delta): ?>
<form method="post">
<input type="hidden" name="action" value="adjust_stock">
<input type="hidden" name="product_id" value="<?= htmlspecialchars((string)$product['id']) ?>">
<input type="hidden" name="delta" value="<?= htmlspecialchars((string)$delta) ?>">
<button class="btn <?= $delta > 0 ? 'btn-dark' : 'btn-outline-secondary' ?> btn-sm" type="submit">
<?= $delta > 0 ? '+' . $delta : (string)$delta ?>
</button>
</form>
<?php endforeach; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-3 mb-3">
<div>
<span class="eyebrow">Avisos visibles</span>
<h2 class="section-title mb-1">Stock que requiere apoyo del administrador</h2>
<p class="section-copy mb-0">Como cajero puedes ver qué productos están por acabarse para pedir reposición.</p>
</div>
<span class="badge text-bg-light border">Lectura</span>
</div>
<?php if ($lowStockProducts === []): ?>
<div class="empty-state compact">
<i class="bi bi-check2-square"></i>
<strong>Todo el stock está dentro del nivel esperado.</strong>
<p>No hay alertas activas por ahora.</p>
</div>
<?php else: ?>
<div class="row g-3">
<?php foreach ($lowStockProducts as $product): ?>
<div class="col-md-6">
<div class="compact-alert-card">
<strong><?= htmlspecialchars((string)$product['name']) ?></strong>
<span><?= htmlspecialchars((string)$product['category']) ?></span>
<small>Actual: <?= htmlspecialchars((string)$product['stock']) ?> · Umbral: <?= htmlspecialchars((string)$product['low_stock_threshold']) ?></small>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
</div>
<div class="col-xl-4">
<aside class="panel cart-panel" id="carrito">
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
<div>
<span class="eyebrow">Canasta visible</span>
<h2 class="section-title mb-1">Venta en curso</h2>
<p class="section-copy mb-0">Revisa productos y total antes de cobrar.</p>
</div>
<span class="count-chip"><?= htmlspecialchars((string)$cartSummary['item_count']) ?> ítems</span>
</div>
<?php if ($cartSummary['items'] === []): ?>
<div class="empty-state compact">
<i class="bi bi-cart-x"></i>
<strong>La canasta está vacía.</strong>
<p>Agrega productos para comenzar una venta.</p>
</div>
<?php else: ?>
<div class="cart-items">
<?php foreach ($cartSummary['items'] as $item): ?>
<article class="cart-item">
<div>
<h3><?= htmlspecialchars((string)$item['name']) ?></h3>
<p><?= htmlspecialchars((string)$item['category']) ?> · <?= htmlspecialchars(pos_format_money((float)$item['price'])) ?></p>
<?php if ($item['low_stock']): ?>
<span class="badge text-bg-warning">Stock bajo</span>
<?php endif; ?>
</div>
<div class="text-end">
<strong class="d-block mb-2"><?= htmlspecialchars(pos_format_money((float)$item['line_total'])) ?></strong>
<div class="cart-item__controls">
<form method="post">
<input type="hidden" name="action" value="change_qty">
<input type="hidden" name="product_id" value="<?= htmlspecialchars((string)$item['id']) ?>">
<input type="hidden" name="delta" value="-1">
<button class="btn btn-outline-secondary btn-sm" type="submit" aria-label="Disminuir cantidad"></button>
</form>
<span class="qty-pill"><?= htmlspecialchars((string)$item['quantity']) ?></span>
<form method="post">
<input type="hidden" name="action" value="change_qty">
<input type="hidden" name="product_id" value="<?= htmlspecialchars((string)$item['id']) ?>">
<input type="hidden" name="delta" value="1">
<button class="btn btn-outline-secondary btn-sm" type="submit" aria-label="Aumentar cantidad" <?= $item['max_reached'] ? 'disabled' : '' ?>>+</button>
</form>
<form method="post">
<input type="hidden" name="action" value="remove_item">
<input type="hidden" name="product_id" value="<?= htmlspecialchars((string)$item['id']) ?>">
<button class="btn btn-outline-danger btn-sm" type="submit">Quitar</button>
</form>
</div>
</div>
</article>
<?php endforeach; ?>
</div>
<div class="totals-box mt-4">
<div class="d-flex justify-content-between">
<span>Subtotal</span>
<strong><?= htmlspecialchars(pos_format_money((float)$cartSummary['subtotal'])) ?></strong>
</div>
<div class="d-flex justify-content-between total-line mt-2">
<span>Total a pagar</span>
<strong><?= htmlspecialchars(pos_format_money((float)$cartSummary['total'])) ?></strong>
</div>
</div>
<div class="d-grid gap-3 mt-4">
<form method="post">
<input type="hidden" name="action" value="checkout">
<button class="btn btn-dark btn-lg w-100" type="submit">
<i class="bi bi-check2-circle me-2"></i>Registrar venta
</button>
</form>
<form method="post" data-confirm="¿Deseas anular la venta en curso? Esta acción vaciará la canasta.">
<input type="hidden" name="action" value="cancel_cart">
<button class="btn btn-outline-danger btn-lg w-100" type="submit">
<i class="bi bi-x-circle me-2"></i>Anular venta
</button>
</form>
</div>
<?php endif; ?>
<div class="info-callout mt-4">
<i class="bi bi-info-circle"></i>
<p>Al confirmar, el sistema descuenta stock y guarda un recibo consultable desde “Ventas recientes”.</p>
</div>
</aside>
</div>
</section>
<?php endif; ?>
</main>
<footer class="site-footer border-top py-4">
<div class="container-xxl d-flex flex-column flex-lg-row justify-content-between gap-2 text-muted small">
<span><?= htmlspecialchars($projectName) ?> · Punto de venta accesible · <?= htmlspecialchars(gmdate('d/m/Y H:i')) ?> UTC</span>
<span>Listo para evolucionar a API + NestJS en una siguiente iteración.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="assets/js/main.js?v=<?= htmlspecialchars($jsVersion) ?>" defer></script>
</body>
</html>