KEMBAR78
Pasted 2 | PDF | Programming Paradigms | Php
0% found this document useful (0 votes)
8 views57 pages

Pasted 2

The document outlines a JavaScript implementation for an API layer that connects to a backend using PHP, providing various functions for user authentication, data retrieval, and order management. It also includes a UI rendering layer that generates HTML for navigation, dashboards, and client management, with features for filtering and displaying data. The code is structured to handle asynchronous requests and dynamically update the user interface based on application state and user interactions.

Uploaded by

devgmxa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views57 pages

Pasted 2

The document outlines a JavaScript implementation for an API layer that connects to a backend using PHP, providing various functions for user authentication, data retrieval, and order management. It also includes a UI rendering layer that generates HTML for navigation, dashboards, and client management, with features for filtering and displaying data. The code is structured to handle asynchronous requests and dynamically update the user interface based on application state and user interactions.

Uploaded by

devgmxa
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 57

document.

addEventListener('DOMContentLoaded', () => {

// =================================================================
// 1. CAPA DE API - Conexión con el Backend Real (PHP)
// =================================================================
const api = {
async request(endpoint, options = {}) {
try {
const defaultOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json', 'Accept':
'application/json' }
};
const config = { ...defaultOptions, ...options };
if (config.body) {
config.body = JSON.stringify(config.body);
}
const response = await fetch(`api/${endpoint}`, config);
const text = await response.text();
if (!response.ok) {
let error = 'Error desconocido en el servidor.';
try {
error = JSON.parse(text).error || `Error del servidor: $
{response.statusText}`;
} catch {
error = text.substring(0, 200);
}
throw new Error(error);
}
return text ? JSON.parse(text) : {};
} catch (error) {
if (!error.message.includes('Acceso no autorizado')) {
App.notify(error.message, 'error');
}
throw error;
}
},
login: (username, password) => api.request('login.php', { method: 'POST',
body: { username, password } }),
fetchInitialData: () => api.request('get_data.php'),
saveOrder: (orderData) => api.request('save_order.php', { method: 'POST',
body: orderData }),
// ===== INICIO DE LA MODIFICACIÓN: Se añade la nueva ruta a la API =====
updateOrderStatus: (orderId, status) =>
api.request('update_order_status.php', { method: 'POST', body: { id: orderId,
status: status } }),
// ===== FIN DE LA MODIFICACIÓN =====
deleteOrder: (orderId) => api.request('delete_order.php', { method: 'POST',
body: { id: orderId } }),
findOrderById: (orderId) => api.request(`get_order.php?id=${orderId}`),
saveClient: (clientData) => api.request('save_client.php', { method:
'POST', body: clientData }),
deleteClient: (clientId) => api.request('delete_client.php', { method:
'POST', body: { id: clientId } }),
saveService: (serviceData) => api.request('save_service.php', { method:
'POST', body: serviceData }),
deleteService: (serviceId) => api.request('delete_service.php', { method:
'POST', body: { id: serviceId } }),
saveProduct: (productData) => api.request('save_product.php', { method:
'POST', body: productData }),
updateStock: (productId, quantityChange) => api.request('update_stock.php',
{ method: 'POST', body: { id: productId, change: quantityChange } }),
saveExpense: (expenseData) => api.request('save_expense.php', { method:
'POST', body: expenseData }),
deleteExpense: (expenseId) => api.request('delete_expense.php', { method:
'POST', body: { id: expenseId } }),
saveSettings: (settingsData) => api.request('save_settings.php', { method:
'POST', body: settingsData }),
fetchUsers: () => api.request('get_users.php'),
saveUser: (userData) => api.request('save_user.php', { method: 'POST',
body: userData }),
verifyPin: (pin) => api.request('verify_pin.php', { method: 'POST', body: {
pin: pin } }),
};

// =================================================================
// 2. CAPA DE RENDERIZADO DE UI (Funciones que generan HTML)
// =================================================================
const ui = {
appContainer: document.getElementById('app-container'),
contentArea: document.getElementById('content-area'),
navList: document.getElementById('nav-list'),

_createPageHeader: (title, icon, actionsHTML = '') => `


<div class="flex flex-col sm:flex-row justify-between items-start
sm:items-center mb-8 gap-4">
<h1 class="text-3xl font-bold text-white flex items-center gap-
3"><i class="${icon} text-sky-400 text-2xl"></i>${title}</h1>
<div class="flex items-center gap-2 w-full sm:w-auto">$
{actionsHTML}</div>
</div>`,

renderNavigation: function() {
const navItems = App.getNavItems().filter(item =>
App.state.settings.menuVisibility[item.id]);
const navHTML = navItems.map(item => `
<li class="w-full sm:w-auto sm:flex-shrink-0">
<a href="#" class="nav-link flex flex-col items-center justify-
center w-full sm:w-24 h-20 bg-slate-800 rounded-lg p-2 text-slate-400 font-medium
text-center" data-page="${item.id}" title="${item.name}">
<i class="${item.icon} text-2xl mb-1"></i>
<span class="text-xs">${item.name}</span>
${item.isPremium && App.state.userInfo.plan === 'Básico' ?
'<i class="fas fa-lock text-amber-400 text-xs absolute top-2 right-2"></i>' : ''}
</a>
</li>
`).join('');
this.navList.innerHTML = navHTML;
},

renderDashboard: function() {
const headerActions = `
<button id="search-btn" class="btn btn-secondary"><i class="fas fa-
search"></i> Buscar Folio</button>
<button id="quick-cash-cut-btn" class="btn btn-ghost" title="Corte
de Caja Rápido"><i class="fas fa-cash-register"></i></button>
<button id="config-dashboard-btn" class="btn btn-ghost"
title="Configurar Dashboard"><i class="fas fa-cog"></i></button>
<button id="new-order-btn" class="btn btn-primary w-full sm:w-
auto">
<i class="fas fa-plus-circle"></i> Nueva Orden
</button>`;

const stats = App.getDashboardStats();

const cardsHTML = `
<div class="grid grid-cols-1 md:grid-cols-5 gap-6 mb-8">
${App.state.settings.dashboardCards.cashBalance ? `
<div id="cash-register-card" class="bg-slate-900 border border-
slate-800 rounded-lg p-4 flex flex-col items-center justify-center text-center
cursor-pointer hover:border-sky-500 transition-colors">
<i class="fas fa-cash-register text-sky-400 text-3xl mb-
2"></i>
<p class="text-slate-400 text-xs whitespace-
nowrap">Efectivo en Caja</p>
<p class="text-2xl font-bold text-white">$$
{stats.cashInRegister.toFixed(2)}</p>
</div>` : ''}
${App.state.settings.dashboardCards.ordersCount ? `
<div class="md:col-span-2 bg-slate-900 border border-slate-800
rounded-lg p-6">
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold text-white">Total de
Órdenes</h4>
<i class="fas fa-receipt text-green-400"></i>
</div>
<div class="flex justify-around text-center mt-4">
<div><p class="text-2xl font-bold text-white">$
{stats.ordersToday}</p><p class="text-xs text-slate-400">Hoy</p></div>
<div><p class="text-2xl font-bold text-white">$
{stats.ordersWeek}</p><p class="text-xs text-slate-400">Semana</p></div>
<div><p class="text-2xl font-bold text-white">$
{stats.ordersMonth}</p><p class="text-xs text-slate-400">Mes</p></div>
</div>
</div>` : ''}
${App.state.settings.dashboardCards.salesTotal ? `
<div class="md:col-span-2 bg-slate-900 border border-slate-800
rounded-lg p-6">
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold text-white">Total de
Ventas</h4>
<i class="fas fa-chart-line text-amber-400"></i>
</div>
<div class="flex justify-around text-center mt-4">
<div><p class="text-xl font-bold text-white">$$
{stats.salesToday.toFixed(0)}</p><p class="text-xs text-slate-400">Hoy</p></div>
<div><p class="text-xl font-bold text-white">$$
{stats.salesWeek.toFixed(0)}</p><p class="text-xs text-slate-400">Semana</p></div>
<div><p class="text-xl font-bold text-white">$$
{stats.salesMonth.toFixed(0)}</p><p class="text-xs text-slate-400">Mes</p></div>
</div>
</div>` : ''}
</div>
`;

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Panel de Inicio', 'fas fa-home',
headerActions)}
${cardsHTML}
<div id="dashboard-table-container" class="bg-slate-900 rounded-lg
shadow-lg overflow-hidden border border-slate-800"></div>
</div>`;

this.renderDashboardTable();
App.attachDashboardListeners();
},

renderDashboardTable: function(filters = {}) {


const container = document.getElementById('dashboard-table-container');
if (!container) return;

const orders = App.filterAndSortOrders(filters);

const statusInfo = {
pending: { text: 'Pendiente', icon: 'fa-hourglass-half', color:
'text-amber-400', bg: 'bg-amber-500/10' },
completed: { text: 'Completado', icon: 'fa-check-circle', color:
'text-green-400', bg: 'bg-green-500/10' },
ready: { text: 'Listo', icon: 'fa-box-check', color: 'text-indigo-
400', bg: 'bg-indigo-500/10' },
delivered: { text: 'Entregado', icon: 'fa-shipping-fast', color:
'text-sky-400', bg: 'bg-sky-500/10' },
};

const statusOptions = Object.entries(statusInfo).map(([key, value]) =>


`<option value="${key}">${value.text}</option>`).join('');

const tableRows = orders.length > 0 ? orders.map(order => {


const status = statusInfo[order.status] || { text: 'Desconocido',
icon: 'fa-question-circle', color: 'text-slate-400', bg: 'bg-slate-500/10' };
const receptionDate = new Date(order.reception_date);
const daysElapsed = Math.floor((new Date() - receptionDate) / (1000
* 60 * 60 * 24));
const daysText = daysElapsed === 0 ? 'Hoy' : `Hace ${daysElapsed}
día(s)`;

return `
<tr class="hover:bg-slate-800/50 transition-colors">
<td class="p-4 border-b border-slate-800">
<div class="font-semibold text-white">#${order.id}</div>
<div class="text-xs text-slate-400">${order.cashier}</div>
</td>
<td class="p-4 border-b border-slate-800 capitalize">$
{order.client_name}</td>
<td class="p-4 border-b border-slate-800 text-sm">
<div><span class="font-semibold text-slate-
400">Recibido:</span> ${receptionDate.toLocaleDateString('es-ES')}</div>
<div><span class="font-semibold
text-slate-400">Entrega:</span> ${new
Date(order.delivery_date).toLocaleDateString('es-ES')}</div>
<div class="text-xs text-sky-400 mt-1">${daysText}</div>
</td>
<td class="p-4 border-b border-slate-800 text-right font-mono
text-lg">$${parseFloat(order.total).toFixed(2)}</td>
<td class="p-4 border-b border-slate-800">
<span class="inline-flex items-center gap-2 px-3 py-1
rounded-full text-xs font-medium ${status.bg} ${status.color}">
<i class="fas ${status.icon}"></i> ${status.text}
</span>
</td>
<td class="p-4 border-b border-slate-800 text-center">
<div class="flex items-center justify-center gap-1">
${ (order.status === 'pending' &&
App.state.settings.enableQuickFinish) ? `
<button class="btn btn-ghost quick-complete-btn" data-
id="${order.id}" title="Marcar como Terminado y Listo">
<i class="fas fa-check-double text-green-400"></i>
</button>` : '' }

<button class="btn btn-ghost update-status-btn" data-


id="${order.id}" title="Liquidar / Abonar">
<i class="fas fa-truck-fast"></i>
</button>

${ (order.status === 'ready' || order.status ===


'completed') ? `
<button class="btn btn-ghost whatsapp-notify-btn" data-
id="${order.id}" title="Notificar por WhatsApp">
<i class="fab fa-whatsapp text-green-500"></i>
</button>` : '' }

<button class="btn btn-ghost view-ticket-btn" data-


id="${order.id}" title="Ver Ticket"><i class="fas fa-receipt"></i></button>
<button class="btn btn-ghost edit-order-btn" data-id="$
{order.id}" title="Editar"><i class="fas fa-edit"></i></button>
<button class="btn btn-ghost delete-order-btn" data-
id="${order.id}" title="Eliminar"><i class="fas fa-trash
text-red-500"></i></button>
</div>
</td>
</tr>`;
}).join('') : `<tr><td colspan="6" class="p-8 text-center text-slate-
500">No hay órdenes registradas.</td></tr>`;

container.innerHTML = `
<div class="p-4 bg-slate-800/30 flex items-center gap-4">
<div class="flex-1 input-icon-wrapper">
<i class="icon fas fa-search"></i>
<input type="text" id="dashboard-search-input" class="form-
input form-input-icon" placeholder="Buscar por Folio o Cliente..." value="$
{filters.query || ''}">
</div>
<div class="flex-1">
<select id="dashboard-status-filter" class="form-select">
<option value="all">Todos los Estados</option>
${statusOptions}
</select>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-slate-300">
<thead class="bg-slate-800/50">
<tr class="text-slate-400 text-sm">
<th class="p-4 font-semibold">Folio/Cajero</th>
<th class="p-4 font-semibold">Cliente</th>
<th class="p-4 font-semibold">Fechas</th>
<th class="p-4 font-semibold text-right">Total</th>
<th class="p-4 font-semibold">Estado</th>
<th class="p-4 font-semibold text-
center">Acciones</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
</div>`;

document.getElementById('dashboard-status-filter').value =
filters.status || 'all';

const searchInput = document.getElementById('dashboard-search-input');


const statusFilter = document.getElementById('dashboard-status-
filter');

const applyFilters = () => {


const newFilters = { query: searchInput.value, status:
statusFilter.value };
this.renderDashboardTable(newFilters);
};

searchInput.addEventListener('input', applyFilters);
statusFilter.addEventListener('change', applyFilters);
},

