Autosave: 20260610-161940

This commit is contained in:
Flatlogic Bot 2026-06-10 16:19:28 +00:00
parent 3f9d04425f
commit 7da0c2905a
9 changed files with 264 additions and 35 deletions

View File

@ -13,15 +13,23 @@ if (!isset($_SESSION['user_id']) || !in_array($_SESSION['user_role'], ['Administ
$db = db();
// --- LÓGICA DE FILTROS ---
$period = $_GET['period'] ?? '7';
$start_date = $_GET['start_date'] ?? '';
$end_date = $_GET['end_date'] ?? '';
$period = $_GET['period'] ?? 'today';
$start_date = trim((string)($_GET['start_date'] ?? ''));
$end_date = trim((string)($_GET['end_date'] ?? ''));
$isValidDashboardDate = static function (string $date): bool {
$parsed = DateTimeImmutable::createFromFormat('Y-m-d', $date);
return $parsed instanceof DateTimeImmutable && $parsed->format('Y-m-d') === $date;
};
$date_condition = "";
$label_period = "";
if ($period === 'custom' && !empty($start_date) && !empty($end_date)) {
$date_condition = "DATE(p.created_at) BETWEEN '$start_date' AND '$end_date'";
if ($period === 'custom' && $isValidDashboardDate($start_date) && $isValidDashboardDate($end_date)) {
if ($start_date > $end_date) {
[$start_date, $end_date] = [$end_date, $start_date];
}
$date_condition = "DATE(p.created_at) BETWEEN " . $db->quote($start_date) . " AND " . $db->quote($end_date);
$label_period = "Desde " . date('d/m/Y', strtotime($start_date)) . " hasta " . date('d/m/Y', strtotime($end_date));
} else {
switch ($period) {
@ -66,9 +74,9 @@ if ($period === 'custom' && !empty($start_date) && !empty($end_date)) {
$label_period = "Último Año";
break;
default:
$date_condition = "DATE(p.created_at) >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)";
$label_period = "Últimos 7 días";
$period = '7';
$date_condition = "DATE(p.created_at) = CURDATE()";
$label_period = "Hoy";
$period = 'today';
}
}

View File

@ -0,0 +1,3 @@
-- Deprecated/no-op.
-- PENDIENTE belongs to pedidos.estado, not to pedidos.tipo_paquete.
-- The final package ENUM is enforced in 078_clean_tipo_paquete_enum.sql.

View File

@ -0,0 +1,4 @@
-- Keep pedidos.tipo_paquete aligned with the package selector.
-- PENDIENTE belongs to pedidos.estado, not to tipo_paquete.
UPDATE pedidos SET tipo_paquete = NULL WHERE tipo_paquete = 'PENDIENTE';
ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM('RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO', 'COMPLETADO', 'EMPAQUETADO', 'RETORNADO', 'ANULADO') DEFAULT NULL;

View File

@ -0,0 +1,5 @@
-- Rename package status EMPAQUETADO to PREPARADO in pedidos.tipo_paquete.
-- EMPAQUETADO is kept only as a temporary legacy value during migration so existing rows can be converted safely.
ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM('RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO', 'COMPLETADO', 'EMPAQUETADO', 'PREPARADO', 'RETORNADO', 'ANULADO') DEFAULT NULL;
UPDATE pedidos SET tipo_paquete = 'PREPARADO' WHERE tipo_paquete = 'EMPAQUETADO';
ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM('RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO', 'COMPLETADO', 'PREPARADO', 'RETORNADO', 'ANULADO') DEFAULT NULL;

136
includes/tipo_paquete.php Normal file
View File

@ -0,0 +1,136 @@
<?php
if (!function_exists('tipoPaqueteValidValues')) {
function tipoPaqueteValidValues(): array
{
return [
'RUTA',
'CONTRAENTREGA',
'NO CONTESTA, VOLVER A LLAMAR',
'PENDIENTE A RETORNO',
'COMPLETADO',
'PREPARADO',
'RETORNADO',
'ANULADO',
];
}
}
if (!function_exists('tipoPaquetePrimarySelectorValues')) {
function tipoPaquetePrimarySelectorValues(): array
{
return [
'PREPARADO',
'RUTA',
'ANULADO',
'PENDIENTE A RETORNO',
'RETORNADO',
'COMPLETADO',
];
}
}
if (!function_exists('normalizeTipoPaqueteValue')) {
function normalizeTipoPaqueteValue($value): ?string
{
if ($value === null || is_array($value)) {
return null;
}
$value = (string) $value;
$value = preg_replace('/[\x{00A0}\x{200B}-\x{200D}\x{FEFF}]+/u', ' ', $value);
$value = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $value);
$value = preg_replace('/\s+/u', ' ', trim($value));
if ($value === '') {
return null;
}
$normalized = function_exists('mb_strtoupper') ? mb_strtoupper($value, 'UTF-8') : strtoupper($value);
$match = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
if ($match === false || $match === null || $match === '') {
$match = $normalized;
}
$canonicalKey = preg_replace('/[^A-Z]+/', '', strtoupper($match));
$aliases = [
'RUTA' => 'RUTA',
'CONTRAENTREGA' => 'CONTRAENTREGA',
'NOCONTESTAVOLVERALLAMAR' => 'NO CONTESTA, VOLVER A LLAMAR',
'PENDIENTEARETORNO' => 'PENDIENTE A RETORNO',
'COMPLETADO' => 'COMPLETADO',
'EMPAQUETADO' => 'PREPARADO',
'PREPARADO' => 'PREPARADO',
'EMPACADO' => 'PREPARADO',
'EMPAQUETARDO' => 'PREPARADO',
'RETORNADO' => 'RETORNADO',
'ANULADO' => 'ANULADO',
];
return $aliases[$canonicalKey] ?? $normalized;
}
}
if (!function_exists('tipoPaqueteIsValid')) {
function tipoPaqueteIsValid(?string $value): bool
{
return $value === null || in_array($value, tipoPaqueteValidValues(), true);
}
}
if (!function_exists('ensureTipoPaqueteEnumDefinition')) {
function ensureTipoPaqueteEnumDefinition(PDO $pdo, bool $force = false): void
{
static $checked = false;
if ($checked && !$force) {
return;
}
$stmt = $pdo->query("SHOW COLUMNS FROM pedidos LIKE 'tipo_paquete'");
$column = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : false;
if (!$column || empty($column['Type'])) {
$checked = true;
return;
}
$currentType = (string) $column['Type'];
$validValues = tipoPaqueteValidValues();
$missingRequiredValue = false;
foreach ($validValues as $value) {
if (strpos($currentType, "'" . str_replace("'", "''", $value) . "'") === false) {
$missingRequiredValue = true;
break;
}
}
$hasLegacyPendiente = strpos($currentType, "'PENDIENTE'") !== false;
$hasLegacyEmpaquetado = strpos($currentType, "'EMPAQUETADO'") !== false;
if ($force || $missingRequiredValue || $hasLegacyPendiente || $hasLegacyEmpaquetado) {
$quotedValues = array_map(static function (string $value) use ($pdo): string {
return $pdo->quote($value);
}, $validValues);
$enumValuesSql = implode(', ', $quotedValues);
$transitionValues = array_values(array_unique(array_merge(
$validValues,
['EMPAQUETADO', 'PENDIENTE', 'NO CONTESTA', 'VOLVER A LLAMAR']
)));
$quotedTransitionValues = array_map(static function (string $value) use ($pdo): string {
return $pdo->quote($value);
}, $transitionValues);
$transitionValuesSql = implode(', ', $quotedTransitionValues);
$pdo->exec("ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM($transitionValuesSql) DEFAULT NULL");
$pdo->exec("UPDATE pedidos SET tipo_paquete = 'PREPARADO' WHERE tipo_paquete = 'EMPAQUETADO'");
$pdo->exec("UPDATE pedidos SET tipo_paquete = 'NO CONTESTA, VOLVER A LLAMAR' WHERE tipo_paquete IN ('NO CONTESTA', 'VOLVER A LLAMAR')");
$pdo->exec("UPDATE pedidos SET tipo_paquete = NULL WHERE tipo_paquete = 'PENDIENTE'");
$pdo->exec("UPDATE pedidos SET tipo_paquete = NULL WHERE tipo_paquete IS NOT NULL AND tipo_paquete NOT IN ($enumValuesSql)");
$pdo->exec("ALTER TABLE pedidos MODIFY COLUMN tipo_paquete ENUM($enumValuesSql) DEFAULT NULL");
}
$checked = true;
}
}

View File

@ -7,7 +7,9 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db/config.php';
require_once 'includes/contraentrega_cobertura.php';
require_once 'includes/tipo_paquete.php';
$pdo = db();
ensureTipoPaqueteEnumDefinition($pdo);
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
@ -320,7 +322,7 @@ include 'layout_header.php';
<option value="">Seleccionar</option>
<?php
$current_paquete = $pedido['tipo_paquete'] ?? '';
$paquetes = ['RUTA', 'EMPAQUETADO', 'ANULADO', 'PENDIENTE A RETORNO', 'RETORNADO'];
$paquetes = tipoPaquetePrimarySelectorValues();
foreach ($paquetes as $paquete) {
$selected = ($current_paquete == $paquete) ? 'selected' : '';
echo "<option value='{$paquete}' {$selected}>" . htmlspecialchars($paquete) . "</option>";
@ -333,7 +335,7 @@ include 'layout_header.php';
<select class="form-select" id="estado" name="estado" required>
<?php
$current_status = $pedido['estado'] ?? 'RUTA_CONTRAENTREGA';
$statuses = ['RUTA_CONTRAENTREGA', 'NO CONTESTO, VOLVER A LLAMAR', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA'];
$statuses = ['RUTA_CONTRAENTREGA', 'PENDIENTE', 'NO CONTESTO, VOLVER A LLAMAR', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA'];
foreach ($statuses as $status) {
$selected = ($current_status == $status) ? 'selected' : '';
echo "<option value='{$status}' {$selected}>" . htmlspecialchars($status) . "</option>";

View File

@ -6,6 +6,7 @@ if (!isset($_SESSION['user_id'])) {
}
require_once 'db/config.php';
require_once __DIR__ . '/includes/tipo_paquete.php';
function getStatusStyle($status) {
$style = 'color: white;'; // Default text color
@ -64,7 +65,8 @@ function getPaqueteStyle($paquete) {
if ($paquete === 'NO CONTESTA, VOLVER A LLAMAR') return 'background-color: #fd7e14; color: white;'; // Orange
if ($paquete === 'PENDIENTE A RETORNO') return 'background-color: #dc3545; color: white;'; // Red
if ($paquete === 'COMPLETADO') return 'background-color: #198754; color: white;'; // Success green
if ($paquete === "EMPAQUETADO") return "background-color: #20c997; color: white;"; // Empaquetado
if ($paquete === 'PREPARADO') return 'background-color: #20c997; color: white;'; // Preparado
if ($paquete === 'EMPAQUETADO') return 'background-color: #20c997; color: white;'; // Legacy alias
if ($paquete === 'RETORNADO') return 'background-color: #dc3545; color: white;'; // Same red as PENDIENTE A RETORNO
if ($paquete === 'ANULADO') return 'background-color: #212529; color: white;'; // Dark
return 'background-color: #6c757d; color: white;'; // Secondary grey
@ -82,6 +84,7 @@ function getFechaEntregaStyle($fecha) {
}
$pdo = db();
ensureTipoPaqueteEnumDefinition($pdo);
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
@ -506,10 +509,38 @@ function getPaqueteStyleJS(paquete) {
if (paquete === 'PENDIENTE A RETORNO') return 'background-color: #dc3545; color: white;';
if (paquete === 'RETORNADO') return 'background-color: #dc3545; color: white;';
if (paquete === 'COMPLETADO') return 'background-color: #198754; color: white;';
if (paquete === "EMPAQUETADO") return "background-color: #20c997; color: white;";
if (paquete === 'PREPARADO') return 'background-color: #20c997; color: white;';
if (paquete === 'EMPAQUETADO') return 'background-color: #20c997; color: white;';
return 'background-color: #6c757d; color: white;';
}
function normalizeTipoPaqueteValueClient(value) {
if (!value) return '';
const cleaned = String(value)
.replace(/[\u00A0\u200B-\u200D\uFEFF]/g, ' ')
.replace(/[\x00-\x1F\x7F]/g, ' ')
.trim()
.replace(/\s+/g, ' ')
.toUpperCase();
const compact = cleaned.replace(/[^A-Z]+/g, '');
const aliases = {
RUTA: 'RUTA',
CONTRAENTREGA: 'CONTRAENTREGA',
NOCONTESTAVOLVERALLAMAR: 'NO CONTESTA, VOLVER A LLAMAR',
PENDIENTEARETORNO: 'PENDIENTE A RETORNO',
COMPLETADO: 'COMPLETADO',
EMPAQUETADO: 'PREPARADO',
PREPARADO: 'PREPARADO',
EMPAQUETARDO: 'PREPARADO',
RETORNADO: 'RETORNADO',
ANULADO: 'ANULADO'
};
return aliases[compact] || cleaned;
}
function getStatusStyleJS(status) {
let bgColor = '#0dcaf0';
let style = 'color: white;';
@ -576,15 +607,12 @@ document.addEventListener('DOMContentLoaded', function() {
const select = document.createElement('select');
select.className = 'form-select form-select-sm';
const options = [
{val: '', text: 'Seleccionar'},
{val: 'RUTA', text: 'RUTA'},
{val: 'ANULADO', text: 'ANULADO'},
{val: 'PENDIENTE A RETORNO', text: 'PENDIENTE A RETORNO'},
{val: 'RETORNADO', text: 'RETORNADO'},
{val: "EMPAQUETADO", text: "EMPAQUETADO"},
{val: 'COMPLETADO', text: 'COMPLETADO'}
];
const options = <?php echo json_encode(array_merge(
[['val' => '', 'text' => 'Seleccionar']],
array_map(static function ($paquete) {
return ['val' => $paquete, 'text' => $paquete];
}, tipoPaquetePrimarySelectorValues())
), JSON_UNESCAPED_UNICODE); ?>;
options.forEach(opt => {
const option = document.createElement('option');
@ -602,7 +630,7 @@ document.addEventListener('DOMContentLoaded', function() {
select.focus();
const save = () => {
const newVal = select.value;
const newVal = normalizeTipoPaqueteValueClient(select.value);
// Optimistic update
cell.dataset.value = newVal;

View File

@ -11,6 +11,7 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db/config.php';
require_once __DIR__ . '/includes/contraentrega_cobertura.php';
require_once __DIR__ . '/includes/tipo_paquete.php';
function redirectPedidoContraentregaWithError(string $message, $id = null, bool $embed = false): void
{
@ -46,6 +47,7 @@ function normalizeContraentregaCoordinates(string $value): ?string
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo = db();
ensureTipoPaqueteEnumDefinition($pdo);
$id = $_POST['id'] ?? null;
$embed_mode = !empty($_POST['embed']) && $_POST['embed'] === '1';
@ -115,11 +117,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Normalizar Paquete: el selector "Seleccionar" llega como cadena vacía,
// pero la columna ENUM permite NULL y no permite ''.
$tipo_paquete = isset($_POST['tipo_paquete']) ? trim((string)$_POST['tipo_paquete']) : null;
if ($tipo_paquete === '') {
$tipo_paquete = null;
}
$tipos_paquete_validos = ['RUTA', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'PENDIENTE A RETORNO', 'COMPLETADO', 'EMPAQUETADO', 'RETORNADO', 'ANULADO'];
$tipo_paquete = normalizeTipoPaqueteValue($_POST['tipo_paquete'] ?? null);
$tipos_paquete_validos = tipoPaqueteValidValues();
if ($tipo_paquete !== null && !in_array($tipo_paquete, $tipos_paquete_validos, true)) {
redirectPedidoContraentregaWithError('El paquete seleccionado no es válido. Seleccione una opción de la lista.', $id, $embed_mode);
}

View File

@ -1,17 +1,60 @@
<?php
session_start();
require_once 'db/config.php';
require_once __DIR__ . '/includes/tipo_paquete.php';
header('Content-Type: application/json');
if (!function_exists('normalizeTipoPaqueteValue')) {
function normalizeTipoPaqueteValue($value)
{
if ($value === null || is_array($value)) {
return null;
}
$value = (string) $value;
$value = preg_replace('/[\x{00A0}\x{200B}-\x{200D}\x{FEFF}]+/u', ' ', $value);
$value = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $value);
$value = preg_replace('/\s+/u', ' ', trim($value));
if ($value === '') {
return null;
}
$normalized = function_exists('mb_strtoupper') ? mb_strtoupper($value, 'UTF-8') : strtoupper($value);
$match = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalized);
if ($match === false || $match === null || $match === '') {
$match = $normalized;
}
$canonicalKey = preg_replace('/[^A-Z]+/', '', strtoupper($match));
$aliases = [
'RUTA' => 'RUTA',
'CONTRAENTREGA' => 'CONTRAENTREGA',
'NOCONTESTAVOLVERALLAMAR' => 'NO CONTESTA, VOLVER A LLAMAR',
'PENDIENTEARETORNO' => 'PENDIENTE A RETORNO',
'COMPLETADO' => 'COMPLETADO',
'EMPAQUETADO' => 'PREPARADO',
'PREPARADO' => 'PREPARADO',
'EMPAQUETARDO' => 'PREPARADO',
'RETORNADO' => 'RETORNADO',
'ANULADO' => 'ANULADO',
];
return $aliases[$canonicalKey] ?? $normalized;
}
}
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'message' => 'No autorizado']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pedido_id = $_POST['pedido_id'] ?? null;
$tipo_paquete = $_POST['tipo_paquete'] ?? null;
$pedido_id_raw = $_POST['pedido_id'] ?? null;
$pedido_id = (is_numeric($pedido_id_raw) && (int)$pedido_id_raw > 0) ? (int)$pedido_id_raw : null;
$tipo_paquete = normalizeTipoPaqueteValue($_POST['tipo_paquete'] ?? null);
if (!$pedido_id) {
echo json_encode(['success' => false, 'message' => 'ID de pedido faltante']);
@ -20,18 +63,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$pdo = db();
if (empty($tipo_paquete)) {
ensureTipoPaqueteEnumDefinition($pdo);
if ($tipo_paquete === null) {
// Allow clearing the selection
$stmt = $pdo->prepare("UPDATE pedidos SET tipo_paquete = NULL WHERE id = ?");
$result = $stmt->execute([$pedido_id]);
} else {
$valid_types = ['RUTA', 'ANULADO', 'PENDIENTE A RETORNO', 'RETORNADO', 'CONTRAENTREGA', 'NO CONTESTA, VOLVER A LLAMAR', 'COMPLETADO', 'EMPAQUETADO'];
if (!in_array($tipo_paquete, $valid_types)) {
$valid_types = tipoPaqueteValidValues();
if (!in_array($tipo_paquete, $valid_types, true)) {
echo json_encode(['success' => false, 'message' => 'Tipo inválido']);
exit;
}
$stmt = $pdo->prepare("UPDATE pedidos SET tipo_paquete = ? WHERE id = ?");
$result = $stmt->execute([$tipo_paquete, $pedido_id]);
}
@ -47,4 +91,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
echo json_encode(['success' => false, 'message' => 'Método no permitido']);
}
?>
?>