Autosave: 20260708-065342
This commit is contained in:
parent
c4432f5693
commit
6c3b83d672
@ -411,4 +411,20 @@ h1, .h1 {
|
||||
.table td, .table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Call center: Estado badge colors */
|
||||
.cc-badge-orange {
|
||||
background-color: #ffb74d !important;
|
||||
color: #9a5a00 !important;
|
||||
border: 1px solid #ff9800 !important;
|
||||
}
|
||||
|
||||
/* Call center: Estado de fila (sombreado completo) */
|
||||
.cc-callcenter-row td {
|
||||
background-color: var(--cc-row-bg, #ffffff) !important;
|
||||
}
|
||||
|
||||
.cc-callcenter-row:hover td {
|
||||
background-color: var(--cc-row-bg, #ffffff) !important;
|
||||
}
|
||||
|
||||
@ -84,16 +84,18 @@ function cc_test_order_label(array $order): string
|
||||
function cc_test_badge_class(string $estado): string
|
||||
{
|
||||
return match (cc_test_normalize_state($estado)) {
|
||||
'CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO' => 'bg-success-subtle text-success-emphasis',
|
||||
'DEVOLVER LLAMADA' => 'bg-info-subtle text-info-emphasis',
|
||||
'POR LLAMAR', 'DEVOLVER LLAMADA' => 'bg-white text-dark border',
|
||||
'OBSERVADO' => 'bg-warning-subtle text-warning-emphasis',
|
||||
'SE ENVIO NUMERO DE CUENTA' => 'bg-success-subtle text-success-emphasis',
|
||||
'CONFIRMADO CONTRAENTREGA' => 'cc-badge-orange',
|
||||
'CONFIRMADO ENVIO' => 'bg-success text-white',
|
||||
'CANCELADO' => 'bg-danger-subtle text-danger-emphasis',
|
||||
'REPETIDO' => 'bg-secondary-subtle text-secondary-emphasis',
|
||||
'SE ENVIO NUMERO DE CUENTA' => 'bg-primary-subtle text-primary-emphasis',
|
||||
default => 'bg-dark-subtle text-dark-emphasis',
|
||||
default => 'bg-white text-dark border',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function cc_test_order_time(array $order): int
|
||||
{
|
||||
foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) {
|
||||
@ -117,7 +119,7 @@ function cc_test_followup_semaforo(array $order): ?array
|
||||
|
||||
$view = $_GET['view'] ?? 'pendientes_hoy';
|
||||
$allowedViews = [
|
||||
'pendientes_hoy' => 'Pendientes de hoy',
|
||||
'pendientes_hoy' => 'Pendientes por llamar',
|
||||
'nuevos_hoy' => 'Nuevos de hoy',
|
||||
'confirmados' => 'Confirmados',
|
||||
'seguimiento' => 'Seguimiento',
|
||||
@ -191,7 +193,27 @@ try {
|
||||
throw new RuntimeException('No se encontró la asesora seleccionada.');
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, $sourceKey, $selectedAssessor ? (int) $selectedAssessor['id'] : null);
|
||||
$targetUserId = $selectedAssessor ? (int) $selectedAssessor['id'] : null;
|
||||
|
||||
$stmtCurrent = $pdo->prepare('SELECT user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
|
||||
$stmtCurrent->execute([$sourceKey]);
|
||||
$currentUserIdRaw = $stmtCurrent->fetchColumn();
|
||||
$currentUserId = $currentUserIdRaw !== null ? ((int) $currentUserIdRaw > 0 ? (int) $currentUserIdRaw : null) : null;
|
||||
|
||||
// Si el pedido ya está asignado, evitamos que se reasigne a otra asesora (solo permitimos dejarlo sin asignar).
|
||||
if ($currentUserId !== null && $targetUserId !== null && (int) $currentUserId !== (int) $targetUserId) {
|
||||
$currentLabel = 'otra asesora';
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $currentUserId) {
|
||||
$currentLabel = (string) ($assessor['label'] ?? $assessorKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Este pedido ya está asignado a ' . $currentLabel . '. Para cambiarlo primero déjalo en "Sin asignar".');
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, $sourceKey, $targetUserId);
|
||||
$noticeMessage = $selectedAssessor
|
||||
? 'Pedido asignado a ' . $selectedAssessor['label'] . '.'
|
||||
: 'Pedido dejado sin asignar.';
|
||||
@ -229,8 +251,7 @@ try {
|
||||
$proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null);
|
||||
|
||||
$order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false;
|
||||
$order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true)
|
||||
&& ($proximaDate === null || $proximaDate <= $todayEnd);
|
||||
$order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true);
|
||||
$order['es_cerrado'] = in_array($order['estado'], $closedStates, true);
|
||||
|
||||
$stats['total']++;
|
||||
@ -336,7 +357,7 @@ require_once 'layout_header.php';
|
||||
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes_hoy' ? 'bg-dark text-white' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase opacity-75 mb-2">Pendientes de hoy</div>
|
||||
<div class="small text-uppercase opacity-75 mb-2">Pendientes por llamar</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['pendientes_hoy']; ?></div>
|
||||
</div>
|
||||
</article>
|
||||
@ -447,7 +468,7 @@ require_once 'layout_header.php';
|
||||
$historial = cc_test_fetch_historial(db(), $order['source_key']);
|
||||
ob_start();
|
||||
?>
|
||||
<tr data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars(cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR'))); ?>">
|
||||
<td>
|
||||
<div class="fw-bold fs-6"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
|
||||
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
|
||||
@ -494,18 +515,69 @@ require_once 'layout_header.php';
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column gap-2 align-items-stretch">
|
||||
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
|
||||
<form method="post" class="border rounded-3 bg-light p-2 text-start shadow-sm">
|
||||
<input type="hidden" name="action" value="assign_assessor">
|
||||
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<div class="small text-uppercase text-muted fw-semibold mb-1">Asignar a asesora</div>
|
||||
<select name="target_assessor" class="form-select form-select-sm mb-2">
|
||||
<option value=""<?php echo empty($order['user_id']) ? ' selected' : ''; ?>>Sin asignar</option>
|
||||
<?php foreach ($assessors as $assessorKey => $assessor): ?>
|
||||
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ((int) ($order['user_id'] ?? 0) === (int) ($assessor['id'] ?? 0)) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100">Asignar pedido</button>
|
||||
</form>
|
||||
<?php
|
||||
$orderUserId = $order['user_id'] ?? null;
|
||||
$orderUserIdNorm = $orderUserId !== null ? ((int) $orderUserId > 0 ? (int) $orderUserId : null) : null;
|
||||
$unassigned = $orderUserIdNorm === null;
|
||||
$assignedAssessorKey = null;
|
||||
if (!$unassigned) {
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $orderUserIdNorm) {
|
||||
$assignedAssessorKey = (string) $assessorKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$assignFormClass = 'border rounded-3 p-2 text-start shadow-sm';
|
||||
if ($unassigned) {
|
||||
$assignFormClass .= ' bg-light';
|
||||
} elseif ($assignedAssessorKey === 'KARINA') {
|
||||
$assignFormClass .= ' bg-primary-subtle border-primary';
|
||||
} elseif ($assignedAssessorKey === 'ESTEFANYA') {
|
||||
$assignFormClass .= ' bg-success-subtle border-success';
|
||||
} else {
|
||||
$assignFormClass .= ' bg-secondary-subtle border-secondary';
|
||||
}
|
||||
|
||||
$selectedAssessorKeyForSelect = $unassigned ? '' : ($assignedAssessorKey !== null ? (string) $assignedAssessorKey : '');
|
||||
|
||||
$buttonLabel = $unassigned
|
||||
? 'Asignar pedido'
|
||||
: ($assignedAssessorKey !== null ? 'Guardar asignación' : 'Desasignar');
|
||||
?>
|
||||
|
||||
<form method="post" class="<?php echo $assignFormClass; ?>">
|
||||
<input type="hidden" name="action" value="assign_assessor">
|
||||
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-1">
|
||||
<div class="small text-uppercase text-muted fw-semibold">Asignar a asesora</div>
|
||||
<?php if ($unassigned): ?>
|
||||
<span class="badge bg-light text-dark border">Sin asignar</span>
|
||||
<?php elseif ($assignedAssessorKey === 'KARINA'): ?>
|
||||
<span class="badge bg-primary-subtle text-primary-emphasis border">KARINA</span>
|
||||
<?php elseif ($assignedAssessorKey === 'ESTEFANYA'): ?>
|
||||
<span class="badge bg-success-subtle text-success-emphasis border">ESTEFANYA</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary-subtle text-secondary-emphasis border">Asignado</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<select name="target_assessor" class="form-select form-select-sm mb-2">
|
||||
<option value=""<?php echo ($selectedAssessorKeyForSelect === '') ? ' selected' : ''; ?>>Sin asignar</option>
|
||||
|
||||
<?php if ($unassigned): ?>
|
||||
<?php foreach ($assessors as $assessorKey => $assessor): ?>
|
||||
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ($selectedAssessorKeyForSelect === $assessorKey) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php elseif ($assignedAssessorKey !== null): ?>
|
||||
<option value="<?php echo htmlspecialchars((string) $assignedAssessorKey); ?>" selected><?php echo htmlspecialchars((string) ($assessors[$assignedAssessorKey]['label'] ?? $assignedAssessorKey)); ?></option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($order['telefono_url'])): ?>
|
||||
<button
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
-- Agrega la columna de observación para Producción de Videos
|
||||
ALTER TABLE marketing_videos
|
||||
ADD COLUMN observacion TEXT NULL AFTER estado;
|
||||
ADD COLUMN IF NOT EXISTS observacion TEXT NULL AFTER estado;
|
||||
|
||||
13
db/migrations/087_add_contraentrega_verificacion_fields.sql
Normal file
13
db/migrations/087_add_contraentrega_verificacion_fields.sql
Normal file
@ -0,0 +1,13 @@
|
||||
-- Contraentrega: verificación con imagen + tracking para que otra persona lo copie a 'seguimiento'
|
||||
|
||||
ALTER TABLE pedidos
|
||||
ADD COLUMN IF NOT EXISTS contraentrega_verificacion_imagen_path VARCHAR(500) NULL;
|
||||
|
||||
ALTER TABLE pedidos
|
||||
ADD COLUMN IF NOT EXISTS contraentrega_verificacion_tracking VARCHAR(255) NULL;
|
||||
|
||||
ALTER TABLE pedidos
|
||||
ADD COLUMN IF NOT EXISTS contraentrega_verificacion_user_id INT NULL;
|
||||
|
||||
ALTER TABLE pedidos
|
||||
ADD COLUMN IF NOT EXISTS contraentrega_verificacion_created_at TIMESTAMP NULL DEFAULT NULL;
|
||||
@ -84,16 +84,18 @@ function cc_test_order_label(array $order): string
|
||||
function cc_test_badge_class(string $estado): string
|
||||
{
|
||||
return match (cc_test_normalize_state($estado)) {
|
||||
'CONFIRMADO CONTRAENTREGA', 'CONFIRMADO ENVIO' => 'bg-success-subtle text-success-emphasis',
|
||||
'DEVOLVER LLAMADA' => 'bg-info-subtle text-info-emphasis',
|
||||
'POR LLAMAR', 'DEVOLVER LLAMADA' => 'bg-white text-dark border',
|
||||
'OBSERVADO' => 'bg-warning-subtle text-warning-emphasis',
|
||||
'SE ENVIO NUMERO DE CUENTA' => 'bg-success-subtle text-success-emphasis',
|
||||
'CONFIRMADO CONTRAENTREGA' => 'cc-badge-orange',
|
||||
'CONFIRMADO ENVIO' => 'bg-success text-white',
|
||||
'CANCELADO' => 'bg-danger-subtle text-danger-emphasis',
|
||||
'REPETIDO' => 'bg-secondary-subtle text-secondary-emphasis',
|
||||
'SE ENVIO NUMERO DE CUENTA' => 'bg-primary-subtle text-primary-emphasis',
|
||||
default => 'bg-dark-subtle text-dark-emphasis',
|
||||
default => 'bg-white text-dark border',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function cc_test_order_time(array $order): int
|
||||
{
|
||||
foreach (['proxima_llamada_at', 'numero_cuenta_enviado_at', 'ultima_gestion_at', 'seguimiento_actualizado', 'import_id'] as $field) {
|
||||
@ -117,7 +119,7 @@ function cc_test_followup_semaforo(array $order): ?array
|
||||
|
||||
$view = $_GET['view'] ?? 'pendientes_hoy';
|
||||
$allowedViews = [
|
||||
'pendientes_hoy' => 'Pendientes de hoy',
|
||||
'pendientes_hoy' => 'Pendientes por llamar',
|
||||
'nuevos_hoy' => 'Nuevos de hoy',
|
||||
'confirmados' => 'Confirmados',
|
||||
'seguimiento' => 'Seguimiento',
|
||||
@ -196,7 +198,27 @@ try {
|
||||
throw new RuntimeException('No se encontró la asesora seleccionada.');
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, $sourceKey, $selectedAssessor ? (int) $selectedAssessor['id'] : null);
|
||||
$targetUserId = $selectedAssessor ? (int) $selectedAssessor['id'] : null;
|
||||
|
||||
$stmtCurrent = $pdo->prepare('SELECT user_id FROM callcenter_test_tracking WHERE source_key = ? LIMIT 1');
|
||||
$stmtCurrent->execute([$sourceKey]);
|
||||
$currentUserIdRaw = $stmtCurrent->fetchColumn();
|
||||
$currentUserId = $currentUserIdRaw !== null ? ((int) $currentUserIdRaw > 0 ? (int) $currentUserIdRaw : null) : null;
|
||||
|
||||
// Si el pedido ya está asignado, evitamos que se reasigne a otra asesora (solo permitimos dejarlo sin asignar).
|
||||
if ($currentUserId !== null && $targetUserId !== null && (int) $currentUserId !== (int) $targetUserId) {
|
||||
$currentLabel = 'otra asesora';
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $currentUserId) {
|
||||
$currentLabel = (string) ($assessor['label'] ?? $assessorKey);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Este pedido ya está asignado a ' . $currentLabel . '. Para cambiarlo primero déjalo en "Sin asignar".');
|
||||
}
|
||||
|
||||
cc_test_upsert_assignee($pdo, $sourceKey, $targetUserId);
|
||||
$noticeMessage = $selectedAssessor
|
||||
? 'Pedido asignado a ' . $selectedAssessor['label'] . '.'
|
||||
: 'Pedido dejado sin asignar.';
|
||||
@ -375,8 +397,7 @@ try {
|
||||
$proximaDate = cc_test_parse_datetime($order['proxima_llamada_at'] ?? null);
|
||||
|
||||
$order['es_nuevo_hoy'] = $importDate ? $importDate->format('Y-m-d') === $todayStart->format('Y-m-d') : false;
|
||||
$order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true)
|
||||
&& ($proximaDate === null || $proximaDate <= $todayEnd);
|
||||
$order['es_pendiente_hoy'] = in_array($order['estado'], $openStates, true);
|
||||
$order['es_cerrado'] = in_array($order['estado'], $closedStates, true);
|
||||
|
||||
if ($order['user_id'] !== null) {
|
||||
@ -510,7 +531,7 @@ require_once 'layout_header.php';
|
||||
<a href="?view=pendientes_hoy&store=<?php echo htmlspecialchars($storeKey); ?>" class="text-decoration-none">
|
||||
<article class="card border-0 shadow-sm h-100 <?php echo $view === 'pendientes_hoy' ? 'bg-dark text-white' : 'bg-white'; ?>">
|
||||
<div class="card-body">
|
||||
<div class="small text-uppercase opacity-75 mb-2">Pendientes de hoy</div>
|
||||
<div class="small text-uppercase opacity-75 mb-2">Pendientes por llamar</div>
|
||||
<div class="display-6 fw-bold mb-0"><?php echo (int) $stats['pendientes_hoy']; ?></div>
|
||||
</div>
|
||||
</article>
|
||||
@ -678,7 +699,7 @@ require_once 'layout_header.php';
|
||||
$historial = cc_test_fetch_historial(db(), $order['source_key']);
|
||||
ob_start();
|
||||
?>
|
||||
<tr data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<tr class="cc-callcenter-row" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>" style="<?php echo htmlspecialchars(cc_test_row_background_style((string) ($order['estado'] ?? 'POR LLAMAR'))); ?>">
|
||||
<td>
|
||||
<div class="fw-bold fs-6"><?php echo htmlspecialchars(cc_test_order_label($order)); ?></div>
|
||||
<div class="small text-muted">ID Drive: <?php echo htmlspecialchars(cc_test_display_value($order['import_id'] ?? null, 'Sin ID')); ?></div>
|
||||
@ -725,23 +746,72 @@ require_once 'layout_header.php';
|
||||
<td class="text-center">
|
||||
<div class="d-flex flex-column gap-2 align-items-stretch">
|
||||
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
|
||||
<form method="post" class="border rounded-3 bg-light p-2 text-start shadow-sm">
|
||||
<?php
|
||||
$orderUserId = $order['user_id'] ?? null;
|
||||
$orderUserIdNorm = $orderUserId !== null ? ((int) $orderUserId > 0 ? (int) $orderUserId : null) : null;
|
||||
$unassigned = $orderUserIdNorm === null;
|
||||
$assignedAssessorKey = null;
|
||||
if (!$unassigned) {
|
||||
foreach ($assessors as $assessorKey => $assessor) {
|
||||
if ((int) ($assessor['id'] ?? 0) === (int) $orderUserIdNorm) {
|
||||
$assignedAssessorKey = (string) $assessorKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$assignFormClass = 'border rounded-3 p-2 text-start shadow-sm';
|
||||
if ($unassigned) {
|
||||
$assignFormClass .= ' bg-light';
|
||||
} elseif ($assignedAssessorKey === 'KARINA') {
|
||||
$assignFormClass .= ' bg-primary-subtle border-primary';
|
||||
} elseif ($assignedAssessorKey === 'ESTEFANYA') {
|
||||
$assignFormClass .= ' bg-success-subtle border-success';
|
||||
} else {
|
||||
$assignFormClass .= ' bg-secondary-subtle border-secondary';
|
||||
}
|
||||
|
||||
if ($unassigned) {
|
||||
$selectedAssessorKeyForSelect = !empty($recommendedAssessorKey) ? (string) $recommendedAssessorKey : '';
|
||||
} else {
|
||||
$selectedAssessorKeyForSelect = $assignedAssessorKey !== null ? (string) $assignedAssessorKey : '';
|
||||
}
|
||||
|
||||
$buttonLabel = $unassigned
|
||||
? 'Asignar pedido'
|
||||
: ($assignedAssessorKey !== null ? 'Guardar asignación' : 'Desasignar');
|
||||
?>
|
||||
|
||||
<form method="post" class="<?php echo $assignFormClass; ?>">
|
||||
<input type="hidden" name="action" value="assign_assessor">
|
||||
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
|
||||
<div class="small text-uppercase text-muted fw-semibold mb-1">Asignar a asesora</div>
|
||||
<?php
|
||||
$unassigned = ($order['user_id'] === null);
|
||||
?>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-1">
|
||||
<div class="small text-uppercase text-muted fw-semibold">Asignar a asesora</div>
|
||||
<?php if ($unassigned): ?>
|
||||
<span class="badge bg-light text-dark border">Sin asignar</span>
|
||||
<?php elseif ($assignedAssessorKey === 'KARINA'): ?>
|
||||
<span class="badge bg-primary-subtle text-primary-emphasis border">KARINA</span>
|
||||
<?php elseif ($assignedAssessorKey === 'ESTEFANYA'): ?>
|
||||
<span class="badge bg-success-subtle text-success-emphasis border">ESTEFANYA</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary-subtle text-secondary-emphasis border">Asignado</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<select name="target_assessor" class="form-select form-select-sm mb-2">
|
||||
<option value=""<?php echo ($unassigned && empty($recommendedAssessorKey)) ? ' selected' : ''; ?>>Sin asignar</option>
|
||||
<?php foreach ($assessors as $assessorKey => $assessor):
|
||||
$isSelected = ($order['user_id'] !== null && (int) ($order['user_id'] ?? 0) === (int) ($assessor['id'] ?? 0))
|
||||
|| ($unassigned && $recommendedAssessorKey === $assessorKey);
|
||||
?>
|
||||
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo $isSelected ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
<option value=""<?php echo ($selectedAssessorKeyForSelect === '') ? ' selected' : ''; ?>>Sin asignar</option>
|
||||
|
||||
<?php if ($unassigned): ?>
|
||||
<?php foreach ($assessors as $assessorKey => $assessor): ?>
|
||||
<option value="<?php echo htmlspecialchars($assessorKey); ?>"<?php echo ($selectedAssessorKeyForSelect === $assessorKey) ? ' selected' : ''; ?>><?php echo htmlspecialchars($assessor['label']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
<?php elseif ($assignedAssessorKey !== null): ?>
|
||||
<option value="<?php echo htmlspecialchars((string) $assignedAssessorKey); ?>" selected><?php echo htmlspecialchars((string) ($assessors[$assignedAssessorKey]['label'] ?? $assignedAssessorKey)); ?></option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100">Asignar pedido</button>
|
||||
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($order['telefono_url'])): ?>
|
||||
|
||||
@ -311,6 +311,41 @@ function cc_test_state_label(string $estado): string
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function cc_test_row_background_style(string $estado): string
|
||||
{
|
||||
$color = match (cc_test_normalize_state($estado)) {
|
||||
// Blanco
|
||||
'POR LLAMAR', 'DEVOLVER LLAMADA' => '#ffffff',
|
||||
|
||||
// Amarillo
|
||||
'OBSERVADO' => '#fff3cd',
|
||||
|
||||
// Verde claro
|
||||
'SE ENVIO NUMERO DE CUENTA' => '#d1e7dd',
|
||||
|
||||
// Naranja
|
||||
'CONFIRMADO CONTRAENTREGA' => '#ffb74d',
|
||||
|
||||
// Verde
|
||||
'CONFIRMADO ENVIO' => '#b6f1c7',
|
||||
|
||||
// Rojo
|
||||
'CANCELADO' => '#f8d7da',
|
||||
|
||||
// Plomo
|
||||
'REPETIDO' => '#d6d8db',
|
||||
|
||||
default => '#ffffff',
|
||||
};
|
||||
|
||||
// We set a CSS variable so we can paint the entire row (all <td> cells).
|
||||
// Bootstrap/table styles often apply opaque backgrounds to <td>, which can hide
|
||||
// the <tr> background. A CSS rule below will apply this variable to every cell.
|
||||
return '--cc-row-bg:' . $color . ';';
|
||||
}
|
||||
|
||||
if (!function_exists('cc_test_table_exists')) {
|
||||
function cc_test_table_exists(PDO $pdo, string $tableName): bool
|
||||
{
|
||||
|
||||
@ -219,7 +219,7 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
|
||||
cc_test_ensure_tracking_table($pdo);
|
||||
|
||||
$placeholders = implode(',', array_fill(0, count($sourceKeys), '?'));
|
||||
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, producto, cantidad, precio, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
|
||||
$stmt = $pdo->prepare("SELECT source_key, estado, nota_seguimiento, user_id, direccion, referencia, agencia, sede_agencia, sede, ciudad, distrito, dni, observaciones, producto, cantidad, precio, confirmacion_producto, confirmacion_cantidad, confirmacion_precio, confirmacion_producto_extra, confirmacion_cantidad_extra, confirmacion_precio_extra, proxima_llamada_at, fecha_entrega_programada, numero_cuenta_enviado_at, ultima_gestion_at, updated_at FROM callcenter_test_tracking WHERE source_key IN ($placeholders)");
|
||||
$stmt->execute($sourceKeys);
|
||||
|
||||
$tracking = [];
|
||||
@ -232,13 +232,14 @@ function drive_test_fetch_tracking(PDO $pdo, array $sourceKeys): array
|
||||
|
||||
function drive_test_merge_tracking(array $orders, array $tracking): array
|
||||
{
|
||||
$editableFields = ['direccion', 'referencia', 'agencia', 'sede_agencia', 'sede', 'ciudad', 'distrito', 'dni', 'observaciones', 'producto', 'cantidad', 'precio', 'confirmacion_producto', 'confirmacion_cantidad', 'confirmacion_precio'];
|
||||
$editableFields = ['direccion', 'referencia', 'agencia', 'sede_agencia', 'sede', 'ciudad', 'distrito', 'dni', 'observaciones', 'producto', 'cantidad', 'precio', 'confirmacion_producto', 'confirmacion_cantidad', 'confirmacion_precio', 'confirmacion_producto_extra', 'confirmacion_cantidad_extra', 'confirmacion_precio_extra'];
|
||||
|
||||
foreach ($orders as &$order) {
|
||||
$current = $tracking[$order['source_key']] ?? null;
|
||||
$order['estado'] = $current['estado'] ?? 'POR LLAMAR';
|
||||
$order['nota_seguimiento'] = $current['nota_seguimiento'] ?? '';
|
||||
$order['user_id'] = array_key_exists('user_id', (array) $current) ? ($current['user_id'] !== null ? (int) $current['user_id'] : null) : null;
|
||||
$userId = array_key_exists('user_id', (array) $current) ? $current['user_id'] : null;
|
||||
$order['user_id'] = $userId !== null ? ((int) $userId > 0 ? (int) $userId : null) : null;
|
||||
$order['seguimiento_actualizado'] = $current['updated_at'] ?? null;
|
||||
$order['proxima_llamada_at'] = $current['proxima_llamada_at'] ?? null;
|
||||
$order['fecha_entrega_programada'] = $current['fecha_entrega_programada'] ?? null;
|
||||
|
||||
@ -247,6 +247,48 @@ include 'layout_header.php';
|
||||
white-space: normal;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
|
||||
/* Contraentrega: icono para ver/subir verificación (foto + tracking) */
|
||||
#pedidos-table .cc-verificacion-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
#pedidos-table .cc-verificacion-btn.cc-status-none {
|
||||
background: #e9ecef;
|
||||
color: #0d6efd;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
#pedidos-table .cc-verificacion-btn.cc-status-pending {
|
||||
background: #fff3cd;
|
||||
color: #5f4700;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
#pedidos-table .cc-verificacion-btn.cc-status-match {
|
||||
background: #198754;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
.cc-verificacion-img-preview {
|
||||
max-width: 100%;
|
||||
max-height: 360px;
|
||||
object-fit: contain;
|
||||
background: #f8f9fa;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
}
|
||||
.cc-inline-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="card mb-4">
|
||||
@ -317,7 +359,7 @@ include 'layout_header.php';
|
||||
<table id="pedidos-table" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 35px;">ID</th>
|
||||
<th style="width: 120px;">ID</th>
|
||||
<th style="width: 120px;">Seguimiento</th>
|
||||
<th style="width: 130px;">Paquete</th>
|
||||
<th style="width: 180px;">Estado</th>
|
||||
@ -346,7 +388,31 @@ include 'layout_header.php';
|
||||
<tbody>
|
||||
<?php foreach ($pedidos as $pedido): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($pedido['id']); ?></td>
|
||||
<?php
|
||||
$ccVerifTracking = trim((string)($pedido['contraentrega_verificacion_tracking'] ?? ''));
|
||||
$ccVerifImg = trim((string)($pedido['contraentrega_verificacion_imagen_path'] ?? ''));
|
||||
$ccVerifReady = $ccVerifTracking !== '' && $ccVerifImg !== '';
|
||||
$ccSeguimiento = trim((string)($pedido['seguimiento'] ?? ''));
|
||||
$ccVerifMatch = $ccVerifReady && $ccSeguimiento !== '' && $ccSeguimiento === $ccVerifTracking;
|
||||
|
||||
$ccVerifBtnClass = $ccVerifMatch ? 'cc-status-match' : ($ccVerifReady ? 'cc-status-pending' : 'cc-status-none');
|
||||
$ccVerifIcon = $ccVerifMatch ? '✅' : '📷';
|
||||
?>
|
||||
<td data-order="<?php echo (int)$pedido['id']; ?>">
|
||||
<div class="cc-id-verif d-flex align-items-center gap-2">
|
||||
<span class="cc-order-id"><?php echo htmlspecialchars($pedido['id']); ?></span>
|
||||
<button type="button" class="btn btn-sm cc-verificacion-btn <?php echo $ccVerifBtnClass; ?>"
|
||||
data-bs-toggle="modal" data-bs-target="#contraentregaVerificacionModal"
|
||||
data-pedido-id="<?php echo htmlspecialchars((string)$pedido['id'], ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-nombre-completo="<?php echo htmlspecialchars((string)($pedido['nombre_completo'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-verificacion-tracking="<?php echo htmlspecialchars($ccVerifTracking, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-verificacion-imagen="<?php echo htmlspecialchars($ccVerifImg, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
data-seguimiento="<?php echo htmlspecialchars($ccSeguimiento, ENT_QUOTES, 'UTF-8'); ?>"
|
||||
aria-label="Verificación de contraentrega">
|
||||
<?php echo $ccVerifIcon; ?>
|
||||
</button>
|
||||
</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;">
|
||||
<span class="badge" style="<?php echo getPaqueteStyle($pedido['tipo_paquete'] ?? ''); ?>">
|
||||
@ -456,7 +522,291 @@ include 'layout_header.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<?php endif; ?><div class="modal fade" id="contraentregaVerificacionModal" tabindex="-1" aria-labelledby="contraentregaVerificacionModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="contraentregaVerificacionModalLabel">Verificación Contraentrega (Foto + Tracking)</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cerrar"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-7">
|
||||
<div class="mb-2">
|
||||
<div class="text-muted small">Pedido #<span id="ccVerifPedidoIdText"></span></div>
|
||||
<div class="fw-semibold" id="ccVerifNombreDestinatario"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<img id="ccVerifPreviewImg" class="cc-verificacion-img-preview" src="" alt="Imagen de verificación" style="display:none;">
|
||||
<div id="ccVerifNoImg" class="text-muted" style="display:none;">Sin imagen cargada todavía.</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mb-0" role="alert" style="padding: 0.6rem 0.85rem;">
|
||||
<div class="small">
|
||||
<strong>Tip:</strong> La otra persona debe revisar que el <em>tracking</em> coincida con el <em>nombre del destinatario</em> que aparece en la foto.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
<input type="hidden" id="ccVerifPedidoIdHidden" value="">
|
||||
|
||||
<div id="ccVerifEditSection" style="<?php echo ($user_role === 'Logistica') ? 'display:none;' : ''; ?>">
|
||||
<div class="mb-3">
|
||||
<label for="ccVerifImageInput" class="form-label">Subir imagen del paquete</label>
|
||||
<input type="file" class="form-control" id="ccVerifImageInput" accept="image/*">
|
||||
<div class="form-text">Foto del estado del paquete donde se vea el tracking y el nombre.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="ccVerifTrackingInput" class="form-label">Tracking (código)</label>
|
||||
<input type="text" class="form-control" id="ccVerifTrackingInput" placeholder="Ej: 123456...">
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary w-100" id="ccVerifSaveBtn">Guardar verificación</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tracking guardado (para copiar a “Seguimiento”)</label>
|
||||
<div class="p-2 rounded bg-light">
|
||||
<span id="ccVerifTrackingSaved" class="cc-inline-code text-break"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Seguimiento actual (campo editable en la tabla)</label>
|
||||
<div class="p-2 rounded bg-light">
|
||||
<span id="ccVerifSeguimientoActual" class="cc-inline-code text-break"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-success" id="ccVerifCopyBtn" disabled>Copiar tracking</button>
|
||||
<div class="text-muted small" id="ccVerifCopyHint">Luego pegalo en la celda “Seguimiento”.</div>
|
||||
</div>
|
||||
|
||||
<div id="ccVerifErrorBox" class="alert alert-danger mt-3 d-none" role="alert"></div>
|
||||
<div id="ccVerifSuccessBox" class="alert alert-success mt-3 d-none" role="alert"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const modalEl = document.getElementById('contraentregaVerificacionModal');
|
||||
if (!modalEl) return;
|
||||
|
||||
const pedidoIdText = document.getElementById('ccVerifPedidoIdText');
|
||||
const pedidoIdHidden = document.getElementById('ccVerifPedidoIdHidden');
|
||||
const nombreEl = document.getElementById('ccVerifNombreDestinatario');
|
||||
|
||||
const previewImg = document.getElementById('ccVerifPreviewImg');
|
||||
const noImgEl = document.getElementById('ccVerifNoImg');
|
||||
|
||||
const trackingSavedEl = document.getElementById('ccVerifTrackingSaved');
|
||||
const seguimientoActualEl = document.getElementById('ccVerifSeguimientoActual');
|
||||
|
||||
const editSection = document.getElementById('ccVerifEditSection');
|
||||
const imageInput = document.getElementById('ccVerifImageInput');
|
||||
const trackingInput = document.getElementById('ccVerifTrackingInput');
|
||||
|
||||
const saveBtn = document.getElementById('ccVerifSaveBtn');
|
||||
const copyBtn = document.getElementById('ccVerifCopyBtn');
|
||||
|
||||
const errorBox = document.getElementById('ccVerifErrorBox');
|
||||
const successBox = document.getElementById('ccVerifSuccessBox');
|
||||
|
||||
function showBox(boxEl, msg) {
|
||||
boxEl.textContent = msg;
|
||||
boxEl.classList.remove('d-none');
|
||||
}
|
||||
function hideBoxes() {
|
||||
errorBox.classList.add('d-none');
|
||||
errorBox.textContent = '';
|
||||
successBox.classList.add('d-none');
|
||||
successBox.textContent = '';
|
||||
}
|
||||
|
||||
function setPreviewImage(imgPath) {
|
||||
if (imgPath) {
|
||||
previewImg.src = imgPath;
|
||||
previewImg.style.display = 'block';
|
||||
noImgEl.style.display = 'none';
|
||||
} else {
|
||||
previewImg.src = '';
|
||||
previewImg.style.display = 'none';
|
||||
noImgEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
modalEl.addEventListener('show.bs.modal', (event) => {
|
||||
const btn = event.relatedTarget;
|
||||
if (!btn) return;
|
||||
|
||||
hideBoxes();
|
||||
|
||||
const pedidoId = btn.getAttribute('data-pedido-id') || '';
|
||||
const nombre = btn.getAttribute('data-nombre-completo') || '';
|
||||
const verifTracking = btn.getAttribute('data-verificacion-tracking') || '';
|
||||
const verifImg = btn.getAttribute('data-verificacion-imagen') || '';
|
||||
const seguimiento = btn.getAttribute('data-seguimiento') || '';
|
||||
|
||||
pedidoIdHidden.value = pedidoId;
|
||||
if (pedidoIdText) pedidoIdText.textContent = pedidoId;
|
||||
if (nombreEl) nombreEl.textContent = nombre;
|
||||
|
||||
trackingSavedEl.textContent = verifTracking || 'N/A';
|
||||
seguimientoActualEl.textContent = seguimiento || 'N/A';
|
||||
|
||||
if (editSection && editSection.style.display !== 'none') {
|
||||
// Solo precargar tracking si hay editor visible (evita confusiones a Logistica)
|
||||
if (trackingInput) trackingInput.value = verifTracking || '';
|
||||
}
|
||||
|
||||
setPreviewImage(verifImg);
|
||||
|
||||
// Copy button enabled only when there is a saved tracking and an image
|
||||
copyBtn.disabled = !(verifTracking && verifImg);
|
||||
|
||||
// Reset file input
|
||||
if (imageInput) {
|
||||
imageInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
if (imageInput) {
|
||||
imageInput.addEventListener('change', () => {
|
||||
const file = imageInput.files && imageInput.files[0] ? imageInput.files[0] : null;
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
previewImg.src = e.target.result;
|
||||
previewImg.style.display = 'block';
|
||||
if (noImgEl) noImgEl.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
hideBoxes();
|
||||
|
||||
const pedidoId = pedidoIdHidden.value;
|
||||
const tracking = (trackingInput && trackingInput.value ? trackingInput.value : '').trim();
|
||||
const file = imageInput && imageInput.files && imageInput.files[0] ? imageInput.files[0] : null;
|
||||
|
||||
if (!tracking) {
|
||||
showBox(errorBox, 'Ingresá el tracking (código) para guardar la verificación.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
showBox(errorBox, 'Seleccioná una imagen para guardar la verificación.');
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
const oldText = saveBtn.textContent;
|
||||
saveBtn.textContent = 'Guardando...';
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('pedido_id', pedidoId);
|
||||
fd.append('tracking_verificacion', tracking);
|
||||
fd.append('imagen', file);
|
||||
|
||||
const resp = await fetch('save_contraentrega_verificacion.php', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
if (!data || !data.success) {
|
||||
showBox(errorBox, (data && data.error) ? data.error : 'No se pudo guardar la verificación.');
|
||||
return;
|
||||
}
|
||||
|
||||
showBox(successBox, 'Verificación guardada ✅');
|
||||
|
||||
const modalInstance = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl);
|
||||
modalInstance.hide();
|
||||
|
||||
// Reload to update icon/status
|
||||
window.location.href = window.location.pathname + window.location.search;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showBox(errorBox, 'Error de conexión al guardar.');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = oldText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.top = '-1000px';
|
||||
ta.style.left = '-1000px';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
let ok = false;
|
||||
try {
|
||||
ok = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
ok = false;
|
||||
}
|
||||
document.body.removeChild(ta);
|
||||
return ok;
|
||||
}
|
||||
|
||||
if (copyBtn) {
|
||||
copyBtn.addEventListener('click', async () => {
|
||||
hideBoxes();
|
||||
|
||||
const text = (trackingSavedEl && trackingSavedEl.textContent ? trackingSavedEl.textContent : '').trim();
|
||||
if (!text) {
|
||||
showBox(errorBox, 'No hay tracking guardado para copiar.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ok = await copyTextToClipboard(text);
|
||||
if (!ok) {
|
||||
showBox(errorBox, 'No se pudo copiar al portapapeles.');
|
||||
return;
|
||||
}
|
||||
showBox(successBox, 'Copiado ✅ Pegalo en la celda “Seguimiento”.');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showBox(errorBox, 'No se pudo copiar al portapapeles.');
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@ -794,7 +1144,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (data.error) {
|
||||
console.error('Error:', data.error);
|
||||
// Optionally revert the change and show an error message
|
||||
cell.textContent = originalContent;
|
||||
cell.textContent = originalContent;
|
||||
return;
|
||||
}
|
||||
|
||||
// Si se actualizó "Seguimiento", actualizamos el icono de verificación en la misma fila
|
||||
if (field === 'seguimiento') {
|
||||
const row = cell.closest('tr');
|
||||
if (row) {
|
||||
const btn = row.querySelector('button.cc-verificacion-btn');
|
||||
if (btn) {
|
||||
const verifTracking = (btn.getAttribute('data-verificacion-tracking') || '').trim();
|
||||
const verifImg = (btn.getAttribute('data-verificacion-imagen') || '').trim();
|
||||
|
||||
const hasVerif = verifTracking !== '' && verifImg !== '';
|
||||
const match = hasVerif && (newValue === verifTracking);
|
||||
|
||||
btn.classList.remove('cc-status-none', 'cc-status-pending', 'cc-status-match');
|
||||
|
||||
if (!hasVerif) {
|
||||
btn.classList.add('cc-status-none');
|
||||
} else if (match) {
|
||||
btn.classList.add('cc-status-match');
|
||||
} else {
|
||||
btn.classList.add('cc-status-pending');
|
||||
}
|
||||
|
||||
btn.textContent = match ? '✅' : '📷';
|
||||
btn.setAttribute('data-seguimiento', newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
175
save_contraentrega_verificacion.php
Normal file
175
save_contraentrega_verificacion.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require_once 'db/config.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Acceso no autorizado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$pdo = db();
|
||||
|
||||
function cc_column_exists(PDO $pdo, string $column): bool {
|
||||
$stmt = $pdo->prepare('SHOW COLUMNS FROM pedidos LIKE ?');
|
||||
$stmt->execute([$column]);
|
||||
return (bool) $stmt->fetch();
|
||||
}
|
||||
|
||||
function cc_ensure_contraentrega_verificacion_columns(PDO $pdo): void {
|
||||
$columns = [
|
||||
'contraentrega_verificacion_imagen_path' => 'VARCHAR(500) NULL',
|
||||
'contraentrega_verificacion_tracking' => 'VARCHAR(255) NULL',
|
||||
'contraentrega_verificacion_user_id' => 'INT NULL',
|
||||
'contraentrega_verificacion_created_at' => 'TIMESTAMP NULL DEFAULT NULL',
|
||||
];
|
||||
|
||||
foreach ($columns as $col => $def) {
|
||||
if (!cc_column_exists($pdo, $col)) {
|
||||
// DDL: safe because we only use constant column names/definitions.
|
||||
$pdo->exec('ALTER TABLE pedidos ADD COLUMN ' . $col . ' ' . $def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$user_id = (int) ($_SESSION['user_id'] ?? 0);
|
||||
$user_role = (string) ($_SESSION['user_role'] ?? 'Asesor');
|
||||
|
||||
$pedido_id = (int) (($_POST['pedido_id'] ?? '') !== '' ? $_POST['pedido_id'] : 0);
|
||||
$tracking_verificacion = trim((string) ($_POST['tracking_verificacion'] ?? ''));
|
||||
|
||||
if ($pedido_id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'pedido_id es obligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($tracking_verificacion === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'tracking_verificacion es obligatorio.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Permission check
|
||||
$stmtPedido = $pdo->prepare('SELECT id, asesor_id FROM pedidos WHERE id = ? LIMIT 1');
|
||||
$stmtPedido->execute([$pedido_id]);
|
||||
$pedido = $stmtPedido->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$pedido) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'error' => 'Pedido no encontrado.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($user_role === 'Asesor' && (int) ($pedido['asesor_id'] ?? 0) !== $user_id) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'No autorizado para gestionar este pedido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_FILES['imagen']) || !is_array($_FILES['imagen'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'imagen es obligatoria.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$imagen = $_FILES['imagen'];
|
||||
if (($imagen['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Error al subir la imagen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$maxSize = 8 * 1024 * 1024; // 8MB
|
||||
if (!isset($imagen['size']) || (int)$imagen['size'] <= 0 || (int)$imagen['size'] > $maxSize) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'La imagen excede el tamaño máximo permitido (8MB).']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$tmpName = (string) ($imagen['tmp_name'] ?? '');
|
||||
if ($tmpName === '' || !file_exists($tmpName)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Temp file inválido.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$imgInfo = @getimagesize($tmpName);
|
||||
if ($imgInfo === false || empty($imgInfo['mime'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'El archivo no parece ser una imagen válida.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$mime = (string) $imgInfo['mime'];
|
||||
$allowed = [
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/webp' => 'webp',
|
||||
];
|
||||
|
||||
if (!isset($allowed[$mime])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Formato no permitido. Use JPG, PNG o WEBP.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ext = $allowed[$mime];
|
||||
|
||||
$uploadDir = __DIR__ . '/assets/uploads/contraentrega_verificaciones';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0775, true);
|
||||
}
|
||||
|
||||
$filename = sprintf(
|
||||
'contraentrega_verificacion_%d_%d_%d_%s.%s',
|
||||
$pedido_id,
|
||||
$user_id,
|
||||
time(),
|
||||
bin2hex(random_bytes(4)),
|
||||
$ext
|
||||
);
|
||||
|
||||
$targetPath = $uploadDir . '/' . $filename;
|
||||
|
||||
if (!move_uploaded_file($tmpName, $targetPath)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'No se pudo guardar la imagen en el servidor.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$relativePath = 'assets/uploads/contraentrega_verificaciones/' . $filename;
|
||||
|
||||
try {
|
||||
cc_ensure_contraentrega_verificacion_columns($pdo);
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE pedidos
|
||||
SET contraentrega_verificacion_imagen_path = ?,
|
||||
contraentrega_verificacion_tracking = ?,
|
||||
contraentrega_verificacion_user_id = ?,
|
||||
contraentrega_verificacion_created_at = NOW()
|
||||
WHERE id = ?');
|
||||
|
||||
$stmt->execute([$relativePath, $tracking_verificacion, $user_id, $pedido_id]);
|
||||
|
||||
if ($stmt->rowCount() <= 0) {
|
||||
// Still treat as success because UPDATE might not change values
|
||||
echo json_encode(['success' => true, 'info' => 'La verificación se guardó, pero no hubo cambios.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode(['success' => true, 'imagen' => $relativePath, 'tracking_verificacion' => $tracking_verificacion]);
|
||||
} catch (Throwable $e) {
|
||||
error_log('save_contraentrega_verificacion.php: ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
|
||||
$msg = $e->getMessage();
|
||||
if (is_string($msg) && strlen($msg) > 300) {
|
||||
$msg = substr($msg, 0, 300) . '...';
|
||||
}
|
||||
|
||||
echo json_encode(['success' => false, 'error' => 'Error al guardar la verificación. ' . $msg]);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user