renderClientsPage: function() {
const clients = App.state.clients;
const headerActions = `<button id="new-client-btn" class="btn btn-
primary"><i class="fas fa-user-plus"></i> Nuevo Cliente</button>`;
const tableRows = clients.length > 0 ? clients.map(client => `
<tr class="hover:bg-slate-800/50">
<td class="p-4 border-b border-slate-800 font-semibold text-
white">${client.id}</td>
<td class="p-4 border-b border-slate-800 capitalize">$
{client.name}</td>
<td class="p-4 border-b border-slate-800">${client.address ||
'N/A'}</td>
<td class="p-4 border-b border-slate-800">${client.phone ||
'N/A'}</td>
<td class="p-4 border-b border-slate-800 text-center">
<button class="btn btn-ghost edit-client-btn" data-id="$
{client.id}" title="Editar"><i class="fas fa-edit"></i></button>
</td>
</tr>
`).join('') : `<tr><td colspan="5" class="p-8 text-center text-slate-
500">No hay clientes registrados.</td></tr>`;

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Clientes', 'fas fa-users',
headerActions)}
<div class="bg-slate-900 rounded-lg shadow-lg overflow-hidden
border border-slate-800">
<div class="overflow-x-auto">
<table class="w-full text-left text-slate-300">
<thead><tr class="text-slate-400 bg-slate-800/50 text-
sm"><th class="p-4 font-semibold">ID</th><th class="p-4
font-semibold">Nombre</th><th class="p-4 font-semibold">Dirección</th><th class="p-
4 font-semibold">Teléfono</th><th class="p-4 font-semibold
text-center">Acciones</th></tr></thead>
<tbody>${tableRows}</tbody>
</table>
</div>
</div>
</div>`;
},

renderServicesPage: function() {
const serviceCategories = App.state.services;
const headerActions = `<button id="new-service-btn" class="btn btn-
primary"><i class="fas fa-plus"></i> Nuevo Servicio</button>`;

const allServices = serviceCategories.flatMap(category =>


(category.items || []).map(item => ({...item, category:
category.category}))
);

const tableRows = allServices.length > 0 ? allServices.map(s => `


<tr class="hover:bg-slate-800/50">
<td class="p-4 border-b border-slate-800 font-semibold text-
white">${s.id}</td>
<td class="p-4 border-b border-slate-800">
<div>${s.name}</div>
<div class="text-xs text-slate-400">${s.category}</div>
</td>
<td class="p-4 border-b border-slate-800">${s.unit ||
'N/A'}</td>
<td class="p-4 border-b border-slate-800 text-right font-
mono">$${parseFloat(s.price).toFixed(2)}</td>
<td class="p-4 border-b border-slate-800 text-center">
<button class="btn btn-ghost edit-service-btn" data-id="$
{s.id}" title="Editar"><i class="fas fa-edit"></i></button>
</td>
</tr>
`).join('') : `<tr><td colspan="5" class="p-8 text-center text-slate-
500">No hay servicios registrados.</td></tr>`;

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Catálogo de Servicios', 'fas fa-tshirt',
headerActions)}
<div class="bg-slate-900 rounded-lg shadow-lg overflow-hidden
border border-slate-800">
<div class="overflow-x-auto">
<table class="w-full text-left text-slate-300">
<thead>
<tr class="text-slate-400 bg-slate-800/50 text-sm">
<th class="p-4 font-semibold">ID</th>
<th class="p-4 font-semibold">Descripción</th>
<th class="p-4 font-semibold">Unidad</th>
<th class="p-4 font-semibold text-
right">Costo</th>
<th class="p-4 font-semibold text-
center">Acciones</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
</div>
</div>
</div>`;
},

renderProductsPage: function() {
const products = App.state.products;
const headerActions = `<button id="new-product-btn" class="btn btn-
primary"><i class="fas fa-plus"></i> Nuevo Suministro</button>`;
const tableRows = products.length > 0 ? products.map(p => `
<tr class="hover:bg-slate-800/50">
<td class="p-4 border-b border-slate-800 font-semibold text-
white">${p.id}</td>
<td class="p-4 border-b border-slate-800">${p.name}</td>
<td class="p-4 border-b border-slate-800 text-right font-
mono">$${(parseFloat(p.cost) || 0).toFixed(2)}</td>
<td class="p-4 border-b border-slate-800 text-right font-
mono">${p.stock}</td>
<td class="p-4 border-b border-slate-800">${p.unit}</td>
<td class="p-4 border-b border-slate-800 text-center">
<button class="btn btn-ghost manage-stock-btn" data-id="$
{p.id}" title="Gestionar Stock"><i class="fas fa-exchange-alt"></i></button>
<button class="btn btn-ghost edit-product-btn" data-id="$
{p.id}" title="Editar Suministro"><i class="fas fa-edit"></i></button>
</td>
</tr>
`).join('') : `<tr><td colspan="6" class="p-8 text-center text-slate-
500">No hay suministros registrados.</td></tr>`;

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Suministros', 'fas fa-box-open',
headerActions)}
<div class="bg-slate-900 rounded-lg shadow-lg overflow-hidden
border border-slate-800">
<div class="overflow-x-auto">
<table class="w-full text-left text-slate-300">
<thead><tr class="text-slate-400 bg-slate-800/50 text-
sm"><th class="p-4 font-semibold">ID/SKU</th><th class="p-4 font-
semibold">Nombre</th><th class="p-4 font-semibold text-right">Costo</th><th
class="p-4 font-semibold text-right">Existencia</th><th class="p-4 font-
semibold">Unidad</th><th class="p-4 font-semibold
text-center">Acciones</th></tr></thead>
<tbody>${tableRows}</tbody>
</table>
</div>
</div>
</div>`;
},

renderExpensesPage: function() {
const expenses = App.state.expenses;
const headerActions = `<button id="new-expense-btn" class="btn btn-
primary"><i class="fas fa-plus"></i> Nuevo Gasto</button>`;
const totalExpenses = expenses.reduce((sum, exp) => sum +
parseFloat(exp.total), 0);

const tableRows = expenses.map(exp => `


<tr class="hover:bg-slate-800/50">
<td class="p-4 border-b border-slate-800">${new
Date(exp.date).toLocaleDateString('es-ES')}</td>
<td class="p-4 border-b border-slate-800">${exp.concept}</td>
<td class="p-4 border-b border-slate-800">${exp.document ||
'N/A'}</td>
<td class="p-4 border-b border-slate-800 text-right font-
mono">$${parseFloat(exp.total).toFixed(2)}</td>
<td class="p-4 border-b border-slate-800 text-center">
<button class="btn btn-ghost edit-expense-btn" data-id="$
{exp.id}" title="Editar"><i class="fas fa-edit"></i></button>
<button class="btn btn-ghost delete-expense-btn" data-
id="${exp.id}" title="Eliminar"><i class="fas fa-trash text-red-500"></i></button>
</td>
</tr>
`).join('');

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Control de Gastos', 'fas fa-money-bill-
wave', headerActions)}
<div class="bg-slate-900 rounded-lg shadow-lg overflow-hidden
border border-slate-800">
<div class="overflow-x-auto">
<table class="w-full text-left text-slate-300">
<thead><tr class="text-slate-400 bg-slate-800/50 text-
sm"><th class="p-4 font-semibold">Fecha</th><th class="p-4 font-
semibold">Concepto</th><th class="p-4 font-semibold">Documento</th><th class="p-4
font-semibold text-right">Total</th><th class="p-4 font-semibold text-
center">Acciones</th></tr></thead>
<tbody>${tableRows}</tbody>
<tfoot><tr class="bg-slate-800 font-bold"><td
colspan="3" class="p-4 text-right">Total:</td><td class="p-4 text-right font-mono
text-lg">$${totalExpenses.toFixed(2)}</td><td></td></tr></tfoot>
</table>
</div>
</div>
</div>`;
},

renderReportsPage: function() {
const headerActions = `
<button id="cash-cut-btn" class="btn btn-secondary"><i class="fas
fa-cash-register"></i> Corte de Caja</button>
<button id="sales-report-btn" class="btn btn-primary"><i class="fas
fa-file-pdf"></i> Reporte de Ventas</button>
`;
this.contentArea.innerHTML = `<div class="page-enter">
${this._createPageHeader('Reportes', 'fas fa-chart-pie',
headerActions)}
<div class="bg-slate-900 p-6 rounded-lg border border-slate-800
text-center">
<i class="fas fa-chart-line text-5xl text-slate-500 mb-4"></i>
<h3 class="text-2xl font-bold text-white">Generación de
Reportes</h3>
<p class="text-slate-400 mt-2">Utiliza los botones de arriba
para generar reportes y realizar el corte de caja.</p>
</div>
</div>`;
},
renderMarketingPage: function() {
const clients = App.state.clients;
const clientsListHTML = clients.map(client => `
<label for="client-${client.id}" class="flex items-center p-3 bg-
slate-800 rounded-lg hover:bg-slate-700/50 cursor-pointer">
<input type="checkbox" id="client-${client.id}" class="h-4 w-4
rounded border-slate-600 bg-slate-700 text-sky-500 focus:ring-sky-600">
<div class="ml-3">
<p class="font-semibold text-white capitalize">$
{client.name}</p>
<p class="text-xs text-slate-400">${client.email || 'Sin
email'}</p>
</div>
</label>
`).join('');

const pageHTML = `
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-1">
<h3 class="text-xl font-bold text-white mb-4">Lista de
Clientes</h3>
<div class="bg-slate-900 p-4 rounded-lg border border-
slate-800 max-h-[60vh] overflow-y-auto space-y-2">
${clientsListHTML}
</div>
</div>
<div class="lg:col-span-2">
<h3 class="text-xl font-bold text-white mb-4">Crear
Campaña</h3>
<div class="bg-slate-900 p-6 rounded-lg border border-
slate-800 space-y-4">
<div>
<label class="form-label">Asunto del
Mensaje</label>
<input type="text" class="form-input"
placeholder="¡Tenemos una promoción para ti!">
</div>
<div>
<label class="form-label">Contenido del
Mensaje</label>
<textarea class="form-textarea" rows="8"
placeholder="Hola {nombre_cliente}, aprovecha nuestro 20% de descuento
en..."></textarea>
</div>
<div class="flex justify-end">
<button id="send-campaign-btn" class="btn btn-
primary"><i class="fas fa-paper-plane"></i> Enviar Campaña (Simulado)</button>
</div>
</div>
</div>
</div>
`;

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Marketing', 'fas fa-bullhorn')}
${pageHTML}
</div>`;
document.getElementById('send-campaign-btn').addEventListener('click',
() => {
App.notify('Simulando envío de campaña de marketing...', 'info');
});
},

