initial modularization + templates

This commit is contained in:
2026-03-10 18:40:47 -03:00
parent 3c4b2e148d
commit ef9a9296dd
10 changed files with 841 additions and 782 deletions

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Build SekiPOS (F5)",
"type": "node",
"request": "launch",
"preLaunchTask": "build-sekipos"
}
]
}

15
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-sekipos",
"type": "shell",
"command": "docker build -t sekipos:latest .",
"group": "build",
"presentation": {
"reveal": "always",
"panel": "new"
}
}
]
}

17
static/cookieStuff.js Normal file
View File

@@ -0,0 +1,17 @@
function setCookie(name, value, days = 365) {
const d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/;SameSite=Lax";
}
function getCookie(name) {
let nameEQ = name + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}

255
static/style.css Normal file
View File

@@ -0,0 +1,255 @@
:root {
--bg: #ebedef;
--card-bg: #ffffff;
--text-main: #2e3338;
--text-muted: #4f5660;
--border: #e3e5e8;
--navbar-bg: #ffffff;
--input-bg: #e3e5e8;
--table-head: #f2f3f5;
--accent: #5865f2;
--accent-hover: #4752c4;
--danger: #ed4245;
}
[data-theme="dark"] {
--bg: #36393f;
--card-bg: #2f3136;
--text-main: #dcddde;
--text-muted: #b9bbbe;
--border: #202225;
--navbar-bg: #202225;
--input-bg: #202225;
--table-head: #292b2f;
}
body {
background: var(--bg);
color: var(--text-main);
font-family: "gg sans", "Segoe UI", sans-serif;
transition: background 0.2s, color 0.2s;
}
/* ── Navbar ── */
.navbar {
background: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border);
}
.navbar-brand {
color: var(--text-main) !important;
font-weight: 700;
}
.nav-link,
.dropdown-item {
color: var(--text-main) !important;
}
.dropdown-menu {
background: var(--card-bg);
border: 1px solid var(--border);
}
.dropdown-item:hover {
background: var(--input-bg);
}
.dropdown-item.text-danger {
color: var(--danger) !important;
}
/* ── Cards ── */
.discord-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* ── Inputs ── */
.form-control,
.form-control:focus {
background: var(--input-bg);
color: var(--text-main);
border: none;
box-shadow: none;
}
.form-control:focus {
outline: 2px solid var(--accent);
}
.form-control::placeholder {
color: var(--text-muted);
}
/* ── Buttons ── */
.btn-accent {
background: var(--accent);
color: #fff;
border: none;
}
.btn-accent:hover {
background: var(--accent-hover);
color: #fff;
}
.btn-danger-discord {
background: var(--danger);
color: #fff;
border: none;
}
.btn-danger-discord:hover {
background: #c23235;
color: #fff;
}
/* ── Price tag ── */
.price-tag {
font-size: 2.8rem;
/* Slightly larger for the wider card */
font-weight: 800;
color: var(--accent);
/* Optional: uses your accent color for better visibility */
}
/* ── Table ── */
.table {
color: var(--text-main);
--bs-table-color: var(--text-main);
--bs-table-bg: transparent;
--bs-table-border-color: var(--border);
}
/* -- Checkbox Size Fix -- */
#select-all {
transform: scale(1.3);
margin-top: 2px;
}
[data-theme="dark"] .table {
--bs-table-color: var(--text-main);
color: var(--text-main);
}
.table thead th {
background: var(--table-head);
color: var(--text-muted);
font-size: 0.72rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
}
.table tbody td {
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
/* ── Bulk bar ── */
.bulk-bar {
background: var(--accent);
color: #fff;
border-radius: 8px;
}
.bulk-bar .form-control {
width: 110px;
background: rgba(0, 0, 0, 0.2) !important;
color: #fff !important;
border: 1px solid rgba(255, 255, 255, 0.25) !important;
}
.bulk-bar .form-control::placeholder {
color: rgba(255, 255, 255, 0.6);
}
/* ── New-product prompt ── */
.new-product-prompt {
background: var(--accent);
color: #fff;
border-radius: 8px;
}
/* ── Product image ── */
#display-img {
width: 100%;
/* Allows it to fill the new width */
max-width: 250px;
/* Increased from 160px */
height: auto;
max-height: 250px;
/* Increased from 160px */
object-fit: contain;
}
/* ── Checkbox ── */
input[type="checkbox"] {
cursor: pointer;
}
/* ── Mobile: hide barcode column ── */
@media (max-width: 576px) {
.col-barcode {
display: none;
}
.btn-edit-sm,
.btn-del-sm {
padding: 4px 7px;
font-size: 0.75rem;
}
}
.modal-content {
background: var(--card-bg);
color: var(--text-main);
border: 1px solid var(--border);
}
.modal-header,
.modal-footer {
border-color: var(--border);
}
.btn-close {
/* Makes the X button visible in dark mode */
filter: var(--bs-theme-placeholder, invert(0.7) grayscale(100%) brightness(200%));
}
[data-theme="dark"] .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
/* Add this inside your <style> tag */
[data-theme="dark"] .text-muted {
color: var(--text-muted) !important;
}
[data-theme="dark"] .modal-body .text-muted {
color: #f6f6f7 !important;
}
.btn-close {
filter: none;
}
[data-theme="dark"] .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
.form-select {
background-color: var(--input-bg) !important;
color: var(--text-main) !important;
border: none !important;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23adb5bd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
background-repeat: no-repeat !important;
background-position: right 0.75rem center !important;
background-size: 16px 12px !important;
}
[data-theme="dark"] .form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dcddde' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
}

39
static/themeStuff.js Normal file
View File

@@ -0,0 +1,39 @@
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
localStorage.setItem('theme', t);
const isDark = (t === 'dark');
const themeIcon = document.getElementById('theme-icon');
const themeLabel = document.getElementById('theme-label');
if (themeIcon) {
themeIcon.className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
}
if (themeLabel) {
themeLabel.innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
}
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
}
function initTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
applyTheme(savedTheme);
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
}
// Listen for system theme changes only if the user hasn't set a manual override
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
initTheme();

View File

