Autosave: 20260722-224452

This commit is contained in:
Flatlogic Bot 2026-07-22 22:43:29 +00:00
parent 0897d311c3
commit 88aaaa4cae
9 changed files with 303 additions and 49 deletions

View File

@ -528,6 +528,64 @@ h1, .h1 {
height: 320px !important;
}
.cc-kpi-status-legend {
border: 1px solid rgba(15, 23, 42, .08);
border-radius: 1rem;
padding: 1rem;
background: linear-gradient(180deg, rgba(248, 250, 252, .94), rgba(255, 255, 255, .98));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6);
display: flex;
flex-direction: column;
gap: .65rem;
}
.cc-kpi-status-item {
display: flex;
align-items: center;
gap: .75rem;
padding: .75rem .85rem;
border-radius: .95rem;
border: 1px solid rgba(15, 23, 42, .06);
background: #fff;
box-shadow: 0 .55rem 1rem rgba(15, 23, 42, .04);
}
.cc-kpi-status-dot {
width: .9rem;
height: .9rem;
border-radius: 999px;
flex: 0 0 .9rem;
border: 2px solid rgba(255, 255, 255, .9);
box-shadow: 0 0 0 1px rgba(15, 23, 42, .06);
}
.cc-kpi-status-text {
min-width: 0;
flex: 1 1 auto;
}
.cc-kpi-status-label {
font-size: .94rem;
font-weight: 700;
line-height: 1.1;
color: #111827;
}
.cc-kpi-status-count {
font-size: .78rem;
color: #6c757d;
line-height: 1.1;
margin-top: .18rem;
}
.cc-kpi-status-percent {
font-size: 1.05rem;
font-weight: 800;
line-height: 1;
color: #111827;
white-space: nowrap;
}
.cc-kpi-footnote {
font-size: .85rem;
color: #6c757d;
@ -543,6 +601,18 @@ h1, .h1 {
min-height: 280px;
height: 280px !important;
}
.cc-kpi-status-legend {
padding: .9rem;
}
.cc-kpi-status-item {
padding: .7rem .75rem;
}
.cc-kpi-status-percent {
font-size: 1rem;
}
}
@media (max-width: 575.98px) {

View File

@ -479,15 +479,15 @@ try {
? ($selectedAssessorFilterLabel !== '' ? 'KPI diario · ' . $selectedAssessorFilterLabel : 'KPI diario por asesora')
: 'Mi KPI diario';
$performanceSubtitle = $isAdmin
? 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos de días anteriores confirmados hoy.'
: 'Tus KPIs de hoy se calculan sobre tus pedidos asignados. Confirmación y recuperación se separan para ver tu productividad real.';
? 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos asignados antes de hoy confirmados hoy.'
: 'Tus KPIs de hoy se calculan sobre tus pedidos asignados hoy. Confirmación = asignados hoy confirmados hoy. Recuperación = asignados antes de hoy confirmados hoy.';
$performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [
'title' => $performanceTitle,
'subtitle' => $performanceSubtitle,
'chart1_title' => $isAdmin ? 'Rendimiento por asesora hoy' : 'Mi rendimiento de hoy',
'chart1_note' => 'Asignados, confirmados nuevos, recuperados y cancelados de hoy.',
'chart1_title' => $isAdmin ? 'Distribución de estados' : 'Mi distribución de estados',
'chart1_note' => 'Verde = confirmados de pedidos asignados hoy, amarillo = confirmados de pedidos asignados antes de hoy, azul = seguimiento abierto, gris = repetidos, rojo = cancelados. La lista lateral muestra el % de cada estado.',
'chart2_title' => $isAdmin ? 'Evolución de los últimos 7 días' : 'Tu evolución de los últimos 7 días',
'chart2_note' => 'La línea de confirmados separa pedidos nuevos y recuperados.',
'chart2_note' => 'La línea de confirmados separa pedidos asignados hoy y pedidos asignados antes de hoy.',
'footnote' => 'Seguimiento abierto = pedidos en SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.',
]);

View File

@ -678,11 +678,11 @@ try {
$performanceDashboard = cc_test_build_performance_dashboard($orders, $assessors, 7);
$performanceDashboardHtml = cc_test_render_performance_dashboard($performanceDashboard, [
'title' => 'KPI diario por asesora',
'subtitle' => 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos de días anteriores confirmados hoy.',
'chart1_title' => 'Rendimiento por asesora hoy',
'chart1_note' => 'Asignados, confirmados nuevos, recuperados y cancelados de hoy.',
'subtitle' => 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos asignados antes de hoy confirmados hoy.',
'chart1_title' => 'Distribución de estados',
'chart1_note' => 'Verde = confirmados de pedidos asignados hoy, amarillo = confirmados de pedidos asignados antes de hoy, azul = seguimiento abierto, gris = repetidos, rojo = cancelados. La lista lateral muestra el % de cada estado.',
'chart2_title' => 'Evolución de los últimos 7 días',
'chart2_note' => 'La línea de confirmados separa pedidos nuevos y recuperados.',
'chart2_note' => 'La línea de confirmados separa pedidos asignados hoy y pedidos asignados antes de hoy.',
'footnote' => 'Seguimiento abierto = pedidos en SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.',
]);

View File

@ -725,6 +725,7 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
'recovered' => 0,
'canceled' => 0,
'followup_open' => 0,
'repeated' => 0,
];
}
@ -735,6 +736,7 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
'confirmed_today_recovered' => 0,
'confirmed_today_total' => 0,
'canceled_today' => 0,
'repeated_today' => 0,
'followup_open' => 0,
'confirm_rate_today' => 0.0,
'recovery_rate_today' => 0.0,
@ -760,6 +762,7 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
'recovered' => 0,
'canceled' => 0,
'followup_open' => 0,
'repeated' => 0,
];
}
@ -774,6 +777,7 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
$isPromoFinal = !empty($followupInfo['is_promo_final']) && !empty($order['promo_final_evidence_path']);
$isConfirmed = in_array($estado, cc_test_confirmed_states(), true);
$isCanceled = $estado === 'CANCELADO';
$isRepeated = $estado === 'REPETIDO';
$isFollowupOpen = $estado === 'SE ENVIO NUMERO DE CUENTA' && !$isPromoFinal;
if ($assignedKey === $todayKey) {
@ -790,8 +794,10 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
$summary['followup_open']++;
}
$confirmedReferenceKey = $assignedKey ?? $originKey;
if ($isConfirmed && $actionKey === $todayKey) {
if ($originKey !== null && $originKey < $todayKey) {
if ($confirmedReferenceKey !== null && $confirmedReferenceKey < $todayKey) {
$bucketStats[$bucketKey]['recovered']++;
$summary['confirmed_today_recovered']++;
} else {
@ -800,7 +806,7 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
}
if (isset($trend[$actionKey])) {
if ($originKey !== null && $originKey < $actionKey) {
if ($confirmedReferenceKey !== null && $confirmedReferenceKey < $actionKey) {
$trend[$actionKey]['recovered']++;
} else {
$trend[$actionKey]['confirmed_new']++;
@ -816,6 +822,11 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
$trend[$actionKey]['canceled']++;
}
}
if ($isRepeated && $actionKey === $todayKey) {
$bucketStats[$bucketKey]['repeated']++;
$summary['repeated_today']++;
}
}
$summary['confirmed_today_total'] = $summary['confirmed_today_new'] + $summary['confirmed_today_recovered'];
@ -886,6 +897,37 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
return [
'summary' => $summary,
'status_chart' => [
'labels' => ['Confirmados nuevos', 'Recuperados', 'Seguimiento abierto', 'Repetidos', 'Cancelados'],
'datasets' => [
[
'label' => 'Estados',
'data' => [
(int) ($summary['confirmed_today_new'] ?? 0),
(int) ($summary['confirmed_today_recovered'] ?? 0),
(int) ($summary['followup_open'] ?? 0),
(int) ($summary['repeated_today'] ?? 0),
(int) ($summary['canceled_today'] ?? 0),
],
'backgroundColor' => [
'rgba(25, 135, 84, 0.9)',
'rgba(255, 193, 7, 0.92)',
'rgba(13, 110, 253, 0.88)',
'rgba(108, 117, 125, 0.84)',
'rgba(220, 53, 69, 0.9)',
],
'borderColor' => [
'rgba(25, 135, 84, 1)',
'rgba(245, 158, 11, 1)',
'rgba(13, 110, 253, 1)',
'rgba(108, 117, 125, 1)',
'rgba(220, 53, 69, 1)',
],
'borderWidth' => 2,
'hoverOffset' => 6,
],
],
],
'advisor_count' => $advisorCount,
'advisor_chart' => [
'labels' => $advisorLabels,
@ -983,11 +1025,11 @@ function cc_test_build_performance_dashboard(array $orders, array $assessors, in
function cc_test_render_performance_dashboard(array $dashboard, array $options = []): string
{
$title = trim((string) ($options['title'] ?? 'KPI diario por asesora'));
$subtitle = trim((string) ($options['subtitle'] ?? 'Confirmación = pedidos de hoy cerrados hoy. Recuperación = pedidos de otros días cerrados hoy.'));
$chart1Title = trim((string) ($options['chart1_title'] ?? 'Rendimiento por asesora hoy'));
$chart1Note = trim((string) ($options['chart1_note'] ?? 'Asignados, confirmados nuevos, recuperados y cancelados de hoy.'));
$subtitle = trim((string) ($options['subtitle'] ?? 'Confirmación = pedidos asignados hoy confirmados hoy. Recuperación = pedidos asignados antes de hoy confirmados hoy.'));
$chart1Title = trim((string) ($options['chart1_title'] ?? 'Distribución de estados'));
$chart1Note = trim((string) ($options['chart1_note'] ?? 'Verde = confirmados de pedidos asignados hoy, amarillo = confirmados de pedidos asignados antes de hoy, azul = seguimiento abierto, gris = repetidos, rojo = cancelados. La lista lateral muestra el % de cada estado.'));
$chart2Title = trim((string) ($options['chart2_title'] ?? 'Evolución de los últimos 7 días'));
$chart2Note = trim((string) ($options['chart2_note'] ?? 'La línea de confirmados separa pedidos nuevos y recuperados.'));
$chart2Note = trim((string) ($options['chart2_note'] ?? 'La línea de confirmados separa pedidos asignados hoy y pedidos asignados antes de hoy.'));
$footnote = trim((string) ($options['footnote'] ?? 'Seguimiento abierto = pedidos en SE ENVIO NUMERO DE CUENTA que todavía no pasan a Promo Final.'));
$sectionId = preg_replace('/[^A-Za-z0-9_-]+/', '', (string) ($options['section_id'] ?? 'ccPerformanceDashboard'));
if ($sectionId === '') {
@ -995,9 +1037,38 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options =
}
$summary = (array) ($dashboard['summary'] ?? []);
$advisorChart = (array) ($dashboard['advisor_chart'] ?? []);
$trendChart = (array) ($dashboard['trend_chart'] ?? []);
$advisorCount = (int) ($dashboard['advisor_count'] ?? 0);
$statusTotal = (int) ($summary['confirmed_today_new'] ?? 0)
+ (int) ($summary['confirmed_today_recovered'] ?? 0)
+ (int) ($summary['followup_open'] ?? 0)
+ (int) ($summary['repeated_today'] ?? 0)
+ (int) ($summary['canceled_today'] ?? 0);
$statusLegendItems = [];
$statusLegendRows = [
['label' => 'Confirmados nuevos', 'value' => (int) ($summary['confirmed_today_new'] ?? 0), 'color' => 'rgba(25, 135, 84, 0.9)'],
['label' => 'Recuperados', 'value' => (int) ($summary['confirmed_today_recovered'] ?? 0), 'color' => 'rgba(255, 193, 7, 0.92)'],
['label' => 'Seguimiento abierto', 'value' => (int) ($summary['followup_open'] ?? 0), 'color' => 'rgba(13, 110, 253, 0.88)'],
['label' => 'Repetidos', 'value' => (int) ($summary['repeated_today'] ?? 0), 'color' => 'rgba(108, 117, 125, 0.84)'],
['label' => 'Cancelados', 'value' => (int) ($summary['canceled_today'] ?? 0), 'color' => 'rgba(220, 53, 69, 0.9)'],
];
if ($statusTotal > 0) {
foreach ($statusLegendRows as $row) {
$statusLegendItems[] = [
'label' => $row['label'],
'value' => (int) $row['value'],
'percentage' => round(((float) $row['value'] / $statusTotal) * 100, 1),
'color' => $row['color'],
];
}
} else {
$statusLegendItems[] = [
'label' => 'Sin estados',
'value' => 0,
'percentage' => 0.0,
'color' => 'rgba(108, 117, 125, 0.65)',
];
}
$badges = [
['label' => 'Asignados hoy', 'value' => (int) ($summary['assigned_today'] ?? 0), 'class' => 'cc-kpi-pill-primary'],
@ -1016,6 +1087,9 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options =
if (isset($summary['confirmed_today_recovered'])) {
$extraLineParts[] = (int) $summary['confirmed_today_recovered'] . ' recuperados';
}
if (isset($summary['repeated_today'])) {
$extraLineParts[] = (int) $summary['repeated_today'] . ' repetidos';
}
if (isset($summary['canceled_today'])) {
$extraLineParts[] = (int) $summary['canceled_today'] . ' cancelados';
}
@ -1057,10 +1131,29 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options =
<h3 class="h6 fw-bold mb-1"><?php echo htmlspecialchars($chart1Title); ?></h3>
<div class="text-muted small"><?php echo htmlspecialchars($chart1Note); ?></div>
</div>
<span class="badge text-bg-light border align-self-start"><?php echo (int) $advisorCount; ?> <?php echo $advisorCount === 1 ? 'asesora' : 'asesoras'; ?></span>
<span class="badge text-bg-light border align-self-start"><?php echo (int) $statusTotal; ?> <?php echo $statusTotal === 1 ? 'estado' : 'estados'; ?></span>
</div>
<div class="cc-kpi-chart-wrap mt-3">
<canvas id="<?php echo htmlspecialchars($sectionId . 'AdvisorChart'); ?>"></canvas>
<div class="row g-3 mt-3 align-items-stretch">
<div class="col-12 col-xl-7">
<div class="cc-kpi-chart-wrap">
<canvas id="<?php echo htmlspecialchars($sectionId . 'StatusChart'); ?>"></canvas>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="cc-kpi-status-legend h-100">
<div class="small text-uppercase text-muted fw-semibold mb-2">Detalle por estado (%)</div>
<?php foreach ($statusLegendItems as $item): ?>
<div class="cc-kpi-status-item">
<span class="cc-kpi-status-dot" style="background: <?php echo htmlspecialchars((string) $item['color']); ?>;"></span>
<div class="cc-kpi-status-text">
<div class="cc-kpi-status-label"><?php echo htmlspecialchars((string) $item['label']); ?></div>
<div class="cc-kpi-status-count"><?php echo (int) $item['value']; ?> pedidos</div>
</div>
<div class="cc-kpi-status-percent"><?php echo htmlspecialchars(number_format((float) $item['percentage'], 1)); ?>%</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
@ -1092,51 +1185,104 @@ function cc_test_render_performance_dashboard(array $dashboard, array $options =
<script>
(function () {
const dashboard = <?php echo json_encode($dashboard, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
const advisorCanvas = document.getElementById(<?php echo json_encode($sectionId . 'AdvisorChart', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>);
const statusCanvas = document.getElementById(<?php echo json_encode($sectionId . 'StatusChart', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>);
const trendCanvas = document.getElementById(<?php echo json_encode($sectionId . 'TrendChart', JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>);
if (!window.Chart) {
return;
}
const advisorData = dashboard.advisor_chart || { labels: [], datasets: [] };
const statusData = dashboard.status_chart || { labels: [], datasets: [] };
const trendData = dashboard.trend_chart || { labels: [], datasets: [] };
const statusLabels = Array.isArray(statusData.labels) ? statusData.labels : [];
const statusDataset = Array.isArray(statusData.datasets) && statusData.datasets.length > 0 && statusData.datasets[0]
? statusData.datasets[0]
: { label: 'Estados', data: [] };
const statusValues = Array.isArray(statusDataset.data)
? statusDataset.data.map(function (value) {
return Number(value) || 0;
})
: [];
const statusTotal = statusValues.reduce(function (sum, value) {
return sum + value;
}, 0);
const statusHasData = statusTotal > 0;
const statusBackgroundColors = Array.isArray(statusDataset.backgroundColor) && statusDataset.backgroundColor.length === statusValues.length
? statusDataset.backgroundColor
: [
'rgba(25, 135, 84, 0.9)',
'rgba(255, 193, 7, 0.92)',
'rgba(13, 110, 253, 0.88)',
'rgba(108, 117, 125, 0.84)',
'rgba(220, 53, 69, 0.9)'
];
const statusBorderColors = Array.isArray(statusDataset.borderColor) && statusDataset.borderColor.length === statusValues.length
? statusDataset.borderColor
: [
'rgba(25, 135, 84, 1)',
'rgba(245, 158, 11, 1)',
'rgba(13, 110, 253, 1)',
'rgba(108, 117, 125, 1)',
'rgba(220, 53, 69, 1)'
];
const statusChartData = statusHasData
? {
labels: statusLabels,
datasets: [{
label: statusDataset.label || 'Estados',
data: statusValues,
backgroundColor: statusBackgroundColors,
borderColor: statusBorderColors,
borderWidth: 2,
hoverOffset: 6
}]
}
: {
labels: ['Sin estados'],
datasets: [{
label: 'Sin datos',
data: [1],
backgroundColor: ['rgba(108, 117, 125, 0.22)'],
borderColor: ['rgba(108, 117, 125, 0.55)'],
borderWidth: 2,
hoverOffset: 0
}]
};
if (advisorCanvas) {
new Chart(advisorCanvas, {
type: 'bar',
data: advisorData,
if (statusCanvas) {
new Chart(statusCanvas, {
type: 'doughnut',
data: statusChartData,
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
cutout: '58%',
radius: '92%',
plugins: {
legend: {
position: 'bottom'
display: false
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
x: {
beginAtZero: true,
ticks: {
precision: 0,
stepSize: 1
}
},
y: {
ticks: {
autoSkip: false
callbacks: {
label: function(context) {
if (!statusHasData) {
return 'Sin estados';
}
const label = context.label || '';
const value = typeof context.raw === 'number' ? context.raw : Number(context.raw || 0);
const data = Array.isArray(context.dataset.data) ? context.dataset.data : [];
const total = data.reduce(function (sum, item) {
return sum + (Number(item) || 0);
}, 0);
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
return label + ': ' + value + ' (' + percentage + '%)';
}
}
}
}
}
});
}
if (trendCanvas) {
new Chart(trendCanvas, {
type: 'line',

View File

@ -13,6 +13,8 @@ ensureTipoPaqueteEnumDefinition($pdo);
$user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
$can_edit_estado_contraentrega = !in_array($user_role, ['Logistica', 'Logística'], true);
$can_edit_paquete_contraentrega = !in_array($user_role, ['Control Logistico', 'Control Logístico'], true);
$pedido = [
'id' => '',
@ -318,7 +320,7 @@ include 'layout_header.php';
<div class="row">
<div class="col-md-4 mb-3">
<label for="tipo_paquete" class="form-label">Paquete</label>
<select class="form-select" id="tipo_paquete" name="tipo_paquete">
<select class="form-select" id="tipo_paquete" name="tipo_paquete" <?php echo (!empty($pedido['id']) && !$can_edit_paquete_contraentrega) ? 'disabled' : ''; ?>>
<option value="">Seleccionar</option>
<?php
$current_paquete = $pedido['tipo_paquete'] ?? '';
@ -332,7 +334,7 @@ include 'layout_header.php';
</div>
<div class="col-md-4 mb-3">
<label for="estado" class="form-label">Estado del Pedido</label>
<select class="form-select" id="estado" name="estado" required>
<select class="form-select" id="estado" name="estado" required <?php echo (!empty($pedido['id']) && !$can_edit_estado_contraentrega) ? 'disabled' : ''; ?>>
<?php
$current_status = $pedido['estado'] ?? 'RUTA_CONTRAENTREGA';
$statuses = ['RUTA_CONTRAENTREGA', 'PENDIENTE', 'NO CONTESTO, VOLVER A LLAMAR', 'CANCELADO', 'REPROGRAMADO', 'ENTREGA EXITOSA'];

View File

@ -86,6 +86,11 @@ $user_id = $_SESSION['user_id'];
$user_role = $_SESSION['user_role'] ?? 'Asesor';
$is_admin = in_array($user_role, ['Administrador', 'admin'], true);
$can_edit_status = !in_array($user_role, ['Logistica', 'Logística'], true);
$can_edit_paquete = !in_array($user_role, ['Control Logistico', 'Control Logístico'], true);
$estado_cell_title = $can_edit_status ? 'Haz clic para editar el estado' : 'Tu rol no puede editar el estado en esta sección';
$paquete_cell_title = $can_edit_paquete ? 'Haz clic para editar el paquete' : 'Tu rol no puede editar el paquete en esta sección';
// Fetch years for the filter
$years_query = "SELECT DISTINCT YEAR(created_at) as year FROM pedidos";
if ($user_role === 'Asesor') {
@ -244,6 +249,10 @@ include 'layout_header.php';
white-space: normal;
font-size: 0.82rem;
}
#pedidos-table .cc-cell-readonly {
cursor: not-allowed !important;
opacity: 0.9;
}
/* Contraentrega: icono para ver/subir verificación (foto + tracking) */
@ -414,12 +423,12 @@ include 'layout_header.php';
</div>
</td>
<td class="editable" data-id="<?php echo $pedido['id']; ?>" data-field="seguimiento"><?php echo htmlspecialchars($pedido['seguimiento'] ?? ''); ?></td>
<td class="editable-paquete" data-id="<?php echo $pedido['id']; ?>" data-value="<?php echo htmlspecialchars($pedido['tipo_paquete'] ?? ''); ?>" style="cursor: pointer;">
<td class="editable-paquete<?php echo $can_edit_paquete ? '' : ' cc-cell-readonly'; ?>" data-id="<?php echo $pedido['id']; ?>" data-value="<?php echo htmlspecialchars($pedido['tipo_paquete'] ?? ''); ?>" style="cursor: <?php echo $can_edit_paquete ? 'pointer' : 'not-allowed'; ?>;" title="<?php echo htmlspecialchars($paquete_cell_title, ENT_QUOTES, 'UTF-8'); ?>" aria-disabled="<?php echo $can_edit_paquete ? 'false' : 'true'; ?>">
<span class="badge" style="<?php echo getPaqueteStyle($pedido['tipo_paquete'] ?? ''); ?>">
<?php echo htmlspecialchars(!empty($pedido['tipo_paquete']) ? $pedido['tipo_paquete'] : 'Seleccionar'); ?>
</span>
</td>
<td class="editable-estado" data-id="<?php echo $pedido['id']; ?>" data-value="<?php echo htmlspecialchars($pedido['estado'] ?? ''); ?>" style="cursor: pointer;">
<td class="editable-estado<?php echo $can_edit_status ? '' : ' cc-cell-readonly'; ?>" data-id="<?php echo $pedido['id']; ?>" data-value="<?php echo htmlspecialchars($pedido['estado'] ?? ''); ?>" style="cursor: <?php echo $can_edit_status ? 'pointer' : 'not-allowed'; ?>;" title="<?php echo htmlspecialchars($estado_cell_title, ENT_QUOTES, 'UTF-8'); ?>" aria-disabled="<?php echo $can_edit_status ? 'false' : 'true'; ?>">
<span class="badge" style="<?php echo getStatusStyle($pedido['estado'] ?? ''); ?>">
<?php echo htmlspecialchars(!empty($pedido['estado']) ? $pedido['estado'] : 'Seleccionar'); ?>
</span>
@ -931,6 +940,9 @@ function getStatusStyleJS(status) {
return `background-color: ${bgColor} !important; ${style}`;
}
const canEditPaquete = <?php echo $can_edit_paquete ? 'true' : 'false'; ?>;
const canEditEstado = <?php echo $can_edit_status ? 'true' : 'false'; ?>;
$(document).ready(function() {
$('#pedidos-table').DataTable({
"language": {
@ -952,6 +964,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (e.target && (e.target.classList.contains('editable-paquete') || e.target.closest('.editable-paquete'))) {
const cell = e.target.classList.contains('editable-paquete') ? e.target : e.target.closest('.editable-paquete');
if (!canEditPaquete) return;
if (cell.querySelector('select')) return; // Already editing
const currentVal = cell.dataset.value;
@ -1024,6 +1037,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (e.target && (e.target.classList.contains('editable-estado') || e.target.closest('.editable-estado'))) {
const cell = e.target.classList.contains('editable-estado') ? e.target : e.target.closest('.editable-estado');
if (!canEditEstado) return;
if (cell.querySelector('select')) return; // Already editing
const currentVal = cell.dataset.value;

View File

@ -197,6 +197,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($id) {
// UPDATE: Explicit and safe query
$user_role = $_SESSION['user_role'] ?? '';
$can_edit_estado_contraentrega = !in_array($user_role, ['Logistica', 'Logística'], true);
$can_edit_paquete_contraentrega = !in_array($user_role, ['Control Logistico', 'Control Logístico'], true);
$stmt_current = $pdo->prepare('SELECT estado, tipo_paquete FROM pedidos WHERE id = ?');
$stmt_current->execute([$id]);
$current_pedido = $stmt_current->fetch(PDO::FETCH_ASSOC);
if (!$current_pedido) {
die('Pedido no encontrado.');
}
$sql_parts = [
"dni_cliente = :dni_cliente",
@ -224,7 +233,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
"estado = :estado",
"tipo_paquete = :tipo_paquete",
];
$params['estado'] = $_POST['estado'] ?? 'RUTA_CONTRAENTREGA';
$params['estado'] = $can_edit_estado_contraentrega ? ($_POST['estado'] ?? 'RUTA_CONTRAENTREGA') : ($current_pedido['estado'] ?? 'RUTA_CONTRAENTREGA');
$params['tipo_paquete'] = $can_edit_paquete_contraentrega ? $tipo_paquete : ($current_pedido['tipo_paquete'] ?? null);
// Conditionally add asesor_id
if ($user_role === 'Administrador' && !empty($_POST['asesor_id'])) {

View File

@ -9,6 +9,12 @@ if (!isset($_SESSION['user_id'])) {
exit;
}
$user_role = $_SESSION['user_role'] ?? '';
if (in_array($user_role, ['Logistica', 'Logística'], true)) {
echo json_encode(['success' => false, 'message' => 'Tu rol no puede editar el estado en ruta contraentrega.']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pedido_id = $_POST['pedido_id'] ?? $_POST['id'] ?? null;
$estado = $_POST['estado'] ?? null;

View File

@ -50,6 +50,12 @@ if (!isset($_SESSION['user_id'])) {
exit;
}
$user_role = $_SESSION['user_role'] ?? '';
if (in_array($user_role, ['Control Logistico', 'Control Logístico'], true)) {
echo json_encode(['success' => false, 'message' => 'Tu rol no puede editar el paquete en ruta contraentrega.']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pedido_id_raw = $_POST['pedido_id'] ?? null;
$pedido_id = (is_numeric($pedido_id_raw) && (int)$pedido_id_raw > 0) ? (int)$pedido_id_raw : null;