renderSettingsPage: function() {
const settings = App.state.settings;
settings.security = settings.security || { loginRequired: false };

const pinActions = [
{ section: 'dashboard', actions: ['create_order', 'deliver_order',
'search'], label: 'Dashboard' },
{ section: 'orders', actions: ['view_details', 'edit', 'delete',
'filter'], label: 'Órdenes' },
{ section: 'services', actions: ['create', 'edit', 'delete'],
label: 'Servicios' },
{ section: 'products', actions: ['create', 'edit', 'delete'],
label: 'Suministros' },
{ section: 'clients', actions: ['create', 'edit', 'delete'], label:
'Clientes' },
{ section: 'expenses', actions: ['create', 'delete'], label:
'Gastos' },
{ section: 'reports', actions: ['cut_cash'], label: 'Reportes' },
{ section: 'settings', actions: ['view'], label: 'Ajustes' },
];

const pinTogglesHTML = pinActions.map(group => `


<div>
<h4 class="font-semibold text-white mb-2">${group.label}</h4>
<div class="grid grid-cols-2 gap-3">
${group.actions.map(action => {
const isChecked =
settings.pinProtection[group.section]?.[action] || false;
return `
<div class="flex items-center justify-between bg-slate-
800 p-2 rounded-md">
<label for="pin-${group.section}-${action}"
class="text-sm capitalize">${action.replace(/_/g, ' ')}</label>
<label class="toggle-switch">
<input type="checkbox" id="pin-$
{group.section}-${action}" data-section="${group.section}" data-action="${action}"
${isChecked ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>`;
}).join('')}
</div>
</div>
`).join('');

const menuTogglesHTML = App.getNavItems().map(item => `


<div class="flex items-center justify-between bg-slate-800 p-3
rounded-md">
<label for="menu-${item.id}" class="font-medium text-white">$
{item.name}</label>
<label class="toggle-switch">
<input type="checkbox" id="menu-${item.id}" data-key="$
{item.id}" ${settings.menuVisibility[item.id] ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
`).join('');

const logoOptionHTML = settings.premiumFeatures.changeLogo


? `<div>
<label class="form-label">URL del Logo</label>
<input type="text" name="logoUrl" class="form-input"
placeholder="https://example.com/logo.png" value="${settings.logoUrl || ''}">
</div>`
: `<div class="text-center p-4 bg-slate-800 rounded-lg">
<p class="text-slate-300">Personaliza tu marca cambiando el
logo.</p>
<button type="button" id="buy-logo-feature-btn" class="btn
btn-primary mt-4 bg-amber-500 border-amber-500 hover:bg-amber-400">
<i class="fas fa-gem"></i> Comprar Función de Logo
</button>
</div>`;

const posOptionHTML = `
<div>
<label class="form-label">Método de "Nueva Orden"</label>
<select name="newServiceMethod" class="form-select">
<option value="modal" ${settings.newServiceMethod ===
'modal' ? 'selected' : ''}>Modal Clásico</option>
<option value="pos" ${settings.newServiceMethod === 'pos' ?
'selected' : ''}>Punto de Venta (POS)</option>
<option value="pos_v2" ${settings.newServiceMethod ===
'pos_v2' ? 'selected' : ''}>Plantilla de Cobro Completa</option>
</select>
</div>`;

const pageContent = `
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 space-y-8">
<div class="bg-slate-900 p-6 rounded-lg border border-
slate-800">
<h3 class="text-xl font-bold text-white mb-
4">Información del Negocio</h3>
<form id="business-settings-form" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div><label
class="form-label">Nombre</label><input type="text" name="businessName"
class="form-input" value="${settings.businessName}"></div>
<div><label class="form-label">Folio
Inicial</label><input type="number" name="folioCounter" class="form-input" value="$
{settings.folioCounter}"></div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div><label
class="form-label">Teléfono</label><input type="text" name="phone" class="form-
input" value="${settings.phone}"></div>
<div><label
class="form-label">Email</label><input type="email" name="email" class="form-input"
value="${settings.email}"></div>
</div>
<div><label
class="form-label">Dirección</label><input type="text" name="address" class="form-
input" value="${settings.address}"></div>
${posOptionHTML}
<div>
<label class="form-label">Logo del
Negocio</label>
${logoOptionHTML}
</div>
<div class="flex items-center justify-between bg-
slate-800 p-3 rounded-md">
<label for="toggle-quick-finish" class="font-
medium text-white">Activar estado "Listo" y Acción Rápida</label>
<label class="toggle-switch">
<input type="checkbox" id="toggle-quick-
finish" name="enableQuickFinish" ${settings.enableQuickFinish ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
</form>
</div>
<div class="bg-slate-900 p-6 rounded-lg border border-
slate-800">
<h3 class="text-xl font-bold text-white mb-
4">Seguridad</h3>

<div class="flex items-center justify-between bg-slate-


800 p-3 rounded-md mb-6 border border-slate-700">
<label for="toggle-login-required" class="font-
medium text-white">Requerir inicio de sesión al entrar</label>
<label class="toggle-switch">
<input type="checkbox" id="toggle-login-
required" name="loginRequired" ${settings.security.loginRequired ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>

<h4 class="text-lg font-semibold text-white mb-


2">Protección con NIP por Acción</h4>
<p class="text-slate-400 text-sm mb-4">Requerir NIP de
administrador para realizar acciones específicas.</p>
<div id="pin-settings-form" class="space-y-4">$
{pinTogglesHTML}</div>
</div>
</div>
<div class="space-y-8">
<div class="bg-slate-900 p-6 rounded-lg border border-
slate-800">
<h3 class="text-xl font-bold text-white mb-
4">Administración de Cuenta</h3>
<div class="space-y-3">
<button id="change-username-btn" class="btn btn-
secondary w-full"><i class="fas fa-user-edit"></i> Cambiar Nombre de
Usuario</button>
<button id="change-pin-btn" class="btn btn-
secondary w-full"><i class="fas fa-key"></i> Cambiar NIP</button>
<button id="manage-users-btn" class="btn btn-
secondary w-full"><i class="fas fa-users-cog"></i> Gestionar Otros
Usuarios</button>
</div>
</div>
<div class="bg-slate-900 p-6 rounded-lg border border-
slate-800">
<h3 class="text-xl font-bold text-white mb-
4">Administrar Menú</h3>
<div id="menu-settings-form" class="space-y-3">$
{menuTogglesHTML}</div>
</div>
<div class="bg-slate-900 p-6 rounded-lg border border-
slate-800">
<h3 class="text-xl font-bold text-white mb-
4">Membresía</h3>
${ui.renderMembershipCard(App.state.userInfo.plan)}
</div>
</div>
</div>
<div class="mt-8 flex justify-end">
<button id="save-settings-btn" class="btn btn-primary"><i
class="fas fa-save"></i> Guardar Cambios</button>
</div>
`;

this.contentArea.innerHTML = `<div class="page-enter">


${this._createPageHeader('Ajustes', 'fas fa-cog')}
${pageContent}
</div>`;

const upgradePreviewBtn = document.getElementById('upgrade-btn-


preview');
if (upgradePreviewBtn) {
upgradePreviewBtn.addEventListener('click',
App.handleUpgradeToPremium);
}
},

renderMembershipCard: function(plan) {
const isPremium = plan === 'Premium';
return `
<div class="bg-slate-800 rounded-lg p-6 border ${isPremium ?
'border-amber-400' : 'border-sky-500'}">
<h4 class="text-2xl font-bold ${isPremium ? 'text-amber-400' :
'text-sky-400'}">Plan ${plan}</h4>
<p class="text-slate-400 mt-2">${isPremium ? 'Todas las
funciones avanzadas activadas.' : 'Funciones esenciales para tu negocio.'}</p>
${!isPremium ? `
<button id="upgrade-btn" class="w-full btn btn-primary mt-6 bg-
amber-500 border-amber-500 hover:bg-amber-400">
<i class="fas fa-gem"></i> Actualizar a Premium
</button>
` : ''}
</div>
`;
},
};

// =================================================================
// 4. CAPA CONTROLADORA (Lógica de la Aplicación)
// =================================================================
const App = {
state: {
clients: [], orders: [], products: [], expenses: [], services: [],
users: [],
settings: {
menuVisibility: {},
dashboardCards: {},
pinProtection: {},
security: { loginRequired: false }
},
userInfo: {},
cashierData: {},
},
modalElement: document.getElementById('main-modal'),
modalContent: document.getElementById('modal-content'),
loadingSpinner: document.getElementById('loading-spinner'),
notificationContainer: document.getElementById('notification-container'),
currentOrderItems: [],

navItems: [
{ id: 'dashboard', name: 'INICIO', icon: 'fas fa-home', isPremium:
false },
{ id: 'clients', name: 'CLIENTES', icon: 'fas fa-users', isPremium:
false },
{ id: 'services', name: 'SERVICIOS', icon: 'fas fa-tshirt', isPremium:
false },
{ id: 'products', name: 'SUMINISTROS', icon: 'fas fa-box-open',
isPremium: false },
{ id: 'expenses', name: 'GASTOS', icon: 'fas fa-money-bill-wave',
isPremium: false },
{ id: 'reports', name: 'REPORTES', icon: 'fas fa-chart-pie', isPremium:
true },
{ id: 'marketing', name: 'MARKETING', icon: 'fas fa-bullhorn',
isPremium: true },
{ id: 'settings', name: 'AJUSTES', icon: 'fas fa-cog', isPremium: false
},
],

init: async function() {


this.setupPersistentEventListeners();
this.setupDelegatedEventListeners();
listenForOrderFromPOS((orderData) =>
this.handleOrderFromPOS(orderData));

this.showLoading(true);
try {
const initialData = await api.fetchInitialData();

this.state = initialData;
this.state.users = await api.fetchUsers();

this.showInitialCashModal(this.state.userInfo, true);

} catch (error) {
if (error.message && error.message.includes('Acceso no
autorizado')) {
window.location.href = 'login.html';
} else {
this.notify('Error fatal al cargar la aplicación. Revisa la
conexión con el servidor.', 'error');
console.error(error);
}
} finally {
this.showLoading(false);
}
},

getNavItems: () => App.navItems,


showLoading: (show) => App.loadingSpinner.classList.toggle('hidden', !
show),

getDashboardStats: function() {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(),
now.getDate());
const oneDay = 24 * 60 * 60 * 1000;

const stats = {
cashInRegister: this.state.cashierData.cashBalance || 0,
ordersToday: 0, ordersWeek: 0, ordersMonth: 0,
salesToday: 0, salesWeek: 0, salesMonth: 0,
};

(this.state.orders || []).forEach(order => {


const orderDate = new Date(order.reception_date);
const diffDays = Math.round((today - orderDate) / oneDay);

if (diffDays === 0) {
stats.ordersToday++;
stats.salesToday += parseFloat(order.total);
}
if (diffDays < 7) {
stats.ordersWeek++;
stats.salesWeek += parseFloat(order.total);
}
if (diffDays < 30) {
stats.ordersMonth++;
stats.salesMonth += parseFloat(order.total);
}
});
return stats;
},

filterAndSortOrders: function(filters = {}) {


let orders = [...(this.state.orders || [])];
if (filters.query) {
const query = filters.query.toLowerCase();
orders = orders.filter(o => o.id.toString().includes(query) ||
o.client_name.toLowerCase().includes(query));
}
if (filters.status && filters.status !== 'all') {
orders = orders.filter(o => o.status === filters.status);
}
return orders.sort((a, b) => new Date(b.reception_date) - new
Date(a.reception_date));
},

notify: function(message, type = 'info') {


const icons = { success: 'fa-check-circle', error: 'fa-times-circle',
info: 'fa-info-circle' };
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.innerHTML = `<i class="fas ${icons[type]}"></i><span>$
{message}</span>`;
this.notificationContainer.appendChild(notification);
setTimeout(() => { notification.remove(); }, 5000);
},

updateUIOnLogin: function() {
const user = this.state.userInfo;
if (!user) {
window.location.href = 'login.html';
return;
}
document.getElementById('current-user').textContent = user.username;
this.updatePlanDisplay();
this.updateLogo();

if (ui.navList) {
ui.navList.className = 'grid grid-cols-4 gap-2 p-2 sm:flex
sm:items-center sm:gap-2 sm:p-0 sm:overflow-x-auto';
}

ui.renderNavigation();
this.setupNavEventListeners();
this.navigateTo('dashboard');
},

updatePlanDisplay: function() {
const planText = `Plan ${this.state.userInfo.plan}`;
document.getElementById('current-plan').textContent = planText;
},

updateLogo: function() {
const logoImg = document.getElementById('business-logo');
if (this.state.settings.logoUrl) {
logoImg.src = this.state.settings.logoUrl;
logoImg.onerror = () => { logoImg.src =
'https://placehold.co/100x40/020617/38bdf8?text=Error'; };
} else {
logoImg.src = 'https://placehold.co/100x40/020617/38bdf8?
text=DevGMXA';
}
},

navigateTo: function(pageId) {
const pageInfo = this.getNavItems().find(item => item.id === pageId);
if (!pageInfo) return;

document.querySelectorAll('.nav-link').forEach(link => {
link.classList.toggle('active', link.dataset.page === pageId);
});

switch (pageId) {
case 'dashboard': ui.renderDashboard(); break;
case 'clients': ui.renderClientsPage(); break;
case 'services': ui.renderServicesPage(); break;
case 'products': ui.renderProductsPage(); break;
case 'expenses': ui.renderExpensesPage(); break;
case 'reports': ui.renderReportsPage(); break;
case 'marketing': ui.renderMarketingPage(); break;
case 'settings': ui.renderSettingsPage(); break;
default: this.contentArea.innerHTML = `<p>Página no
encontrada.</p>`;
}
},

showModal: function(contentHTML) {
this.modalElement.classList.remove('hidden');
this.modalElement.classList.add('flex');
this.modalContent.innerHTML = contentHTML;
void this.modalContent.offsetWidth;
this.modalElement.classList.add('modal-open');
},

hideModal: function(callback) {
this.modalElement.classList.remove('modal-open');
setTimeout(() => {
this.modalElement.classList.add('hidden');
this.modalElement.classList.remove('flex');
if (callback && typeof callback === 'function') {
callback();
}
}, 300);
},

showConfirmationModal: function({ title, message, icon, confirmText,


onConfirm }) {
const modalHTML = `
<div class="p-6 text-center">
<i class="${icon} text-5xl mb-4"></i>
<h3 class="text-xl font-bold text-white">${title}</h3>
<p class="text-slate-400 mt-2">${message}</p>
<div class="mt-6 flex justify-center gap-4">
<button class="btn btn-secondary modal-close-
btn">Cancelar</button>
<button id="confirm-action-btn" class="btn btn-primary">$
{confirmText}</button>
</div>
</div>`;
this.showModal(modalHTML);
document.getElementById('confirm-action-btn').addEventListener('click',
() => { onConfirm(); });
},

showPinModal: function(onSuccess) {
const pinHTML = `
<div class="p-8 text-center">
<h3 class="text-xl font-bold text-white mb-2">Se requiere NIP
de Administrador</h3>
<p class="text-slate-400 mb-4">Ingresa el NIP para
continuar.</p>
<input type="password" id="pin-input" class="form-input text-
center text-2xl tracking-[.5em] w-48 mx-auto" maxlength="4" autofocus>
<div class="mt-6 flex justify-center gap-4">
<button class="btn btn-secondary modal-close-
btn">Cancelar</button>
<button id="verify-pin-btn" class="btn btn-
primary">Verificar</button>
</div>
</div>`;
this.showModal(pinHTML);
const pinInput = document.getElementById('pin-input');
const verifyBtn = document.getElementById('verify-pin-btn');

const verify = async () => {


const pin = pinInput.value;
if (pin.length < 4) return;

verifyBtn.disabled = true;
verifyBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i>`;

try {
const response = await api.verifyPin(pin);
if (response.success) {
this.hideModal(onSuccess);
} else {
this.notify(response.message || 'NIP incorrecto.',
'error');
pinInput.value = '';
pinInput.focus();
}
} catch (error) {
// El error ya es notificado por la capa API
} finally {
verifyBtn.disabled = false;
verifyBtn.innerHTML = `Verificar`;
}
};
verifyBtn.addEventListener('click', verify);
pinInput.addEventListener('keydown', (e) => { if(e.key === 'Enter')
verify() });
},

runProtectedAction: function(permissionKey, callback) {


const user = this.state.userInfo;
if (!user) return;

const [section, action] = permissionKey.split('.');


const isPinProtected = this.state.settings.pinProtection[section]?.
[action];

const checkPermissionsAndRun = () => {


if (user.role === 'admin') {
callback();
return;
}

const hasPermission = user.permissions?.[section]?.[action];


if (hasPermission) {
callback();
} else {
this.notify('Acceso denegado. No tienes permiso para esta
acción.', 'error');
}
};

if (isPinProtected) {
this.showPinModal(checkPermissionsAndRun);
} else {
checkPermissionsAndRun();
}
},

setupPersistentEventListeners: function() {
document.getElementById('logo-container').addEventListener('click', ()
=> this.navigateTo('dashboard'));

document.getElementById('logout-btn').addEventListener('click', () => {
window.location.href = 'api/logout.php';
});

document.getElementById('current-plan').addEventListener('click', () =>
this.handlePlanClick());

document.addEventListener('keydown', (e) => {


if (e.key === 'Escape' && !
this.modalElement.classList.contains('hidden')) {
this.hideModal();
}
});

document.body.addEventListener('click', (e) => {


if (e.target.closest('.modal-close-btn')) {
this.hideModal();
}
});
},

handlePlanClick: function() {
if (this.state.userInfo.plan === 'Básico') {
this.showPremiumBenefitsModal();
} else {
this.runProtectedAction('settings.view', () =>
this.navigateTo('settings'));
}
},

showPremiumBenefitsModal: function() {
const modalHTML = `
<div class="p-6 text-center">
<i class="fas fa-gem text-5xl mb-4 text-amber-400"></i>
<h3 class="text-2xl font-bold text-white">Desbloquea todo el
Potencial con Premium</h3>
<ul class="text-slate-300 mt-4 space-y-2 text-left list-disc
list-inside">
<li>Genera reportes de ventas avanzados.</li>
<li>Envía notificaciones por WhatsApp a tus clientes.</li>
<li>Utiliza herramientas de marketing para fidelizar
clientes.</li>
<li>Personaliza el logo de tu negocio.</li>
<li>Soporte prioritario.</li>
</ul>
<div class="mt-6 flex justify-center gap-4">
<button class="btn btn-secondary
modal-close-btn">Cerrar</button>
<button id="go-to-upgrade-btn" class="btn btn-primary bg-
amber-500 border-amber-500 hover:bg-amber-400">
<i class="fas fa-rocket"></i> Ver Planes
</button>
</div>
</div>`;
this.showModal(modalHTML);
document.getElementById('go-to-upgrade-btn').addEventListener('click',
() => {
this.hideModal();
this.navigateTo('settings');
});
},

setupNavEventListeners: function() {
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
const pageId = link.dataset.page;
const navItem = this.getNavItems().find(item => item.id ===
pageId);

if (navItem.isPremium && this.state.userInfo.plan === 'Básico')


{
this.showPremiumBenefitsModal();
return;
}
if (pageId === 'settings') {
this.runProtectedAction('settings.view', () =>
this.navigateTo(pageId));
} else {
this.navigateTo(pageId);
}
});
});
},

attachDashboardListeners: function() {
document.getElementById('new-order-btn').addEventListener('click', ()
=> this.runProtectedAction('dashboard.create_order', () =>
this.startNewOrderFlow()));
const cashCard = document.getElementById('cash-register-card');
if(cashCard) cashCard.addEventListener('click', () =>
this.handleShowCashDetails());

const configBtn = document.getElementById('config-dashboard-btn');


if (configBtn) configBtn.addEventListener('click', () =>
this.handleConfigureDashboard());

const cashCutBtn = document.getElementById('quick-cash-cut-btn');


if(cashCutBtn) cashCutBtn.addEventListener('click', () =>
this.runProtectedAction('reports.cut_cash', () => this.handleCashCut()));

const searchBtn = document.getElementById('search-btn');


if(searchBtn) searchBtn.addEventListener('click', () =>
this.handleSearch());
},

setupDelegatedEventListeners: function() {
document.body.addEventListener('click', (e) => {
const targetButton = e.target.closest('button');
if (!targetButton) return;

const id = targetButton.dataset.id;

if (id) {
if (targetButton.classList.contains('update-status-btn'))
this.handleUpdateStatus(id);
else if (targetButton.classList.contains('quick-complete-btn'))
this.handleQuickCompleteOrder(id);
else if (targetButton.classList.contains('whatsapp-notify-
btn')) this.handleWhatsAppNotify(id);
else if (targetButton.classList.contains('view-ticket-btn'))
this.runProtectedAction('orders.view_details', () => this.handleViewTicket(id));
else if (targetButton.classList.contains('edit-order-btn'))
this.runProtectedAction('orders.edit', () => this.handleEditOrder(id));
else if (targetButton.classList.contains('delete-order-btn'))
this.runProtectedAction('orders.delete', () => this.handleDeleteOrder(id));
else if (targetButton.classList.contains('edit-client-btn'))
this.runProtectedAction('clients.edit', () => this.handleEditClient(id));
else if (targetButton.classList.contains('edit-service-btn'))
this.runProtectedAction('services.edit', () => this.handleNewService(id));
else if (targetButton.classList.contains('edit-product-btn'))
this.runProtectedAction('products.edit', () => this.handleEditProduct(id));
else if (targetButton.classList.contains('edit-expense-btn'))
this.runProtectedAction('expenses.edit', () => this.handleEditExpense(id));
else if (targetButton.classList.contains('delete-expense-btn'))
this.runProtectedAction('expenses.delete', () => this.handleDeleteExpense(id));
else if (targetButton.classList.contains('manage-stock-btn'))
this.runProtectedAction('products.edit', () => this.handleManageStock(id));
return;
}

switch (targetButton.id) {
case 'new-client-btn':
this.runProtectedAction('clients.create', () => this.handleEditClient(null));
break;
case 'new-product-btn':
this.runProtectedAction('products.create', () => this.handleEditProduct(null));
break;
case 'new-service-btn':
this.runProtectedAction('services.create', () => this.handleNewService()); break;
case 'new-expense-btn':
this.runProtectedAction('expenses.create', () => this.handleEditExpense(null));
break;
case 'sales-report-btn': this.handleGenerateSalesReport();
break;
case 'cash-cut-btn':
this.runProtectedAction('reports.cut_cash', () => this.handleCashCut()); break;
case 'save-settings-btn':
this.handleSaveSettings(targetButton); break;
case 'manage-users-btn': this.handleManageUsers(); break;
case 'change-username-btn': this.handleChangeUsername(); break;
case 'change-pin-btn': this.handleChangePin(); break;
case 'upgrade-btn': this.showPremiumBenefitsModal(); break;
case 'buy-logo-feature-btn': this.handleBuyLogoFeature();
break;
}
});
},

showInitialCashModal: function(user, autoStart = false) {


if (autoStart) {
const initialCash = 0.00;
this.state.cashierData = {
initialCash: initialCash,
cashBalance: initialCash,
todaySales: { total: 0, cash: 0, card: 0, transfer: 0, vales: 0
},
todayExpenses: 0
};
this.updateUIOnLogin();
this.notify(`Turno iniciado automáticamente para ${user.username}`,
'success');
return;
}

const modalHTML = `
<div class="p-8 text-center">
<h3 class="text-xl font-bold text-white mb-2">Iniciar
Turno</h3>
<p class="text-slate-400 mb-4">Ingresa el efectivo inicial en
caja.</p>
<form id="initial-cash-form">
<input type="number" id="initial-cash-input" class="form-
input text-center text-2xl w-48 mx-auto" step="0.01" min="0" value="0.00"
autofocus>
<div class="mt-6 flex justify-center gap-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Comenzar
Turno</button>
</div>
</form>
</div>
`;
this.showModal(modalHTML);
document.getElementById('initial-cash-form').addEventListener('submit',
(e) => {
e.preventDefault();
const initialCash = parseFloat(document.getElementById('initial-
cash-input').value);
if (isNaN(initialCash) || initialCash < 0) {
this.notify('Por favor, ingresa un monto válido.', 'error');
return;
}

this.state.cashierData = {
initialCash: initialCash,
cashBalance: initialCash,
todaySales: { total: 0, cash: 0, card: 0, transfer: 0, vales: 0
},
todayExpenses: 0
};

this.hideModal();
this.updateUIOnLogin();
this.notify(`Turno iniciado para ${user.username} con $$
{initialCash.toFixed(2)}`, 'success');
});
},

startNewOrderFlow: function() {
this.showClientSelectorForNewOrder();
},

showClientSelectorForNewOrder: function() {
const clients = this.state.clients;
const modalHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Seleccionar
Cliente</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<div class="space-y-4">
<div class="input-icon-wrapper">
<i class="icon fas fa-search"></i>
<input type="text" id="client-search-input"
class="form-input form-input-icon" placeholder="Buscar cliente por nombre..."
autofocus>
</div>
<div id="client-search-results" class="max-h-60 overflow-y-
auto space-y-2 border-t border-slate-700 pt-2"></div>
</div>
<div class="flex justify-end items-center gap-4 pt-4 mt-4
border-t border-slate-700">
<button type="button" id="quick-add-client-btn" class="btn
btn-secondary"><i class="fas fa-plus"></i> Añadir Cliente</button>
</div>
</div>`;
this.showModal(modalHTML);

const searchInput = document.getElementById('client-search-input');


const resultsContainer = document.getElementById('client-search-
results');

const renderResults = (filteredClients) => {


if (filteredClients.length === 0) {
resultsContainer.innerHTML = `<p class="text-slate-500 text-
center p-4">No se encontraron clientes.</p>`;
return;
}
resultsContainer.innerHTML = filteredClients.map(c => `
<div class="p-3 bg-slate-800 rounded-lg hover:bg-sky-500/50
cursor-pointer client-select-item" data-id="${c.id}">
<p class="font-semibold text-white
capitalize">${c.name}</p>
<p class="text-xs text-slate-400">${c.phone || 'Sin
teléfono'}</p>
</div>
`).join('');
};

searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
const filtered = clients.filter(c =>
c.name.toLowerCase().includes(query));
renderResults(filtered);
});

resultsContainer.addEventListener('click', (e) => {


const item = e.target.closest('.client-select-item');
if (!item) return;
const selectedId = item.dataset.id;
this.proceedToCreateOrder(selectedId);
});

document.getElementById('quick-add-client-
btn').addEventListener('click', () => {
this.handleEditClient(null, (newClient) => {
this.proceedToCreateOrder(newClient.id);
});
});

renderResults(clients);
},

proceedToCreateOrder: function(clientId) {
const method = this.state.settings.newServiceMethod;
const client = this.state.clients.find(c => c.id == clientId);
if (!client) {
this.notify('Cliente no encontrado.', 'error');
return;
}

this.hideModal(() => {
const dataToPOS = {
client,
services: this.state.services,
settings: this.state.settings
};

try {
localStorage.setItem('posInitialData',
JSON.stringify(dataToPOS));
} catch (e) {
console.error("Error al guardar datos para el POS:", e);
this.notify("No se pudo iniciar el POS.", 'error');
return;
}

const posWindowFeatures =
'width=1200,height=800,scrollbars=yes,resizable=yes';

if (method === 'pos') {


window.open(`pos_interactivo.html`, '_blank',
posWindowFeatures);
} else if (method === 'pos_v2') {
window.open(`pos_completo.html`, '_blank', posWindowFeatures);
} else {
this.handleEditOrder(null, clientId);
}
});
},
handleOrderFromPOS: async function(orderData) {
const { client, items, total, payment, deliveryDate, observations } =
orderData;

let payments = [];


if (payment && payment.amount > 0) {
payments.push({
amount: payment.amount,
method: payment.method,
date: new Date().toISOString()
});
this.processPayment(payments[0]);
}

const newOrderData = {
clientId: client.id,
clientName: client.name,
deliveryDate,
total,
details: items,
payments,
totalPaid: payments.reduce((sum, p) => sum + p.amount, 0),
observations,
cashier: this.state.userInfo.username
};

await api.saveOrder(newOrderData);
await this.reloadData('dashboard');
this.notify('Orden guardada desde el POS.', 'success');
},

reloadData: async function(pageToNavigate) {


this.showLoading(true);
try {
const reloadedData = await api.fetchInitialData();
this.state.clients = reloadedData.clients;
this.state.orders = reloadedData.orders;
this.state.products = reloadedData.products;
this.state.expenses = reloadedData.expenses;
this.state.services = reloadedData.services;
this.state.settings = reloadedData.settings;

if (pageToNavigate) this.navigateTo(pageToNavigate);
} catch (error) {
this.notify('No se pudo recargar la información.', 'error');
} finally {
this.showLoading(false);
}
},

handleEditOrder: async function(orderId, clientId = null) {


this.showLoading(true);
let order = orderId ? await api.findOrderById(orderId) : null;

if(orderId && !order) {


this.showLoading(false);
this.notify(`Orden #${orderId} no encontrada.`, 'error');
return;
}

let client = null;


if (clientId) {
client = this.state.clients.find(c => c.id == clientId);
} else if (order) {
client = this.state.clients.find(c => c.id == order.client_id);
}

this.currentOrderItems = order ? order.details : [];


this.showLoading(false);

const defaultDeliveryDate = new Date();


defaultDeliveryDate.setDate(defaultDeliveryDate.getDate() + 3);

const deliveryDateValue = order ? new


Date(order.delivery_date).toISOString().split('T')[0] :
defaultDeliveryDate.toISOString().split('T')[0];
const observationsValue = order?.observations ?? '';
const paymentValue = order?.total_paid ?? 0;
const paymentMethodsOptions = this.state.settings.paymentMethods.map(m
=> `<option value="${m}">${m}</option>`).join('');
const allServices = this.state.services.flatMap(c => c.items);

const formHTML = `
<div class="p-6 flex flex-col h-[90vh] sm:h-auto">
<div class="flex-shrink-0">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">${orderId ?
`Editando Orden #${orderId}` : 'Nueva Orden de Servicio'}</h3>
<button class="btn btn-ghost modal-close-
btn">&times;</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label class="form-label">Cliente</label>
<input type="text" id="client-name-display"
class="form-input bg-slate-800" readonly value="${client?.name || 'N/A'}">
<input type="hidden" id="client-id" value="$
{client?.id || ''}">
</div>
<div><label class="form-label">Fecha de
Entrega</label><input type="date" id="delivery-date" class="form-input mt-1"
value="${deliveryDateValue}"></div>
</div>
</div>

<div class="flex-grow overflow-y-auto border-y border-slate-800


py-4 my-4">
<div class="grid grid-cols-1 md:grid-cols-5 gap-2 items-end
mb-4 px-1">
<div class="md:col-span-3"><label class="form-
label">Servicio</label><select id="service-select" class="form-select mt-1">$
{allServices.map(s => `<option value="${s.id}" data-price="${s.price}">${s.name} ($
${parseFloat(s.price).toFixed(2)})</option>`).join('')}</select></div>
<div><label class="form-label">Cantidad</label><input
type="number" id="service-quantity" value="1" min="0.1" step="0.1" class="form-
input mt-1"></div>
<button id="add-service-btn" class="btn btn-secondary
w-full">Añadir</button>
</div>
<table class="w-full text-left"><tbody id="order-items-
table"></tbody></table>
</div>

<div class="flex-shrink-0 space-y-4">


<div><label
class="form-label">Observaciones</label><textarea id="observations" class="form-
textarea" rows="2">${observationsValue}</textarea></div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 items-
end">
<div class="sm:col-span-1">
<label class="form-label">Abono Inicial</label>
<input type="number" id="payment-input"
class="form-input" placeholder="0.00" value="${paymentValue}" ${orderId ?
'disabled' : ''}>
</div>
<div class="sm:col-span-1">
<label class="form-label">Método</label>
<select id="payment-method" class="form-select" $
{orderId ? 'disabled' : ''}>${paymentMethodsOptions}</select>
</div>
<div class="sm:col-span-1 text-right">
<div class="text-slate-400">Total: <span id="order-
total" class="text-2xl font-bold text-white">$0.00</span></div>
<div class="text-red-400">Pendiente: <span
id="order-due" class="font-semibold">$0.00</span></div>
</div>
</div>
<div class="flex justify-end gap-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button id="save-order-btn" class="btn btn-primary">$
{orderId ? 'Guardar Cambios' : 'Crear Orden'}</button>
</div>
</div>
</div>`;
this.showModal(formHTML);

const paymentInput = document.getElementById('payment-input');

const updateTotals = () => {


const total = this.currentOrderItems.reduce((sum, item) => sum +
(parseFloat(item.subtotal) || 0), 0);
const paid = parseFloat(paymentInput.value) || 0;
const due = total - paid;
document.getElementById('order-total').textContent = `$$
{total.toFixed(2)}`;
document.getElementById('order-due').textContent = `$$
{due.toFixed(2)}`;
};

const renderOrderItems = () => {


const tableBody = document.getElementById('order-items-table');
tableBody.innerHTML = this.currentOrderItems.map((item, index) => `
<tr class="hover:bg-slate-800/50">
<td class="p-2 font-semibold
text-sky-300">${item.name}</td>
<td class="p-2 text-white">${item.quantity}</td>
<td class="p-2 text-right font-mono text-lg text-white">$$
{(parseFloat(item.subtotal) || 0).toFixed(2)}</td>
<td class="p-2 text-center"><button class="btn btn-ghost
text-red-500 remove-item-btn" data-index="${index}"><i class="fas
fa-trash"></i></button></td>
</tr>`).join('');
updateTotals();
tableBody.querySelectorAll('.remove-item-btn').forEach(btn => {
btn.addEventListener('click', (e) => {

this.currentOrderItems.splice(e.currentTarget.dataset.index, 1);
renderOrderItems();
});
});
};

paymentInput.addEventListener('input', updateTotals);

document.getElementById('add-service-btn').addEventListener('click', ()
=> {
const serviceSelect = document.getElementById('service-select');
const selectedOption =
serviceSelect.options[serviceSelect.selectedIndex];
const quantityInput = document.getElementById('service-quantity');
const quantity = parseFloat(quantityInput.value);
const price = parseFloat(selectedOption.dataset.price);

if (!selectedOption || isNaN(quantity) || quantity <= 0 ||


isNaN(price)) {
this.notify('Servicio o cantidad inválida. Por favor, verifica
los datos.', 'error');
return;
}

this.currentOrderItems.push({
id: selectedOption.value,
name: selectedOption.text.split('(')[0].trim(),
price: price,
quantity: quantity,
subtotal: quantity * price
});
renderOrderItems();
quantityInput.value = '1';
serviceSelect.focus();
});

document.getElementById('save-order-btn').addEventListener('click',
async (e) => {
const btn = e.currentTarget;
const currentClientId = document.getElementById('client-id').value;
const clientName = document.getElementById('client-name-
display').value;
const deliveryDate = document.getElementById('delivery-
date').value;

if (!currentClientId || !deliveryDate ||
this.currentOrderItems.length === 0) {
this.notify("Cliente, fecha y al menos un servicio son
requeridos.", 'error'); return;
}

const initialPaymentAmount = parseFloat(paymentInput.value) || 0;


const paymentMethod = document.getElementById('payment-
method').value;
let payments = order ? order.payments : [];

if (!orderId && initialPaymentAmount > 0) {


const newPayment = {
amount: initialPaymentAmount,
method: paymentMethod,
date: new Date().toISOString()
};
payments.push(newPayment);
this.processPayment(newPayment);
}

const totalAmount = this.currentOrderItems.reduce((sum, item) =>


sum + (parseFloat(item.subtotal) || 0), 0);

const orderData = {
id: orderId,
clientId: parseInt(currentClientId),
clientName: clientName,
deliveryDate,
total: totalAmount,
details: this.currentOrderItems,
payments: payments,
totalPaid: payments.reduce((sum, p) => sum +
(parseFloat(p.amount) || 0), 0),
observations: document.getElementById('observations').value,
status: order ? order.status : 'pending',
cashier: this.state.userInfo.username
};

if (isNaN(orderData.total)) {
this.notify('Error al calcular el total. Revisa los servicios y
precios añadidos.', 'error');
return;
}

btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i>
Guardando...`;
await api.saveOrder(orderData);
btn.disabled = false;
btn.innerHTML = orderId ? 'Guardar Cambios' : 'Crear Orden';
this.hideModal();
await this.reloadData('dashboard');
this.notify('Orden guardada con éxito', 'success');
});
renderOrderItems();
},

