Autosave: 20260523-234035

This commit is contained in:
Flatlogic Bot 2026-05-23 23:40:36 +00:00
parent 7b765bd9d0
commit f587c9e57c
16 changed files with 718 additions and 21 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

View File

@ -0,0 +1,17 @@
-- Add PENDIENTE as a valid route status for contraentrega pedidos.
ALTER TABLE pedidos MODIFY COLUMN estado ENUM(
'ROTULADO 📦',
'EN TRANSITO 🚛',
'EN DESTINO 🏬',
'COMPLETADO ✅',
'Gestion',
'RUTA_CONTRAENTREGA',
'ENTREGA EXITOSA',
'RETORNADO',
'Duplicados',
'NO CONTESTO, DEVOLVER LLAMADA',
'CANCELADO',
'REPROGRAMADO',
'NO CONTESTO, VOLVER A LLAMAR',
'PENDIENTE'
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'ROTULADO 📦';

View File

@ -0,0 +1,3 @@
-- Add EAN code column to products for admin-maintained barcode/catalog codes.
ALTER TABLE products
ADD COLUMN IF NOT EXISTS ean VARCHAR(32) NULL AFTER sku;

View File

@ -0,0 +1,173 @@
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
require_once 'db/config.php';
require_once 'vendor/autoload.php';
use Shuchkin\SimpleXLSXGen;
function splitProvinciaDistrito(?string $value): array
{
$value = trim((string)$value);
if ($value === '') {
return ['', ''];
}
$clean = preg_replace('/\s+/', ' ', $value);
$parts = preg_split('/\s*[\/\-,|]\s*/', $clean, 2);
if (is_array($parts) && count($parts) === 2) {
return [trim($parts[0]), trim($parts[1])];
}
return [$clean, ''];
}
function extractProductNames(array $pedido): array
{
$names = [];
$notas = (string)($pedido['notas'] ?? '');
if (preg_match('/Detalle de productos:\s*(.+)$/mi', $notas, $match)) {
$items = explode(',', $match[1]);
foreach ($items as $item) {
$name = preg_replace('/\s*\(x\d+\)\s*$/i', '', trim($item));
if ($name !== '') {
$names[] = $name;
}
}
}
if (empty($names) && !empty($pedido['producto'])) {
foreach (explode(',', (string)$pedido['producto']) as $item) {
$name = trim($item);
if ($name !== '') {
$names[] = $name;
}
}
}
return array_values(array_unique($names));
}
function getEansForProducts(PDO $pdo, array $productNames): string
{
if (empty($productNames)) {
return '';
}
$placeholders = implode(',', array_fill(0, count($productNames), '?'));
$stmt = $pdo->prepare("SELECT nombre, ean FROM products WHERE nombre IN ($placeholders)");
$stmt->execute($productNames);
$map = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$ean = trim((string)($row['ean'] ?? ''));
if ($ean !== '') {
$map[$row['nombre']] = $ean;
}
}
$eans = [];
foreach ($productNames as $name) {
if (!empty($map[$name])) {
$eans[] = $map[$name];
}
}
return implode(' | ', array_values(array_unique($eans)));
}
try {
$pdo = db();
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
$selected_month = $_GET['mes'] ?? '';
$selected_year = $_GET['año'] ?? '';
$search_query = trim($_GET['q'] ?? '');
$sql = "SELECT p.* FROM pedidos p WHERE p.estado IN ('RUTA_CONTRAENTREGA', 'PENDIENTE', 'NO CONTESTO, VOLVER A LLAMAR', 'NO CONTESTO, DEVOLVER LLAMADA', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA', 'RETORNADO')";
$params = [];
if ($user_role === 'Asesor') {
$sql .= " AND p.asesor_id = ?";
$params[] = $user_id;
}
if ($search_query !== '') {
$sql .= " AND (p.nombre_completo LIKE ? OR p.dni_cliente LIKE ? OR p.celular LIKE ?)";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
$params[] = "%$search_query%";
}
if ($selected_month !== '') {
$sql .= " AND MONTH(p.created_at) = ?";
$params[] = $selected_month;
}
if ($selected_year !== '') {
$sql .= " AND YEAR(p.created_at) = ?";
$params[] = $selected_year;
}
$sql .= " ORDER BY p.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$pedidos = $stmt->fetchAll(PDO::FETCH_ASSOC);
$rows = [];
$rows[] = [
'Nombre y apellido',
'Celular',
'Pais',
'Departamento',
'Provincia',
'Distrito',
'Direccion',
'Referencia',
'Coordenadas',
'Codigo EAN',
'Cantidad',
'Precio',
'Total'
];
foreach ($pedidos as $pedido) {
[$provincia, $distrito] = splitProvinciaDistrito($pedido['codigo_rastreo'] ?? '');
$cantidad = (int)($pedido['cantidad'] ?? 0);
$total = (float)($pedido['monto_total'] ?? 0);
$precio = $cantidad > 0 ? round($total / $cantidad, 2) : 0;
$ean = getEansForProducts($pdo, extractProductNames($pedido));
$rows[] = [
(string)($pedido['nombre_completo'] ?? ''),
(string)($pedido['celular'] ?? ''),
'Perú',
(string)($pedido['sede_envio'] ?? ''),
$provincia,
$distrito,
(string)($pedido['direccion_exacta'] ?? ''),
(string)($pedido['referencia_domicilio'] ?? ''),
(string)($pedido['coordenadas'] ?? ''),
(string)$ean,
$cantidad,
$precio,
$total
];
}
$filename = 'ruta_contraentrega_' . date('Y-m-d_H-i') . '.xlsx';
SimpleXLSXGen::fromArray($rows, 'Ruta Contraentrega')->downloadAs($filename);
exit;
} catch (Throwable $e) {
error_log('Error exportando Ruta Contraentrega: ' . $e->getMessage());
header('HTTP/1.1 500 Internal Server Error');
echo 'Error al generar el Excel de Ruta Contraentrega.';
}

