modified: routes_admin.py modified: routes_worker.py modified: templates/admin_productos.html modified: templates/admin_workers.html modified: templates/macros/modals.html modified: templates/worker_dashboard.html
327 lines
14 KiB
HTML
327 lines
14 KiB
HTML
{% extends "macros/base.html" %}
|
|
{% from 'macros/modals.html' import confirm_modal, edit_product_modal %}
|
|
|
|
{% block title %}Catálogo de Productos{% endblock %}
|
|
|
|
{% block head %}
|
|
<!-- HEAD -->
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<h2 class="mb-4">Catálogo de Productos por Zona</h2>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
{% for category, message in messages %}
|
|
<div class="alert alert-{{ category }}">{{ message|safe }}</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
{{ edit_product_modal(zonas) }}
|
|
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-header bg-primary text-white">Agregar Producto Maestro</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{{ url_for('manage_products') }}">
|
|
<div class="row g-3">
|
|
<div class="col-md-10">
|
|
<input type="text" class="form-control" name="name" placeholder="Nombre del Producto" required>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary w-100">Crear Producto</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-group mb-3 shadow-sm">
|
|
<span class="input-group-text bg-body-tertiary"><i class="bi bi-search"></i></span>
|
|
<input type="text" class="form-control" id="searchProduct" placeholder="Buscar producto maestro...">
|
|
</div>
|
|
|
|
<div class="card shadow-sm">
|
|
<div class="card-body p-0">
|
|
<table class="table table-striped table-hover mb-0">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>Producto Maestro</th>
|
|
<th class="text-end">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for prod in productos %}
|
|
<tr class="product-row">
|
|
<td class="align-middle fw-bold">{{ prod.name }}</td>
|
|
<td class="text-end">
|
|
<button type="button" class="btn btn-info btn-sm text-white" onclick="showHistory({{ prod.id }}, '{{ prod.name }}')">
|
|
<i class="bi bi-graph-up"></i> Historial
|
|
</button>
|
|
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#pricesModal{{ prod.id }}">
|
|
<i class="bi bi-currency-dollar"></i> Precios
|
|
</button>
|
|
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteProd{{ prod.id }}">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
|
|
{{ confirm_modal(
|
|
id='deleteProd' ~ prod.id,
|
|
title='Eliminar Producto',
|
|
message='¿Eliminar "' ~ prod.name ~ '"? Esto fallará si el producto ya tiene ventas registradas.',
|
|
action_url=url_for('delete_product', id=prod.id),
|
|
btn_class='btn-danger',
|
|
btn_text='Eliminar'
|
|
) }}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{% for prod in productos %}
|
|
<div class="modal fade" id="pricesModal{{ prod.id }}" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<form method="POST" action="{{ url_for('update_product_prices', id=prod.id) }}">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Precios: {{ prod.name }}</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<div class="row fw-bold mb-2">
|
|
<div class="col-4">Zona</div>
|
|
<div class="col-4">Precio</div>
|
|
<div class="col-4">Comisión</div>
|
|
</div>
|
|
{% for z_id, datos in prod.precios.items() %}
|
|
<div class="row mb-2">
|
|
<div class="col-4 d-flex align-items-center">
|
|
<span class="badge bg-info text-dark w-100">{{ datos.zona_name }}</span>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text">$</span>
|
|
<input type="text" class="form-control money-input" name="price_{{ z_id }}" value="{{ datos.price }}">
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text">$</span>
|
|
<input type="text" class="form-control money-input" name="comm_{{ z_id }}" value="{{ datos.commission }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="modal-body bg-body-tertiary border-top pb-3">
|
|
<label class="form-label text-warning fw-bold mb-1">
|
|
<i class="bi bi-clock-history"></i> Programar Cambio de Precio (Opcional)
|
|
</label>
|
|
<input type="datetime-local" class="form-control form-control-sm mb-1" name="fecha_activacion">
|
|
<small class="text-muted" style="font-size: 0.85em;">Si lo dejas en blanco, el cambio se aplicará inmediatamente.</small>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
|
<button type="submit" class="btn btn-primary">Guardar Precios</button>
|
|
</div>
|
|
</form>
|
|
|
|
{% if prod.futuros %}
|
|
<div class="modal-body border-top">
|
|
<h6 class="text-info fw-bold mb-3">
|
|
<i class="bi bi-calendar-event"></i> Cambios Programados a Futuro
|
|
</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered text-center mb-0" style="font-size: 0.85rem;">
|
|
<thead class="table-dark">
|
|
<tr>
|
|
<th>Fecha Programada</th>
|
|
<th>Zona</th>
|
|
<th>Precio</th>
|
|
<th>Comisión</th>
|
|
<th>Cancelar</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for futuro in prod.futuros %}
|
|
<tr>
|
|
<td class="align-middle text-nowrap text-warning">{{ futuro.fecha }}</td>
|
|
<td class="align-middle fw-bold">{{ futuro.zona_name }}</td>
|
|
<td class="align-middle">${{ "{:,.0f}".format(futuro.price).replace(',', '.') }}</td>
|
|
<td class="align-middle">${{ "{:,.0f}".format(futuro.commission).replace(',', '.') }}</td>
|
|
<td class="align-middle">
|
|
<form action="{{ url_for('cancel_scheduled_price', id=futuro.id) }}" method="POST" class="d-inline">
|
|
<button type="submit" class="btn btn-danger btn-sm py-0 px-2" title="Cancelar este cambio">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="chartModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="chartModalTitle">Historial de Precios</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<canvas id="priceChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
|
<script>
|
|
let priceChartInstance = null;
|
|
|
|
async function showHistory(prodId, prodName) {
|
|
// Abrir el modal inmediatamente para que no parezca que la app murió
|
|
const modal = new bootstrap.Modal(document.getElementById('chartModal'));
|
|
document.getElementById('chartModalTitle').innerText = 'Fluctuación de Precio: ' + prodName;
|
|
modal.show();
|
|
|
|
// Traer la data desde nuestra nueva API
|
|
const res = await fetch(`/admin/api/productos/${prodId}/historial`);
|
|
const data = await res.json();
|
|
|
|
// Extraer zonas únicas y fechas únicas (limpiando la hora para el eje X)
|
|
const zonas = [...new Set(data.map(d => d.zona))];
|
|
const fechas = [...new Set(data.map(d => d.fecha.split(' ')[0]))].sort();
|
|
|
|
const colors = ['#0d6efd', '#198754', '#dc3545', '#ffc107', '#0dcaf0'];
|
|
|
|
const datasets = zonas.map((zona, index) => {
|
|
let lastPrice = 0;
|
|
|
|
// Rellenar huecos: Si no hubo cambio un día, se mantiene el precio del día anterior
|
|
const dataPoints = fechas.map(f => {
|
|
const hits = data.filter(d => d.zona === zona && d.fecha.startsWith(f));
|
|
if (hits.length > 0) {
|
|
lastPrice = hits[hits.length - 1].price; // Tomar el último cambio de ese día
|
|
}
|
|
return lastPrice;
|
|
});
|
|
|
|
return {
|
|
label: zona,
|
|
data: dataPoints,
|
|
borderColor: colors[index % colors.length],
|
|
backgroundColor: colors[index % colors.length],
|
|
stepped: true, // Hace que se vea como escaleras en vez de curvas raras
|
|
borderWidth: 2
|
|
};
|
|
});
|
|
|
|
// Dibujar (o redibujar) el gráfico
|
|
const ctx = document.getElementById('priceChart').getContext('2d');
|
|
if (priceChartInstance) {
|
|
priceChartInstance.destroy();
|
|
}
|
|
|
|
priceChartInstance = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: fechas,
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
callback: function(value) { return '$' + value.toLocaleString('es-CL'); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
const editModal = document.getElementById('editProductModal');
|
|
if (editModal) {
|
|
editModal.addEventListener('show.bs.modal', function (event) {
|
|
const button = event.relatedTarget;
|
|
|
|
// Atributos extraídos del botón
|
|
const id = button.getAttribute('data-id');
|
|
const name = button.getAttribute('data-name');
|
|
const price = button.getAttribute('data-price');
|
|
const commission = button.getAttribute('data-commission');
|
|
const zonaId = button.getAttribute('data-zona');
|
|
|
|
// Actualizamos el destino del formulario
|
|
const form = editModal.querySelector('#editProductForm');
|
|
form.action = `/admin/productos/edit/${id}`;
|
|
|
|
// Llenamos los inputs
|
|
editModal.querySelector('#edit_name').value = name;
|
|
editModal.querySelector('#edit_price').value = price;
|
|
editModal.querySelector('#edit_commission').value = commission;
|
|
editModal.querySelector('#edit_zona_id').value = zonaId;
|
|
|
|
// Forzamos el formato de miles (puntos) inmediatamente
|
|
editModal.querySelectorAll('.money-input').forEach(input => {
|
|
input.dispatchEvent(new Event('input'));
|
|
});
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.money-input').forEach(function(input) {
|
|
input.addEventListener('input', function(e) {
|
|
let value = this.value.replace(/\D/g, '');
|
|
if (value !== '') {
|
|
this.value = parseInt(value, 10).toLocaleString('es-CL');
|
|
} else {
|
|
this.value = '';
|
|
}
|
|
});
|
|
});
|
|
|
|
const searchInput = document.getElementById('searchProduct');
|
|
const productRows = document.querySelectorAll('.product-row');
|
|
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function() {
|
|
const term = this.value.toLowerCase();
|
|
productRows.forEach(row => {
|
|
// Asume que el nombre está en la primera celda
|
|
const name = row.cells[0].textContent.toLowerCase();
|
|
row.style.display = name.includes(term) ? '' : 'none';
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |