feat(ui): redesign client form and enhance UI

- Reverted to a dark/grey theme for UI elements.
- Replaced text-based logout button with a red icon.
- Redesigned the client form to a cleaner, single-column layout.
- Added 'Gender' field to the client form.
- Added a comprehensive section for 'Oncological Patient' status with conditional fields.
- Updated client database schema and API to support new fields.
- Standardized form input styles, including the telephone field.
- Updated version in the footer to 0.3.0.
This commit is contained in:
Marco Gallegos
2025-08-13 09:09:34 -06:00
parent 164eaccac1
commit b59cb2f122
4 changed files with 265 additions and 100 deletions

View File

@@ -115,12 +115,20 @@ async function saveClient(clientData) {
} else {
isUpdate = !!document.getElementById('c-id').value;
const id = isUpdate ? document.getElementById('c-id').value : crypto.randomUUID();
clientToSave = {
id: id,
nombre: document.getElementById('c-nombre').value,
telefono: document.getElementById('c-telefono').value,
genero: document.getElementById('c-genero').value,
cumpleaños: document.getElementById('c-cumple').value,
consentimiento: document.getElementById('c-consent').checked,
esOncologico: document.getElementById('c-esOncologico').checked,
oncologoAprueba: document.getElementById('c-oncologoAprueba').checked,
nombreMedico: document.getElementById('c-nombreMedico').value,
telefonoMedico: document.getElementById('c-telefonoMedico').value,
cedulaMedico: document.getElementById('c-cedulaMedico').value,
pruebaAprobacion: document.getElementById('c-pruebaAprobacion').checked,
};
}
@@ -143,6 +151,7 @@ async function saveClient(clientData) {
if (!clientData) {
document.getElementById('formClient').reset();
document.getElementById('c-id').value = '';
document.getElementById('oncologico-fields').classList.add('hidden');
}
}
@@ -445,9 +454,26 @@ function handleTableClick(e) {
if (action === 'edit-client') {
document.getElementById('c-id').value = client.id;
document.getElementById('c-nombre').value = client.nombre;
document.getElementById('c-telefono').value = client.telefono;
document.getElementById('c-telefono').value = client.telefono || '';
document.getElementById('c-genero').value = client.genero || '';
document.getElementById('c-cumple').value = client.cumpleaños;
document.getElementById('c-consent').checked = client.consentimiento;
// Campos oncológicos
const esOncologicoCheckbox = document.getElementById('c-esOncologico');
const oncologicoFields = document.getElementById('oncologico-fields');
esOncologicoCheckbox.checked = client.esOncologico;
if (client.esOncologico) {
oncologicoFields.classList.remove('hidden');
} else {
oncologicoFields.classList.add('hidden');
}
document.getElementById('c-oncologoAprueba').checked = client.oncologoAprueba;
document.getElementById('c-nombreMedico').value = client.nombreMedico || '';
document.getElementById('c-telefonoMedico').value = client.telefonoMedico || '';
document.getElementById('c-cedulaMedico').value = client.cedulaMedico || '';
document.getElementById('c-pruebaAprobacion').checked = client.pruebaAprobacion;
} else if (action === 'delete-client') {
deleteClient(id);
}
@@ -632,6 +658,16 @@ async function initializeApp() {
document.getElementById('btnCancelEditClient')?.addEventListener('click', () => {
formClient.reset();
document.getElementById('c-id').value = '';
document.getElementById('oncologico-fields').classList.add('hidden');
});
document.getElementById('c-esOncologico')?.addEventListener('change', (e) => {
const oncologicoFields = document.getElementById('oncologico-fields');
if (e.target.checked) {
oncologicoFields.classList.remove('hidden');
} else {
oncologicoFields.classList.add('hidden');
}
});
btnCancelEditUser?.addEventListener('click', (e) => {

View File

@@ -4,8 +4,15 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ale Ponce | AlMa</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="styles.css?v=1.1" />
<!-- Favicon SVG -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23444' d='M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4V6h16v12zM6 10h2v4H6zm3 0h2v4H9zm3 0h2v4h-2zm3 0h2v4h-2z'/%3E%3C/svg%3E">
<!-- Google Fonts & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Icons+Outlined" rel="stylesheet">
<!-- Styles -->
<link rel="stylesheet" href="styles.css?v=1.2" />
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
@@ -14,12 +21,22 @@
<!-- Logo del negocio en lugar de texto -->
<img src="src/logo.png" alt="Ale Ponce" class="header-logo">
<nav class="tabs">
<button type="button" class="tab-link" data-tab="tab-dashboard">Dashboard</button>
<button type="button" class="tab-link" data-tab="tab-movements">Recibos</button>
<button type="button" class="tab-link" data-tab="tab-clients">Clientes</button>
<button type="button" class="tab-link" data-tab="tab-settings">Configuración</button>
<button type="button" class="tab-link" data-tab="tab-dashboard">
<span class="material-icons-outlined">dashboard</span>Dashboard
</button>
<button type="button" class="tab-link" data-tab="tab-movements">
<span class="material-icons-outlined">receipt_long</span>Recibos
</button>
<button type="button" class="tab-link" data-tab="tab-clients">
<span class="material-icons-outlined">groups</span>Clientes
</button>
<button type="button" class="tab-link" data-tab="tab-settings">
<span class="material-icons-outlined">settings</span>Configuración
</button>
</nav>
<button type="button" id="btnLogout" class="btn-danger">Cerrar Sesión</button>
<button type="button" id="btnLogout" class="btn-icon btn-danger">
<span class="material-icons-outlined">logout</span>
</button>
</header>
<!-- Pestaña de Dashboard -->
@@ -114,20 +131,64 @@
<h2>Administrar Clientes</h2>
<form id="formClient">
<input type="hidden" id="c-id" />
<div class="form-grid client-grid">
<label>Nombre:</label>
<div class="form-grid-single">
<label for="c-nombre">Nombre:</label>
<input type="text" id="c-nombre" required />
<label>Teléfono:</label>
<label for="c-telefono">Teléfono:</label>
<input type="tel" id="c-telefono" />
<label>Cumpleaños:</label>
<label for="c-genero">Género:</label>
<select id="c-genero">
<option value="">-- Seleccionar --</option>
<option value="Mujer">Mujer</option>
<option value="Hombre">Hombre</option>
</select>
<label for="c-cumple">Cumpleaños:</label>
<input type="date" id="c-cumple" />
<label></label> <!-- Etiqueta vacía para alinear el checkbox -->
<div class="checkbox-container">
<input type="checkbox" id="c-consent" />
<label for="c-consent">Consentimiento médico</label>
<label for="c-consent">¿Consentimiento médico informado completo?</label>
</div>
<div class="checkbox-container">
<input type="checkbox" id="c-esOncologico" />
<label for="c-esOncologico">¿Es paciente oncológico?</label>
</div>
</div>
<div class="form-actions">
<!-- Campos condicionales para paciente oncológico -->
<div id="oncologico-fields" class="sub-section hidden">
<h3>Información Oncológica</h3>
<div class="form-grid-single">
<div class="checkbox-container">
<input type="checkbox" id="c-oncologoAprueba" />
<label for="c-oncologoAprueba">¿Oncólogo aprueba procedimiento?</label>
</div>
<label for="c-nombreMedico">Nombre del Médico:</label>
<input type="text" id="c-nombreMedico" />
<label for="c-telefonoMedico">Teléfono del Médico:</label>
<input type="tel" id="c-telefonoMedico" />
<label for="c-cedulaMedico">Cédula Profesional:</label>
<input type="text" id="c-cedulaMedico" />
<div class="checkbox-container">
<input type="checkbox" id="c-pruebaAprobacion" />
<label for="c-pruebaAprobacion">¿Presenta prueba de aprobación?</label>
</div>
<p class="data-location-info">
El consentimiento del médico debe ser presentado físicamente antes de la evaluación.
</p>
</div>
</div>
<div class="form-actions-single">
<button type="submit">Guardar Cliente</button>
<button type="reset" id="btnCancelEditClient" class="btn-danger">Limpiar</button>
</div>
@@ -244,7 +305,7 @@
</main>
<footer class="main-footer-credits">
<p>v0.2.1</p>
<p>v0.3.0</p>
<p>Autores: Gemini + Marco G.</p>
</footer>

View File

@@ -82,7 +82,20 @@ db.serialize(() => {
// Tablas existentes
db.run(`CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)`);
db.run(`CREATE TABLE IF NOT EXISTS clients (id TEXT PRIMARY KEY, nombre TEXT, telefono TEXT, cumpleaños TEXT, consentimiento INTEGER)`);
db.run(`CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
nombre TEXT,
telefono TEXT,
genero TEXT,
cumpleaños TEXT,
consentimiento INTEGER,
esOncologico INTEGER,
oncologoAprueba INTEGER,
nombreMedico TEXT,
telefonoMedico TEXT,
cedulaMedico TEXT,
pruebaAprobacion INTEGER
)`);
db.run(`CREATE TABLE IF NOT EXISTS movements (id TEXT PRIMARY KEY, folio TEXT, fechaISO TEXT, clienteId TEXT, tipo TEXT, monto REAL, metodo TEXT, concepto TEXT, staff TEXT, notas TEXT, fechaCita TEXT, horaCita TEXT, FOREIGN KEY (clienteId) REFERENCES clients (id))`);
});
@@ -211,9 +224,18 @@ apiRouter.get('/clients', (req, res) => {
apiRouter.post('/clients', (req, res) => {
const { client } = req.body;
const { id, nombre, telefono, cumpleaños, consentimiento } = client;
db.run(`INSERT OR REPLACE INTO clients (id, nombre, telefono, cumpleaños, consentimiento) VALUES (?, ?, ?, ?, ?)`,
[id, nombre, telefono, cumpleaños, consentimiento], function(err) {
const {
id, nombre, telefono, genero, cumpleaños, consentimiento,
esOncologico, oncologoAprueba, nombreMedico, telefonoMedico, cedulaMedico, pruebaAprobacion
} = client;
db.run(`INSERT OR REPLACE INTO clients (
id, nombre, telefono, genero, cumpleaños, consentimiento,
esOncologico, oncologoAprueba, nombreMedico, telefonoMedico, cedulaMedico, pruebaAprobacion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id, nombre, telefono, genero, cumpleaños, consentimiento,
esOncologico, oncologoAprueba, nombreMedico, telefonoMedico, cedulaMedico, pruebaAprobacion
], function(err) {
if (err) {
res.status(500).json({ error: err.message });
return;

View File

@@ -4,13 +4,13 @@
- Fondo: #f8f9fa (gris muy claro)
- Texto principal: #212529 (casi negro)
- Bordes/Divisores: #dee2e6 (gris claro)
- Botón primario: #343a40 (gris oscuro)
- Botón secundario: #6c757d (gris medio)
- Primario: #007bff (azul)
- Secundario: #6c757d (gris medio)
*/
/* Estilos generales */
/* Estilos generales y tipografías */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
background-color: #f8f9fa;
color: #212529;
@@ -27,20 +27,22 @@ body {
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
}
h1, h2 {
h1, h2, h3 {
font-family: 'Montserrat', sans-serif;
font-weight: 600;
color: #212529;
}
h2 {
font-size: 22px;
border-bottom: 1px solid #dee2e6;
padding-bottom: 12px;
margin-top: 0;
margin-bottom: 25px;
}
h1 {
font-size: 28px;
}
h2 {
font-size: 22px;
h3 {
font-size: 18px;
}
.section {
@@ -56,29 +58,38 @@ h2 {
}
label {
font-weight: 500;
font-weight: 600;
text-align: right;
color: #495057;
font-size: 14px;
}
input[type="text"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="tel"],
select,
textarea {
width: 100%;
padding: 10px;
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
box-sizing: border-box;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
font-family: 'Open Sans', sans-serif;
font-size: 15px;
}
input:focus, select:focus, textarea:focus {
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
input:disabled {
background-color: #e9ecef;
cursor: not-allowed;
}
button {
background-color: #343a40;
@@ -88,7 +99,8 @@ button {
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
font-family: 'Open Sans', sans-serif;
font-weight: 600;
margin-right: 10px;
transition: background-color 0.2s;
}
@@ -97,10 +109,10 @@ button:hover {
background-color: #212529;
}
button[type="reset"], button#btnTestTicket, #btnExport {
.btn-secondary, button[type="reset"], button#btnTestTicket, #btnExport {
background-color: #6c757d;
}
button[type="reset"]:hover, button#btnTestTicket:hover, #btnExport:hover, .btn-secondary:hover {
.btn-secondary:hover, button[type="reset"]:hover, button#btnTestTicket:hover, #btnExport:hover {
background-color: #5a6268;
}
@@ -111,17 +123,26 @@ button[type="reset"]:hover, button#btnTestTicket:hover, #btnExport:hover, .btn-s
background-color: #c82333;
}
.form-actions button {
margin-right: 0; /* Remove right margin */
margin-left: 10px; /* Add left margin for spacing */
.btn-icon {
padding: 8px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon .material-icons-outlined {
font-size: 20px;
}
.form-actions {
/* Empuja los botones a la derecha en el grid */
grid-column-start: 2;
margin-top: 20px; /* Aumentar espacio superior */
margin-top: 20px;
display: flex;
justify-content: flex-end; /* Alinea botones a la derecha */
justify-content: flex-end;
}
.form-actions button {
margin-right: 0;
margin-left: 10px;
}
/* Tabla */
@@ -131,32 +152,36 @@ button[type="reset"]:hover, button#btnTestTicket:hover, #btnExport:hover, .btn-s
border-radius: 5px;
}
#tblMoves, #tblClients {
#tblMoves, #tblClients, #tblUsers {
width: 100%;
border-collapse: collapse;
margin: 0;
}
#tblMoves th, #tblMoves td,
#tblClients th, #tblClients td {
#tblClients th, #tblClients td,
#tblUsers th, #tblUsers td {
border-bottom: 1px solid #dee2e6;
padding: 12px 15px;
text-align: left;
white-space: nowrap;
}
#tblMoves td:last-child, #tblMoves th:last-child,
#tblClients td:last-child, #tblClients th:last-child {
#tblClients td:last-child, #tblClients th:last-child,
#tblUsers td:last-child, #tblUsers th:last-child {
text-align: center;
}
#tblMoves th, #tblClients th {
#tblMoves th, #tblClients th, #tblUsers th {
background-color: #f8f9fa;
font-weight: 600;
font-family: 'Montserrat', sans-serif;
font-weight: 500;
border-bottom-width: 2px;
}
#tblMoves tbody tr:last-child td,
#tblClients tbody tr:last-child td {
#tblClients tbody tr:last-child td,
#tblUsers tbody tr:last-child td {
border-bottom: none;
}
@@ -240,26 +265,48 @@ button.action-btn {
border-bottom: 1px solid #dee2e6;
}
.main-header h1 {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
/* Logo en header */
.main-header .header-logo {
.header-logo {
height: 50px;
width: auto;
margin-right: 2rem;
}
.main-header {
.tabs {
display: flex;
flex-grow: 1;
}
.tab-link {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
gap: 8px;
padding: 10px 20px;
cursor: pointer;
background: none;
border: none;
font-family: 'Montserrat', sans-serif;
font-weight: 500;
font-size: 16px;
color: #6c757d;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
}
.tab-link:hover {
color: #343a40;
}
.tab-link.active {
color: #343a40;
border-bottom-color: #343a40;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Dashboard Styles */
@@ -282,13 +329,14 @@ button.action-btn {
margin: 0 0 0.5rem 0;
font-size: 1rem;
color: #555;
font-weight: 500;
}
.stat-card p {
margin: 0;
font-size: 2rem;
font-weight: bold;
color: var(--color-primary);
font-weight: 600;
color: #343a40;
}
.dashboard-chart {
@@ -296,59 +344,48 @@ button.action-btn {
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
/* Fijar altura para evitar bucle infinito de redibujo de Chart.js */
position: relative;
height: 300px;
}
.tabs {
display: flex;
}
.tab-link {
padding: 10px 20px;
cursor: pointer;
background: none;
border: none;
font-size: 16px;
color: #6c757d;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab-link.active {
color: #343a40;
border-bottom-color: #343a40;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* --- Estilos específicos de Clientes --- */
.client-grid {
.form-grid-single {
display: grid;
grid-template-columns: 120px 1fr 120px 1fr; /* Dos columnas de etiquetas y campos */
gap: 18px 12px; /* Espacio vertical y horizontal */
align-items: center;
grid-template-columns: 1fr;
gap: 12px;
}
.form-grid-single label {
text-align: left;
margin-bottom: -5px; /* Acercar la etiqueta al campo */
}
.form-grid-single input[type="tel"] {
max-width: 250px;
}
.form-grid-single .checkbox-container {
margin-top: 10px;
}
.form-grid-single .checkbox-container label {
font-weight: 400; /* Peso normal para checkboxes */
}
.form-actions-single {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.client-grid .checkbox-container {
grid-column: 2 / span 3; /* El checkbox ocupa las columnas de la derecha */
.checkbox-container {
display: flex;
align-items: center;
}
.checkbox-container input[type="checkbox"] {
width: auto;
margin-right: 8px;
}
.hidden {
display: none !important;
}
/* --- Estilos de Configuración --- */
.data-location-info {
background-color: #e9ecef;
@@ -361,6 +398,15 @@ button.action-btn {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}
.sub-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
}
.sub-section h3 {
margin-top: 0;
}
/* --- Estilos del Pie de Página --- */
.main-footer-credits {
text-align: center;