View File

@ -94,6 +94,11 @@ include 'layout_header.php';
<input type="text" class="form-control" id="sku" name="sku" value="<?php echo htmlspecialchars($product['sku'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="ean">EAN</label>
<input type="text" class="form-control" id="ean" name="ean" maxlength="32" inputmode="numeric" value="<?php echo htmlspecialchars($product['ean'] ?? ''); ?>" placeholder="Código EAN del producto">
</div>
<div class="form-group">
<label for="costo">Costo</label>
<input type="number" step="0.01" class="form-control" id="costo" name="costo" value="<?php echo htmlspecialchars($product['costo'] ?? '0.00'); ?>">

View File

@ -0,0 +1,72 @@
<?php
if (!function_exists('contraentregaProvinciasPorDepartamento')) {
function contraentregaProvinciasPorDepartamento(): array
{
return [
'Ancash' => ['Carhuaz', 'Huaraz', 'Huaylas', 'Santa', 'Yungay'],
'Apurimac' => ['Abancay'],
'Arequipa' => ['Arequipa'],
'Ayacucho' => ['Huamanga'],
'Cajamarca' => ['Cajamarca'],
'Cusco' => ['Cusco'],
'Huanuco' => ['Huanuco'],
'Ica' => ['Ica'],
'Junin' => ['Chupaca', 'Huancayo'],
'La Libertad' => ['Trujillo'],
'Lambayeque' => ['Ferreñafe', 'Chiclayo', 'Lambayeque'],
'Lima' => ['Callao', 'Lima'],
'Loreto' => ['Maynas'],
'Madre de Dios' => ['Tambopata'],
'Moquegua' => ['Ilo', 'Mariscal Nieto'],
'Piura' => ['Paita', 'Piura', 'Sullana', 'Talara'],
'Puno' => ['Puno', 'San Roman'],
'San Martin' => ['Lamas', 'San Martin'],
'Tacna' => ['Tacna'],
'Ucayali' => ['Coronel Portillo'],
];
}
}
if (!function_exists('contraentregaDistritosPorProvincia')) {
function contraentregaDistritosPorProvincia(): array
{
return [
'Abancay' => ['Abancay'],
'Arequipa' => ['Alto Selva Alegre', 'Arequipa', 'Cayma', 'Cerro Colorado', 'Characato', 'Jacobo Hunter', 'Jose Luis Bustamante y Rivero', 'Mariano Melgar', 'Miraflores', 'Paucarpata', 'Sabandia', 'Sachaca', 'Socabaya', 'Tiabaya', 'Yanahuara'],
'Cajamarca' => ['Los Baqos del Inca', 'Cajamarca'],
'Callao' => ['Bellavista', 'Callao', 'Carmen de la Legua Reynoso', 'La Perla', 'La Punta', 'Ventanilla'],
'Carhuaz' => ['Carhuaz'],
'Chupaca' => ['Chongos Bajo', 'Chupaca', 'Huamancaca Chico', 'Tres de Diciembre'],
'Cusco' => ['Cusco', 'San Jeronimo', 'San Sebastian', 'Santiago', 'Saylla', 'Wanchaq'],
'Huamanga' => ['Ayacucho', 'Jesús Nazareno', 'San Juan Bautista'],
'Huancayo' => ['Chilca', 'El Tambo', 'Hualhuas', 'Huancan', 'Huancayo', 'Huayucachi', 'Pariahuanca', 'Pilcomayo', 'Pucara', 'Quilcas', 'San Agustin', 'San Jeronimo de Tunan', 'Sapallanga', 'Sicaya', 'Viques'],
'Huanuco' => ['Amarilis', 'Huanuco', 'Pillcomarca'],
'Huaraz' => ['Huaraz', 'Independencia'],
'Huaylas' => ['Caraz', 'Huaylas'],
'Ilo' => ['El Algarrobal', 'Ilo', 'Pacocha'],
'Lima' => ['Ancon', 'Ate', 'Barranco', 'Breña', 'Carabayllo', 'Cercado de Lima', 'Chaclacayo', 'Chorrillos', 'Cieneguilla', 'Comas', 'El Agustino', 'Independencia', 'Jesus Maria', 'La Molina', 'La Victoria', 'Lince', 'Los Olivos', 'Lurigancho - Chosica', 'Lurin', 'Magdalena del Mar', 'Miraflores', 'Pachacamac', 'Pueblo Libre', 'Puente Piedra', 'Punta Hermosa', 'Punta Negra', 'Rimac', 'San Borja', 'San Isidro', 'San Juan de Lurigancho', 'San Juan de Miraflores', 'San Luis', 'San Martin de Porres', 'San Miguel', 'Santa Anita', 'Santa Maria del Mar', 'Santa Rosa', 'Santiago de Surco', 'Surquillo', 'Villa El Salvador', 'Villa Maria del Triunfo'],
'Mariscal Nieto' => ['Carumas', 'Cuchumbaya', 'Moquegua', 'Samegua', 'San Cristobal', 'Torata'],
'Paita' => ['Amotape', 'Arenal', 'Colan', 'La Huaca', 'Paita', 'Tamarindo', 'Vichayal'],
'Piura' => ['26 de Octubre', 'Castilla', 'Catacaos', 'Piura'],
'Santa' => ['Chimbote', 'Coishco', 'Nuevo Chimbote'],
'Sullana' => ['Sullana'],
'Talara' => ['Pariñas'],
'Trujillo' => ['El Porvenir', 'Florencia de Mora', 'Huanchaco', 'La Esperanza', 'Laredo', 'Moche', 'Salaverry', 'Trujillo', 'Victor Larco Herrera'],
'Yungay' => ['Yungay'],
];
}
}
if (!function_exists('splitProvinciaDistritoContraentrega')) {
function splitProvinciaDistritoContraentrega(?string $value): array
{
$value = trim((string)$value);
if ($value === '') {
return ['', ''];
}
$parts = preg_split('/\s*(?:\/|-|,|\|)\s*/', $value, 2);
return [trim($parts[0] ?? ''), trim($parts[1] ?? '')];
}
}

