/* ============================================================
ADMIN CORE FUNCTIONS
============================================================ */
let clientSortState = {
field: 'name',
direction: 'asc'
};
let clientSearchQuery = '';
let clientCurrentPage = 1;
let clientsPerPage = 20;
function toggleAdminMode() {
STATE.isAdminMode = !STATE.isAdminMode;
const viewStore = document.getElementById('view-store');
const viewAdmin = document.getElementById('view-admin');
const navStore = document.getElementById('nav-store');
const navAdmin = document.getElementById('nav-admin');
const headerSearch = document.getElementById('header-search');
const cartBtn = document.getElementById('btn-cart-header');
if (STATE.isAdminMode) {
if (viewStore) viewStore.classList.add('hidden');
if (navStore) navStore.classList.add('hidden');
if (headerSearch) headerSearch.classList.add('hidden');
if (cartBtn) cartBtn.classList.add('hidden');
if (viewAdmin) viewAdmin.classList.remove('hidden');
if (navAdmin) navAdmin.classList.remove('hidden');
switchAdminTab('facturacion');
initAdminPanel();
showToast('Modo Admin activado', 'info');
} else {
if (viewAdmin) viewAdmin.classList.add('hidden');
if (navAdmin) navAdmin.classList.add('hidden');
if (viewStore) viewStore.classList.remove('hidden');
if (navStore) navStore.classList.remove('hidden');
if (headerSearch) headerSearch.classList.remove('hidden');
if (cartBtn) cartBtn.classList.remove('hidden');
renderProducts();
showToast('Modo Tienda activado', 'info');
}
}
function initAdminPanel() {
try {
// Only init POS components if they exist on the page
try {
if (document.getElementById('admin-client-input') && typeof renderPosClientCombobox === 'function') {
renderPosClientCombobox();
console.log('renderPosClientCombobox inicializado');
}
if (document.getElementById('pos-price-list') && typeof renderPosListSelector === 'function') {
renderPosListSelector();
console.log('renderPosListSelector inicializado');
}
if (document.getElementById('admin-prod-list') && typeof renderPosProductList === 'function') {
renderPosProductList();
console.log('renderPosProductList inicializado');
}
if (document.getElementById('admin-cart-items') && typeof renderPosCart === 'function') {
renderPosCart();
console.log('renderPosCart inicializado');
}
} catch (error) {
console.error('Error inicializando componentes POS en admin:', error);
}
// Init other tables if they exist
if (document.getElementById('admin-orders-tbody') && typeof renderOrdersTable === 'function') {
renderOrdersTable();
console.log('renderOrdersTable inicializado');
}
if (document.getElementById('admin-clients-tbody') && typeof renderClientsTable === 'function') {
renderClientsTable();
console.log(`renderClientsTable inicializado con ${STATE.clients.length} clientes`);
}
// Escuchar evento appDataLoaded para re-renderizar si es necesario
document.addEventListener('appDataLoaded', () => {
if (document.getElementById('admin-clients-tbody') && typeof renderClientsTable === 'function') {
renderClientsTable();
console.log(`appDataLoaded: renderClientsTable actualizado con ${STATE.clients.length} clientes`);
}
}, { once: true });
if (document.getElementById('bulk-edit-tbody') && typeof renderStockTable === 'function') {
renderStockTable();
console.log('renderStockTable inicializado');
}
console.log('Admin panel inicializado completamente');
} catch (error) {
console.error('Error inicializando panel admin:', error);
}
}
function getOrderInvoice(order) {
if (!order) return null;
if (order.invoiceId) {
const linkedInvoice = STATE.adminInvoices.find(inv => inv.id === order.invoiceId);
if (linkedInvoice) return linkedInvoice;
}
return STATE.adminInvoices.find(inv => inv.orderId === order.id) || null;
}
function buildClientFromOrder(order) {
const existingClient = STATE.clients.find(c => c.id === order.clientId)
|| STATE.clients.find(c => c.name === order.client);
return {
name: order.client || existingClient?.name || 'Cliente',
address: order.address || existingClient?.address || '—',
tax: existingClient?.tax || 'Consumidor Final',
cuit: existingClient?.cuit || ''
};
}
function createAndStoreOrderInvoice(order) {
const existingInvoice = getOrderInvoice(order);
if (existingInvoice) return existingInvoice;
const invoice = {
id: generateId('inv'),
orderId: order.id,
orderNumber: order.orderNumber || nextOrderNumber(),
date: getCurrentDateTime(),
client: order.client,
clientId: order.clientId || null,
docType: 'negro',
items: deepClone(order.items || []),
total: order.total || 0,
itemsCount: (order.items || []).reduce((sum, item) => sum + (item.qty || 0), 0)
};
STATE.adminInvoices.unshift(invoice);
order.invoiceId = invoice.id;
persistData();
return invoice;
}
function downloadOrderInvoice(orderId) {
const order = STATE.adminOrders.find(o => o.id === orderId);
if (!order) {
showToast('No se encontró el pedido seleccionado', 'error');
return;
}
let invoice = getOrderInvoice(order);
if (!invoice && order.source === 'web') {
invoice = createAndStoreOrderInvoice(order);
}
if (!invoice) {
showToast('Este pedido no tiene factura asociada', 'warning');
return;
}
const clientData = buildClientFromOrder(order);
const items = deepClone(invoice.items || order.items || []);
const total = invoice.total || order.total || 0;
const docType = invoice.docType || (order.source === 'web' ? 'negro' : 'blanco');
const orderNumber = invoice.orderNumber || order.orderNumber || null;
// Pass AFIP data if available so CAE appears in the PDF
const invoiceData = (invoice.cae) ? {
cae: invoice.cae,
cae_vencimiento: invoice.cae_vencimiento,
afip_numero: invoice.afip_numero,
afip_punto_venta: invoice.afip_punto_venta,
qr_data: invoice.qr_data,
} : null;
generateInvoicePDF(clientData, docType, items, total, invoice.id, orderNumber, invoiceData);
}
// ORDERS
function renderOrdersTable() {
const tbody = document.getElementById('admin-orders-tbody');
if (!tbody) return;
if (!STATE.adminOrders.length) {
tbody.innerHTML = '
| No hay pedidos registrados |
';
return;
}
tbody.innerHTML = STATE.adminOrders.map(o => {
const isWeb = o.source === 'web';
const statusColor = o.status === 'pending' ? 'background: #fef3c7; color: #92400e;' : 'background: #d1fae5; color: #065f46;';
const pedidoNumber = o.orderNumber ? String(o.orderNumber).padStart(6, '0') : '—';
return `
| ${o.date} |
${pedidoNumber} |
${escapeHtml(o.client)} |
${o.address} |
${fmt(o.total)} |
${isWeb ? 'Web' : 'POS'} |
${o.status === 'pending' ? 'Pendiente' : 'Completado'} |
${o.status === 'pending' ? `` : ''}
|
`;
}).join('');
}
function viewOrderDetail(orderId) {
const order = STATE.adminOrders.find(o => o.id === orderId);
if (!order) return;
const pedidoNumber = order.orderNumber ? String(order.orderNumber).padStart(6, '0') : '—';
let content = `
Número: ${pedidoNumber}
Cliente: ${escapeHtml(order.client)}
Dirección: ${order.address}
Fecha: ${order.date}
Origen: ${order.source === 'web' ? 'Web' : 'POS'}
Estado: ${order.status === 'pending' ? 'Pendiente' : 'Completado'}
Productos:
`;
order.items.forEach(item => {
content += `
-
${item.qty}x ${escapeHtml(item.name)}
${fmt(item.price * item.qty)}
`;
});
content += `
`;
content += `
`;
const modal = document.getElementById('order-detail-modal') || document.getElementById('admin-order-modal');
const modalContent = document.getElementById('order-detail-content') || document.getElementById('order-modal-content');
const modalTotal = document.getElementById('order-detail-total') || document.getElementById('order-modal-total');
if (modalContent) modalContent.innerHTML = content;
if (modalTotal) modalTotal.textContent = fmt(order.total);
if (modal) openModal(modal.id);
}
function markOrderCompleted(orderId) {
const order = STATE.adminOrders.find(o => o.id === orderId);
if (!order) return;
showConfirmDialog('¿Marcar este pedido como completado?', () => {
order.status = 'completed';
persistData();
renderOrdersTable();
closeModal('order-detail-modal');
closeModal('admin-order-modal');
showToast('Pedido marcado como completado', 'success');
});
}
// CLIENTS (básico, extendido en clients.js)
function filterAndSortClients() {
let clientsToRender = [...STATE.clients];
// Aplicar búsqueda
if (clientSearchQuery.trim()) {
const query = clientSearchQuery.toLowerCase();
clientsToRender = clientsToRender.filter(c =>
(c.name || '').toLowerCase().includes(query) ||
(c.client_code || '').toLowerCase().includes(query) ||
(c.phone || '').toLowerCase().includes(query) ||
(c.address || '').toLowerCase().includes(query)
);
}
// Aplicar ordenamiento
clientsToRender.sort((a, b) => {
let aVal = a[clientSortState.field];
let bVal = b[clientSortState.field];
if (typeof aVal === 'string') aVal = aVal.toLowerCase();
if (typeof bVal === 'string') bVal = bVal.toLowerCase();
if (!aVal) aVal = '';
if (!bVal) bVal = '';
if (aVal < bVal) return clientSortState.direction === 'asc' ? -1 : 1;
if (aVal > bVal) return clientSortState.direction === 'asc' ? 1 : -1;
return 0;
});
return clientsToRender;
}
function renderClientsTablePaginated() {
const tbody = document.getElementById('admin-clients-tbody');
if (!tbody) return;
const filteredClients = filterAndSortClients();
const totalClients = filteredClients.length;
clientsPerPage = parseInt(document.getElementById('clients-per-page')?.value || '10');
const totalPages = Math.ceil(totalClients / clientsPerPage);
// Asegurar que la página actual es válida
if (clientCurrentPage > totalPages) {
clientCurrentPage = Math.max(1, totalPages);
}
const startIdx = (clientCurrentPage - 1) * clientsPerPage;
const endIdx = startIdx + clientsPerPage;
const clientsToDisplay = filteredClients.slice(startIdx, endIdx);
tbody.innerHTML = clientsToDisplay.map(c => `
| ${escapeHtml(c.name)} |
${escapeHtml(c.client_code || '') || '—'} |
${c.address || '—'}
${c.cuit || '—'}
|
${c.tax} |
${c.id === 'c1' ? '' : ``}
|
`).join('');
// Actualizar información de paginación
document.getElementById('pagination-start').textContent = totalClients === 0 ? 0 : startIdx + 1;
document.getElementById('pagination-end').textContent = Math.min(endIdx, totalClients);
document.getElementById('pagination-total').textContent = totalClients;
document.getElementById('clients-count-display').textContent = totalClients > 0 ? `(${totalClients} total)` : '';
// Renderizar botones de paginación (responsive)
const paginationControls = document.getElementById('pagination-controls');
if (!paginationControls) return;
paginationControls.innerHTML = '';
paginationControls.style.display = 'flex';
paginationControls.style.flexWrap = 'wrap';
paginationControls.style.justifyContent = 'center';
paginationControls.style.gap = '4px';
if (totalPages > 1) {
// Botón anterior
if (clientCurrentPage > 1) {
const prevBtn = document.createElement('button');
prevBtn.textContent = '◀';
prevBtn.className = 'px-2 py-1 text-xs font-semibold bg-gray-200 hover:bg-gray-300 rounded transition-colors';
prevBtn.title = 'Anterior';
prevBtn.onclick = () => { clientCurrentPage--; renderClientsTablePaginated(); };
paginationControls.appendChild(prevBtn);
}
// Calcular rango de páginas a mostrar (máx 5 números)
let startPage = Math.max(1, clientCurrentPage - 2);
let endPage = Math.min(totalPages, startPage + 4);
if (endPage - startPage < 4) {
startPage = Math.max(1, endPage - 4);
}
// Primera página si no está en rango
if (startPage > 1) {
const firstBtn = document.createElement('button');
firstBtn.textContent = '1';
firstBtn.className = 'px-2 py-1 text-xs font-semibold bg-gray-200 hover:bg-gray-300 rounded transition-colors';
firstBtn.onclick = () => { clientCurrentPage = 1; renderClientsTablePaginated(); };
paginationControls.appendChild(firstBtn);
if (startPage > 2) {
const dots = document.createElement('span');
dots.textContent = '...';
dots.className = 'px-1 text-gray-600';
paginationControls.appendChild(dots);
}
}
// Números de página
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.textContent = i;
pageBtn.className = `px-2 py-1 text-xs font-semibold rounded transition-colors ${i === clientCurrentPage ? 'bg-indigo-600 text-white' : 'bg-gray-200 hover:bg-gray-300'}`;
pageBtn.onclick = () => { clientCurrentPage = i; renderClientsTablePaginated(); };
paginationControls.appendChild(pageBtn);
}
// Última página si no está en rango
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const dots = document.createElement('span');
dots.textContent = '...';
dots.className = 'px-1 text-gray-600';
paginationControls.appendChild(dots);
}
const lastBtn = document.createElement('button');
lastBtn.textContent = totalPages;
lastBtn.className = 'px-2 py-1 text-xs font-semibold bg-gray-200 hover:bg-gray-300 rounded transition-colors';
lastBtn.onclick = () => { clientCurrentPage = totalPages; renderClientsTablePaginated(); };
paginationControls.appendChild(lastBtn);
}
// Botón siguiente
if (clientCurrentPage < totalPages) {
const nextBtn = document.createElement('button');
nextBtn.textContent = '▶';
nextBtn.className = 'px-2 py-1 text-xs font-semibold bg-gray-200 hover:bg-gray-300 rounded transition-colors';
nextBtn.title = 'Siguiente';
nextBtn.onclick = () => { clientCurrentPage++; renderClientsTablePaginated(); };
paginationControls.appendChild(nextBtn);
}
}
updateSortArrows();
}
function renderClientsTable() {
renderClientsTablePaginated();
}
function sortClientsBy(field) {
// Si se hace clic en el mismo campo, cambiar la dirección
if (clientSortState.field === field) {
clientSortState.direction = clientSortState.direction === 'asc' ? 'desc' : 'asc';
} else {
// Si se hace clic en un campo diferente, establecer el campo y dirección ascendente
clientSortState.field = field;
clientSortState.direction = 'asc';
}
clientCurrentPage = 1;
renderClientsTable();
}
function updateSortArrows() {
const fieldToArrowId = {
'name': 'sort-name-arrow',
'client_code': 'sort-code-arrow',
'address': 'sort-address-arrow',
'tax': 'sort-tax-arrow'
};
// Limpiar todas las flechas
Object.values(fieldToArrowId).forEach(id => {
const el = document.getElementById(id);
if (el) {
el.textContent = '⇅';
el.style.color = '#9ca3af';
}
});
// Actualizar la flecha del campo actual
const arrowId = fieldToArrowId[clientSortState.field];
const arrowEl = document.getElementById(arrowId);
if (arrowEl) {
arrowEl.textContent = clientSortState.direction === 'asc' ? '↑' : '↓';
arrowEl.style.color = '#4f46e5';
}
}
function editClientForm(clientId) {
const client = STATE.clients.find(c => c.id === clientId);
if (!client) return;
const idInput = document.getElementById('c-id');
const nameInput = document.getElementById('c-name');
const codeInput = document.getElementById('c-code');
const addressInput = document.getElementById('c-address');
const phoneInput = document.getElementById('c-phone');
const emailInput = document.getElementById('c-email');
const cuitInput = document.getElementById('c-cuit');
const taxSelect = document.getElementById('c-tax');
const notesInput = document.getElementById('c-notes');
const priceListSelect = document.getElementById('c-price-list');
if (idInput) idInput.value = client.id;
if (nameInput) nameInput.value = client.name;
if (codeInput) codeInput.value = client.client_code || '';
if (addressInput) addressInput.value = client.address || '';
if (phoneInput) phoneInput.value = client.phone || '';
if (emailInput) emailInput.value = client.email || '';
if (cuitInput) cuitInput.value = client.cuit || '';
if (taxSelect) taxSelect.value = client.tax;
if (notesInput) notesInput.value = client.notes || '';
if (priceListSelect) {
populatePriceListSelector(priceListSelect);
priceListSelect.value = client.price_list_id || '';
}
const title = document.getElementById('client-form-title');
if (title) title.textContent = 'Editar Cliente';
switchAdminTab('clientes');
// Abrir el modal
const modal = document.getElementById('client-modal');
if (modal) modal.classList.remove('hidden');
}
function resetClientForm() {
const idInput = document.getElementById('c-id');
const nameInput = document.getElementById('c-name');
const codeInput = document.getElementById('c-code');
const addressInput = document.getElementById('c-address');
const phoneInput = document.getElementById('c-phone');
const emailInput = document.getElementById('c-email');
const cuitInput = document.getElementById('c-cuit');
const taxSelect = document.getElementById('c-tax');
const notesInput = document.getElementById('c-notes');
const priceListSelect = document.getElementById('c-price-list');
if (idInput) idInput.value = '';
if (nameInput) nameInput.value = '';
if (codeInput) codeInput.value = '';
if (addressInput) addressInput.value = '';
if (phoneInput) phoneInput.value = '';
if (emailInput) emailInput.value = '';
if (cuitInput) cuitInput.value = '';
if (taxSelect) taxSelect.value = 'Consumidor Final';
if (notesInput) notesInput.value = '';
if (priceListSelect) {
populatePriceListSelector(priceListSelect);
priceListSelect.value = '';
}
const title = document.getElementById('client-form-title');
if (title) title.textContent = 'Nuevo Cliente';
}
function saveClient() {
const idInput = document.getElementById('c-id');
const nameInput = document.getElementById('c-name');
const codeInput = document.getElementById('c-code');
const addressInput = document.getElementById('c-address');
const phoneInput = document.getElementById('c-phone');
const emailInput = document.getElementById('c-email');
const cuitInput = document.getElementById('c-cuit');
const taxSelect = document.getElementById('c-tax');
const notesInput = document.getElementById('c-notes');
const priceListSelect = document.getElementById('c-price-list');
const name = nameInput?.value.trim();
if (!name) {
showToast('Por favor ingresa el nombre del cliente', 'warning');
return;
}
const priceListId = priceListSelect?.value ? parseInt(priceListSelect.value) : null;
const isNewClient = !idInput?.value;
let clientCode = codeInput?.value.trim() || '';
// Auto-generar código si es nuevo cliente y no tiene código
if (isNewClient && !clientCode) {
clientCode = getNextClientCode();
}
const clientData = {
id: idInput?.value || generateId('c'),
name,
client_code: clientCode,
address: addressInput?.value.trim() || '',
phone: phoneInput?.value.trim() || '',
email: emailInput?.value.trim() || '',
cuit: cuitInput?.value.trim() || '',
tax: taxSelect?.value || 'Consumidor Final',
notes: notesInput?.value.trim() || '',
price_list_id: priceListId
};
const existingIdx = STATE.clients.findIndex(c => c.id === clientData.id);
if (existingIdx > -1) {
STATE.clients[existingIdx] = clientData;
showToast('Cliente actualizado', 'success');
} else {
STATE.clients.push(clientData);
showToast('Cliente agregado', 'success');
}
persistData();
renderClientsTable();
if (typeof renderPosClientCombobox === 'function') renderPosClientCombobox();
// Si estamos en Facturación, auto-seleccionar el cliente recién creado en la caja.
const posClientInput = document.getElementById('admin-client-input');
if (posClientInput) {
posClientInput.value = clientData.name;
STATE.adminSelectedClient = clientData.id;
}
resetClientForm();
closeClientModal();
if (typeof loadClientMarkers === 'function') loadClientMarkers();
}
function deleteClient(clientId) {
if (clientId === 'c1') {
showToast('No se puede eliminar el cliente por defecto', 'error');
return;
}
showConfirmDialog('¿Eliminar este cliente?', async () => {
// Borrado EXPLÍCITO vía DELETE /api/clients/{id}. No usamos persistData() porque el
// sync masivo ahora es aditivo (nunca borra), así que un replace total no eliminaría nada.
try {
await deleteClientApi(clientId);
} catch (err) {
console.warn('Error al eliminar cliente:', err);
showToast('No se pudo eliminar el cliente', 'error');
return;
}
STATE.clients = STATE.clients.filter(c => c.id !== clientId);
renderClientsTable();
if (typeof renderPosClientCombobox === 'function') renderPosClientCombobox();
resetClientForm();
if (typeof loadClientMarkers === 'function') loadClientMarkers();
showToast('Cliente eliminado', 'success');
});
}
// STOCK
function renderStockTable() {
const tbody = document.getElementById('admin-stock-tbody');
if (!tbody) return;
tbody.innerHTML = STATE.products.map(p => `
${escapeHtml(p.name)}
SKU: ${escapeHtml(p.sku || '') || '—'}
|
${p.stock} u. |
${fmt(p.cost)} |
${p.margin.toFixed(1)}% |
${fmt(p.sale)} |
|
`).join('');
}
function openEditProductModal(productId) {
const product = STATE.products.find(p => p.id === productId);
if (!product) return;
const idInput = document.getElementById('edit-p-id');
const nameEl = document.getElementById('edit-p-name');
const productNameInput = document.getElementById('edit-p-product-name');
const skuInput = document.getElementById('edit-p-sku');
const categoryInput = document.getElementById('edit-p-cat');
const imageInput = document.getElementById('edit-p-img');
const stockInput = document.getElementById('edit-p-stock');
const costInput = document.getElementById('edit-p-cost');
const marginInput = document.getElementById('edit-p-margin');
const saleInput = document.getElementById('edit-p-sale');
const deleteBtn = document.getElementById('btn-delete-product');
if (idInput) idInput.value = product.id;
if (nameEl) nameEl.textContent = 'Editar Producto';
if (productNameInput) productNameInput.value = product.name || '';
if (skuInput) skuInput.value = product.sku || '';
if (categoryInput) categoryInput.value = product.cat || 'congelados';
if (imageInput) imageInput.value = product.img || '';
if (stockInput) stockInput.value = product.stock;
if (costInput) costInput.value = product.cost;
if (marginInput) marginInput.value = product.margin;
if (saleInput) saleInput.value = product.sale;
if (deleteBtn) deleteBtn.classList.remove('hidden');
openModal('edit-product-modal');
openModal('admin-modal-overlay');
}
function openCreateProductModal() {
const idInput = document.getElementById('edit-p-id');
const nameEl = document.getElementById('edit-p-name');
const productNameInput = document.getElementById('edit-p-product-name');
const skuInput = document.getElementById('edit-p-sku');
const categoryInput = document.getElementById('edit-p-cat');
const imageInput = document.getElementById('edit-p-img');
const stockInput = document.getElementById('edit-p-stock');
const costInput = document.getElementById('edit-p-cost');
const marginInput = document.getElementById('edit-p-margin');
const saleInput = document.getElementById('edit-p-sale');
const deleteBtn = document.getElementById('btn-delete-product');
if (idInput) idInput.value = '';
if (nameEl) nameEl.textContent = 'Nuevo Producto';
if (productNameInput) productNameInput.value = '';
if (skuInput) skuInput.value = '';
if (categoryInput) categoryInput.value = 'congelados';
if (imageInput) imageInput.value = '';
if (stockInput) stockInput.value = 0;
if (costInput) costInput.value = 0;
if (marginInput) marginInput.value = 35;
if (saleInput) saleInput.value = 0;
if (deleteBtn) deleteBtn.classList.add('hidden');
recalculatePrice();
openModal('edit-product-modal');
openModal('admin-modal-overlay');
}
function recalculatePrice(source = 'margin') {
const costInput = document.getElementById('edit-p-cost');
const marginInput = document.getElementById('edit-p-margin');
const saleInput = document.getElementById('edit-p-sale');
if (!costInput || !marginInput || !saleInput) return;
const cost = Number.parseFloat(costInput.value) || 0;
let margin = Number.parseFloat(marginInput.value) || 0;
let sale = Number.parseFloat(saleInput.value) || 0;
if (source === 'sale') {
margin = cost > 0 ? ((sale - cost) / cost) * 100 : 0;
margin = Number(margin.toFixed(2));
marginInput.value = margin;
saleInput.value = roundPrice(sale);
return;
}
if (source === 'cost') {
const currentlyEditingSale = document.activeElement === saleInput;
if (currentlyEditingSale) {
margin = cost > 0 ? ((sale - cost) / cost) * 100 : 0;
marginInput.value = Number(margin.toFixed(2));
return;
}
}
sale = calculateSalePrice(cost, margin);
saleInput.value = sale;
marginInput.value = Number(margin.toFixed(2));
}
function saveEditedProduct() {
const idInput = document.getElementById('edit-p-id');
const productId = idInput?.value;
const productNameInput = document.getElementById('edit-p-product-name');
const skuInput = document.getElementById('edit-p-sku');
const categoryInput = document.getElementById('edit-p-cat');
const imageInput = document.getElementById('edit-p-img');
const stockInput = document.getElementById('edit-p-stock');
const costInput = document.getElementById('edit-p-cost');
const marginInput = document.getElementById('edit-p-margin');
const saleInput = document.getElementById('edit-p-sale');
const productName = productNameInput?.value.trim();
if (!productName) {
showToast('Ingresa el nombre del producto', 'warning');
return;
}
const normalizedImage = imageInput?.value.trim() || 'https://images.unsplash.com/photo-1488477181946-6428a0291777?w=200&q=80';
const productData = {
id: productId || generateId('p'),
sku: skuInput?.value.trim() || '',
name: productName,
short: productName,
desc: productName,
price: 0,
sale: 0,
cat: categoryInput?.value || 'congelados',
badge: '',
img: normalizedImage,
stock: parseInt(stockInput?.value) || 0,
cost: parseFloat(costInput?.value) || 0,
margin: Number((parseFloat(marginInput?.value) || 0).toFixed(2))
};
productData.sale = parseFloat(saleInput?.value);
if (!Number.isFinite(productData.sale)) {
productData.sale = calculateSalePrice(productData.cost, productData.margin);
} else {
productData.sale = roundPrice(productData.sale);
}
productData.price = productData.sale;
const existingIdx = STATE.products.findIndex(p => p.id === productData.id);
if (existingIdx > -1) {
const prev = STATE.products[existingIdx];
if (typeof logChange === 'function') {
if (prev.sale !== productData.sale) {
logChange({ type: 'precio', productId: productData.id, productName: productData.name, field: 'precio_venta', oldValue: prev.sale, newValue: productData.sale });
}
if (prev.stock !== productData.stock) {
logChange({ type: 'stock', productId: productData.id, productName: productData.name, field: 'stock', oldValue: prev.stock, newValue: productData.stock });
}
if (prev.name !== productData.name || prev.cat !== productData.cat) {
logChange({ type: 'producto', productId: productData.id, productName: productData.name, field: 'datos', oldValue: prev.name, newValue: productData.name, note: `cat: ${productData.cat}` });
}
}
STATE.products[existingIdx] = {
...prev,
...productData
};
} else {
if (typeof logChange === 'function') {
logChange({ type: 'producto', productId: productData.id, productName: productData.name, field: 'nuevo', oldValue: null, newValue: productData.name });
}
STATE.products.unshift(productData);
}
persistData();
renderStockTable();
renderPosProductList();
renderProducts();
closeModal('edit-product-modal');
closeModal('admin-modal-overlay');
showToast(existingIdx > -1 ? 'Producto actualizado' : 'Producto creado', 'success');
}
function deleteProductWithConfirm(productId) {
const product = STATE.products.find(p => p.id === productId);
if (!product) return;
showConfirmDialog(`¿Eliminar el producto "${product.name}"?`, async () => {
// Borrado EXPLÍCITO vía DELETE /api/products/{id}. No usamos persistData() porque el
// sync masivo ahora es aditivo (nunca borra), así que un replace total no eliminaría nada.
try {
await deleteProductApi(productId);
} catch (err) {
console.warn('Error al eliminar producto:', err);
showToast('No se pudo eliminar el producto', 'error');
return;
}
STATE.products = STATE.products.filter(p => p.id !== productId);
renderStockTable();
renderPosProductList();
renderProducts();
showToast('Producto eliminado', 'success');
});
}
function deleteProductFromModal() {
const idInput = document.getElementById('edit-p-id');
const productId = idInput?.value;
if (!productId) return;
closeModal('edit-product-modal');
closeModal('admin-modal-overlay');
deleteProductWithConfirm(productId);
}
// Legacy compatibility for inline handlers in index.html
function renderAdminProdList() {
renderPosProductList();
}
function procesarFactura(printPDF = false) {
procesarVenta(printPDF);
}
function recalcPrice() {
recalculatePrice();
}
function saveAdminProduct() {
saveEditedProduct();
}
function closeAdminModal() {
closeModal('edit-product-modal');
closeModal('admin-modal-overlay');
}
function closeOrderModal() {
closeModal('order-detail-modal');
closeModal('admin-order-modal');
}
function renderSlidesList() {
const list = document.getElementById('slides-list');
const empty = document.getElementById('slides-empty');
if (!list || !empty) return;
const slides = [...(STATE.slides || [])].sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0));
if (!slides.length) {
list.innerHTML = '';
empty.classList.remove('hidden');
return;
}
empty.classList.add('hidden');
list.innerHTML = slides.map((slide, index) => `
${escapeHtml(slide.title || 'Sin texto')}
${escapeHtml(slide.image_url)}
Orden: ${Number(slide.sort_order || 0)} · ${Number(slide.is_active || 0) ? 'Activo' : 'Inactivo'}
`).join('');
}
async function createSlide(event) {
event.preventDefault();
const imageUrlInput = document.getElementById('slide-image-url');
const titleInput = document.getElementById('slide-title');
const sortOrderInput = document.getElementById('slide-sort-order');
const isActiveInput = document.getElementById('slide-is-active');
const payload = {
image_url: normalizeSlideImageUrl(imageUrlInput?.value || ''),
title: titleInput?.value.trim() || '',
sort_order: parseInt(sortOrderInput?.value || '0', 10) || 0,
is_active: isActiveInput?.checked ? 1 : 0
};
if (!payload.image_url) {
showToast('Ingresá una URL de imagen válida', 'warning');
return;
}
try {
const slide = await apiFetch('/api/slides', { method: 'POST', body: JSON.stringify(payload) });
STATE.slides = [...(STATE.slides || []), slide];
renderSlidesList();
renderSlider();
if (imageUrlInput) imageUrlInput.value = '';
if (titleInput) titleInput.value = '';
if (sortOrderInput) sortOrderInput.value = '0';
if (isActiveInput) isActiveInput.checked = true;
showToast('Slide agregado', 'success');
} catch (error) {
console.error(error);
showToast('No se pudo guardar el slide', 'error');
}
}
async function deleteSlide(id) {
if (!id) return;
showConfirmDialog('¿Eliminar este slide?', async () => {
try {
await apiFetch(`/api/slides/${id}`, { method: 'DELETE' });
STATE.slides = (STATE.slides || []).filter(slide => Number(slide.id) !== Number(id));
renderSlidesList();
renderSlider();
showToast('Slide eliminado', 'success');
} catch (error) {
console.error(error);
showToast('No se pudo eliminar el slide', 'error');
}
});
}
// DRAG & DROP REORDERING
let _draggedSlideElement = null;
function handleSlideDragStart(event) {
_draggedSlideElement = event.currentTarget;
event.currentTarget.style.opacity = '0.5';
}
function handleSlideDrop(event) {
event.preventDefault();
if (!_draggedSlideElement || _draggedSlideElement === event.currentTarget) {
return;
}
const draggedId = Number(_draggedSlideElement.dataset.slideId);
const targetId = Number(event.currentTarget.dataset.slideId);
const draggedSlide = STATE.slides.find(s => s.id === draggedId);
const targetSlide = STATE.slides.find(s => s.id === targetId);
if (!draggedSlide || !targetSlide) return;
const dragged_idx = STATE.slides.indexOf(draggedSlide);
const target_idx = STATE.slides.indexOf(targetSlide);
if (dragged_idx < target_idx) {
draggedSlide.sort_order = targetSlide.sort_order + 1;
} else {
draggedSlide.sort_order = targetSlide.sort_order - 1;
}
persistData();
renderSlidesList();
renderSlider();
}
function handleSlideDragEnd() {
if (_draggedSlideElement) {
_draggedSlideElement.style.opacity = '1';
_draggedSlideElement = null;
}
}
// EDIT SLIDE MODAL
function openEditSlideModal(slideId) {
const slide = STATE.slides.find(s => Number(s.id) === Number(slideId));
if (!slide) return;
const idInput = document.getElementById('edit-slide-id');
const imageUrlInput = document.getElementById('edit-slide-image-url');
const titleInput = document.getElementById('edit-slide-title');
const sortOrderInput = document.getElementById('edit-slide-sort-order');
const isActiveInput = document.getElementById('edit-slide-is-active');
const imagePreview = document.getElementById('edit-slide-image-preview');
if (idInput) idInput.value = slide.id;
if (imageUrlInput) imageUrlInput.value = slide.image_url || '';
if (titleInput) titleInput.value = slide.title || '';
if (sortOrderInput) sortOrderInput.value = slide.sort_order || 0;
if (isActiveInput) isActiveInput.checked = slide.is_active ? true : false;
if (imagePreview) {
imagePreview.src = normalizeSlideImageUrl(slide.image_url);
imagePreview.classList.remove('hidden');
}
openModal('edit-slide-modal');
openModal('admin-modal-overlay');
}
async function saveEditedSlide() {
const idInput = document.getElementById('edit-slide-id');
const imageUrlInput = document.getElementById('edit-slide-image-url');
const titleInput = document.getElementById('edit-slide-title');
const sortOrderInput = document.getElementById('edit-slide-sort-order');
const isActiveInput = document.getElementById('edit-slide-is-active');
const slideId = idInput?.value;
if (!slideId) return;
const imageUrl = imageUrlInput?.value.trim();
if (!imageUrl) {
showToast('Ingresá una URL de imagen válida', 'warning');
return;
}
const payload = {
id: slideId,
image_url: normalizeSlideImageUrl(imageUrl),
title: titleInput?.value.trim() || '',
sort_order: parseInt(sortOrderInput?.value || '0', 10) || 0,
is_active: isActiveInput?.checked ? 1 : 0
};
try {
await apiFetch(`/api/slides/${slideId}`, { method: 'PUT', body: JSON.stringify(payload) });
const slideIdx = STATE.slides.findIndex(s => Number(s.id) === Number(slideId));
if (slideIdx > -1) {
STATE.slides[slideIdx] = payload;
}
persistData();
renderSlidesList();
renderSlider();
closeModal('edit-slide-modal');
closeModal('admin-modal-overlay');
showToast('Slide actualizado', 'success');
} catch (error) {
console.error(error);
showToast('No se pudo guardar el slide', 'error');
}
}
function initSlidesSettings() {
const form = document.getElementById('slides-form');
if (!form || form.dataset.bound === '1') {
renderSlidesList();
return;
}
form.addEventListener('submit', createSlide);
form.dataset.bound = '1';
renderSlidesList();
}
/* ============================================================
CLIENTE AUTO-CODE Y EDITOR MASIVO
============================================================ */
function getNextClientCode() {
if (!STATE.clients.length) return '001';
const codes = STATE.clients
.map(c => parseInt(c.client_code || '0', 10))
.filter(code => !isNaN(code) && code > 0)
.sort((a, b) => b - a);
const nextCode = (codes[0] || 0) + 1;
return String(nextCode).padStart(3, '0');
}
function autoFillClientCode() {
const codeInput = document.getElementById('c-code');
if (codeInput && !codeInput.value) {
codeInput.value = getNextClientCode();
}
}
function openBulkClientEditor() {
// Navegar a la página del editor masivo
window.location.href = '/admin/clientes/editor-masivo';
}
function renderBulkClientTable() {
const tbody = document.getElementById('bulk-clients-tbody');
if (!tbody) return;
tbody.innerHTML = STATE.clients.map((c, idx) => `
|
|
|
|
|
|
|
|
|
`).join('');
// Agregar fila vacía al final con código auto-incrementado
const nextCode = getNextClientCode();
const newRow = document.createElement('tr');
newRow.innerHTML = `
|
|
|
|
|
|
|
|
|
`;
tbody.appendChild(newRow);
}
function addBulkClientRow() {
const tbody = document.getElementById('bulk-clients-tbody');
// Obtener el código de la última fila (la fila vacía actual)
const lastCodeInput = tbody.querySelector('tr:last-child [data-field="client_code"]');
let nextCode = '001';
if (lastCodeInput) {
const lastCode = parseInt(lastCodeInput.value || '0', 10);
if (!isNaN(lastCode) && lastCode > 0) {
nextCode = String(lastCode + 1).padStart(3, '0');
}
}
const newRow = document.createElement('tr');
newRow.innerHTML = `
|
|
|
|
|
|
|
|
|
`;
tbody.appendChild(newRow);
// Auto-focus en el campo de nombre
const nameInput = newRow.querySelector('[data-field="name"]');
if (nameInput) nameInput.focus();
}
function deleteBulkClientRow(clientId) {
if (clientId === 'c1') {
showToast('No se puede eliminar el cliente por defecto', 'error');
return;
}
showConfirmDialog('¿Eliminar este cliente?', async () => {
// Borrado EXPLÍCITO (sync masivo aditivo no borra). Ver deleteClient().
try {
await deleteClientApi(clientId);
} catch (err) {
console.warn('Error al eliminar cliente:', err);
showToast('No se pudo eliminar el cliente', 'error');
return;
}
STATE.clients = STATE.clients.filter(c => c.id !== clientId);
renderBulkClientTable();
showToast('Cliente eliminado', 'success');
});
}
function saveBulkClients() {
const rows = document.querySelectorAll('#bulk-clients-tbody tr');
const existingIds = new Set(STATE.clients.map(c => c.id));
rows.forEach((row, idx) => {
const inputs = row.querySelectorAll('input, select');
if (inputs.length < 2) return;
const clientIdInput = row.querySelector('[data-client-id]');
const clientId = clientIdInput?.getAttribute('data-client-id');
const name = row.querySelector('[data-field="name"]')?.value.trim();
const client_code = row.querySelector('[data-field="client_code"]')?.value.trim();
const address = row.querySelector('[data-field="address"]')?.value.trim();
const phone = row.querySelector('[data-field="phone"]')?.value.trim();
const email = row.querySelector('[data-field="email"]')?.value.trim();
const cuit = row.querySelector('[data-field="cuit"]')?.value.trim();
const tax = row.querySelector('[data-field="tax"]')?.value || 'Consumidor Final';
// Nueva fila (sin cliente ID)
if (!clientId && name) {
const newClientId = generateId('c');
const finalCode = client_code || getNextClientCode();
STATE.clients.push({
id: newClientId,
name,
client_code: finalCode,
address: address || '',
phone: phone || '',
email: email || '',
cuit: cuit || '',
tax: tax,
notes: ''
});
} else if (clientId && existingIds.has(clientId)) {
// Actualizar cliente existente. Solo pisar un campo si el valor nuevo es no-vacío
// (mismo criterio que el backend: proteger datos existentes contra limpiezas accidentales).
const client = STATE.clients.find(c => c.id === clientId);
if (client) {
if (name) client.name = name;
if (client_code) client.client_code = client_code;
if (address !== undefined) client.address = address || client.address;
if (phone !== undefined) client.phone = phone || client.phone;
if (email !== undefined) client.email = email || client.email;
if (cuit !== undefined) client.cuit = cuit || client.cuit;
if (tax) client.tax = tax;
}
}
});
persistData();
renderClientsTable();
showToast('Clientes guardados correctamente', 'success');
}
function exportClientsCSV() {
const headers = ['Código', 'Nombre', 'Dirección', 'Teléfono', 'Email', 'CUIT', 'Condición'];
const rows = STATE.clients.map(c => [
c.client_code || '',
c.name,
c.address || '',
c.phone || '',
c.email || '',
c.cuit || '',
c.tax || 'Consumidor Final'
]);
let csv = headers.join(',') + '\n';
rows.forEach(row => {
csv += row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',') + '\n';
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `clientes_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
showToast('Clientes exportados', 'success');
}
function importClientsCSV(event) {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const csv = e.target?.result;
const lines = csv.split('\n').filter(line => line.trim());
if (lines.length < 2) {
showToast('Archivo vacío', 'warning');
return;
}
const headers = lines[0].split(',').map(h => h.trim().toLowerCase().replace(/"/g, ''));
const importedClients = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
const clientObj = {};
headers.forEach((header, idx) => {
if (header === 'código') clientObj.client_code = values[idx] || '';
else if (header === 'nombre') clientObj.name = values[idx] || '';
else if (header === 'dirección') clientObj.address = values[idx] || '';
else if (header === 'teléfono') clientObj.phone = values[idx] || '';
else if (header === 'email') clientObj.email = values[idx] || '';
else if (header === 'cuit') clientObj.cuit = values[idx] || '';
else if (header === 'condición') clientObj.tax = values[idx] || 'Consumidor Final';
});
if (clientObj.name) {
clientObj.id = generateId('c');
clientObj.notes = '';
importedClients.push(clientObj);
}
}
if (importedClients.length === 0) {
showToast('No se encontraron clientes válidos', 'warning');
return;
}
STATE.clients.push(...importedClients);
persistData();
renderClientsTable();
document.getElementById('import-clients-input').value = '';
showToast(`${importedClients.length} clientes importados correctamente`, 'success');
} catch (error) {
console.error(error);
showToast('Error al importar clientes', 'error');
}
};
reader.readAsText(file);
}
function downloadClientTemplate() {
const template = 'Código,Nombre,Dirección,Teléfono,Email,CUIT,Condición\n"001","Cliente Ejemplo","Calle 1 123","2262345678","cliente@email.com","20123456789","Responsable Inscripto"';
const blob = new Blob([template], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'plantilla_clientes.csv';
a.click();
URL.revokeObjectURL(url);
}
// Modal de cliente (global, compartido entre Clientes y Facturación).
// El HTML #client-modal vive en layout.php. Para nuevo cliente: openClientModal().
// Para editar: openClientModal(id) -> delega en editClientForm().
function openClientModal(clientId = null) {
const modal = document.getElementById('client-modal');
if (!modal) {
showToast('Modal no encontrado', 'error');
return;
}
if (clientId) {
editClientForm(clientId);
return;
}
resetClientForm();
const title = document.getElementById('client-form-title');
if (title) title.innerText = 'Nuevo Cliente';
autoFillClientCode();
modal.classList.remove('hidden');
document.getElementById('c-name')?.focus();
}
function closeClientModal() {
const modal = document.getElementById('client-modal');
if (modal) modal.classList.add('hidden');
}
// Alias por compatibilidad: el botón "+" de Facturación abre el modal completo.
function openQuickClientModal() {
openClientModal();
}