handleDeleteOrder: function(orderId) {
this.showConfirmationModal({
title: 'Eliminar Orden',
message: `¿Estás seguro de que quieres eliminar la orden #$
{orderId}? Esta acción no se puede deshacer.`,
icon: 'fas fa-exclamation-triangle text-red-400',
confirmText: 'Sí, Eliminar',
onConfirm: async () => {
this.showLoading(true);
await api.deleteOrder(orderId);
this.showLoading(false);
this.hideModal();
await this.reloadData('dashboard');
this.notify('Orden eliminada con éxito.', 'success');
}
});
},

handleEditClient: async function(clientId, callback) {


const client = clientId ? this.state.clients.find(c => c.id ==
clientId) : null;
const modalHTML = `
<div class="p-0">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl">
<h3 class="text-xl font-bold text-white flex items-center
gap-3"><i class="fas fa-user-edit text-sky-400"></i>${clientId ? 'Editar Cliente' :
'Nuevo Cliente'}</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<form id="client-form" class="p-6 space-y-4">
<input type="hidden" name="id" value="${client?.id || ''}">
<div class="input-icon-wrapper">
<i class="icon fas fa-user"></i>
<input type="text" name="name" class="form-input form-
input-icon" value="${client?.name || ''}" placeholder="Nombre Completo" required>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-map-marker-alt"></i>
<input type="text" name="address" class="form-input
form-input-icon" value="${client?.address || ''}" placeholder="Dirección">
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="input-icon-wrapper">
<i class="icon fas fa-phone"></i>
<input type="tel" name="phone" class="form-input
form-input-icon" value="${client?.phone || ''}" placeholder="Teléfono">
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-envelope"></i>
<input type="email" name="email" class="form-input
form-input-icon" value="${client?.email || ''}" placeholder="Email">
</div>
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar
Cliente</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);
document.getElementById('client-form').addEventListener('submit', async
(e) => {
e.preventDefault();
const formData = new FormData(e.target);
const clientData = Object.fromEntries(formData.entries());
const result = await api.saveClient(clientData);

if(callback) {
await this.reloadData();
callback(this.state.clients.find(c => c.name ===
result.client.name));
} else {
this.hideModal();
await this.reloadData('clients');
}
this.notify('Cliente guardado con éxito.', 'success');
});
},

handleNewService: async function(serviceId = null) {


const allServices = this.state.services.flatMap(c => c.items);
const service = serviceId ? allServices.find(s => s.id == serviceId) :
null;
const categoryOptions = this.state.services.map(c => `<option value="$
{c.category}"></option>`).join('');

const modalHTML = `
<div class="p-0">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl">
<h3 class="text-xl font-bold text-white flex items-center
gap-3"><i class="fas fa-plus-circle text-sky-400"></i>${service ? 'Editar Servicio'
: 'Nuevo Servicio'}</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<form id="service-form" class="p-6 space-y-4">
<input type="hidden" name="id" value="${service?.id ||
''}">
<div class="input-icon-wrapper">
<i class="icon fas fa-tags"></i>
<input list="category-list" name="category"
class="form-input form-input-icon" placeholder="Selecciona o escribe una categoría"
value="${service?.category || ''}" required>
<datalist
id="category-list">${categoryOptions}</datalist>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-tshirt"></i>
<input type="text" name="name" class="form-input form-
input-icon" placeholder="Nombre del Servicio" value="${service?.name || ''}"
required>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-ruler-combined"></i>
<input type="text" name="unit" class="form-input form-
input-icon" placeholder="Unidad (ej. Pieza, Lavado, Kg)" value="${service?.unit ||
''}" required>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-dollar-sign"></i>
<input type="number" name="price" class="form-input
form-input-icon" step="0.01" min="0" placeholder="Precio" value="${service?.price
|| ''}" required>
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar
Servicio</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);
document.getElementById('service-form').addEventListener('submit',
async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const serviceData = Object.fromEntries(formData.entries());
await api.saveService(serviceData);
this.hideModal();
await this.reloadData('services');
this.notify('Servicio guardado con éxito.', 'success');
});
},

handleEditProduct: async function(productId) {


const product = productId ? this.state.products.find(p => p.id ===
productId) : null;
const modalHTML = `
<div class="p-0">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl">
<h3 class="text-xl font-bold text-white flex items-center
gap-3"><i class="fas fa-box text-sky-400"></i>${productId ? 'Editar Suministro' :
'Nuevo Suministro'}</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<form id="product-form" class="p-6 space-y-4">
<input type="hidden" name="id" value="${product?.id ||
''}">
<div class="input-icon-wrapper">
<i class="icon fas fa-barcode"></i>
<input type="text" name="id" class="form-input form-
input-icon" value="${product?.id || ''}" placeholder="ID / SKU (Opcional)">
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-tag"></i>
<input type="text" name="name" class="form-input form-
input-icon" value="${product?.name || ''}" placeholder="Nombre del Suministro"
required>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="input-icon-wrapper">
<i class="icon fas fa-dollar-sign"></i>
<input type="number" name="cost" class="form-input
form-input-icon" value="${product?.cost || ''}" step="0.01" min="0"
placeholder="Costo" required>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-boxes"></i>
<input type="number" name="stock" class="form-input
form-input-icon" value="${product?.stock || '0'}" step="1" min="0"
placeholder="Existencia Inicial" required>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-ruler"></i>
<input type="text" name="unit" class="form-input
form-input-icon" value="${product?.unit || ''}" placeholder="Unidad (pza, kg, lt)"
required>
</div>
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar
Suministro</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);
document.getElementById('product-form').addEventListener('submit',
async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const productData = Object.fromEntries(formData.entries());
productData.cost = parseFloat(productData.cost);
productData.stock = parseInt(productData.stock);
await api.saveProduct(productData);
this.hideModal();
await this.reloadData('products');
this.notify('Suministro guardado con éxito.', 'success');
});
},