View File

@ -27,7 +27,7 @@ try {
$sedes_stmt = $pdo->query("SELECT id, nombre FROM sedes ORDER BY nombre");
$sedes = $sedes_stmt->fetchAll(PDO::FETCH_ASSOC);
$products_stmt = $pdo->query("SELECT id, nombre, sku FROM products ORDER BY nombre");
$products_stmt = $pdo->query("SELECT id, nombre, sku, ean FROM products ORDER BY nombre");
$products = $products_stmt->fetchAll(PDO::FETCH_ASSOC);
$stock_stmt = $pdo->query("SELECT product_id AS producto_id, sede_id, quantity FROM stock_sedes");
@ -38,6 +38,7 @@ try {
$inventario[$product['id']] = [
'nombre' => $product['nombre'],
'sku' => $product['sku'],
'ean' => $product['ean'] ?? '',
'total' => 0,
'sedes' => array_fill_keys(array_column($sedes, 'id'), 0)
];
@ -146,6 +147,29 @@ try {
}
?>
<style>
.editable-ean {
min-width: 140px;
cursor: pointer;
background: #fffdf5;
}
.editable-ean:hover {
background: #fff3cd;
box-shadow: inset 0 0 0 1px #f0c36d;
}
.ean-input {
width: 100%;
min-width: 130px;
border: 1px solid #f0c36d;
border-radius: 6px;
padding: 4px 8px;
}
.ean-saving {
opacity: .65;
}
</style>
<div class="container-fluid mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Dashboard de Inventario</h1>
@ -211,6 +235,7 @@ try {
<tr>
<th>Producto</th>
<th>SKU</th>
<th>EAN</th>
<th class="text-center">Stock Total</th>
<?php foreach ($sedes as $sede): ?>
<th class="text-center"><?php echo htmlspecialchars($sede['nombre']); ?></th>
@ -219,12 +244,13 @@ try {
</thead>
<tbody>
<?php if (empty($inventario)): ?>
<tr><td colspan="<?php echo count($sedes) + 3; ?>" class="text-center">No hay productos.</td></tr>
<tr><td colspan="<?php echo count($sedes) + 4; ?>" class="text-center">No hay productos.</td></tr>
<?php else: ?>
<?php foreach ($inventario as $product_id => $datos_producto): ?>
<tr>
<td><?php echo htmlspecialchars($datos_producto['nombre']); ?></td>
<td><?php echo htmlspecialchars($datos_producto['sku']); ?></td>
<td><?php echo htmlspecialchars($datos_producto['sku'] ?? ''); ?></td>
<td class="editable-ean" data-product-id="<?php echo (int)$product_id; ?>" title="Doble clic para editar EAN"><?php echo htmlspecialchars($datos_producto['ean'] ?? ''); ?></td>
<td class="text-center fw-bold"><?php echo $datos_producto['total']; ?></td>
<?php foreach ($datos_producto['sedes'] as $sede_id => $cantidad): ?>
<td class="text-center">
@ -386,6 +412,62 @@ document.addEventListener('DOMContentLoaded', function () {
<script>
$(document).ready(function() {
$(document).on('dblclick', '.editable-ean', function () {
const cell = $(this);
if (cell.find('input').length) return;
const originalValue = cell.text().trim();
const input = $('<input type="text" class="ean-input" maxlength="32" inputmode="numeric">').val(originalValue);
cell.empty().append(input);
input.trigger('focus').trigger('select');
function finish(save) {
const newValue = input.val().trim();
if (!save || newValue === originalValue) {
cell.text(originalValue);
return;
}
cell.addClass('ean-saving');
$.ajax({
url: 'update_product_ean.php',
method: 'POST',
dataType: 'json',
data: {
product_id: cell.data('product-id'),
ean: newValue
}
}).done(function (response) {
if (response && response.success) {
cell.text(response.ean || '');
} else {
alert('Error: ' + ((response && response.message) ? response.message : 'No se pudo guardar el EAN.'));
cell.text(originalValue);
}
}).fail(function () {
alert('Error: No se pudo conectar para guardar el EAN.');
cell.text(originalValue);
}).always(function () {
cell.removeClass('ean-saving');
});
}
input.on('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
finish(true);
}
if (event.key === 'Escape') {
event.preventDefault();
finish(false);
}
});
input.on('blur', function () {
finish(true);
});
});
$('.ver-codigos').on('click', function(e) {
e.preventDefault();

View File

@ -6,6 +6,7 @@ if (!isset($_SESSION['user_id'])) {
}
require_once 'db/config.php';
require_once 'includes/contraentrega_cobertura.php';
$pdo = db();
$user_id = $_SESSION['user_id'];
@ -30,6 +31,11 @@ $pedido = [
'asesor_id' => $user_id, // Default to current user
'notas' => '',
];
$provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento();
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$page_title = 'Agregar Pedido Contraentrega';
if (isset($_GET['id'])) {
@ -101,6 +107,9 @@ if (empty($display_products)) {
$display_products[] = ['nombre' => '', 'cantidad' => 1];
}
[$provinciaContraentrega, $distritoContraentrega] = splitProvinciaDistritoContraentrega($pedido['codigo_rastreo'] ?? '');
$departamentoSeleccionado = trim((string)($pedido['sede_envio'] ?? ''));
?>
<?php
$pageTitle = 'Agregar Pedidos Contraentrega';
@ -166,17 +175,45 @@ include 'layout_header.php';
</select>
</div>
<div class="col-md-4 mb-3">
<label for="sede_envio" class="form-label">Ciudad</label>
<input type="text" class="form-control" id="sede_envio" name="sede_envio" value="<?php echo htmlspecialchars($pedido['sede_envio']); ?>" required>
<label for="sede_envio" class="form-label">Departamento</label>
<select class="form-select" id="sede_envio" name="sede_envio" required>
<option value="">Seleccione departamento</option>
<?php foreach ($departamentosContraentrega as $departamento): ?>
<option value="<?php echo htmlspecialchars($departamento); ?>" <?php echo ($departamentoSeleccionado === $departamento) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($departamento); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="codigo_rastreo" class="form-label">Provincia/Distrito</label>
<input type="text" class="form-control" id="codigo_rastreo" name="codigo_rastreo" value="<?php echo htmlspecialchars($pedido['codigo_rastreo']); ?>">
<div class="col-md-4 mb-3">
<label for="provincia" class="form-label">Provincia</label>
<select class="form-select" id="provincia" name="provincia" required>
<option value="">Seleccione provincia</option>
<?php foreach (($provinciasPorDepartamentoContraentrega[$departamentoSeleccionado] ?? []) as $provinciaOpcion): ?>
<option value="<?php echo htmlspecialchars($provinciaOpcion); ?>" <?php echo ($provinciaContraentrega === $provinciaOpcion) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($provinciaOpcion); ?>
</option>
<?php endforeach; ?>
<?php if ($provinciaContraentrega !== '' && !in_array($provinciaContraentrega, $provinciasPorDepartamentoContraentrega[$departamentoSeleccionado] ?? [], true)): ?>
<option value="<?php echo htmlspecialchars($provinciaContraentrega); ?>" selected>
<?php echo htmlspecialchars($provinciaContraentrega); ?>
</option>
<?php endif; ?>
</select>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="distrito_select" class="form-label">Distrito</label>
<select class="form-select" id="distrito_select">
<option value="">Seleccione primero provincia</option>
</select>
<input type="text" class="form-control mt-2 d-none" id="distrito_manual" placeholder="Escriba el distrito si aún no está en cobertura">
<input type="hidden" id="distrito" name="distrito" value="<?php echo htmlspecialchars($distritoContraentrega); ?>" required>
<div class="form-text">Si una provincia aún no tiene cobertura cargada, podrás escribir el distrito manualmente.</div>
</div>
<div class="col-md-4 mb-3">
<label for="direccion_exacta" class="form-label">Direccion exacta</label>
<input type="text" class="form-control" id="direccion_exacta" name="direccion_exacta" value="<?php echo htmlspecialchars($pedido['direccion_exacta'] ?? ''); ?>" required>
</div>
@ -306,6 +343,120 @@ include 'layout_header.php';
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const provinciasPorDepartamento = <?php echo json_encode($provinciasPorDepartamentoContraentrega, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const distritosPorProvincia = <?php echo json_encode($distritosPorProvinciaContraentrega, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const departamentoSelect = document.getElementById('sede_envio');
const provinciaSelect = document.getElementById('provincia');
const distritoSelect = document.getElementById('distrito_select');
const distritoManualInput = document.getElementById('distrito_manual');
const distritoHiddenInput = document.getElementById('distrito');
const provinciaSeleccionada = <?php echo json_encode($provinciaContraentrega, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const distritoSeleccionado = <?php echo json_encode($distritoContraentrega, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
function syncDistrito(value) {
distritoHiddenInput.value = value || '';
}
function renderDistritos(useInitialSelection = false) {
const provincia = provinciaSelect.value;
const distritos = distritosPorProvincia[provincia] || [];
const targetValue = useInitialSelection ? distritoSeleccionado : '';
distritoSelect.innerHTML = '';
if (distritos.length > 0) {
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = 'Seleccione distrito';
distritoSelect.appendChild(emptyOption);
distritos.forEach(function(distrito) {
const option = document.createElement('option');
option.value = distrito;
option.textContent = distrito;
if (distrito === targetValue) {
option.selected = true;
}
distritoSelect.appendChild(option);
});
if (targetValue && !distritos.includes(targetValue)) {
const legacyOption = document.createElement('option');
legacyOption.value = targetValue;
legacyOption.textContent = targetValue;
legacyOption.selected = true;
distritoSelect.appendChild(legacyOption);
}
distritoSelect.disabled = false;
distritoSelect.classList.remove('d-none');
distritoManualInput.classList.add('d-none');
distritoManualInput.value = '';
syncDistrito(distritoSelect.value);
} else {
distritoSelect.disabled = true;
distritoSelect.classList.add('d-none');
distritoManualInput.classList.remove('d-none');
distritoManualInput.value = targetValue;
syncDistrito(distritoManualInput.value);
}
}
function renderProvincias(useInitialSelection = false) {
const departamento = departamentoSelect.value;
const provincias = provinciasPorDepartamento[departamento] || [];
const targetValue = useInitialSelection ? provinciaSeleccionada : '';
provinciaSelect.innerHTML = '';
const emptyOption = document.createElement('option');
emptyOption.value = '';
emptyOption.textContent = provincias.length ? 'Seleccione provincia' : 'Seleccione primero departamento';
provinciaSelect.appendChild(emptyOption);
provincias.forEach(function(provincia) {
const option = document.createElement('option');
option.value = provincia;
option.textContent = provincia;
if (provincia === targetValue) {
option.selected = true;
}
provinciaSelect.appendChild(option);
});
if (targetValue && !provincias.includes(targetValue)) {
const legacyOption = document.createElement('option');
legacyOption.value = targetValue;
legacyOption.textContent = targetValue;
legacyOption.selected = true;
provinciaSelect.appendChild(legacyOption);
}
provinciaSelect.disabled = provincias.length === 0;
renderDistritos(useInitialSelection);
}
departamentoSelect.addEventListener('change', function() {
renderProvincias(false);
});
provinciaSelect.addEventListener('change', function() {
renderDistritos(false);
});
distritoSelect.addEventListener('change', function() {
syncDistrito(distritoSelect.value);
});
distritoManualInput.addEventListener('input', function() {
syncDistrito(distritoManualInput.value);
});
renderProvincias(true);
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const numeroOperacionInput = document.getElementById('numero_operacion');

View File

@ -54,6 +54,7 @@ $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
<th>Código de Barras</th>
<th>Nombre</th>
<th>SKU</th>
<th>EAN</th>
<th>Costo</th>
<th>Precio Venta</th>
<th>Unidades Vendidas</th>
@ -76,7 +77,8 @@ $products = $stmt->fetchAll(PDO::FETCH_ASSOC);
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($product['nombre']); ?></td>
<td><?php echo htmlspecialchars($product['sku']); ?></td>
<td><?php echo htmlspecialchars($product['sku'] ?? ''); ?></td>
<td class="editable-ean" data-product-id="<?php echo (int)$product['id']; ?>" title="Doble clic para editar EAN"><?php echo htmlspecialchars($product['ean'] ?? ''); ?></td>
<td><?php echo htmlspecialchars($product['costo']); ?></td>
<td><?php echo htmlspecialchars($product['precio_venta']); ?></td>
<td><?php echo htmlspecialchars($product['unidades_vendidas']); ?></td>
@ -133,4 +135,84 @@ $(function () {
"autoWidth": false,
});
});
</script>
</script>
<style>
.editable-ean {
min-width: 140px;
cursor: pointer;
background: #fffdf5;
}
.editable-ean:hover {
background: #fff3cd;
box-shadow: inset 0 0 0 1px #f0c36d;
}
.ean-input {
width: 100%;
min-width: 130px;
border: 1px solid #f0c36d;
border-radius: 6px;
padding: 4px 8px;
}
.ean-saving {
opacity: .65;
}
</style>
<script>
$(function () {
$(document).on('dblclick', '.editable-ean', function () {
const cell = $(this);
if (cell.find('input').length) return;
const originalValue = cell.text().trim();
const input = $('<input type="text" class="ean-input" maxlength="32" inputmode="numeric">').val(originalValue);
cell.empty().append(input);
input.trigger('focus').trigger('select');
function finish(save) {
const newValue = input.val().trim();
if (!save || newValue === originalValue) {
cell.text(originalValue);
return;
}
cell.addClass('ean-saving');
$.ajax({
url: 'update_product_ean.php',
method: 'POST',
dataType: 'json',
data: {
product_id: cell.data('product-id'),
ean: newValue
}
}).done(function (response) {
if (response && response.success) {
cell.text(response.ean || '');
} else {
alert('Error: ' + ((response && response.message) ? response.message : 'No se pudo guardar el EAN.'));
cell.text(originalValue);
}
}).fail(function () {
alert('Error: No se pudo conectar para guardar el EAN.');
cell.text(originalValue);
}).always(function () {
cell.removeClass('ean-saving');
});
}
input.on('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
finish(true);
}
if (event.key === 'Escape') {
event.preventDefault();
finish(false);
}
});
input.on('blur', function () {
finish(true);
});
});
});
</script>

View File

@ -145,6 +145,22 @@ $months = [
7 => 'Julio', 8 => 'Agosto', 9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre'
];
function splitProvinciaDistrito($value) {
$value = trim((string)($value ?? ''));
if ($value === '') {
return ['N/A', 'N/A'];
}
$parts = preg_split('/\s*(?:\/|\||,|-)\s*/', $value, 2);
$provincia = trim($parts[0] ?? '');
$distrito = trim($parts[1] ?? '');
return [
$provincia !== '' ? $provincia : 'N/A',
$distrito !== '' ? $distrito : 'N/A',
];
}
?>
<?php
$pageTitle = "Ruta Contraentrega";
@ -164,7 +180,7 @@ include 'layout_header.php';
<style>
/* Ajustes puntuales para Ruta Contraentrega: opciones largas sin invadir columnas vecinas */
#pedidos-table {
min-width: 1750px;
min-width: 1850px;
}
#pedidos-table th:nth-child(3),
#pedidos-table td:nth-child(3) {
@ -224,6 +240,19 @@ include 'layout_header.php';
<div class="col-auto mt-4">
<button type="submit" class="btn btn-info">Filtrar</button>
<a href="ruta_contraentrega.php" class="btn btn-secondary">Limpiar</a>
<?php
$excelParams = array_filter([
'q' => $search_query,
'mes' => $selected_month,
'año' => $selected_year,
], static function ($value) {
return $value !== '' && $value !== null;
});
$excelUrl = 'download_ruta_contraentrega.php' . (!empty($excelParams) ? '?' . http_build_query($excelParams) : '');
?>
<a href="<?php echo htmlspecialchars($excelUrl); ?>" class="btn btn-success">
<i class="fas fa-file-excel me-1"></i> Descargar Excel
</a>
</div>
</form>
</div>
@ -244,8 +273,9 @@ include 'layout_header.php';
<th style="width: 120px;">Celular</th>
<th style="width: 250px;">Dirección</th>
<th style="width: 200px;">Referencia</th>
<th style="width: 120px;">Ciudad</th>
<th style="width: 150px;">Provincia/Distrito</th>
<th style="width: 140px;">Departamento</th>
<th style="width: 140px;">Provincia</th>
<th style="width: 140px;">Distrito</th>
<th style="width: 150px;">Coordenadas</th>
<th style="width: 200px;">Producto</th>
<th style="width: 80px;">Cant.</th>
@ -313,8 +343,10 @@ include 'layout_header.php';
</td>
<td><?php echo htmlspecialchars($pedido['direccion_exacta'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['referencia_domicilio'] ?? 'N/A'); ?></td>
<?php [$provinciaPedido, $distritoPedido] = splitProvinciaDistrito($pedido['codigo_rastreo'] ?? ''); ?>
<td><?php echo htmlspecialchars($pedido['sede_envio'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['codigo_rastreo'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($provinciaPedido); ?></td>
<td><?php echo htmlspecialchars($distritoPedido); ?></td>
<td><?php echo htmlspecialchars($pedido['coordenadas'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pedido['producto']); ?></td>
<td><?php echo htmlspecialchars($pedido['cantidad']); ?></td>

View File

@ -10,6 +10,7 @@ if (!isset($_SESSION['user_id'])) {
}
require_once 'db/config.php';
require_once __DIR__ . '/includes/contraentrega_cobertura.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo = db();
@ -20,8 +21,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nombre_completo = trim($_POST['nombre_completo'] ?? '');
$celular = trim($_POST['celular'] ?? '');
$agencia = trim($_POST['agencia'] ?? 'CONTRAENTREGA');
$provinciasPorDepartamentoContraentrega = contraentregaProvinciasPorDepartamento();
$distritosPorProvinciaContraentrega = contraentregaDistritosPorProvincia();
$departamentosContraentrega = array_keys($provinciasPorDepartamentoContraentrega);
$sede_envio = trim($_POST['sede_envio'] ?? '');
$provincia = trim($_POST['provincia'] ?? '');
$distrito = trim($_POST['distrito'] ?? '');
$codigo_rastreo = trim($_POST['codigo_rastreo'] ?? '');
if ($provincia !== '' || $distrito !== '') {
$codigo_rastreo = trim($provincia . ' / ' . $distrito, ' /');
}
$codigo_tracking = trim($_POST['codigo_tracking'] ?? '');
$direccion_exacta = trim($_POST['direccion_exacta'] ?? '');
$referencia_domicilio = trim($_POST['referencia_domicilio'] ?? '');
@ -76,9 +85,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($productos_detalle)) {
$notas .= "\n\n" . $notas_adicionales;
}
if (!in_array($sede_envio, $departamentosContraentrega, true)) {
$error_message = urlencode('Seleccione un departamento válido para el pedido contra entrega.');
$id_param = $id ? '&id=' . $id : '';
header('Location: pedidos_contraentrega.php?error=' . $error_message . $id_param);
exit;
}
if ($provincia !== '' && !in_array($provincia, $provinciasPorDepartamentoContraentrega[$sede_envio] ?? [], true)) {
$error_message = urlencode('Seleccione una provincia válida para el departamento elegido.');
$id_param = $id ? '&id=' . $id : '';
header('Location: pedidos_contraentrega.php?error=' . $error_message . $id_param);
exit;
}
if ($distrito !== '' && isset($distritosPorProvinciaContraentrega[$provincia]) && !in_array($distrito, $distritosPorProvinciaContraentrega[$provincia], true)) {
$error_message = urlencode('Seleccione un distrito válido para la provincia elegida.');
$id_param = $id ? '&id=' . $id : '';
header('Location: pedidos_contraentrega.php?error=' . $error_message . $id_param);
exit;
}
if (empty($nombre_completo) || empty($celular) || empty($sede_envio) || empty($producto) || $cantidad === false || $monto_total === false || empty($coordenadas)) {
$error_message = urlencode('Por favor, complete todos los campos obligatorios, incluyendo las coordenadas.');
if (empty($nombre_completo) || empty($celular) || empty($sede_envio) || empty($provincia) || empty($distrito) || empty($producto) || $cantidad === false || $monto_total === false || empty($coordenadas)) {
$error_message = urlencode('Por favor, complete todos los campos obligatorios: departamento, provincia, distrito y coordenadas.');
$id_param = $id ? '&id=' . $id : '';
header('Location: pedidos_contraentrega.php?error=' . $error_message . $id_param);
exit;

View File

@ -26,6 +26,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nombre = $_POST['nombre'] ?? '';
$codigo_base = $_POST['codigo_base'] ?? '';
$sku = !empty($_POST['sku']) ? $_POST['sku'] : null;
$ean = isset($_POST['ean']) ? preg_replace('/\s+/', '', trim($_POST['ean'])) : '';
$ean = $ean !== '' ? $ean : null;
if ($ean !== null && !preg_match('/^[0-9]{1,32}$/', $ean)) {
$_SESSION['error_message'] = 'El EAN solo debe contener números.';
header('Location: ' . (!empty($id) ? 'edit_product.php?id=' . urlencode($id) : 'edit_product.php'));
exit;
}
$costo = !empty($_POST['costo']) ? (float)$_POST['costo'] : 0.00;
$precio_venta = !empty($_POST['precio_venta']) ? (float)$_POST['precio_venta'] : 0.00;
$description = $_POST['description'] ?? '';
@ -42,15 +49,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!empty($id)) {
// Actualizar producto existente
$sql = "UPDATE products SET nombre = ?, sku = ?, costo = ?, precio_venta = ?, description = ?, provincia_id = ?, show_on_panel = ?, codigo_base = ? WHERE id = ?";
$sql = "UPDATE products SET nombre = ?, sku = ?, ean = ?, costo = ?, precio_venta = ?, description = ?, provincia_id = ?, show_on_panel = ?, codigo_base = ? WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->execute([$nombre, $sku, $costo, $precio_venta, $description, $provincia_id, $show_on_panel, $codigo_base, $id]);
$stmt->execute([$nombre, $sku, $ean, $costo, $precio_venta, $description, $provincia_id, $show_on_panel, $codigo_base, $id]);
$_SESSION['success_message'] = "Producto actualizado exitosamente.";
} else {
// Crear nuevo producto
$sql = "INSERT INTO products (nombre, sku, costo, precio_venta, description, provincia_id, show_on_panel, codigo_base, unidades_vendidas) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)";
$sql = "INSERT INTO products (nombre, sku, ean, costo, precio_venta, description, provincia_id, show_on_panel, codigo_base, unidades_vendidas) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)";
$stmt = $db->prepare($sql);
$stmt->execute([$nombre, $sku, $costo, $precio_venta, $description, $provincia_id, $show_on_panel, $codigo_base]);
$stmt->execute([$nombre, $sku, $ean, $costo, $precio_venta, $description, $provincia_id, $show_on_panel, $codigo_base]);
$_SESSION['success_message'] = "Producto creado exitosamente.";
}

43
update_product_ean.php Normal file
View File

@ -0,0 +1,43 @@
<?php
session_start();
require_once 'db/config.php';
header('Content-Type: application/json; charset=utf-8');
try {
$role = $_SESSION['user_role'] ?? ($_SESSION['role'] ?? '');
if (!isset($_SESSION['user_id']) || !in_array($role, ['Administrador', 'admin', 'Control Logistico', 'Logistica'], true)) {
throw new Exception('No autorizado.');
}
$productId = isset($_POST['product_id']) ? (int)$_POST['product_id'] : 0;
$ean = isset($_POST['ean']) ? preg_replace('/\s+/', '', trim($_POST['ean'])) : '';
if ($productId <= 0) {
throw new Exception('Producto inválido.');
}
if ($ean !== '' && !preg_match('/^[0-9]{1,32}$/', $ean)) {
throw new Exception('El EAN solo debe contener números.');
}
$eanToSave = $ean !== '' ? $ean : null;
$stmt = db()->prepare('UPDATE products SET ean = :ean WHERE id = :id');
$stmt->bindValue(':ean', $eanToSave, $eanToSave === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
$stmt->bindValue(':id', $productId, PDO::PARAM_INT);
$stmt->execute();
if ($stmt->rowCount() === 0) {
$check = db()->prepare('SELECT id FROM products WHERE id = :id');
$check->bindValue(':id', $productId, PDO::PARAM_INT);
$check->execute();
if (!$check->fetchColumn()) {
throw new Exception('Producto no encontrado.');
}
}
echo json_encode(['success' => true, 'ean' => $eanToSave ?? '']);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}