@@ -121,7 +121,8 @@
.form-control::placeholder { .form-control::placeholder {
color: var(--text-muted) !important; color: var(--text-muted) !important;
opacity: 1; /* Forces Firefox to respect the color */ opacity: 1;
/* Forces Firefox to respect the color */
} }
#grand-total { #grand-total {
@@ -191,24 +192,32 @@
/* ── Thermal Printer Styles (80mm) ── */ /* ── Thermal Printer Styles (80mm) ── */
@media print { @media print {
/* Kill all animations instantly so the printer doesn't photograph a mid-fade background */ /* Kill all animations instantly so the printer doesn't photograph a mid-fade background */
*, *::before, *::after { *,
*::before,
*::after {
transition: none !important; transition: none !important;
animation: none !important; animation: none !important;
} }
/* Force true white background */ /* Force true white background */
html, body { html,
background: #ffffff !important; body {
background: #ffffff !important;
color: #000000 !important; color: #000000 !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
-webkit-print-color-adjust: exact !important; -webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important; print-color-adjust: exact !important;
} }
.navbar, .container-fluid, .modal { display: none !important; } .navbar,
.container-fluid,
.modal {
display: none !important;
}
#receipt-print-zone { #receipt-print-zone {
display: block !important; display: block !important;
width: 80mm; width: 80mm;
@@ -224,18 +233,41 @@
background: transparent !important; background: transparent !important;
color: #000000 !important; color: #000000 !important;
opacity: 1 !important; opacity: 1 !important;
font-weight: 800 !important; font-weight: 800 !important;
text-shadow: none !important; text-shadow: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
@page { margin: 0; } @page {
margin: 0;
.receipt-header { text-align: center; margin-bottom: 10px; } }
.receipt-table { width: 100%; margin-bottom: 10px; }
.receipt-table th { text-align: left; border-bottom: 1px dashed #000 !important; padding-bottom: 3px; } .receipt-header {
.receipt-table td { padding: 3px 0; vertical-align: top; } text-align: center;
.receipt-total-row { border-top: 1px dashed #000 !important; font-weight: 800 !important; font-size: 14px; } margin-bottom: 10px;
}
.receipt-table {
width: 100%;
margin-bottom: 10px;
}
.receipt-table th {
text-align: left;
border-bottom: 1px dashed #000 !important;
padding-bottom: 3px;
}
.receipt-table td {
padding: 3px 0;
vertical-align: top;
}
.receipt-total-row {
border-top: 1px dashed #000 !important;
font-weight: 800 !important;
font-size: 14px;
}
} }
/* ── Dropdown Select Fix ── */ /* ── Dropdown Select Fix ── */
@@ -259,7 +291,9 @@
</head> </head>
<body> <body>
<div class="modal fade" id="customProductModal" tabindex="-1" data-bs-backdrop="static"> {% with active_page='checkout' %}{% include 'navbar.html' %}{% endwith %}
<div class="modal fade" id="customProductModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -269,13 +303,13 @@
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label text-muted small mb-1">Descripción</label> <label class="form-label text-muted small mb-1">Descripción</label>
<input type="text" id="custom-name" class="form-control" placeholder="Ej: Varios, Bolsa, etc." <input type="text" id="custom-name" class="form-control" placeholder="Ej: Varios, Bolsa, etc."
onkeydown="if(event.key === 'Enter') document.getElementById('custom-price').focus()"> onkeydown="if(event.key === 'Enter') document.getElementById('custom-price').focus()">
</div> </div>
<div class="row g-2"> <div class="row g-2">
<div class="col-8"> <div class="col-8">
<label class="form-label text-muted small mb-1">Precio Unitario</label> <label class="form-label text-muted small mb-1">Precio Unitario</label>
<input type="number" id="custom-price" class="form-control" placeholder="Ej: 1500" <input type="number" id="custom-price" class="form-control" placeholder="Ej: 1500"
onkeydown="if(event.key === 'Enter') addCustomProduct()"> onkeydown="if(event.key === 'Enter') addCustomProduct()">
</div> </div>
<div class="col-4"> <div class="col-4">
@@ -301,7 +335,7 @@
<div style="font-size: 11px; font-weight: bold;">Ticket Nº <span id="receipt-ticket-id"></span></div> <div style="font-size: 11px; font-weight: bold;">Ticket Nº <span id="receipt-ticket-id"></span></div>
<div id="receipt-date" style="font-size: 11px;"></div> <div id="receipt-date" style="font-size: 11px;"></div>
</div> </div>
<table class="receipt-table"> <table class="receipt-table">
<thead> <thead>
<tr> <tr>
@@ -311,14 +345,14 @@
</tr> </tr>
</thead> </thead>
<tbody id="receipt-items-print"> <tbody id="receipt-items-print">
</tbody> </tbody>
</table> </table>
<div class="receipt-total-row d-flex justify-content-between pt-2"> <div class="receipt-total-row d-flex justify-content-between pt-2">
<span>TOTAL:</span> <span>TOTAL:</span>
<span id="receipt-total-print"></span> <span id="receipt-total-print"></span>
</div> </div>
<div style="text-align: center; margin-top: 20px; font-size: 10px;"> <div style="text-align: center; margin-top: 20px; font-size: 10px;">
¡Gracias por su compra! ¡Gracias por su compra!
</div> </div>
@@ -375,18 +409,23 @@
<div class="modal-dialog modal-dialog-centered modal-sm"> <div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header border-0 pb-0"> <div class="modal-header border-0 pb-0">
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">Total a Pagar</h5> <h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">Total
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button> a Pagar</h5>
<button type="button" class="btn-close position-absolute end-0 top-0 m-3"
data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body text-center pt-1 pb-4"> <div class="modal-body text-center pt-1 pb-4">
<h1 id="payment-modal-total" class="mb-4" style="color: var(--accent); font-weight: 800; font-size: 3rem;">$0</h1> <h1 id="payment-modal-total" class="mb-4"
style="color: var(--accent); font-weight: 800; font-size: 3rem;">$0</h1>
<div class="d-grid gap-3 px-3"> <div class="d-grid gap-3 px-3">
<button class="btn btn-lg btn-success py-3" onclick="openVueltoModal()"> <button class="btn btn-lg btn-success py-3" onclick="openVueltoModal()">
<i class="bi bi-cash-coin me-2" style="font-size: 1.5rem; vertical-align: middle;"></i> Efectivo <i class="bi bi-cash-coin me-2" style="font-size: 1.5rem; vertical-align: middle;"></i>
Efectivo
</button> </button>
<button class="btn btn-lg btn-secondary py-3" onclick="executeCheckout('tarjeta')"> <button class="btn btn-lg btn-secondary py-3" onclick="executeCheckout('tarjeta')">
<i class="bi bi-credit-card me-2" style="font-size: 1.5rem; vertical-align: middle;"></i> Tarjeta (Pronto) <i class="bi bi-credit-card me-2" style="font-size: 1.5rem; vertical-align: middle;"></i>
Tarjeta (Pronto)
</button> </button>
</div> </div>
</div> </div>
@@ -397,30 +436,34 @@
<div class="modal-dialog modal-dialog-centered modal-sm"> <div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header border-0 pb-0"> <div class="modal-header border-0 pb-0">
<h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">Pago en Efectivo</h5> <h5 class="modal-title w-100 text-center text-muted text-uppercase" style="font-size: 0.8rem;">Pago
<button type="button" class="btn-close position-absolute end-0 top-0 m-3" data-bs-dismiss="modal"></button> en Efectivo</h5>
<button type="button" class="btn-close position-absolute end-0 top-0 m-3"
data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body text-center pt-1 pb-4"> <div class="modal-body text-center pt-1 pb-4">
<div class="mb-3"> <div class="mb-3">
<span class="text-muted small">Total a Pagar:</span><br> <span class="text-muted small">Total a Pagar:</span><br>
<span id="vuelto-total-display" class="fs-4 fw-bold" style="color: var(--text-main);">$0</span> <span id="vuelto-total-display" class="fs-4 fw-bold" style="color: var(--text-main);">$0</span>
</div> </div>
<div class="mb-3 text-start"> <div class="mb-3 text-start">
<label class="text-muted small mb-1">Monto Recibido</label> <label class="text-muted small mb-1">Monto Recibido</label>
<input type="number" id="monto-recibido" class="form-control form-control-lg text-center fw-bold fs-4" <input type="number" id="monto-recibido"
placeholder="$0" onkeyup="calculateVuelto()" onchange="calculateVuelto()" class="form-control form-control-lg text-center fw-bold fs-4" placeholder="$0"
onkeyup="calculateVuelto()" onchange="calculateVuelto()"
onkeydown="if(event.key === 'Enter' && !document.getElementById('btn-confirm-vuelto').disabled) executeCheckout('efectivo')"> onkeydown="if(event.key === 'Enter' && !document.getElementById('btn-confirm-vuelto').disabled) executeCheckout('efectivo')">
</div> </div>
<div class="d-flex justify-content-center gap-2 mb-3" id="vuelto-quick-buttons"></div> <div class="d-flex justify-content-center gap-2 mb-3" id="vuelto-quick-buttons"></div>
<div class="p-3 mb-3" style="background: var(--input-bg); border-radius: 8px;"> <div class="p-3 mb-3" style="background: var(--input-bg); border-radius: 8px;">
<span class="text-muted small text-uppercase fw-bold">Vuelto a Entregar</span><br> <span class="text-muted small text-uppercase fw-bold">Vuelto a Entregar</span><br>
<span id="vuelto-amount" class="fs-1 fw-bold text-muted">$0</span> <span id="vuelto-amount" class="fs-1 fw-bold text-muted">$0</span>
</div> </div>
<button id="btn-confirm-vuelto" class="btn btn-success w-100 py-3 fw-bold" onclick="executeCheckout('efectivo')" disabled> <button id="btn-confirm-vuelto" class="btn btn-success w-100 py-3 fw-bold"
onclick="executeCheckout('efectivo')" disabled>
Confirmar Venta Confirmar Venta
</button> </button>
</div> </div>
@@ -436,8 +479,9 @@
<div class="modal-body text-center pt-0 pb-4"> <div class="modal-body text-center pt-0 pb-4">
<i class="bi bi-question-circle text-warning mb-3" style="font-size: 3rem;"></i> <i class="bi bi-question-circle text-warning mb-3" style="font-size: 3rem;"></i>
<h4 class="mb-2">Producto No Registrado</h4> <h4 class="mb-2">Producto No Registrado</h4>
<p class="text-muted px-3 small">El código <strong id="not-found-barcode" style="color: var(--text-main);"></strong> no existe en la base de datos.</p> <p class="text-muted px-3 small">El código <strong id="not-found-barcode"
style="color: var(--text-main);"></strong> no existe en la base de datos.</p>
<div class="d-flex flex-column gap-2 px-3 mt-4"> <div class="d-flex flex-column gap-2 px-3 mt-4">
<button class="btn btn-accent w-100 py-2" onclick="goToInventory()"> <button class="btn btn-accent w-100 py-2" onclick="goToInventory()">
<i class="bi bi-database-add me-2"></i>Registrar en Inventario <i class="bi bi-database-add me-2"></i>Registrar en Inventario
@@ -450,48 +494,6 @@
</div> </div>
</div> </div>
</div> </div>
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
<span class="navbar-brand">SekiPOS <small class="text-muted fw-normal"
style="font-size:0.65rem;">Caja</small></span>
<div class="ms-3 gap-2 d-flex">
<a href="/" class="btn btn-outline-primary btn-sm">
<i class="bi bi-box-seam me-1"></i>Inventario
</a>
<a href="/sales" class="btn btn-outline-primary btn-sm">
<i class="bi bi-receipt me-1"></i>Ventas
</a>
<a href="/dicom" class="btn btn-outline-danger btn-sm">
<i class="bi bi-journal-x me-1"></i>Dicom
</a>
</div>
<div class="ms-auto">
<div class="dropdown">
<button class="btn btn-accent dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>
<span class="d-none d-sm-inline">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li>
<button class="dropdown-item" onclick="toggleTheme()">
<i class="bi bi-moon-stars me-2" id="theme-icon"></i>
<span id="theme-label">Modo Oscuro</span>
</button>
</li>
<li>
<hr class="dropdown-divider" style="border-color: var(--border);">
</li>
<li>
<a class="dropdown-item text-danger" href="/logout">
<i class="bi bi-box-arrow-right me-2"></i>Salir
</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid"> <div class="container-fluid">
<div class="row g-3"> <div class="row g-3">
@@ -500,17 +502,20 @@
<h4><i class="bi bi-cart3"></i> Carrito</h4> <h4><i class="bi bi-cart3"></i> Carrito</h4>
<div class="position-relative mb-4"> <div class="position-relative mb-4">
<div class="input-group"> <div class="input-group">
<span class="input-group-text border-0 position-absolute" style="background: transparent; z-index: 10;"> <span class="input-group-text border-0 position-absolute"
style="background: transparent; z-index: 10;">
<i class="bi bi-search text-muted"></i> <i class="bi bi-search text-muted"></i>
</span> </span>
<input type="text" id="manual-search" class="form-control ps-5 py-2 rounded" <input type="text" id="manual-search" class="form-control ps-5 py-2 rounded"
placeholder="Buscar producto por nombre o código..." autocomplete="off" onkeyup="filterSearch()"> placeholder="Buscar producto por nombre o código..." autocomplete="off"
<button class="btn btn-accent px-3" type="button" onclick="openCustomProductModal()" title="Agregar manual"> onkeyup="filterSearch()">
<button class="btn btn-accent px-3" type="button" onclick="openCustomProductModal()"
title="Agregar manual">
<i class="bi bi-plus-lg"></i> <span class="d-none d-sm-inline ms-1">Manual</span> <i class="bi bi-plus-lg"></i> <span class="d-none d-sm-inline ms-1">Manual</span>
</button> </button>
</div> </div>
<div id="search-results" class="dropdown-menu w-100 shadow-sm mt-1" <div id="search-results" class="dropdown-menu w-100 shadow-sm mt-1"
style="display: none; position: absolute; top: 100%; left: 0; z-index: 1000; max-height: 300px; overflow-y: auto;"> style="display: none; position: absolute; top: 100%; left: 0; z-index: 1000; max-height: 300px; overflow-y: auto;">
</div> </div>
</div> </div>
<table class="table mt-3" id="cart-table"> <table class="table mt-3" id="cart-table">
@@ -585,7 +590,7 @@
socket.on('scan_error', (data) => { socket.on('scan_error', (data) => {
missingProductData = data; missingProductData = data;
document.getElementById('not-found-barcode').innerText = data.barcode; document.getElementById('not-found-barcode').innerText = data.barcode;
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('notFoundModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('notFoundModal'));
modal.show(); modal.show();
}); });
@@ -598,18 +603,18 @@
function openTempProduct() { function openTempProduct() {
bootstrap.Modal.getInstance(document.getElementById('notFoundModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('notFoundModal')).hide();
// Save the actual scanned barcode so it prints on the receipt instead of "MANUAL-XXX" // Save the actual scanned barcode so it prints on the receipt instead of "MANUAL-XXX"
tempBarcode = missingProductData.barcode; tempBarcode = missingProductData.barcode;
// Pre-fill the name if the OpenFoodFacts API managed to find it // Pre-fill the name if the OpenFoodFacts API managed to find it
document.getElementById('custom-name').value = missingProductData.name || ''; document.getElementById('custom-name').value = missingProductData.name || '';
document.getElementById('custom-price').value = ''; document.getElementById('custom-price').value = '';
document.getElementById('custom-unit').value = 'unit'; document.getElementById('custom-unit').value = 'unit';
const customModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customProductModal')); const customModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customProductModal'));
customModal.show(); customModal.show();
// Auto-focus the price if we already have a name, otherwise focus the name // Auto-focus the price if we already have a name, otherwise focus the name
setTimeout(() => { setTimeout(() => {
if (missingProductData.name) document.getElementById('custom-price').focus(); if (missingProductData.name) document.getElementById('custom-price').focus();
@@ -659,11 +664,11 @@
`; `;
tbody.appendChild(row); tbody.appendChild(row);
}); });
document.getElementById('grand-total').innerText = clp.format(total); document.getElementById('grand-total').innerText = clp.format(total);
// This will actually run now // This will actually run now
saveCart(); saveCart();
} }
function clearCart() { function clearCart() {
@@ -680,11 +685,11 @@
function processSale() { function processSale() {
if (cart.length === 0) return; if (cart.length === 0) return;
// Calculate total and show the payment modal // Calculate total and show the payment modal
const total = cart.reduce((sum, item) => sum + item.subtotal, 0); const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
document.getElementById('payment-modal-total').innerText = clp.format(total); document.getElementById('payment-modal-total').innerText = clp.format(total);
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('paymentModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('paymentModal'));
modal.show(); modal.show();
} }
@@ -706,25 +711,25 @@
// Safely hide whichever modal was open // Safely hide whichever modal was open
const pModal = bootstrap.Modal.getInstance(document.getElementById('paymentModal')); const pModal = bootstrap.Modal.getInstance(document.getElementById('paymentModal'));
if (pModal) pModal.hide(); if (pModal) pModal.hide();
const vModal = bootstrap.Modal.getInstance(document.getElementById('vueltoModal')); const vModal = bootstrap.Modal.getInstance(document.getElementById('vueltoModal'));
if (vModal) vModal.hide(); if (vModal) vModal.hide();
// Pass the new sale_id from the result to the printer // Pass the new sale_id from the result to the printer
const total = cart.reduce((sum, item) => sum + item.subtotal, 0); const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
printReceipt(total, result.sale_id); printReceipt(total, result.sale_id);
// Show the success checkmark // Show the success checkmark
const successModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('successModal')); const successModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('successModal'));
successModal.show(); successModal.show();
// Nuke the cart and auto-save the empty state // Nuke the cart and auto-save the empty state
cart = []; cart = [];
renderCart(); renderCart();
// Auto-hide the success modal after 2 seconds so you don't have to click it // Auto-hide the success modal after 2 seconds so you don't have to click it
setTimeout(() => successModal.hide(), 2000); setTimeout(() => successModal.hide(), 2000);
} else { } else {
alert("Error en la venta: " + (result.error || "Error desconocido")); alert("Error en la venta: " + (result.error || "Error desconocido"));
} }
@@ -737,10 +742,10 @@
function confirmWeight() { function confirmWeight() {
const weightInput = document.getElementById('weight-input'); const weightInput = document.getElementById('weight-input');
const weightGrams = parseInt(weightInput.value, 10); const weightGrams = parseInt(weightInput.value, 10);
if (weightGrams > 0) { if (weightGrams > 0) {
const weightKg = weightGrams / 1000; const weightKg = weightGrams / 1000;
if (editingCartIndex !== null) { if (editingCartIndex !== null) {
// We are editing an existing row // We are editing an existing row
cart[editingCartIndex].qty = weightKg; cart[editingCartIndex].qty = weightKg;
@@ -750,7 +755,7 @@
// We are adding a new scan/search // We are adding a new scan/search
addToCart(pendingProduct, weightKg); addToCart(pendingProduct, weightKg);
} }
bootstrap.Modal.getInstance(document.getElementById('weightModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('weightModal')).hide();
weightInput.value = ''; weightInput.value = '';
editingCartIndex = null; // Clear the tracker editingCartIndex = null; // Clear the tracker
@@ -797,51 +802,32 @@
editingCartIndex = index; editingCartIndex = index;
const item = cart[index]; const item = cart[index];
const weightInput = document.getElementById('weight-input'); const weightInput = document.getElementById('weight-input');
// Convert current kg back to grams for the input // Convert current kg back to grams for the input
weightInput.value = Math.round(item.qty * 1000); weightInput.value = Math.round(item.qty * 1000);
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal'));
modal.show(); modal.show();
// Auto-focus and highlight the existing number so you can just type over it // Auto-focus and highlight the existing number so you can just type over it
setTimeout(() => { setTimeout(() => {
weightInput.focus(); weightInput.focus();
weightInput.select(); weightInput.select();
}, 500); }, 500);
} }
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
const isDark = t === 'dark';
const themeIcon = document.getElementById('theme-icon');
const themeLabel = document.getElementById('theme-label');
if (themeIcon) themeIcon.className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
if (themeLabel) themeLabel.innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
localStorage.setItem('theme', t);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
}
// 1. Load all products from Python into a JavaScript array safely // 1. Load all products from Python into a JavaScript array safely
const allProducts = [ const allProducts = [
{% for p in products %} {% for p in products %}
{ {
barcode: {{ p[0] | tojson }}, barcode: { { p[0] | tojson } },
name: {{ p[1] | tojson }}, name: { { p[1] | tojson } },
price: {{ p[2] | int }}, price: { { p[2] | int } },
image: {{ p[3] | tojson }}, image: { { p[3] | tojson } },
stock: {{ p[4] | int }}, stock: { { p[4] | int } },
unit: {{ p[5] | tojson }} unit: { { p[5] | tojson } }
}, },
{% endfor %} {% endfor %}
]; ];
// 2. Extracted this into a helper so both Scanner and Search can use it // 2. Extracted this into a helper so both Scanner and Search can use it
@@ -856,13 +842,13 @@
if (actualUnit === 'kg') { if (actualUnit === 'kg') {
pendingProduct = product; pendingProduct = product;
pendingProduct.unit = 'kg'; // Force it to match the cart's expected format pendingProduct.unit = 'kg'; // Force it to match the cart's expected format
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal'));
modal.show(); modal.show();
setTimeout(() => document.getElementById('weight-input').focus(), 500); setTimeout(() => document.getElementById('weight-input').focus(), 500);
} else { } else {
// Ensure unit products also get the standardized key // Ensure unit products also get the standardized key
product.unit = 'unit'; product.unit = 'unit';
addToCart(product, 1); addToCart(product, 1);
} }
} }
@@ -876,14 +862,14 @@
function filterSearch() { function filterSearch() {
const query = document.getElementById('manual-search').value.toLowerCase().trim(); const query = document.getElementById('manual-search').value.toLowerCase().trim();
const resultsBox = document.getElementById('search-results'); const resultsBox = document.getElementById('search-results');
if (query.length < 2) { if (query.length < 2) {
resultsBox.style.display = 'none'; resultsBox.style.display = 'none';
return; return;
} }
// Find matches by name or barcode // Find matches by name or barcode
const matches = allProducts.filter(p => const matches = allProducts.filter(p =>
p.name.toLowerCase().includes(query) || p.barcode.includes(query) p.name.toLowerCase().includes(query) || p.barcode.includes(query)
).slice(0, 10); // Limit to 10 results so it doesn't lag out ).slice(0, 10); // Limit to 10 results so it doesn't lag out
@@ -904,7 +890,7 @@
</a> </a>
`).join(''); `).join('');
} }
resultsBox.style.display = 'block'; resultsBox.style.display = 'block';
} }
@@ -913,7 +899,7 @@
if (product) { if (product) {
handleProductScan(product); handleProductScan(product);
} }
// Clean up UI after selection // Clean up UI after selection
const searchInput = document.getElementById('manual-search'); const searchInput = document.getElementById('manual-search');
searchInput.value = ''; searchInput.value = '';
@@ -922,7 +908,7 @@
} }
// Close the search dropdown if user clicks outside of it // Close the search dropdown if user clicks outside of it
document.addEventListener('click', function(e) { document.addEventListener('click', function (e) {
const searchArea = document.getElementById('manual-search'); const searchArea = document.getElementById('manual-search');
const resultsBox = document.getElementById('search-results'); const resultsBox = document.getElementById('search-results');
if (e.target !== searchArea && !resultsBox.contains(e.target)) { if (e.target !== searchArea && !resultsBox.contains(e.target)) {
@@ -934,7 +920,7 @@
document.getElementById('weightModal').addEventListener('hidden.bs.modal', function () { document.getElementById('weightModal').addEventListener('hidden.bs.modal', function () {
document.getElementById('weight-input').value = ''; document.getElementById('weight-input').value = '';
pendingProduct = null; pendingProduct = null;
editingCartIndex = null; editingCartIndex = null;
}); });
function calculateSubtotal(price, qty) { function calculateSubtotal(price, qty) {
@@ -953,7 +939,7 @@
try { try {
cart = JSON.parse(saved); cart = JSON.parse(saved);
renderCart(); renderCart();
} catch(e) { } catch (e) {
console.error("Error cargando el carrito", e); console.error("Error cargando el carrito", e);
cart = []; cart = [];
} }
@@ -963,17 +949,17 @@
function removeItem(idx) { function removeItem(idx) {
itemIndexToRemove = idx; itemIndexToRemove = idx;
// Look up the name safely from the array // Look up the name safely from the array
document.getElementById('removeItemName').innerText = cart[idx].name; document.getElementById('removeItemName').innerText = cart[idx].name;
// getOrCreateInstance prevents UI-breaking ghost backdrops // getOrCreateInstance prevents UI-breaking ghost backdrops
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('removeConfirmModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('removeConfirmModal'));
modal.show(); modal.show();
} }
function printReceipt(total, saleId) { function printReceipt(total, saleId) {
const tbody = document.getElementById('receipt-items-print'); const tbody = document.getElementById('receipt-items-print');
tbody.innerHTML = ''; tbody.innerHTML = '';
cart.forEach(item => { cart.forEach(item => {
const qtyStr = item.unit === 'kg' ? item.qty.toFixed(3) : item.qty; const qtyStr = item.unit === 'kg' ? item.qty.toFixed(3) : item.qty;
tbody.innerHTML += ` tbody.innerHTML += `
@@ -984,11 +970,11 @@
</tr> </tr>
`; `;
}); });
document.getElementById('receipt-ticket-id').innerText = saleId || "N/A"; document.getElementById('receipt-ticket-id').innerText = saleId || "N/A";
document.getElementById('receipt-total-print').innerText = clp.format(total); document.getElementById('receipt-total-print').innerText = clp.format(total);
document.getElementById('receipt-date').innerText = new Date().toLocaleString('es-CL'); document.getElementById('receipt-date').innerText = new Date().toLocaleString('es-CL');
// No hacks, just print // No hacks, just print
window.print(); window.print();
} }
@@ -1001,7 +987,7 @@
document.getElementById('custom-name').value = ''; document.getElementById('custom-name').value = '';
document.getElementById('custom-price').value = ''; document.getElementById('custom-price').value = '';
document.getElementById('custom-unit').value = 'unit'; document.getElementById('custom-unit').value = 'unit';
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customProductModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('customProductModal'));
modal.show(); modal.show();
setTimeout(() => document.getElementById('custom-name').focus(), 500); setTimeout(() => document.getElementById('custom-name').focus(), 500);
@@ -1011,28 +997,28 @@
const nameInput = document.getElementById('custom-name').value.trim(); const nameInput = document.getElementById('custom-name').value.trim();
const priceInput = parseInt(document.getElementById('custom-price').value, 10); const priceInput = parseInt(document.getElementById('custom-price').value, 10);
const unitInput = document.getElementById('custom-unit').value; const unitInput = document.getElementById('custom-unit').value;
if (!nameInput || isNaN(priceInput) || priceInput <= 0) { if (!nameInput || isNaN(priceInput) || priceInput <= 0) {
alert("Por favor ingresa un nombre y un precio válido."); alert("Por favor ingresa un nombre y un precio válido.");
return; return;
} }
// Use the scanned barcode if it exists, otherwise generate a fake one // Use the scanned barcode if it exists, otherwise generate a fake one
const finalBarcode = tempBarcode ? tempBarcode : `MANUAL-${Date.now().toString().slice(-6)}`; const finalBarcode = tempBarcode ? tempBarcode : `MANUAL-${Date.now().toString().slice(-6)}`;
const customProduct = { const customProduct = {
barcode: finalBarcode, barcode: finalBarcode,
name: `* ${nameInput}`, name: `* ${nameInput}`,
price: priceInput, price: priceInput,
image: '', image: '',
stock: 0, stock: 0,
unit: unitInput unit: unitInput
}; };
if (unitInput === 'kg') { if (unitInput === 'kg') {
pendingProduct = customProduct; pendingProduct = customProduct;
bootstrap.Modal.getInstance(document.getElementById('customProductModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('customProductModal')).hide();
const weightModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal')); const weightModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('weightModal'));
weightModal.show(); weightModal.show();
setTimeout(() => document.getElementById('weight-input').focus(), 500); setTimeout(() => document.getElementById('weight-input').focus(), 500);
@@ -1040,7 +1026,7 @@
addToCart(customProduct, 1); addToCart(customProduct, 1);
bootstrap.Modal.getInstance(document.getElementById('customProductModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('customProductModal')).hide();
} }
tempBarcode = null; // Reset it after adding tempBarcode = null; // Reset it after adding
} }
@@ -1057,10 +1043,10 @@
function openVueltoModal() { function openVueltoModal() {
// Hide the main payment selection modal // Hide the main payment selection modal
bootstrap.Modal.getInstance(document.getElementById('paymentModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('paymentModal')).hide();
const total = cart.reduce((sum, item) => sum + item.subtotal, 0); const total = cart.reduce((sum, item) => sum + item.subtotal, 0);
document.getElementById('vuelto-total-display').innerText = clp.format(total); document.getElementById('vuelto-total-display').innerText = clp.format(total);
const input = document.getElementById('monto-recibido'); const input = document.getElementById('monto-recibido');
input.value = ''; input.value = '';
document.getElementById('vuelto-amount').innerText = '$0'; document.getElementById('vuelto-amount').innerText = '$0';
@@ -1070,7 +1056,7 @@
// Generate smart quick-buttons (Exacto, 5k, 10k, 20k) // Generate smart quick-buttons (Exacto, 5k, 10k, 20k)
const quickBox = document.getElementById('vuelto-quick-buttons'); const quickBox = document.getElementById('vuelto-quick-buttons');
quickBox.innerHTML = `<button class="btn btn-sm btn-outline-secondary fw-bold" onclick="setMonto(${total})">Exacto</button>`; quickBox.innerHTML = `<button class="btn btn-sm btn-outline-secondary fw-bold" onclick="setMonto(${total})">Exacto</button>`;
[5000, 10000, 20000].forEach(bill => { [5000, 10000, 20000].forEach(bill => {
if (bill > total && (bill - total) <= 20000) { if (bill > total && (bill - total) <= 20000) {
quickBox.innerHTML += `<button class="btn btn-sm btn-outline-secondary fw-bold" onclick="setMonto(${bill})">${clp.format(bill)}</button>`; quickBox.innerHTML += `<button class="btn btn-sm btn-outline-secondary fw-bold" onclick="setMonto(${bill})">${clp.format(bill)}</button>`;
@@ -1079,7 +1065,7 @@
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('vueltoModal')); const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('vueltoModal'));
modal.show(); modal.show();
// Auto-focus input so you can start typing immediately // Auto-focus input so you can start typing immediately
setTimeout(() => input.focus(), 500); setTimeout(() => input.focus(), 500);
} }
@@ -1107,27 +1093,12 @@
} }
} }
// Initialize on load
(function initTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
applyTheme(savedTheme);
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
})();
// Listen for OS theme changes in real-time
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
loadCart(); loadCart();
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="./static/cookieStuff.js"></script>
<script src="./static/themeStuff.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es" data-theme="light"> <html lang="es" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,78 +10,134 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style> <style>
:root { :root {
--bg: #ebedef; --card-bg: #ffffff; --text-main: #2e3338; --bg: #ebedef;
--text-muted: #4f5660; --border: #e3e5e8; --navbar-bg: #ffffff; --card-bg: #ffffff;
--accent: #5865f2; --input-bg: #e3e5e8; --danger: #ed4245; --text-main: #2e3338;
--text-muted: #4f5660;
--border: #e3e5e8;
--navbar-bg: #ffffff;
--accent: #5865f2;
--input-bg: #e3e5e8;
--danger: #ed4245;
} }
[data-theme="dark"] { [data-theme="dark"] {
--bg: #36393f; --card-bg: #2f3136; --text-main: #dcddde; --bg: #36393f;
--text-muted: #b9bbbe; --border: #202225; --navbar-bg: #202225; --card-bg: #2f3136;
--text-main: #dcddde;
--text-muted: #b9bbbe;
--border: #202225;
--navbar-bg: #202225;
--input-bg: #202225; --input-bg: #202225;
} }
body { background: var(--bg); color: var(--text-main); font-family: "gg sans", "Segoe UI", sans-serif; transition: background 0.2s; } body {
.navbar { background: var(--navbar-bg) !important; border-bottom: 1px solid var(--border); } background: var(--bg);
.navbar-brand { color: var(--text-main) !important; font-weight: 700; } color: var(--text-main);
font-family: "gg sans", "Segoe UI", sans-serif;
.discord-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } transition: background 0.2s;
}
.form-control, .form-control:focus { background-color: var(--input-bg) !important; color: var(--text-main) !important; border: 1px solid var(--border) !important; box-shadow: none !important; }
.form-control:focus { border-color: var(--accent) !important; } .navbar {
.form-control::placeholder { color: var(--text-muted) !important; opacity: 1; } background: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border);
.table { color: var(--text-main) !important; --bs-table-bg: transparent; --bs-table-border-color: var(--border); } }
.table thead th { background: var(--bg); color: var(--text-muted); font-size: 0.75rem; text-transform: uppercase; border-bottom: 1px solid var(--border); }
.table td { border-bottom: 1px solid var(--border); vertical-align: middle; } .navbar-brand {
color: var(--text-main) !important;
.btn-accent { background: var(--accent); color: #fff; border: none; } font-weight: 700;
.dropdown-menu { background: var(--card-bg); border: 1px solid var(--border); } }
.dropdown-item { color: var(--text-main) !important; }
.dropdown-item:hover { background: var(--input-bg); } .discord-card {
background: var(--card-bg);
[data-theme="dark"] .table { --bs-table-color: var(--text-main); color: var(--text-main) !important; } border: 1px solid var(--border);
[data-theme="dark"] .table thead th { background: #292b2f; color: var(--text-muted); } border-radius: 8px;
[data-theme="dark"] .text-muted { color: var(--text-muted) !important; } box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.form-control,
.form-control:focus {
background-color: var(--input-bg) !important;
color: var(--text-main) !important;
border: 1px solid var(--border) !important;
box-shadow: none !important;
}
.form-control:focus {
border-color: var(--accent) !important;
}
.form-control::placeholder {
color: var(--text-muted) !important;
opacity: 1;
}
.table {
color: var(--text-main) !important;
--bs-table-bg: transparent;
--bs-table-border-color: var(--border);
}
.table thead th {
background: var(--bg);
color: var(--text-muted);
font-size: 0.75rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
}
.table td {
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.btn-accent {
background: var(--accent);
color: #fff;
border: none;
}
.dropdown-menu {
background: var(--card-bg);
border: 1px solid var(--border);
}
.dropdown-item {
color: var(--text-main) !important;
}
.dropdown-item:hover {
background: var(--input-bg);
}
[data-theme="dark"] .table {
--bs-table-color: var(--text-main);
color: var(--text-main) !important;
}
[data-theme="dark"] .table thead th {
background: #292b2f;
color: var(--text-muted);
}
[data-theme="dark"] .text-muted {
color: var(--text-muted) !important;
}
</style> </style>
</head> </head>
<body>
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
<span class="navbar-brand">SekiPOS <small class="text-danger fw-bold ms-1" style="font-size:0.75rem;"><i class="bi bi-journal-x"></i> Dicom</small></span>
<div class="ms-3 gap-2 d-flex">
<a href="/" class="btn btn-outline-primary btn-sm"><i class="bi bi-box-seam me-1"></i>Inventario</a>
<a href="/checkout" class="btn btn-outline-primary btn-sm"><i class="bi bi-cart-fill me-1"></i>Caja</a>
<a href="/sales" class="btn btn-outline-primary btn-sm"><i class="bi bi-receipt me-1"></i>Ventas</a>
</div>
<div class="ms-auto"> <body>
<div class="dropdown"> {% with active_page='dicom' %}{% include 'navbar.html' %}{% endwith %}
<button class="btn btn-accent dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>
<span class="d-none d-sm-inline">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li>
<button class="dropdown-item" onclick="toggleTheme()">
<i class="bi bi-moon-stars me-2" id="theme-icon"></i>
<span id="theme-label">Modo Oscuro</span>
</button>
</li>
<li><hr class="dropdown-divider" style="border-color: var(--border);"></li>
<li><a class="dropdown-item text-danger" href="/logout"><i class="bi bi-box-arrow-right me-2"></i>Salir</a></li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid px-3"> <div class="container-fluid px-3">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
<div class="discord-card p-3 mb-3"> <div class="discord-card p-3 mb-3">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0 fw-bold">Registrar Movimiento</h5> <h5 class="mb-0 fw-bold">Registrar Movimiento</h5>
<button class="btn btn-sm btn-outline-secondary" onclick="clearDicomForm()" title="Limpiar Formulario"> <button class="btn btn-sm btn-outline-secondary" onclick="clearDicomForm()"
title="Limpiar Formulario">
<i class="bi bi-eraser"></i> Nuevo <i class="bi bi-eraser"></i> Nuevo
</button> </button>
</div> </div>
@@ -94,9 +151,10 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label class="small text-muted mb-1">Nota (Opcional)</label> <label class="small text-muted mb-1">Nota (Opcional)</label>
<input type="text" id="dicom-notes" class="form-control" placeholder="Ej: Pan y bebida" onkeydown="if(event.key === 'Enter') submitDicom('add')"> <input type="text" id="dicom-notes" class="form-control" placeholder="Ej: Pan y bebida"
onkeydown="if(event.key === 'Enter') submitDicom('add')">
</div> </div>
<div class="d-flex flex-column gap-2"> <div class="d-flex flex-column gap-2">
<button class="btn btn-danger py-2 fw-bold" onclick="submitDicom('add')"> <button class="btn btn-danger py-2 fw-bold" onclick="submitDicom('add')">
<i class="bi bi-cart-plus me-1"></i> Fiar (Sumar Deuda) <i class="bi bi-cart-plus me-1"></i> Fiar (Sumar Deuda)
@@ -110,9 +168,10 @@
<div class="col-md-8"> <div class="col-md-8">
<div class="discord-card p-3"> <div class="discord-card p-3">
<div class="position-relative mb-3"> <div class="position-relative mb-3">
<input type="text" id="dicom-search" class="form-control ps-5" placeholder="Buscar cliente por nombre..." onkeyup="filterDicom()"> <input type="text" id="dicom-search" class="form-control ps-5"
placeholder="Buscar cliente por nombre..." onkeyup="filterDicom()">
<i class="bi bi-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i> <i class="bi bi-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i>
</div> </div>
@@ -135,10 +194,12 @@
<td class="text-muted small">{{ d[3] }}</td> <td class="text-muted small">{{ d[3] }}</td>
<td class="text-muted small">{{ d[4] }}</td> <td class="text-muted small">{{ d[4] }}</td>
<td class="text-end"> <td class="text-end">
<button class="btn btn-sm btn-outline-secondary" onclick="selectClient('{{ d[1] }}')" title="Seleccionar"> <button class="btn btn-sm btn-outline-secondary"
onclick="selectClient('{{ d[1] }}')" title="Seleccionar">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</button> </button>
<button class="btn btn-sm btn-outline-danger ms-1" onclick="forgiveDebt({{ d[0] }}, '{{ d[1] }}')" title="Eliminar Registro"> <button class="btn btn-sm btn-outline-danger ms-1"
onclick="forgiveDebt({{ d[0] }}, '{{ d[1] }}')" title="Eliminar Registro">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</td> </td>
@@ -149,26 +210,26 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 }); const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 });
// Smart color formatting for the debt column // Smart color formatting for the debt column
document.querySelectorAll('.price-cell').forEach(td => { document.querySelectorAll('.price-cell').forEach(td => {
const val = parseFloat(td.getAttribute('data-value')); const val = parseFloat(td.getAttribute('data-value'));
td.innerText = clp.format(val); td.innerText = clp.format(val);
// Reversing the logic: Negative is debt (red), Positive is credit (green) // Reversing the logic: Negative is debt (red), Positive is credit (green)
if (val < 0) { if (val < 0) {
td.classList.add('text-danger'); td.classList.add('text-danger');
} else if (val > 0) { } else if (val > 0) {
td.classList.add('text-success'); td.classList.add('text-success');
} else { } else {
td.classList.add('text-muted'); td.classList.add('text-muted');
} }
}); });
@@ -224,44 +285,13 @@
async function forgiveDebt(id, name) { async function forgiveDebt(id, name) {
if (!confirm(`¿Estás seguro de que quieres eliminar completamente a ${name} del registro?`)) return; if (!confirm(`¿Estás seguro de que quieres eliminar completamente a ${name} del registro?`)) return;
const res = await fetch(`/api/dicom/${id}`, { method: 'DELETE' }); const res = await fetch(`/api/dicom/${id}`, { method: 'DELETE' });
if (res.ok) window.location.reload(); if (res.ok) window.location.reload();
} }
/* ── Theme Management ── */
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
const isDark = t === 'dark';
const themeIcon = document.getElementById('theme-icon');
const themeLabel = document.getElementById('theme-label');
if (themeIcon) themeIcon.className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
if (themeLabel) themeLabel.innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
localStorage.setItem('theme', t);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
applyTheme(current === 'dark' ? 'light' : 'dark');
}
(function initTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
applyTheme(savedTheme);
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
})();
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
</script> </script>
<script src="./static/cookieStuff.js"></script>
<script src="./static/themeStuff.js"></script>
</body> </body>
</html> </html>

View File

@@ -10,312 +10,11 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://unpkg.com/html5-qrcode"></script> <script src="https://unpkg.com/html5-qrcode"></script>
<style> <link rel="stylesheet" href="./static/style.css">
:root {
--bg: #ebedef;
--card-bg: #ffffff;
--text-main: #2e3338;
--text-muted: #4f5660;
--border: #e3e5e8;
--navbar-bg: #ffffff;
--input-bg: #e3e5e8;
--table-head: #f2f3f5;
--accent: #5865f2;
--accent-hover: #4752c4;
--danger: #ed4245;
}
[data-theme="dark"] {
--bg: #36393f;
--card-bg: #2f3136;
--text-main: #dcddde;
--text-muted: #b9bbbe;
--border: #202225;
--navbar-bg: #202225;
--input-bg: #202225;
--table-head: #292b2f;
}
body {
background: var(--bg);
color: var(--text-main);
font-family: "gg sans", "Segoe UI", sans-serif;
transition: background 0.2s, color 0.2s;
}
/* ── Navbar ── */
.navbar {
background: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border);
}
.navbar-brand {
color: var(--text-main) !important;
font-weight: 700;
}
.nav-link,
.dropdown-item {
color: var(--text-main) !important;
}
.dropdown-menu {
background: var(--card-bg);
border: 1px solid var(--border);
}
.dropdown-item:hover {
background: var(--input-bg);
}
.dropdown-item.text-danger {
color: var(--danger) !important;
}
/* ── Cards ── */
.discord-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* ── Inputs ── */
.form-control,
.form-control:focus {
background: var(--input-bg);
color: var(--text-main);
border: none;
box-shadow: none;
}
.form-control:focus {
outline: 2px solid var(--accent);
}
.form-control::placeholder {
color: var(--text-muted);
}
/* ── Buttons ── */
.btn-accent {
background: var(--accent);
color: #fff;
border: none;
}
.btn-accent:hover {
background: var(--accent-hover);
color: #fff;
}
.btn-danger-discord {
background: var(--danger);
color: #fff;
border: none;
}
.btn-danger-discord:hover {
background: #c23235;
color: #fff;
}
/* ── Price tag ── */
.price-tag {
font-size: 2.8rem;
/* Slightly larger for the wider card */
font-weight: 800;
color: var(--accent);
/* Optional: uses your accent color for better visibility */
}
/* ── Table ── */
.table {
color: var(--text-main);
--bs-table-color: var(--text-main);
--bs-table-bg: transparent;
--bs-table-border-color: var(--border);
}
/* -- Checkbox Size Fix -- */
#select-all {
transform: scale(1.3);
margin-top: 2px;
}
[data-theme="dark"] .table {
--bs-table-color: var(--text-main);
color: var(--text-main);
}
.table thead th {
background: var(--table-head);
color: var(--text-muted);
font-size: 0.72rem;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
}
.table tbody td {
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
/* ── Bulk bar ── */
.bulk-bar {
background: var(--accent);
color: #fff;
border-radius: 8px;
}
.bulk-bar .form-control {
width: 110px;
background: rgba(0, 0, 0, 0.2) !important;
color: #fff !important;
border: 1px solid rgba(255, 255, 255, 0.25) !important;
}
.bulk-bar .form-control::placeholder {
color: rgba(255, 255, 255, 0.6);
}
/* ── New-product prompt ── */
.new-product-prompt {
background: var(--accent);
color: #fff;
border-radius: 8px;
}
/* ── Product image ── */
#display-img {
width: 100%;
/* Allows it to fill the new width */
max-width: 250px;
/* Increased from 160px */
height: auto;
max-height: 250px;
/* Increased from 160px */
object-fit: contain;
}
/* ── Checkbox ── */
input[type="checkbox"] {
cursor: pointer;
}
/* ── Mobile: hide barcode column ── */
@media (max-width: 576px) {
.col-barcode {
display: none;
}
.btn-edit-sm,
.btn-del-sm {
padding: 4px 7px;
font-size: 0.75rem;
}
}
.modal-content {
background: var(--card-bg);
color: var(--text-main);
border: 1px solid var(--border);
}
.modal-header,
.modal-footer {
border-color: var(--border);
}
.btn-close {
/* Makes the X button visible in dark mode */
filter: var(--bs-theme-placeholder, invert(0.7) grayscale(100%) brightness(200%));
}
[data-theme="dark"] .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
/* Add this inside your <style> tag */
[data-theme="dark"] .text-muted {
color: var(--text-muted) !important;
}
[data-theme="dark"] .modal-body .text-muted {
color: #f6f6f7 !important;
}
.btn-close {
filter: none;
}
[data-theme="dark"] .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
/* Fix for the missing dropdown arrow */
.form-select {
background-color: var(--input-bg) !important;
color: var(--text-main) !important;
border: none !important;
/* This ensures the arrow icon is still rendered over the custom background */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23adb5bd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
background-repeat: no-repeat !important;
background-position: right 0.75rem center !important;
background-size: 16px 12px !important;
}
[data-theme="dark"] .form-select {
/* Lighter arrow for dark mode */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dcddde' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
}
</style>
</head> </head>
<body> <body>
{% with active_page='inventory' %}{% include 'navbar.html' %}{% endwith %}
<!-- ════════════════════════ NAVBAR ════════════════════════ -->
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
<span class="navbar-brand">SekiPOS <small class="text-muted fw-normal"
style="font-size:0.65rem;">v1.6</small></span>
<div class="ms-3 gap-2 d-flex">
<a href="/checkout" class="btn btn-outline-primary btn-sm">
<i class="bi bi-cart-fill me-1"></i>Caja
</a>
<a href="/sales" class="btn btn-outline-primary btn-sm">
<i class="bi bi-receipt me-1"></i>Ventas
</a>
<a href="/dicom" class="btn btn-outline-danger btn-sm">
<i class="bi bi-journal-x me-1"></i>Dicom
</a>
</div>
<!-- Always-visible dropdown on the right -->
<div class="ms-auto">
<div class="dropdown">
<button class="btn btn-accent dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>
<span class="d-none d-sm-inline">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<button class="dropdown-item" onclick="toggleTheme()">
<i class="bi bi-moon-stars me-2" id="theme-icon"></i>
<span id="theme-label">Modo Oscuro</span>
</button>
</li>
<li>
<hr class="dropdown-divider" style="border-color: var(--border);">
</li>
<li>
<a class="dropdown-item text-danger" href="/logout">
<i class="bi bi-box-arrow-right me-2"></i>Salir
</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- ════════════════════════ MAIN ════════════════════════ --> <!-- ════════════════════════ MAIN ════════════════════════ -->
<div class="container-fluid px-3"> <div class="container-fluid px-3">
@@ -453,9 +152,10 @@
<td class="name-cell">{{ p[1] }}</td> <td class="name-cell">{{ p[1] }}</td>
<td> <td>
{% if p[5] == 'kg' %} {% if p[5] == 'kg' %}
<span class="text-muted d-inline-block text-center" style="width: 45px;">-</span> <span class="text-muted d-inline-block text-center"
style="width: 45px;">-</span>
{% else %} {% else %}
{{ p[4] | int }} <small class="text-muted">Uni</small> {{ p[4] | int }} <small class="text-muted">Uni</small>
{% endif %} {% endif %}
</td> </td>
<td class="price-cell" data-value="{{ p[2] }}"></td> <td class="price-cell" data-value="{{ p[2] }}"></td>
@@ -597,20 +297,6 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
/* ── Theme ── */
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
const isDark = t === 'dark';
document.getElementById('theme-icon').className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
document.getElementById('theme-label').innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
}
function toggleTheme() {
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
localStorage.setItem('theme', next);
applyTheme(next);
}
applyTheme(localStorage.getItem('theme') || 'light');
/* ── Socket.IO ── */ /* ── Socket.IO ── */
const socket = io(); const socket = io();
const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 }); const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 });
@@ -1109,73 +795,11 @@
} }
} }
/* ── Theme Management ── */
// Helper to set a cookie
function setCookie(name, value, days = 365) {
const d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
let expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/;SameSite=Lax";
}
// Helper to get a cookie
function getCookie(name) {
let nameEQ = name + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// A single, clean theme manager
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
const isDark = t === 'dark';
// Update UI elements if they exist
const themeIcon = document.getElementById('theme-icon');
const themeLabel = document.getElementById('theme-label');
if (themeIcon) themeIcon.className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
if (themeLabel) themeLabel.innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
// Save to localStorage (much more reliable than cookies for this)
localStorage.setItem('theme', t);
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
}
// Run this immediately on load
(function initTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
applyTheme(savedTheme);
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
applyTheme(prefersDark ? 'dark' : 'light');
}
})();
// Listen for OS theme changes in real-time if no cookie is set
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
if (!getCookie('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
// Function to toggle stock state // Function to toggle stock state
function toggleStockInput() { function toggleStockInput() {
const unitSelect = document.getElementById('form-unit-type'); const unitSelect = document.getElementById('form-unit-type');
const stockInput = document.getElementById('form-stock'); const stockInput = document.getElementById('form-stock');
if (unitSelect.value === 'kg') { if (unitSelect.value === 'kg') {
stockInput.classList.add('d-none'); // Poof. stockInput.classList.add('d-none'); // Poof.
stockInput.disabled = true; // Prevent form submission errors stockInput.disabled = true; // Prevent form submission errors
@@ -1189,8 +813,9 @@
// Listen for manual dropdown changes // Listen for manual dropdown changes
document.getElementById('form-unit-type').addEventListener('change', toggleStockInput); document.getElementById('form-unit-type').addEventListener('change', toggleStockInput);
initTheme();
</script> </script>
<script src="./static/cookieStuff.js"></script>
<script src="./static/themeStuff.js"></script>
</body> </body>
</html> </html>

56
templates/navbar.html Normal file
View File

@@ -0,0 +1,56 @@
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3">
<span class="navbar-brand">
SekiPOS
{% if active_page == 'checkout' %}
<small class="text-muted fw-normal" style="font-size:0.65rem;">v1.6</small>
{% elif active_page == 'sales' %}
<small class="text-muted fw-normal" style="font-size:0.65rem;">v1.6</small>
{% elif active_page == 'dicom' %}
<small class="text-muted fw-normal" style="font-size:0.65rem;">v1.6</small>
{% else %}
<small class="text-muted fw-normal" style="font-size:0.65rem;">v1.6</small>
{% endif %}
</span>
<div class="ms-3 gap-2 d-flex">
<a href="/" class="btn btn-sm {{ 'btn-primary' if active_page == 'inventory' else 'btn-outline-primary' }}">
<i class="bi bi-box-seam me-1"></i>Inventario
</a>
<a href="/checkout"
class="btn btn-sm {{ 'btn-primary' if active_page == 'checkout' else 'btn-outline-primary' }}">
<i class="bi bi-cart-fill me-1"></i>Caja
</a>
<a href="/sales" class="btn btn-sm {{ 'btn-primary' if active_page == 'sales' else 'btn-outline-primary' }}">
<i class="bi bi-receipt me-1"></i>Ventas
</a>
<a href="/dicom" class="btn btn-sm {{ 'btn-danger' if active_page == 'dicom' else 'btn-outline-danger' }}">
<i class="bi bi-journal-x me-1"></i>Dicom
</a>
</div>
<div class="ms-auto">
<div class="dropdown">
<button class="btn btn-accent dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-person-circle me-1"></i>
<span class="d-none d-sm-inline">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li>
<button class="dropdown-item" onclick="toggleTheme()">
<i class="bi bi-moon-stars me-2" id="theme-icon"></i>
<span id="theme-label">Modo Oscuro</span>
</button>
</li>
<li>
<hr class="dropdown-divider" style="border-color: var(--border);">
</li>
<li>
<a class="dropdown-item text-danger" href="/logout">
<i class="bi bi-box-arrow-right me-2"></i>Salir
</a>
</li>
</ul>
</div>
</div>
</nav>

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es" data-theme="light"> <html lang="es" data-theme="light">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -11,21 +11,60 @@
<style> <style>
/* Reusing your standard theme colors */ /* Reusing your standard theme colors */
:root { :root {
--bg: #ebedef; --card-bg: #ffffff; --text-main: #2e3338; --bg: #ebedef;
--text-muted: #4f5660; --border: #e3e5e8; --navbar-bg: #ffffff; --card-bg: #ffffff;
--accent: #5865f2; --accent-hover: #4752c4; --input-bg: #e3e5e8; --text-main: #2e3338;
--text-muted: #4f5660;
--border: #e3e5e8;
--navbar-bg: #ffffff;
--accent: #5865f2;
--accent-hover: #4752c4;
--input-bg: #e3e5e8;
} }
[data-theme="dark"] { [data-theme="dark"] {
--bg: #36393f; --card-bg: #2f3136; --text-main: #dcddde; --bg: #36393f;
--text-muted: #b9bbbe; --border: #202225; --navbar-bg: #202225; --card-bg: #2f3136;
--text-main: #dcddde;
--text-muted: #b9bbbe;
--border: #202225;
--navbar-bg: #202225;
--input-bg: #202225; --input-bg: #202225;
} }
body { background: var(--bg); color: var(--text-main); font-family: "gg sans", sans-serif; min-height: 100vh; }
.navbar { background: var(--navbar-bg) !important; border-bottom: 1px solid var(--border); } body {
.navbar-brand, .nav-link, .dropdown-item { color: var(--text-main) !important; } background: var(--bg);
.dropdown-menu { background: var(--card-bg); border: 1px solid var(--border); } color: var(--text-main);
.dropdown-item:hover { background: var(--input-bg); } font-family: "gg sans", sans-serif;
.discord-card, .modal-content { background: var(--card-bg) !important; border: 1px solid var(--border); border-radius: 8px; } min-height: 100vh;
}
.navbar {
background: var(--navbar-bg) !important;
border-bottom: 1px solid var(--border);
}
.navbar-brand,
.nav-link,
.dropdown-item {
color: var(--text-main) !important;
}
.dropdown-menu {
background: var(--card-bg);
border: 1px solid var(--border);
}
.dropdown-item:hover {
background: var(--input-bg);
}
.discord-card,
.modal-content {
background: var(--card-bg) !important;
border: 1px solid var(--border);
border-radius: 8px;
}
.navbar-brand { .navbar-brand {
color: var(--text-main) !important; color: var(--text-main) !important;
@@ -33,63 +72,70 @@
} }
/* --- Tables --- */ /* --- Tables --- */
.table { .table {
--bs-table-bg: transparent; --bs-table-bg: transparent;
--bs-table-color: var(--text-main); /* This forces Bootstrap to obey */ --bs-table-color: var(--text-main);
--bs-table-border-color: var(--border); /* This forces Bootstrap to obey */
--bs-table-border-color: var(--border);
--bs-table-hover-color: var(--text-main); --bs-table-hover-color: var(--text-main);
--bs-table-hover-bg: var(--input-bg); --bs-table-hover-bg: var(--input-bg);
color: var(--text-main) !important; color: var(--text-main) !important;
} }
.table thead th {
background: var(--bg); .table thead th {
color: var(--text-muted); background: var(--bg);
font-size: 0.75rem; color: var(--text-muted);
text-transform: uppercase; font-size: 0.75rem;
border-bottom: 1px solid var(--border); text-transform: uppercase;
border-bottom: 1px solid var(--border);
} }
.table td, .table th {
border-bottom: 1px solid var(--border); .table td,
vertical-align: middle; .table th {
color: var(--text-main); border-bottom: 1px solid var(--border);
vertical-align: middle;
color: var(--text-main);
} }
/* --- Buttons --- */ /* --- Buttons --- */
.btn-accent { background: var(--accent); color: #fff; border: none; } .btn-accent {
.btn-accent:hover { background: var(--accent-hover); color: #fff; } background: var(--accent);
color: #fff;
border: none;
}
.btn-accent:hover {
background: var(--accent-hover);
color: #fff;
}
/* --- Dark Mode Overrides --- */ /* --- Dark Mode Overrides --- */
[data-theme="dark"] .modal-content, [data-theme="dark"] .modal-content,
[data-theme="dark"] .modal-body { color: var(--text-main); } [data-theme="dark"] .modal-body {
[data-theme="dark"] .table { --bs-table-hover-bg: rgba(255, 255, 255, 0.05); } color: var(--text-main);
[data-theme="dark"] .table thead th { background: #292b2f; color: var(--text-muted); } }
[data-theme="dark"] .btn-close { filter: invert(1) grayscale(100%) brightness(200%); }
[data-theme="dark"] .text-muted { color: var(--text-muted) !important; } [data-theme="dark"] .table {
--bs-table-hover-bg: rgba(255, 255, 255, 0.05);
}
[data-theme="dark"] .table thead th {
background: #292b2f;
color: var(--text-muted);
}
[data-theme="dark"] .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
[data-theme="dark"] .text-muted {
color: var(--text-muted) !important;
}
</style> </style>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-md sticky-top px-3 mb-3"> {% with active_page='sales' %}{% include 'navbar.html' %}{% endwith %}
<span class="navbar-brand">SekiPOS <small class="text-muted fw-normal" style="font-size:0.65rem;">Ventas</small></span>
<div class="ms-3 gap-2 d-flex">
<a href="/" class="btn btn-outline-primary btn-sm"><i class="bi bi-box-seam me-1"></i>Inventario</a>
<a href="/checkout" class="btn btn-outline-primary btn-sm"><i class="bi bi-cart-fill me-1"></i>Caja</a>
<a href="/dicom" class="btn btn-outline-danger btn-sm">
<i class="bi bi-journal-x me-1"></i>Dicom
</a>
</div>
<div class="ms-auto">
<div class="dropdown">
<button class="btn btn-accent dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-1"></i> <span class="d-none d-sm-inline">{{ user.username }}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><button class="dropdown-item" onclick="toggleTheme()"><i class="bi bi-moon-stars me-2" id="theme-icon"></i><span id="theme-label">Modo Oscuro</span></button></li>
<li><hr class="dropdown-divider" style="border-color: var(--border);"></li>
<li><a class="dropdown-item text-danger" href="/logout"><i class="bi bi-box-arrow-right me-2"></i>Salir</a></li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid px-3"> <div class="container-fluid px-3">
@@ -99,19 +145,24 @@
<h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;"> <h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">
{% if selected_date %}Día Seleccionado{% else %}Ventas de Hoy{% endif %} {% if selected_date %}Día Seleccionado{% else %}Ventas de Hoy{% endif %}
</h6> </h6>
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;" data-value="{{ stats.daily }}"></h2> <h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;"
data-value="{{ stats.daily }}"></h2>
</div> </div>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<div class="discord-card p-3 shadow-sm text-center"> <div class="discord-card p-3 shadow-sm text-center">
<h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">Últimos 7 Días</h6> <h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">Últimos 7
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;" data-value="{{ stats.week }}"></h2> Días</h6>
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;"
data-value="{{ stats.week }}"></h2>
</div> </div>
</div> </div>
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<div class="discord-card p-3 shadow-sm text-center"> <div class="discord-card p-3 shadow-sm text-center">
<h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">Este Mes</h6> <h6 class="text-muted text-uppercase mb-1" style="font-size: 0.75rem; font-weight: 700;">Este Mes
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;" data-value="{{ stats.month }}"></h2> </h6>
<h2 class="price-cell mb-0" style="color: var(--accent); font-weight: 800;"
data-value="{{ stats.month }}"></h2>
</div> </div>
</div> </div>
</div> </div>
@@ -120,12 +171,14 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0"><i class="bi bi-receipt-cutoff me-2"></i>Historial</h4> <h4 class="mb-0"><i class="bi bi-receipt-cutoff me-2"></i>Historial</h4>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2">
<label for="date-filter" class="form-label mb-0 text-muted small d-none d-sm-block">Filtrar Día:</label> <label for="date-filter" class="form-label mb-0 text-muted small d-none d-sm-block">Filtrar
<input type="date" id="date-filter" class="form-control form-control-sm" Día:</label>
style="background: var(--input-bg); color: var(--text-main); border-color: var(--border);" <input type="date" id="date-filter" class="form-control form-control-sm"
value="{{ selected_date or '' }}" onchange="filterByDate(this.value)"> style="background: var(--input-bg); color: var(--text-main); border-color: var(--border);"
value="{{ selected_date or '' }}" onchange="filterByDate(this.value)">
{% if selected_date %} {% if selected_date %}
<a href="/sales" class="btn btn-sm btn-outline-secondary" title="Limpiar filtro"><i class="bi bi-x-lg"></i></a> <a href="/sales" class="btn btn-sm btn-outline-secondary" title="Limpiar filtro"><i
class="bi bi-x-lg"></i></a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -148,7 +201,8 @@
<td class="text-capitalize">{{ s[3] }}</td> <td class="text-capitalize">{{ s[3] }}</td>
<td class="price-cell fw-bold" data-value="{{ s[2] }}"></td> <td class="price-cell fw-bold" data-value="{{ s[2] }}"></td>
<td> <td>
<button class="btn btn-sm btn-outline-secondary" onclick="viewSale({{ s[0] }}, '{{ s[1] }}', {{ s[2] }})"> <button class="btn btn-sm btn-outline-secondary"
onclick="viewSale({{ s[0] }}, '{{ s[1] }}', {{ s[2] }})">
<i class="bi bi-eye"></i> Ver Detalle <i class="bi bi-eye"></i> Ver Detalle
</button> </button>
</td> </td>
@@ -164,7 +218,8 @@
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Detalle de Venta <span id="modal-ticket-id" class="text-muted small"></span></h5> <h5 class="modal-title">Detalle de Venta <span id="modal-ticket-id" class="text-muted small"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@@ -178,7 +233,7 @@
</tr> </tr>
</thead> </thead>
<tbody id="receipt-items"> <tbody id="receipt-items">
</tbody> </tbody>
<tfoot> <tfoot>
<tr> <tr>
<th colspan="2" class="text-end">TOTAL:</th> <th colspan="2" class="text-end">TOTAL:</th>
@@ -188,7 +243,7 @@
</table> </table>
</div> </div>
<div class="modal-body"> <div class="modal-body">
</div> </div>
<div class="modal-footer d-flex justify-content-between border-0 pt-0"> <div class="modal-footer d-flex justify-content-between border-0 pt-0">
<button class="btn btn-outline-danger btn-sm" id="btn-reverse-sale"> <button class="btn btn-outline-danger btn-sm" id="btn-reverse-sale">
<i class="bi bi-arrow-counterclockwise me-1"></i>Anular Venta <i class="bi bi-arrow-counterclockwise me-1"></i>Anular Venta
@@ -208,8 +263,9 @@
<div class="modal-body text-center pt-0 pb-4"> <div class="modal-body text-center pt-0 pb-4">
<i class="bi bi-exclamation-triangle-fill text-danger mb-3" style="font-size: 3rem;"></i> <i class="bi bi-exclamation-triangle-fill text-danger mb-3" style="font-size: 3rem;"></i>
<h4 class="mb-3">¿Anular Venta #<span id="reverse-modal-id"></span>?</h4> <h4 class="mb-3">¿Anular Venta #<span id="reverse-modal-id"></span>?</h4>
<p class="text-muted small px-3">Los productos regresarán automáticamente al inventario y el ticket será eliminado permanentemente.</p> <p class="text-muted small px-3">Los productos regresarán automáticamente al inventario y el ticket
será eliminado permanentemente.</p>
<div class="d-flex gap-2 justify-content-center mt-4 px-3"> <div class="d-flex gap-2 justify-content-center mt-4 px-3">
<button class="btn btn-secondary w-50" data-bs-dismiss="modal">Cancelar</button> <button class="btn btn-secondary w-50" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-danger w-50" onclick="executeReverseSale()">Sí, Anular</button> <button class="btn btn-danger w-50" onclick="executeReverseSale()">Sí, Anular</button>
@@ -228,9 +284,9 @@
document.querySelectorAll('.utc-date').forEach(el => { document.querySelectorAll('.utc-date').forEach(el => {
const date = new Date(el.innerText + " UTC"); const date = new Date(el.innerText + " UTC");
if (!isNaN(date)) { if (!isNaN(date)) {
el.innerText = date.toLocaleString('es-CL', { el.innerText = date.toLocaleString('es-CL', {
year: 'numeric', month: '2-digit', day: '2-digit', year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute:'2-digit' hour: '2-digit', minute: '2-digit'
}); });
} }
}); });
@@ -243,13 +299,13 @@
async function viewSale(id, rawDate, total) { async function viewSale(id, rawDate, total) {
document.getElementById('modal-ticket-id').innerText = `#${id}`; document.getElementById('modal-ticket-id').innerText = `#${id}`;
document.getElementById('modal-total').innerText = clp.format(total); document.getElementById('modal-total').innerText = clp.format(total);
const localDate = new Date(rawDate + " UTC").toLocaleString('es-CL'); const localDate = new Date(rawDate + " UTC").toLocaleString('es-CL');
document.getElementById('modal-date').innerText = localDate !== "Invalid Date" ? localDate : rawDate; document.getElementById('modal-date').innerText = localDate !== "Invalid Date" ? localDate : rawDate;
const tbody = document.getElementById('receipt-items'); const tbody = document.getElementById('receipt-items');
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">Cargando...</td></tr>'; tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">Cargando...</td></tr>';
// Attach the ID to the delete button // Attach the ID to the delete button
document.getElementById('btn-reverse-sale').setAttribute('onclick', `reverseSale(${id})`); document.getElementById('btn-reverse-sale').setAttribute('onclick', `reverseSale(${id})`);
@@ -259,7 +315,7 @@
try { try {
const res = await fetch(`/api/sale/${id}`); const res = await fetch(`/api/sale/${id}`);
const items = await res.json(); const items = await res.json();
tbody.innerHTML = items.map(item => ` tbody.innerHTML = items.map(item => `
<tr> <tr>
<td> <td>
@@ -286,10 +342,10 @@
function reverseSale(id) { function reverseSale(id) {
saleToReverse = id; saleToReverse = id;
document.getElementById('reverse-modal-id').innerText = id; document.getElementById('reverse-modal-id').innerText = id;
// Hide the receipt modal so we don't have overlapping popups // Hide the receipt modal so we don't have overlapping popups
bootstrap.Modal.getInstance(document.getElementById('receiptModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('receiptModal')).hide();
// Show the new confirmation modal // Show the new confirmation modal
const confirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('reverseConfirmModal')); const confirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('reverseConfirmModal'));
confirmModal.show(); confirmModal.show();
@@ -297,10 +353,10 @@
async function executeReverseSale() { async function executeReverseSale() {
if (!saleToReverse) return; if (!saleToReverse) return;
try { try {
const res = await fetch(`/api/sale/${saleToReverse}`, { method: 'DELETE' }); const res = await fetch(`/api/sale/${saleToReverse}`, { method: 'DELETE' });
if (res.ok) { if (res.ok) {
window.location.reload(); // Refresh the dashboard window.location.reload(); // Refresh the dashboard
} else { } else {
@@ -311,26 +367,10 @@
console.error(err); console.error(err);
alert("Error de conexión con el servidor."); alert("Error de conexión con el servidor.");
} }
}
/* Theme Management */
function applyTheme(t) {
document.documentElement.setAttribute('data-theme', t);
const isDark = t === 'dark';
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
if (icon) icon.className = isDark ? 'bi bi-sun me-2' : 'bi bi-moon-stars me-2';
if (label) label.innerText = isDark ? 'Modo Claro' : 'Modo Oscuro';
localStorage.setItem('theme', t);
} }
function toggleTheme() {
applyTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
}
(function initTheme() {
applyTheme(localStorage.getItem('theme') || 'light');
})();
</script> </script>
<script src="./static/cookieStuff.js"></script>
<script src="./static/themeStuff.js"></script>
</body> </body>
</html> </html>