handleEditExpense: async function(expenseId) {


const expense = expenseId ? this.state.expenses.find(e => e.id ==
expenseId) : null;
const modalHTML = `
<div class="p-0">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl">
<h3 class="text-xl font-bold text-white flex items-center
gap-3"><i class="fas fa-money-bill-wave text-sky-400"></i>${expenseId ? 'Editar
Gasto' : 'Nuevo Gasto'}</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<form id="expense-form" class="p-6 space-y-4">
<input type="hidden" name="id" value="${expense?.id ||
''}">

<div class="input-icon-wrapper">
<i class="icon fas fa-align-left"></i>
<input type="text" name="concept" class="form-input
form-input-icon" value="${expense?.concept || ''}" placeholder="Concepto del gasto"
required>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="input-icon-wrapper">
<i class="icon fas fa-calendar-alt"></i>
<input type="date" name="date" class="form-input
form-input-icon" value="${expense?.date || new Date().toISOString().split('T')[0]}"
required>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-dollar-sign"></i>
<input type="number" name="total" class="form-input
form-input-icon" value="${expense?.total || ''}" step="0.01" min="0"
placeholder="Total" required>
</div>
</div>
<div class="input-icon-wrapper">
<i class="icon fas fa-file-invoice"></i>
<input type="text" name="document" class="form-input
form-input-icon" value="${expense?.document || ''}" placeholder="Documento
(Factura/Recibo)">
</div>
<div class="flex justify-end gap-4 pt-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar
Gasto</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);
document.getElementById('expense-form').addEventListener('submit',
async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const expenseData = Object.fromEntries(formData.entries());
expenseData.total = parseFloat(expenseData.total);
await api.saveExpense(expenseData);
this.hideModal();
await this.reloadData('expenses');
this.notify('Gasto guardado con éxito.', 'success');
});
},

handleDeleteExpense: function(expenseId) {
this.showConfirmationModal({
title: 'Eliminar Gasto',
message: `¿Estás seguro de que quieres eliminar este gasto?`,
icon: 'fas fa-exclamation-triangle text-red-400',
confirmText: 'Sí, Eliminar',
onConfirm: async () => {
await api.deleteExpense(expenseId);
this.hideModal();
await this.reloadData('expenses');
this.notify('Gasto eliminado.', 'success');
}
});
},
handleManageStock: function(productId) {
const product = this.state.products.find(p => p.id === productId);
if (!product) return;

const modalHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Gestionar
Suministro</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<p class="text-slate-400 mb-2">Producto: <span class="font-
semibold text-white">${product.name}</span></p>
<p class="text-slate-400 mb-4">Stock Actual: <span class="font-
bold text-2xl text-sky-400">${product.stock} ${product.unit}(s)</span></p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 items-end">
<div>
<label class="form-label">Cantidad a ajustar</label>
<input type="number" id="stock-quantity" class="form-
input" value="1" min="0">
</div>
<div class="flex gap-2">
<button id="add-stock-btn" class="btn btn-success flex-
1"><i class="fas fa-plus"></i> Añadir</button>
<button id="remove-stock-btn" class="btn btn-danger
flex-1"><i class="fas fa-minus"></i> Retirar</button>
</div>
</div>
</div>`;
this.showModal(modalHTML);

const quantityInput = document.getElementById('stock-quantity');


document.getElementById('add-stock-btn').addEventListener('click',
async () => {
const qty = parseFloat(quantityInput.value);
if (isNaN(qty) || qty <= 0) { this.notify('Cantidad inválida.',
'error'); return; }
await api.updateStock(productId, qty);
this.hideModal();
await this.reloadData('products');
this.notify('Stock añadido.', 'success');
});
document.getElementById('remove-stock-btn').addEventListener('click',
async () => {
const qty = parseFloat(quantityInput.value);
if (isNaN(qty) || qty <= 0) { this.notify('Cantidad inválida.',
'error'); return; }
await api.updateStock(productId, -qty);
this.hideModal();
await this.reloadData('products');
this.notify('Stock retirado.', 'success');
});
},

