Implemented Dicom
This commit is contained in:
58
app.py
58
app.py
@@ -65,6 +65,13 @@ def init_db():
|
||||
subtotal REAL,
|
||||
FOREIGN KEY(sale_id) REFERENCES sales(id))''')
|
||||
|
||||
conn.execute('''CREATE TABLE IF NOT EXISTS dicom
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE,
|
||||
amount REAL DEFAULT 0,
|
||||
notes TEXT,
|
||||
last_updated TEXT DEFAULT CURRENT_TIMESTAMP)''')
|
||||
|
||||
# Default user logic remains same...
|
||||
user = conn.execute('SELECT * FROM users WHERE username = ?', ('admin',)).fetchone()
|
||||
if not user:
|
||||
@@ -444,6 +451,57 @@ def reverse_sale(sale_id):
|
||||
except Exception as e:
|
||||
print(f"Reverse Sale Error: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/dicom')
|
||||
@login_required
|
||||
def dicom():
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
debtors = conn.execute('SELECT id, name, amount, notes, datetime(last_updated, "localtime") FROM dicom ORDER BY amount DESC').fetchall()
|
||||
return render_template('dicom.html', user=current_user, debtors=debtors)
|
||||
|
||||
@app.route('/api/dicom/update', methods=['POST'])
|
||||
@login_required
|
||||
def update_dicom():
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
amount = float(data.get('amount', 0))
|
||||
notes = data.get('notes', '')
|
||||
action = data.get('action') # 'add' or 'pay'
|
||||
|
||||
if not name or amount <= 0:
|
||||
return jsonify({"error": "Nombre y monto válidos son requeridos"}), 400
|
||||
|
||||
# If we are giving them credit (Fiar), their balance drops into the negative
|
||||
if action == 'add':
|
||||
amount = -amount
|
||||
|
||||
try:
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
cur = conn.cursor()
|
||||
# Upsert logic: if they exist, modify debt. If they don't, create them.
|
||||
cur.execute('''INSERT INTO dicom (name, amount, notes, last_updated)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
amount = amount + excluded.amount,
|
||||
notes = excluded.notes,
|
||||
last_updated = CURRENT_TIMESTAMP''', (name, amount, notes))
|
||||
conn.commit()
|
||||
return jsonify({"status": "success"}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/dicom/<int:debtor_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_dicom(debtor_id):
|
||||
try:
|
||||
with sqlite3.connect(DB_FILE) as conn:
|
||||
conn.execute('DELETE FROM dicom WHERE id = ?', (debtor_id,))
|
||||
conn.commit()
|
||||
return jsonify({"status": "success"}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# @app.route('/process_payment', methods=['POST'])
|
||||
# @login_required
|
||||
# def process_payment():
|
||||
|
||||
@@ -461,6 +461,9 @@
|
||||
<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">
|
||||
|
||||
267
templates/dicom.html
Normal file
267
templates/dicom.html
Normal file
@@ -0,0 +1,267 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SekiPOS - Dicom</title>
|
||||
<link rel="shortcut icon" href="./static/favicon.png" type="image/x-icon">
|
||||
<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">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ebedef; --card-bg: #ffffff; --text-main: #2e3338;
|
||||
--text-muted: #4f5660; --border: #e3e5e8; --navbar-bg: #ffffff;
|
||||
--accent: #5865f2; --input-bg: #e3e5e8; --danger: #ed4245;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg: #36393f; --card-bg: #2f3136; --text-main: #dcddde;
|
||||
--text-muted: #b9bbbe; --border: #202225; --navbar-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; }
|
||||
.navbar { background: var(--navbar-bg) !important; border-bottom: 1px solid var(--border); }
|
||||
.navbar-brand { color: var(--text-main) !important; font-weight: 700; }
|
||||
|
||||
.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); }
|
||||
|
||||
.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>
|
||||
</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">
|
||||
<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 px-3">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="discord-card p-3 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0 fw-bold">Registrar Movimiento</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="clearDicomForm()" title="Limpiar Formulario">
|
||||
<i class="bi bi-eraser"></i> Nuevo
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="small text-muted mb-1">Nombre del Cliente</label>
|
||||
<input type="text" id="dicom-name" class="form-control" placeholder="Ej: Doña Juanita">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="small text-muted mb-1">Monto (CLP)</label>
|
||||
<input type="number" id="dicom-amount" class="form-control" placeholder="Ej: 5000">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<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')">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<button class="btn btn-danger py-2 fw-bold" onclick="submitDicom('add')">
|
||||
<i class="bi bi-cart-plus me-1"></i> Fiar (Sumar Deuda)
|
||||
</button>
|
||||
<button class="btn btn-success py-2 fw-bold" onclick="submitDicom('pay')">
|
||||
<i class="bi bi-cash-coin me-1"></i> Abonar (Restar Deuda)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="discord-card p-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()">
|
||||
<i class="bi bi-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0" id="dicom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Deuda Total</th>
|
||||
<th>Última Nota</th>
|
||||
<th>Actualizado</th>
|
||||
<th class="text-end">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in debtors %}
|
||||
<tr>
|
||||
<td class="fw-bold">{{ d[1] }}</td>
|
||||
<td class="fw-bold price-cell" data-value="{{ d[2] }}"></td>
|
||||
<td class="text-muted small">{{ d[3] }}</td>
|
||||
<td class="text-muted small">{{ d[4] }}</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="selectClient('{{ d[1] }}')" title="Seleccionar">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
const clp = new Intl.NumberFormat('es-CL', { style: 'currency', currency: 'CLP', minimumFractionDigits: 0 });
|
||||
|
||||
// Smart color formatting for the debt column
|
||||
document.querySelectorAll('.price-cell').forEach(td => {
|
||||
const val = parseFloat(td.getAttribute('data-value'));
|
||||
td.innerText = clp.format(val);
|
||||
|
||||
// Reversing the logic: Negative is debt (red), Positive is credit (green)
|
||||
if (val < 0) {
|
||||
td.classList.add('text-danger');
|
||||
} else if (val > 0) {
|
||||
td.classList.add('text-success');
|
||||
} else {
|
||||
td.classList.add('text-muted');
|
||||
}
|
||||
});
|
||||
|
||||
function clearDicomForm() {
|
||||
document.getElementById('dicom-name').value = '';
|
||||
document.getElementById('dicom-amount').value = '';
|
||||
document.getElementById('dicom-notes').value = '';
|
||||
document.getElementById('dicom-name').focus();
|
||||
}
|
||||
|
||||
// Search Filter
|
||||
function filterDicom() {
|
||||
const q = document.getElementById('dicom-search').value.toLowerCase();
|
||||
document.querySelectorAll('#dicom-table tbody tr').forEach(row => {
|
||||
const name = row.cells[0].innerText.toLowerCase();
|
||||
row.style.display = name.includes(q) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Just pre-fills the form so you don't accidentally click the wrong action
|
||||
function selectClient(name) {
|
||||
document.getElementById('dicom-name').value = name;
|
||||
document.getElementById('dicom-amount').focus();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function submitDicom(action) {
|
||||
const name = document.getElementById('dicom-name').value;
|
||||
const amount = document.getElementById('dicom-amount').value;
|
||||
const notes = document.getElementById('dicom-notes').value;
|
||||
|
||||
if (!name || amount <= 0) {
|
||||
alert('Ingresa un nombre y monto válido.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/dicom/update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, amount, notes, action })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error actualizando la base de datos.');
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Error de conexión.");
|
||||
}
|
||||
}
|
||||
|
||||
async function forgiveDebt(id, name) {
|
||||
if (!confirm(`¿Estás seguro de que quieres eliminar completamente a ${name} del registro?`)) return;
|
||||
|
||||
const res = await fetch(`/api/dicom/${id}`, { method: 'DELETE' });
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -285,6 +285,9 @@
|
||||
<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">
|
||||
|
||||
@@ -73,6 +73,9 @@
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user