handleViewTicket: async function(orderId) {


const order = await api.findOrderById(orderId);
if (!order) { this.notify('Orden no encontrada.', 'error'); return; }
const itemsHTML = order.details.map(item => `
<tr>
<td>${item.quantity}</td>
<td class="text-left">${item.name}</td>
<td class="text-right">$${item.subtotal.toFixed(2)}</td>
</tr>
`).join('');

const ticketHTML = `
<div class="p-0 max-w-md mx-auto">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl no-print">
<h3 class="text-xl font-bold text-white">Ticket de
Orden</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<div class="print-area">
<div class="ticket-preview">
<div class="text-center">
<h2 class="font-bold">$
{this.state.settings.businessName}</h2>
<p>${this.state.settings.address}</p>
<p>Tel: ${this.state.settings.phone}</p>
</div>
<hr class="border-dashed border-black my-2">
<p>Folio: ${order.id}</p>
<p>Cliente: ${order.client_name}</p>
<p>Fecha: ${new
Date(order.reception_date).toLocaleString('es-ES')}</p>
<hr class="border-dashed border-black my-2">
<table class="w-full">
<thead><tr><th>Cant.</th><th class="text-
left">Descripción</th><th class="text-right">Importe</th></tr></thead>
<tbody>${itemsHTML}</tbody>
</table>
<hr class="border-dashed border-black my-2">
<p class="text-right font-bold">TOTAL: $$
{parseFloat(order.total).toFixed(2)}</p>
<p class="text-right">Pagado: $$
{(parseFloat(order.total_paid) || 0).toFixed(2)}</p>
<p class="text-right font-bold">Pendiente: $$
{(parseFloat(order.total) - (parseFloat(order.total_paid) || 0)).toFixed(2)}</p>
<hr class="border-dashed border-black my-2">
<p class="text-center text-xs">¡Gracias por su
preferencia!</p>
</div>
</div>
<div class="p-4 flex justify-end gap-3 bg-slate-800 border-t
border-slate-700 rounded-b-xl no-print">
<button class="btn btn-secondary
modal-close-btn">Cerrar</button>
<button id="print-ticket-btn" class="btn btn-primary"><i
class="fas fa-print"></i> Imprimir</button>
</div>
</div>
`;
this.showModal(ticketHTML);
document.getElementById('print-ticket-btn').addEventListener('click',
() => {
window.print();
this.hideModal();
});
},

handleSearch: async function() {


const modalHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Búsqueda
Rápida</h3>
<button class="btn btn-ghost modal-close-btn text-
2xl">&times;</button>
</div>
<input type="text" id="global-search-input" class="form-input"
placeholder="Buscar por Folio de Orden o Nombre de Cliente..." autofocus>
<div id="global-search-results" class="mt-4 max-h-80 overflow-
y-auto"></div>
</div>`;
this.showModal(modalHTML);

const searchInput = document.getElementById('global-search-input');


const resultsContainer = document.getElementById('global-search-
results');

searchInput.addEventListener('input', async (e) => {


const query = e.target.value.toLowerCase();
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}

const filteredOrders = this.state.orders.filter(o =>


o.id.toString().includes(query) || o.client_name.toLowerCase().includes(query));
const filteredClients = this.state.clients.filter(c =>
c.name.toLowerCase().includes(query));

let html = '';


if (filteredOrders.length > 0) {
html += `<h4 class="text-sky-400 font-semibold
mb-2">Órdenes</h4>`;
html += filteredOrders.map(o => `<div class="p-3 bg-slate-800
rounded-lg hover:bg-sky-500/50 cursor-pointer search-result-item" data-type="order"
data-id="${o.id}">#${o.id} - ${o.client_name} ($$
{parseFloat(o.total).toFixed(2)})</div>`).join('');
}
if (filteredClients.length > 0) {
html += `<h4 class="text-green-400 font-semibold mt-4 mb-
2">Clientes</h4>`;
html += filteredClients.map(c => `<div class="p-3 bg-slate-800
rounded-lg hover:bg-green-500/50 cursor-pointer search-result-item" data-
type="client" data-id="${c.id}">${c.name}</div>`).join('');
}

resultsContainer.innerHTML = html || '<p class="text-slate-500


text-center">No se encontraron resultados.</p>';
});
resultsContainer.addEventListener('click', (e) => {
const item = e.target.closest('.search-result-item');
if (!item) return;

const { type, id } = item.dataset;


this.hideModal();
if (type === 'order') {
this.handleUpdateStatus(id);
} else if (type === 'client') {
this.handleEditClient(id);
}
});
},

handleCashCut: function() {
const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Corte de
Caja</h3>
<p class="text-slate-400 mb-4">Genera un reporte del turno
actual o por un rango de fechas.</p>
<div class="flex justify-center gap-4">
<button id="cut-current-shift" class="btn btn-
secondary">Turno Actual</button>
<button id="cut-date-range" class="btn btn-primary">Por
Fechas</button>
</div>
</div>`;
this.showModal(modalHTML);

document.getElementById('cut-current-
shift').addEventListener('click', () => this.generateCashCutReport(true));
document.getElementById('cut-date-range').addEventListener('click',
() => this.showDateRangePickerForCashCut());
},

showDateRangePickerForCashCut: function() {
const formHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Corte por Rango
de Fechas</h3>
<form id="cash-cut-form" class="space-y-4">
<div><label class="form-label">Fecha de
Inicio</label><input type="date" id="start-date" class="form-input" required></div>
<div><label class="form-label">Fecha de Fin</label><input
type="date" id="end-date" class="form-input" required></div>
<div class="flex justify-end pt-4">
<button type="submit" class="btn btn-primary">Generar
Reporte</button>
</div>
</form>
</div>`;
this.showModal(formHTML);

const today = new Date().toISOString().split('T')[0];


document.getElementById('start-date').value = today;
document.getElementById('end-date').value = today;
document.getElementById('cash-cut-form').addEventListener('submit', (e)
=> {
e.preventDefault();
const startDate = new Date(document.getElementById('start-
date').value + 'T00:00:00');
const endDate = new Date(document.getElementById('end-date').value
+ 'T23:59:59');
this.generateCashCutReport(false, startDate, endDate);
});
},

generateCashCutReport: function(isCurrentShift, startDate, endDate) {


let reportData;
let reportTitle = '';

if (isCurrentShift) {
reportTitle = `TURNO ACTUAL (${this.state.userInfo.username})`;
const { initialCash, todaySales, todayExpenses, cashBalance } =
this.state.cashierData;
reportData = {
initialCash,
salesByMethod: todaySales,
totalExpenses: todayExpenses,
netBalance: todaySales.cash - todayExpenses,
finalCash: cashBalance
};
} else {
reportTitle = `REPORTE DE ${startDate.toLocaleDateString()} - $
{endDate.toLocaleDateString()}`;
const ordersInRange = this.state.orders.filter(o => {
const orderDate = new Date(o.reception_date);
return orderDate >= startDate && orderDate <= endDate;
});
const expensesInRange = this.state.expenses.filter(exp => {
const expenseDate = new Date(exp.date + 'T00:00:00');
return expenseDate >= startDate && expenseDate <= endDate;
});

const salesByMethod = { cash: 0, card: 0, transfer: 0, vales: 0,


total: 0 };
ordersInRange.forEach(order => {
(order.payments || []).forEach(p => {
const paymentDate = new Date(p.date);
if (paymentDate >= startDate && paymentDate <= endDate) {
const methodKey = p.method.toLowerCase();
if(salesByMethod[methodKey] !== undefined)
salesByMethod[methodKey] += p.amount;
salesByMethod.total += p.amount;
}
});
});

const totalExpenses = expensesInRange.reduce((sum, exp) => sum +


exp.total, 0);
reportData = {
initialCash: null,
salesByMethod,
totalExpenses,
netBalance: salesByMethod.cash - totalExpenses,
finalCash: null
};
}

const reportHTML = `
<div class="p-0 max-w-md mx-auto">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl no-print">
<h3 class="text-xl font-bold text-white">Corte de Caja</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<div class="print-area">
<div class="ticket-preview">
<div class="text-center">
<h2 class="font-bold">$
{this.state.settings.businessName}</h2>
<p>${reportTitle}</p>
</div>
<hr class="border-dashed border-black my-2">
${reportData.initialCash !== null ? `<p>Fondo Inicial:
$${reportData.initialCash.toFixed(2)}</p>` : ''}
<p class="font-bold">INGRESOS:</p>
<p>Efectivo: $$
{reportData.salesByMethod.cash.toFixed(2)}</p>
<p>Tarjeta: $$
{reportData.salesByMethod.card.toFixed(2)}</p>
<p>Transferencia: $$
{reportData.salesByMethod.transfer.toFixed(2)}</p>
<p>Vales: $$
{reportData.salesByMethod.vales.toFixed(2)}</p>
<p class="font-bold">TOTAL INGRESOS: $$
{reportData.salesByMethod.total.toFixed(2)}</p>
<hr class="border-dashed border-black my-2">
<p class="font-bold">GASTOS:</p>
<p>Total Gastos: -$$
{reportData.totalExpenses.toFixed(2)}</p>
<hr class="border-dashed border-black my-2">
<p class="font-bold">BALANCE (Efectivo - Gastos):</p>
<p class="text-lg font-bold">$$
{reportData.netBalance.toFixed(2)}</p>
${reportData.finalCash !== null ? `<p class="font-bold
mt-2">TOTAL EN CAJA ESPERADO:</p><p class="text-lg font-bold">$$
{reportData.finalCash.toFixed(2)}</p>` : ''}
</div>
</div>
<div class="p-4 flex justify-end gap-3 bg-slate-800 border-t
border-slate-700 rounded-b-xl no-print">
<button class="btn btn-secondary
modal-close-btn">Cerrar</button>
<button id="print-cash-cut-btn" class="btn btn-primary"><i
class="fas fa-print"></i> Imprimir</button>
</div>
</div>
`;
this.showModal(reportHTML);
document.getElementById('print-cash-cut-btn').addEventListener('click',
() => {
window.print();
this.hideModal();
});
},

showCreateAdminPinModal: function(onSuccess) {
const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-2">Crear NIP de
Administrador</h3>
<p class="text-slate-400 mb-4">Estás activando el inicio de
sesión. Por favor, crea un NIP de 4 dígitos para la cuenta 'admin'.</p>
<form id="create-pin-form" class="space-y-4">
<div><label class="form-label">Nuevo NIP (4
dígitos)</label><input type="password" id="new-admin-pin" class="form-input"
maxlength="4" required></div>
<div><label class="form-label">Confirmar NIP</label><input
type="password" id="confirm-admin-pin" class="form-input" maxlength="4"
required></div>
<div class="flex justify-end pt-4">
<button type="submit" class="btn btn-
primary">Establecer NIP y Guardar</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);

document.getElementById('create-pin-form').addEventListener('submit',
async (e) => {
e.preventDefault();
const newPin = document.getElementById('new-admin-pin').value;
const confirmPin = document.getElementById('confirm-admin-
pin').value;

if (newPin.length !== 4) {
this.notify('El NIP debe tener exactamente 4 dígitos.',
'error');
return;
}
if (newPin !== confirmPin) {
this.notify('Los NIPs no coinciden.', 'error');
return;
}

const adminUser = this.state.users.find(u => u.role === 'admin');


if (!adminUser) {
this.notify('Error: No se encontró al usuario administrador.',
'error');
return;
}

adminUser.password = newPin;
await api.saveUser(adminUser);
this.notify('NIP de administrador actualizado con éxito.',
'success');
onSuccess();
});
},

handleSaveSettings: async function(btn) {


btn.disabled = true;
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Guardando...`;

const newSettings = { ...this.state.settings };

const businessForm = document.getElementById('business-settings-form');


newSettings.businessName =
businessForm.querySelector('[name="businessName"]').value;
newSettings.phone = businessForm.querySelector('[name="phone"]').value;
newSettings.address =
businessForm.querySelector('[name="address"]').value;
newSettings.email = businessForm.querySelector('[name="email"]').value;
newSettings.folioCounter =
parseInt(businessForm.querySelector('[name="folioCounter"]').value);
newSettings.logoUrl = businessForm.querySelector('[name="logoUrl"]') ?
businessForm.querySelector('[name="logoUrl"]').value : this.state.settings.logoUrl;
newSettings.enableQuickFinish =
businessForm.querySelector('[name="enableQuickFinish"]').checked;
newSettings.newServiceMethod =
businessForm.querySelector('[name="newServiceMethod"]').value;

const loginWasRequired = this.state.settings.security?.loginRequired;


const loginIsNowRequired = document.getElementById('toggle-login-
required').checked;
newSettings.security = newSettings.security || {};
newSettings.security.loginRequired = loginIsNowRequired;

const menuSettingsForm = document.getElementById('menu-settings-form');


newSettings.menuVisibility = {};
menuSettingsForm.querySelectorAll('input[type="checkbox"]').forEach(el
=> {
newSettings.menuVisibility[el.dataset.key] = el.checked;
});

const pinSettingsForm = document.getElementById('pin-settings-form');


newSettings.pinProtection = {};
pinSettingsForm.querySelectorAll('input[type="checkbox"]').forEach(el
=> {
const { section, action } = el.dataset;
if (!newSettings.pinProtection[section])
newSettings.pinProtection[section] = {};
newSettings.pinProtection[section][action] = el.checked;
});

const finishSaving = async () => {


await api.saveSettings(newSettings);
this.state.settings = newSettings;
this.updateLogo();
ui.renderNavigation();
this.setupNavEventListeners();
this.notify('Ajustes guardados con éxito', 'success');
btn.disabled = false;
btn.innerHTML = `<i class="fas fa-save"></i> Guardar Cambios`;
};

if (loginIsNowRequired && !loginWasRequired) {


this.showCreateAdminPinModal(() => {
this.hideModal();
finishSaving();
});
} else {
finishSaving();
}
},

handleUpgradeToPremium: function() {
App.showPremiumBenefitsModal();
},
handleBuyLogoFeature: function() {
App.showConfirmationModal({
title: 'Comprar Función Premium',
message: '¿Deseas comprar la función para personalizar el logo de
tu negocio?',
icon: 'fas fa-image text-sky-400',
confirmText: 'Sí, Comprar',
onConfirm: async () => {
this.state.settings.premiumFeatures.changeLogo = true;
await api.saveSettings(this.state.settings);
App.hideModal();
App.navigateTo('settings');
App.notify('¡Función de Logo comprada con éxito!', 'success');
}
});
},
handleManageUsers: async function() {
const users = await api.fetchUsers();
this.state.users = users;
const usersHTML = users.map(user => `
<div class="flex items-center justify-between p-3 bg-slate-800
rounded-md">
<div>
<p class="font-semibold text-white">${user.username}</p>
<p class="text-xs text-slate-400 capitalize">$
{user.role}</p>
</div>
<div class="flex items-center gap-2">
${user.role !== 'admin' ? `<button class="btn btn-secondary
btn-sm edit-permissions-btn" data-username="${user.username}"><i class="fas fa-
shield-alt"></i> Permisos</button>` : ''}
</div>
</div>
`).join('');

const modalHTML = `
<div class="p-6 max-w-2xl w-full">
<h3 class="text-xl font-bold text-white mb-4">Gestionar
Usuarios</h3>
<div class="space-y-3 mb-6">${usersHTML}</div>
<button id="add-user-btn" class="btn btn-primary w-full"><i
class="fas fa-user-plus"></i> Añadir Cajero</button>
</div>
`;
this.showModal(modalHTML);
document.getElementById('add-user-btn').addEventListener('click', () =>
this.handleNewUser());
document.querySelectorAll('.edit-permissions-btn').forEach(btn => {
btn.addEventListener('click', (e) =>
this.handleEditPermissions(e.currentTarget.dataset.username));
});
},
handleNewUser: function() {
const cashierCount = this.state.users.filter(u => u.role ===
'cashier').length;
if (cashierCount >= this.state.settings.userSlotLimit) {
this.showConfirmationModal({
title: 'Límite de Cajeros Alcanzado',
message: `Has alcanzado tu límite de $
{this.state.settings.userSlotLimit} cajero(s). ¿Deseas comprar un espacio
adicional?`,
icon: 'fas fa-user-plus text-sky-400',
confirmText: 'Comprar Espacio',
onConfirm: async () => {
this.state.settings.userSlotLimit++;
await api.saveSettings(this.state.settings);
this.notify(`¡Espacio comprado! Ahora tu límite es de $
{this.state.settings.userSlotLimit} cajeros.`, 'success');
this.hideModal();
}
});
return;
}

const formHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Añadir Nuevo
Cajero</h3>
<form id="new-user-form" class="space-y-4">
<div><label class="form-label">Nombre de
Usuario</label><input type="text" id="new-username" class="form-input"
required></div>
<div><label class="form-label">NIP (4
dígitos)</label><input type="password" id="new-userpin" class="form-input"
maxlength="4" required></div>
<div class="flex justify-end gap-4 pt-4"><button
type="button" class="btn btn-secondary modal-close-btn">Cancelar</button><button
type="submit" class="btn btn-primary">Crear Usuario</button></div>
</form>
</div>`;
this.showModal(formHTML);

document.getElementById('new-user-form').addEventListener('submit',
async (e) => {
e.preventDefault();
const name = document.getElementById('new-username').value;
const pin = document.getElementById('new-userpin').value;
if (this.state.users.find(u => u.username === name)) {
this.notify('El nombre de usuario ya existe.', 'error');
return;
}
const newUser = { username: name, password: pin, role: 'cashier',
permissions: {} };
await api.saveUser(newUser);
this.hideModal();
this.handleManageUsers();
this.notify('Nuevo cajero creado con éxito.', 'success');
});
},
handleEditPermissions: async function(username) {
const user = this.state.users.find(u => u.username === username);
if (!user) return;

const permissionSections = [
{ id: 'dashboard', label: 'Dashboard', actions: ['create_order',
'deliver_order', 'search'] },
{ id: 'orders', label: 'Órdenes', actions: ['view_details', 'edit',
'delete', 'filter'] },
{ id: 'clients', label: 'Clientes', actions: ['create', 'edit',
'delete'] },
{ id: 'services', label: 'Servicios', actions: ['create', 'edit',
'delete'] },
{ id: 'products', label: 'Suministros', actions: ['create', 'edit',
'delete'] },
{ id: 'expenses', label: 'Gastos', actions: ['create', 'delete'] },
{ id: 'reports', label: 'Reportes', actions: ['cut_cash'] },
{ id: 'settings', label: 'Ajustes', actions: ['view'] }
];

const permissionsHTML = permissionSections.map(section => `


<div class="bg-slate-800 p-4 rounded-lg border border-slate-700">
<div class="flex justify-between items-center mb-3">
<h4 class="font-bold text-white">${section.label}</h4>
<label class="flex items-center text-xs text-sky-400
cursor-pointer">
<input type="checkbox" class="h-4 w-4 rounded border-
slate-600 bg-slate-700 text-sky-500 focus:ring-sky-600 mr-2 select-all-perms" data-
section="${section.id}">
Marcar/Desmarcar Todo
</label>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
${section.actions.map(action => {
const hasPerm = user.permissions?.[section.id]?.
[action] || false;
return `
<div class="flex items-center justify-between bg-slate-
900/50 p-2 rounded-md">
<label for="perm-${section.id}-${action}"
class="text-sm capitalize">${action.replace(/_/g, ' ')}</label>
<label class="toggle-switch">
<input type="checkbox" id="perm-${section.id}-$
{action}" data-section="${section.id}" data-action="${action}" ${hasPerm ?
'checked' : ''} class="perm-toggle">
<span class="slider"></span>
</label>
</div>`;
}).join('')}
</div>
</div>
`).join('');

const modalHTML = `
<div class="p-6 w-full max-w-4xl">
<h3 class="text-xl font-bold text-white mb-4">Permisos para
<span class="text-sky-400">${username}</span></h3>
<div id="permissions-form" class="space-y-4 max-h-[60vh]
overflow-y-auto pr-2">${permissionsHTML}</div>
<div class="flex justify-end gap-4 pt-6">
<button type="button" class="btn btn-secondary modal-close-
btn">Cancelar</button>
<button id="save-permissions-btn" class="btn btn-
primary">Guardar Permisos</button>
</div>
</div>`;
this.showModal(modalHTML);

document.querySelectorAll('.select-all-perms').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const section = e.target.dataset.section;
const isChecked = e.target.checked;
document.querySelectorAll(`#permissions-form input[data-
section="${section}"]`).forEach(el => {
if (el !== e.target) el.checked = isChecked;
});
});
});

document.getElementById('save-permissions-
btn').addEventListener('click', async () => {
const newPermissions = {};
document.querySelectorAll('#permissions-form .perm-
toggle').forEach(el => {
const { section, action } = el.dataset;
if (!newPermissions[section]) newPermissions[section] = {};
newPermissions[section][action] = el.checked;
});
user.permissions = newPermissions;
await api.saveUser(user);
this.hideModal();
this.notify(`Permisos para ${username} actualizados.`, 'success');
});
},
processPayment: function(payment) {
const { amount, method } = payment;
this.state.cashierData.todaySales.total += amount;

const methodKey = method.toLowerCase();


if (this.state.cashierData.todaySales[methodKey] !== undefined) {
this.state.cashierData.todaySales[methodKey] += amount;
}

if (method === 'Efectivo') {


this.state.cashierData.cashBalance += amount;
}
},

handleQuickCompleteOrder: async function(orderId) {


this.showLoading(true);
try {
const order = await api.findOrderById(orderId);
if (!order) {
this.notify('Orden no encontrada', 'error');
return;
}

if (order.status !== 'pending') {


this.notify('Esta acción solo se puede realizar en órdenes
pendientes.', 'info');
return;
}

const orderDataToSave = {
id: order.id,
clientId: order.client_id || 0,
clientName: order.client_name || 'Cliente no especificado',
deliveryDate: order.delivery_date || new
Date().toISOString().split('T')[0],
total: order.total || 0.00,
details: order.details || [],
payments: order.payments || [],
totalPaid: order.total_paid || 0,
observations: order.observations || '',
status: 'ready',
cashier: order.cashier || this.state.userInfo.username
};

await api.saveOrder(orderDataToSave);

await this.reloadData('dashboard');
this.notify(`Orden #${orderId} marcada como lista.`, 'success');

} catch (error) {
console.error("Error en handleQuickCompleteOrder:", error);
} finally {
this.showLoading(false);
}
},

handleWhatsAppNotify: async function(orderId) {


if (this.state.userInfo.plan !== 'Premium') {
this.showPremiumBenefitsModal();
return;
}

this.showLoading(true);
const order = await api.findOrderById(orderId);
const client = this.state.clients.find(c => c.id == order.client_id);
this.showLoading(false);

if (!client || !client.phone) {
this.notify('El cliente no tiene un número de teléfono
registrado.', 'error');
return;
}

const businessName = this.state.settings.businessName;


const message = encodeURIComponent(`Hola ${client.name}, tu pedido #$
{order.id} está listo para ser recogido en ${businessName}. ¡Te esperamos!`);
const phoneNumber = client.phone.replace(/[\s-()]/g, '');
const finalPhoneNumber = phoneNumber.length === 10 ? `521$
{phoneNumber}` : phoneNumber;
const whatsappUrl = `https://wa.me/${finalPhoneNumber}?text=$
{message}`;

window.open(whatsappUrl, '_blank');
this.notify('Abriendo WhatsApp para notificar al cliente.', 'info');
},

handleUpdateStatus: async function(orderId) {


const order = await api.findOrderById(orderId);
if (!order) { this.notify('Orden no encontrada', 'error'); return; }

if (order.status === 'delivered') {


this.notify('Esta orden ya fue entregada.', 'info');
return;
}

const dueAmount = parseFloat(order.total) -


(parseFloat(order.total_paid) || 0);
const paymentMethodsOptions = this.state.settings.paymentMethods.map(m
=> `<option value="${m}">${m}</option>`).join('');

let paymentHTML = '';


if (dueAmount > 0) {
paymentHTML = `
<div id="payment-section">
<hr class="my-4 border-slate-700">
<h4 class="text-lg font-semibold text-white mb-3">Registrar
Pago</h4>
<div id="payment-form" class="space-y-4">
<div><label class="form-label">Monto a
Pagar</label><input type="number" id="amount-to-pay" class="form-input" value="$
{dueAmount.toFixed(2)}" step="0.01" min="0"></div>
<div><label class="form-label">Método de
Pago</label><select id="payment-method" class="form-select">$
{paymentMethodsOptions}</select></div>
</div>
<div class="mt-6 flex flex-col sm:flex-row gap-2">
<button id="register-payment-btn" class="btn btn-
secondary flex-1">Registrar Abono</button>
<button id="deliver-btn" class="btn btn-primary flex-
1">Liquidar y Entregar</button>
</div>
</div>`;
} else {
paymentHTML = `
<div class="paid-note mt-4 text-center p-4 bg-green-500/10
border border-green-500/30 rounded-lg">
<i class="fas fa-check-circle text-green-400 text-3xl mb-
2"></i>
<p class="font-semibold text-white">Este servicio ya está
pagado.</p>
<button id="deliver-only-btn" class="btn btn-primary mt-
4">Marcar como Entregado</button>
</div>
`;
}

const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Liquidar /
Entregar Orden #${orderId}</h3>
<div class="mb-6 bg-slate-800 p-4 rounded-lg space-y-2">
<div class="flex justify-between"><span class="text-slate-
400">Cliente:</span> <span class="font-semibold
text-white">${order.client_name}</span></div>
<div class="flex justify-between"><span class="text-slate-
400">Total Orden:</span> <span class="font-semibold text-white">$$
{parseFloat(order.total).toFixed(2)}</span></div>
<div class="flex justify-between"><span class="text-slate-
400">Total Pagado:</span> <span class="font-semibold text-green-400">$$
{(parseFloat(order.total_paid) || 0).toFixed(2)}</span></div>
<div class="flex justify-between text-lg"><span
class="text-slate-400">Saldo Pendiente:</span> <span class="font-bold text-red-
400">$${dueAmount.toFixed(2)}</span></div>
</div>
${paymentHTML}
</div>`;

this.showModal(modalHTML);

const registerPayment = async (isFinalPayment = false) => {


const amountInput = document.getElementById('amount-to-pay');
const method = document.getElementById('payment-method').value;
const amount = parseFloat(amountInput.value);

if (isNaN(amount) || amount < 0) {


this.notify('El monto a pagar no es válido.', 'error'); return;
}

if (isFinalPayment) {
if (amount !== dueAmount) {
this.notify(`Para liquidar, el monto debe ser exactamente
el saldo pendiente de $${dueAmount.toFixed(2)}.`, 'error'); return;
}
} else {
if (amount === 0) {
this.notify('El abono debe ser mayor a cero.', 'error');
return;
}
if (amount >= dueAmount) {
this.notify('Para liquidar la orden, usa el botón "Liquidar
y Entregar".', 'error'); return;
}
}

const newPayment = { amount, method, date: new Date().toISOString()


};
if (amount > 0) {
this.processPayment(newPayment);
order.payments = order.payments || [];
order.payments.push(newPayment);
order.total_paid = order.payments.reduce((sum, p) => sum +
p.amount, 0);
}

if (isFinalPayment) {
order.status = 'delivered';
}

await api.saveOrder(order);
this.hideModal();
await this.reloadData('dashboard');
this.notify(isFinalPayment ? `Orden #${orderId} liquidada y
entregada.` : `Abono de $${amount.toFixed(2)} registrado.`, 'success');
};

const paymentBtn = document.getElementById('register-payment-btn');


if(paymentBtn) paymentBtn.addEventListener('click', () =>
registerPayment(false));

const deliverBtn = document.getElementById('deliver-btn');


if(deliverBtn) deliverBtn.addEventListener('click', () =>
registerPayment(true));

const deliverOnlyBtn = document.getElementById('deliver-only-btn');


if (deliverOnlyBtn) {
deliverOnlyBtn.addEventListener('click', async () => {
order.status = 'delivered';
await api.saveOrder(order);
this.hideModal();
await this.reloadData('dashboard');
this.notify(`Orden #${orderId} entregada.`, 'success');
});
}
},
handleConfigureDashboard: function() {
const { dashboardCards } = this.state.settings;
const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Configurar Panel
de Inicio</h3>
<div class="space-y-3">
<div class="flex items-center justify-between bg-slate-800
p-3 rounded-md">
<label for="toggle-cash-card" class="font-medium text-
white">Mostrar Tarjeta de Efectivo en Caja</label>
<label class="toggle-switch">
<input type="checkbox" id="toggle-cash-card" $
{dashboardCards.cashBalance ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
<div class="flex items-center justify-between bg-slate-800
p-3 rounded-md">
<label for="toggle-orders-card" class="font-medium
text-white">Mostrar Tarjeta de Órdenes</label>
<label class="toggle-switch">
<input type="checkbox" id="toggle-orders-card" $
{dashboardCards.ordersCount ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
<div class="flex items-center justify-between bg-slate-800
p-3 rounded-md">
<label for="toggle-sales-card" class="font-medium text-
white">Mostrar Tarjeta de Ventas</label>
<label class="toggle-switch">
<input type="checkbox" id="toggle-sales-card" $
{dashboardCards.salesTotal ? 'checked' : ''}>
<span class="slider"></span>
</label>
</div>
</div>
<div class="mt-6 flex justify-end">
<button id="save-dashboard-config" class="btn btn-
primary">Guardar</button>
</div>
</div>
`;
this.showModal(modalHTML);

document.getElementById('save-dashboard-
config').addEventListener('click', async () => {
this.state.settings.dashboardCards.cashBalance =
document.getElementById('toggle-cash-card').checked;
this.state.settings.dashboardCards.ordersCount =
document.getElementById('toggle-orders-card').checked;
this.state.settings.dashboardCards.salesTotal =
document.getElementById('toggle-sales-card').checked;
await api.saveSettings(this.state.settings);
this.hideModal();
this.navigateTo('dashboard');
this.notify('Panel de inicio actualizado.', 'success');
});
},
handleShowCashDetails: function() {
const { initialCash, todaySales, todayExpenses, cashBalance } =
this.state.cashierData;
const modalHTML = `
<div class="p-0">
<div class="flex justify-between items-center p-4 bg-slate-800
border-b border-slate-700 rounded-t-xl">
<h3 class="text-xl font-bold text-white flex items-center
gap-3"><i class="fas fa-cash-register text-sky-400"></i>Detalle de Caja del
Turno</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<div class="p-6 space-y-3">
<div class="flex justify-between items-center text-lg">
<span class="text-slate-400">Fondo Inicial:</span>
<span class="font-semibold text-white">$$
{initialCash.toFixed(2)}</span>
</div>
<hr class="border-slate-700">
<div class="flex justify-between items-center text-lg">
<span class="text-slate-400">Ventas en Efectivo:</span>
<span class="font-semibold text-green-400">+ $$
{todaySales.cash.toFixed(2)}</span>
</div>
<div class="flex justify-between items-center text-sm ml-
4">
<span class="text-slate-500">Ventas con Tarjeta:</span>
<span class="font-semibold text-slate-400">$$
{todaySales.card.toFixed(2)}</span>
</div>
<div class="flex justify-between items-center text-sm ml-
4">
<span class="text-slate-500">Transferencias:</span>
<span class="font-semibold text-slate-400">$$
{todaySales.transfer.toFixed(2)}</span>
</div>
<hr class="border-slate-700">
<div class="flex justify-between items-center text-lg">
<span class="text-slate-400">Gastos del Turno:</span>
<span class="font-semibold text-red-400">- $$
{todayExpenses.toFixed(2)}</span>
</div>
<hr class="border-slate-700">
<div class="flex justify-between items-center text-2xl">
<span class="font-bold text-white">Total en
Caja:</span>
<span class="font-bold text-sky-400">$$
{cashBalance.toFixed(2)}</span>
</div>
</div>
</div>`;
this.showModal(modalHTML);
},
handleGenerateSalesReport: function() {
const modalHTML = `
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-white">Reporte de
Ventas</h3>
<button class="btn btn-ghost
modal-close-btn">&times;</button>
</div>
<p class="text-slate-400 mb-4">Selecciona un rango de fechas
para generar el reporte.</p>
<form id="sales-report-form" class="space-y-4">
<div><label class="form-label">Fecha de
Inicio</label><input type="date" id="report-start-date" class="form-input"
required></div>
<div><label class="form-label">Fecha de Fin</label><input
type="date" id="report-end-date" class="form-input" required></div>
<div class="flex justify-end gap-2 pt-4">
<button type="button" id="export-pdf-btn" class="btn
btn-secondary"><i class="fas fa-file-pdf"></i> Exportar a PDF</button>
<button type="button" id="email-report-btn" class="btn
btn-primary"><i class="fas fa-envelope"></i> Enviar por Correo</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);

const today = new Date().toISOString().split('T')[0];


document.getElementById('report-start-date').value = today;
document.getElementById('report-end-date').value = today;

document.getElementById('export-pdf-btn').addEventListener('click', ()
=> this.notify('Función de exportar a PDF en desarrollo.', 'info'));
document.getElementById('email-report-btn').addEventListener('click',
() => this.handleEmailReport());
},

handleEmailReport: function() {
const emailProviders = ['gmail.com', 'outlook.com', 'hotmail.com',
'yahoo.com', 'icloud.com'];
const providerButtons = emailProviders.map(p => `<button type="button"
class="btn btn-sm btn-secondary email-provider-btn"
data-provider="@${p}">@${p}</button>`).join('');

const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Enviar Reporte
por Correo</h3>
<div class="input-icon-wrapper">
<i class="icon fas fa-envelope"></i>
<input type="email" id="report-email-input" class="form-
input form-input-icon" placeholder="correo@ejemplo.com">
</div>
<div class="flex flex-wrap gap-2 mt-2">
${providerButtons}
</div>
<div class="flex justify-end gap-2 pt-4">
<button type="button" class="btn btn-secondary modal-close-
btn">Cancelar</button>
<button id="send-email-confirm-btn" class="btn btn-
primary">Enviar</button>
</div>
</div>`;
this.showModal(modalHTML);

const emailInput = document.getElementById('report-email-input');


document.querySelectorAll('.email-provider-btn').forEach(btn => {
btn.addEventListener('click', () => {
const currentEmail = emailInput.value.split('@')[0];
emailInput.value = currentEmail + btn.dataset.provider;
});
});

document.getElementById('send-email-confirm-
btn').addEventListener('click', () => {
const email = emailInput.value;
if (email) {
this.notify(`Simulando envío de reporte a ${email}`,
'success');
this.hideModal();
} else {
this.notify('Por favor, ingresa un correo válido.', 'error');
}
});
},

handleChangeUsername: function() {
const currentUser = this.state.userInfo;
const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Cambiar Nombre de
Usuario</h3>
<form id="change-username-form" class="space-y-4">
<div>
<label class="form-label">Nuevo Nombre de
Usuario</label>
<input type="text" id="new-username" class="form-input"
value="${currentUser.username}" required>
</div>
<div class="flex justify-end gap-2 pt-4">
<button type="button" class="btn btn-secondary modal-
close-btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar
Cambios</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);

document.getElementById('change-username-
form').addEventListener('submit', async (e) => {
e.preventDefault();
const newUsername = document.getElementById('new-
username').value.trim();
if (!newUsername) {
this.notify('El nombre de usuario no puede estar vacío.',
'error');
return;
}
if (this.state.users.some(user => user.username === newUsername &&
user.id !== currentUser.id)) {
this.notify('Ese nombre de usuario ya está en uso.', 'error');
return;
}

const userToUpdate = { ...currentUser, username: newUsername };


await api.saveUser(userToUpdate);
this.state.userInfo.username = newUsername;
document.getElementById('current-user').textContent = newUsername;
this.hideModal();
this.notify('Nombre de usuario actualizado con éxito.', 'success');
});
},

handleChangePin: function() {
const currentUser = this.state.userInfo;
const modalHTML = `
<div class="p-6">
<h3 class="text-xl font-bold text-white mb-4">Cambiar NIP</h3>
<form id="change-pin-form" class="space-y-4">
<div>
<label class="form-label">NIP Anterior</label>
<input type="password" id="old-pin" class="form-input"
maxlength="4" required>
</div>
<div>
<label class="form-label">Nuevo NIP (4 dígitos)</label>
<input type="password" id="new-pin" class="form-input"
maxlength="4" required>
</div>
<div>
<label class="form-label">Confirmar Nuevo NIP</label>
<input type="password" id="confirm-new-pin" class="form-
input" maxlength="4" required>
</div>
<div class="flex justify-end gap-2 pt-4">
<button type="button" class="btn btn-secondary modal-close-
btn">Cancelar</button>
<button type="submit" class="btn btn-primary">Actualizar
NIP</button>
</div>
</form>
</div>`;
this.showModal(modalHTML);

document.getElementById('change-pin-form').addEventListener('submit',
async (e) => {
e.preventDefault();
const oldPin = document.getElementById('old-pin').value;
const newPin = document.getElementById('new-pin').value;
const confirmNewPin = document.getElementById('confirm-new-
pin').value;

try {
const verification = await api.verifyPin(oldPin);
if (!verification.success) {
this.notify('El NIP anterior es incorrecto.', 'error');
return;
}

if (newPin.length !== 4) {
this.notify('El nuevo NIP debe tener 4 dígitos.', 'error');
return;
}
if (newPin !== confirmNewPin) {
this.notify('Los nuevos NIPs no coinciden.', 'error');
return;
}

const userToUpdate = { ...currentUser, password: newPin };


await api.saveUser(userToUpdate);

this.state.userInfo.password = newPin;

this.hideModal();
this.notify('NIP actualizado con éxito.', 'success');

} catch(error) {
console.error("Error al cambiar el NIP", error);
}
});
},
};

// =================================================================
// LÓGICA PARA COMUNICACIÓN ENTRE VENTANAS (POS)
// =================================================================
const POS_ORDER_RESULT_KEY = 'posOrderResult';

function listenForOrderFromPOS(callback) {
window.addEventListener('storage', (event) => {
if (event.key === POS_ORDER_RESULT_KEY) {
try {
const orderData = JSON.parse(event.newValue);
if (orderData) {
callback(orderData);
localStorage.removeItem(POS_ORDER_RESULT_KEY);
}
} catch (e) {
console.error("Error al procesar la orden recibida del POS:",
e);
}
}
});
}

// Iniciar la aplicación
App.init();
});

You might also like