Autosave: 20260715-060021

This commit is contained in:
Flatlogic Bot 2026-07-15 05:59:10 +00:00
parent bb9b9b3670
commit 82bca45582
4 changed files with 656 additions and 81 deletions

View File

@ -445,6 +445,73 @@ try {
require_once 'layout_header.php';
?>
<style>
.cc-callcenter-table tbody td {
padding-top: .5rem;
padding-bottom: .5rem;
}
.cc-callcenter-table .small {
line-height: 1.15;
}
.cc-callcenter-assign-form {
padding: .5rem !important;
}
.cc-callcenter-assign-form.has-accent {
border-left: .45rem solid var(--cc-assessor-accent, #adb5bd);
background: linear-gradient(135deg, rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .12) 0%, rgba(255, 255, 255, .98) 72%);
}
.cc-callcenter-assessor-badge {
display: inline-flex;
align-items: center;
gap: .3rem;
background-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .12);
color: #212529;
border-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .28) !important;
}
.cc-callcenter-color-dot {
width: .55rem;
height: .55rem;
border-radius: 999px;
background: var(--cc-assessor-accent, #adb5bd);
border: 1px solid rgba(0, 0, 0, .12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
flex: 0 0 auto;
}
.cc-callcenter-assign-row {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: .35rem;
}
.cc-callcenter-assign-row .form-select {
min-width: 11rem;
flex: 1 1 11rem;
}
.cc-callcenter-assign-submit {
flex: 0 0 auto;
white-space: nowrap;
}
.cc-callcenter-actions {
gap: .35rem !important;
}
.cc-callcenter-action-btn {
padding: .2rem .55rem;
line-height: 1.1;
font-size: .78rem;
white-space: nowrap;
}
</style>
<main class="container-fluid py-4">
<section class="mb-4">
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-center gap-3">
@ -579,7 +646,7 @@ require_once 'layout_header.php';
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<table class="table table-hover align-middle mb-0 cc-callcenter-table">
<thead class="table-light">
<tr>
<th> Pedido</th>
@ -712,7 +779,7 @@ require_once 'layout_header.php';
<div class="small text-muted mt-1">Nota: <?php echo htmlspecialchars(cc_test_display_value($order['nota_seguimiento'], 'Sin nota interna')); ?></div>
</td>
<td class="text-center">
<div class="d-flex flex-column gap-2 align-items-stretch">
<div class="d-flex flex-column gap-1 align-items-stretch cc-callcenter-actions">
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<?php
$orderUserId = $order['user_id'] ?? null;
@ -728,15 +795,13 @@ require_once 'layout_header.php';
}
}
$assignFormClass = 'border rounded-3 p-2 text-start shadow-sm';
$assignFormClass = 'border rounded-3 p-1 text-start shadow-sm cc-callcenter-assign-form';
$assignFormStyle = '';
if ($unassigned) {
$assignFormClass .= ' bg-light';
} elseif ($assignedAssessorKey === 'KARINA') {
$assignFormClass .= ' bg-primary-subtle border-primary';
} elseif ($assignedAssessorKey === 'ESTEFANYA') {
$assignFormClass .= ' bg-success-subtle border-success';
} elseif ($assignedAssessorKey === 'CARMEN') {
$assignFormClass .= ' bg-warning-subtle border-warning';
} elseif ($assignedAssessorKey !== null) {
$assignFormClass .= ' has-accent';
$assignFormStyle = cc_test_assessor_css_vars($assignedAssessorKey, $assessors[$assignedAssessorKey]['color_hex'] ?? null);
} else {
$assignFormClass .= ' bg-secondary-subtle border-secondary';
}
@ -748,7 +813,7 @@ require_once 'layout_header.php';
: ($assignedAssessorKey !== null ? 'Guardar asignación' : 'Desasignar');
?>
<form method="post" class="<?php echo $assignFormClass; ?>">
<form method="post" class="<?php echo htmlspecialchars($assignFormClass); ?>"<?php echo $assignFormStyle !== '' ? ' style="' . htmlspecialchars($assignFormStyle, ENT_QUOTES) . '"' : ''; ?>>
<input type="hidden" name="action" value="assign_assessor">
<input type="hidden" name="source_key" value="<?php echo htmlspecialchars($order['source_key']); ?>">
@ -756,36 +821,37 @@ require_once 'layout_header.php';
<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 elseif ($assignedAssessorKey === 'CARMEN'): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border">CARMEN</span>
<?php elseif ($assignedAssessorKey !== null): ?>
<span class="badge border cc-callcenter-assessor-badge">
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<?php echo htmlspecialchars((string) ($assessors[$assignedAssessorKey]['label'] ?? $assignedAssessorKey)); ?>
</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>
<div class="cc-callcenter-assign-row">
<select name="target_assessor" class="form-select form-select-sm">
<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>
<?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>
<button type="submit" class="btn btn-sm btn-primary cc-callcenter-assign-submit cc-callcenter-action-btn"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
</div>
</form>
<?php endif; ?>
<?php if (!empty($order['telefono_url'])): ?>
<button
type="button"
class="btn btn-sm btn-primary"
class="btn btn-sm btn-primary cc-callcenter-action-btn"
data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>"
data-modal-id="<?php echo htmlspecialchars($modalId); ?>"
data-phone="<?php echo htmlspecialchars((string) ($order['celular'] ?? '')); ?>"
@ -795,14 +861,14 @@ require_once 'layout_header.php';
<i class="bi bi-telephone-outbound"></i> Llamar / Gestionar
</button>
<?php else: ?>
<button type="button" class="btn btn-sm btn-primary" disabled>Sin teléfono</button>
<button type="button" class="btn btn-sm btn-primary cc-callcenter-action-btn" disabled>Sin teléfono</button>
<?php endif; ?>
<?php if (!empty($order['whatsapp_url'])): ?>
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success">
<a href="<?php echo htmlspecialchars($order['whatsapp_url']); ?>" target="_blank" rel="noopener" class="btn btn-sm btn-outline-success cc-callcenter-action-btn">
<i class="bi bi-whatsapp"></i> WhatsApp
</a>
<?php endif; ?>
<button type="button" class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
<button type="button" class="btn btn-sm btn-outline-dark cc-callcenter-action-btn" data-bs-toggle="modal" data-bs-target="#<?php echo htmlspecialchars($modalId); ?>">
<i class="bi bi-sliders"></i> Gestionar
</button>
</div>

View File

@ -0,0 +1,3 @@
-- Add configurable color for call center assessors in the assignment panel.
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `color_hex` VARCHAR(7) NULL AFTER `nombre_asesor`;

View File

@ -414,6 +414,28 @@ try {
}));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update_assessor_color') {
$assessorKey = cc_test_normalize_user_key((string) ($_POST['assessor_key'] ?? ''));
if ($assessorKey === '' || !isset($assessors[$assessorKey])) {
throw new RuntimeException('No se encontró la asesora seleccionada.');
}
$colorHex = cc_test_normalize_hex_color((string) ($_POST['color_hex'] ?? ''));
if ($colorHex === null) {
throw new RuntimeException('Selecciona un color válido.');
}
cc_test_ensure_column($pdo, 'users', 'color_hex', 'VARCHAR(7) NULL AFTER `nombre_asesor`');
$stmtUpdateAssessorColor = $pdo->prepare('UPDATE users SET color_hex = :color_hex WHERE id = :id LIMIT 1');
$stmtUpdateAssessorColor->bindValue(':color_hex', $colorHex, PDO::PARAM_STR);
$stmtUpdateAssessorColor->bindValue(':id', (int) ($assessors[$assessorKey]['id'] ?? 0), PDO::PARAM_INT);
$stmtUpdateAssessorColor->execute();
$noticeMessage = 'Color actualizado para ' . ($assessors[$assessorKey]['label'] ?? $assessorKey) . '.';
$assessors[$assessorKey]['color_hex'] = $colorHex;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'auto_assign_cupo') {
$openStates = cc_test_open_states();
@ -690,6 +712,21 @@ try {
'remaining' => $remaining,
'text_class' => (string) ($styles['text'] ?? 'text-secondary'),
'badge_class' => (string) ($styles['badge'] ?? 'bg-secondary-subtle text-secondary-emphasis border'),
'accent_style' => cc_test_assessor_css_vars($assessorKey, $assessors[$assessorKey]['color_hex'] ?? null),
];
}
$assessorUiCatalog = [];
foreach ($orderedAssessorKeys as $assessorKey) {
if (!isset($assessors[$assessorKey])) {
continue;
}
$catalogColorHex = cc_test_assessor_effective_color_hex($assessorKey, $assessors[$assessorKey]['color_hex'] ?? null);
$assessorUiCatalog[$assessorKey] = [
'label' => (string) ($assessors[$assessorKey]['label'] ?? $assessorKey),
'color_hex' => $catalogColorHex,
'contrast_hex' => cc_test_assessor_contrast_text_hex($catalogColorHex),
];
}
@ -798,8 +835,62 @@ require_once 'layout_header.php';
box-shadow: inset 4px 0 0 #0d6efd;
}
.cc-callcenter-assessor-summary-card {
border-left: .45rem solid var(--cc-assessor-accent, #adb5bd) !important;
background-color: rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .16);
box-shadow: inset 0 0 0 1px rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .22);
}
.cc-callcenter-assign-form {
padding: .5rem !important;
width: 100%;
padding: .72rem !important;
border: 1px solid rgba(var(--cc-assessor-accent-rgb, 173, 181, 189), .38) !important;
border-left: .55rem solid var(--cc-assessor-accent, #adb5bd);
background-color: var(--cc-assessor-accent, #adb5bd);
color: var(--cc-assessor-accent-contrast, #212529);
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned {
border-color: rgba(0, 0, 0, .10) !important;
border-left-color: #FFFFFF;
background-color: #FFFFFF;
color: #212529;
box-shadow: 0 .65rem 1.4rem rgba(15, 23, 42, .06), inset 0 0 0 1px rgba(0, 0, 0, .05);
}
.cc-callcenter-assign-form .text-muted {
color: var(--cc-assessor-accent-contrast, #212529) !important;
opacity: .88;
}
.cc-callcenter-assign-form.is-unassigned .text-muted {
color: #6C757D !important;
opacity: 1;
}
.cc-callcenter-assign-form .cc-callcenter-assessor-badge {
background-color: rgba(0, 0, 0, .16);
color: var(--cc-assessor-accent-contrast, #212529);
border-color: rgba(0, 0, 0, .16) !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assessor-badge {
background-color: #F8F9FA;
color: #6C757D;
border-color: rgba(0, 0, 0, .12) !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .04);
}
.cc-callcenter-assign-form .cc-callcenter-assessor-badge .cc-callcenter-color-dot {
border: 1px solid rgba(0, 0, 0, .18);
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assessor-badge .cc-callcenter-color-dot {
background: #ADB5BD;
border-color: rgba(0, 0, 0, .12);
}
.cc-callcenter-assign-row {
@ -807,20 +898,126 @@ require_once 'layout_header.php';
flex-wrap: nowrap;
align-items: center;
gap: .35rem;
padding: .5rem;
border-radius: .95rem;
background-color: transparent;
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assign-row {
background-color: #FFFFFF;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.cc-callcenter-assign-row .form-select {
min-width: 11rem;
flex: 1 1 11rem;
min-width: 10rem;
flex: 1 1 10rem;
border-color: rgba(0, 0, 0, .18);
background-color: rgba(0, 0, 0, .12);
color: var(--cc-assessor-accent-contrast, #212529);
font-weight: 700;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-assign-row .form-select {
border-color: rgba(0, 0, 0, .16);
background-color: #FFFFFF;
color: #212529;
}
.cc-callcenter-assign-row .form-select:focus {
border-color: rgba(0, 0, 0, .24);
box-shadow: 0 0 0 .2rem rgba(0, 0, 0, .12);
}
.cc-callcenter-assign-submit {
flex: 0 0 auto;
white-space: nowrap;
border-color: rgba(0, 0, 0, .18) !important;
background-color: rgba(0, 0, 0, .16) !important;
color: var(--cc-assessor-accent-contrast, #212529) !important;
font-weight: 700;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.cc-callcenter-actions {
gap: .35rem !important;
.cc-callcenter-assessor-badge {
display: inline-flex;
align-items: center;
gap: .3rem;
background-color: var(--cc-assessor-accent, #adb5bd);
color: var(--cc-assessor-accent-contrast, #212529);
border-color: var(--cc-assessor-accent, #adb5bd) !important;
font-weight: 600;
}
.cc-callcenter-color-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .3rem;
padding: .2rem .55rem;
white-space: nowrap;
border-color: rgba(0, 0, 0, .18) !important;
background-color: rgba(0, 0, 0, .12);
color: var(--cc-assessor-accent-contrast, #212529);
font-weight: 700;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.cc-callcenter-color-trigger:hover:not(:disabled),
.cc-callcenter-assign-submit:hover:not(:disabled) {
filter: brightness(.97);
transform: translateY(-1px);
}
.cc-callcenter-color-trigger:disabled {
opacity: .72;
background-color: rgba(0, 0, 0, .10);
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger {
background-color: #F1F3F5;
color: #6C757D;
border-color: rgba(0, 0, 0, .12) !important;
box-shadow: none;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger:disabled {
background-color: #F1F3F5;
}
.cc-callcenter-color-trigger .cc-callcenter-color-dot {
width: .72rem;
height: .72rem;
border-radius: 999px;
background: var(--cc-assessor-accent, #adb5bd);
border: 1px solid rgba(0, 0, 0, .24);
box-shadow: none;
flex: 0 0 auto;
}
.cc-callcenter-assign-form.is-unassigned .cc-callcenter-color-trigger .cc-callcenter-color-dot {
background: #ADB5BD;
border-color: rgba(0, 0, 0, .16);
}
.cc-callcenter-assessor-badge .cc-callcenter-color-dot {
width: .55rem;
height: .55rem;
border-radius: 999px;
background: var(--cc-assessor-accent-contrast, #212529);
border: 1px solid rgba(255, 255, 255, .35);
box-shadow: none;
flex: 0 0 auto;
}
.cc-callcenter-assessor-summary-card .cc-callcenter-color-dot {
width: .55rem;
height: .55rem;
border-radius: 999px;
background: var(--cc-assessor-accent, #adb5bd);
border: 1px solid rgba(0, 0, 0, .12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22);
flex: 0 0 auto;
}
.cc-callcenter-action-btn {
@ -1015,10 +1212,13 @@ require_once 'layout_header.php';
<div class="d-flex flex-wrap gap-2 align-items-stretch justify-content-lg-end">
<?php foreach ($assessorSummaryCards as $assessorCard): ?>
<div class="border rounded-3 bg-light px-3 py-2" style="min-width: 240px;">
<div class="border rounded-3 bg-light px-3 py-2 cc-callcenter-assessor-summary-card" style="min-width: 240px; <?php echo htmlspecialchars((string) $assessorCard['accent_style']); ?>">
<div class="d-flex justify-content-between align-items-center gap-2">
<div class="fw-bold <?php echo htmlspecialchars((string) $assessorCard['text_class']); ?>"><?php echo htmlspecialchars((string) $assessorCard['label']); ?></div>
<span class="badge <?php echo htmlspecialchars((string) $assessorCard['badge_class']); ?>">Pendientes: <?php echo (int) $assessorCard['pending']; ?></span>
<div class="fw-bold d-flex align-items-center gap-2">
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<span><?php echo htmlspecialchars((string) $assessorCard['label']); ?></span>
</div>
<span class="badge border cc-callcenter-assessor-badge">Pendientes: <?php echo (int) $assessorCard['pending']; ?></span>
</div>
<div class="mt-2">
<div class="small text-muted">Cupo restante (<?php echo (int) $cupoObjetivoPorAsesora; ?>)</div>
@ -1171,51 +1371,46 @@ require_once 'layout_header.php';
}
}
$assignFormClass = 'border rounded-3 p-1 text-start shadow-sm cc-callcenter-assign-form';
if ($unassigned) {
$assignFormClass .= ' bg-light';
} elseif ($assignedAssessorKey === 'KARINA') {
$assignFormClass .= ' bg-primary-subtle border-primary';
} elseif ($assignedAssessorKey === 'ESTEFANYA') {
$assignFormClass .= ' bg-success-subtle border-success';
} elseif ($assignedAssessorKey === 'CARMEN') {
$assignFormClass .= ' bg-warning-subtle border-warning';
} else {
$assignFormClass .= ' bg-secondary-subtle border-secondary';
}
$selectedAssessorKeyForSelect = $assignedAssessorKey !== null
? (string) $assignedAssessorKey
: '';
if ($unassigned) {
$selectedAssessorKeyForSelect = !empty($recommendedAssessorKey) ? (string) $recommendedAssessorKey : '';
} else {
$selectedAssessorKeyForSelect = $assignedAssessorKey !== null ? (string) $assignedAssessorKey : '';
}
$assignAccentKey = $selectedAssessorKeyForSelect !== ''
? (string) $selectedAssessorKeyForSelect
: ($assignedAssessorKey !== null ? (string) $assignedAssessorKey : '');
$buttonLabel = $unassigned
? 'Asignar pedido'
: ($assignedAssessorKey !== null ? 'Guardar asignación' : 'Desasignar');
$assignAccentHex = $assignAccentKey !== ''
? cc_test_assessor_effective_color_hex(
$assignAccentKey,
$assessors[$assignAccentKey]['color_hex'] ?? null
)
: '#FFFFFF';
$assignFormStyle = cc_test_assessor_css_vars(
$assignAccentKey !== '' ? $assignAccentKey : 'DEFAULT',
$assignAccentHex
);
$assignFormIsUnassigned = $assignAccentKey === '';
$badgeLabel = $unassigned
? 'Sin asignar'
: (string) ($assessors[$assignedAssessorKey]['label'] ?? ($assignedAssessorKey ?? 'Asignado'));
?>
<form method="post" class="<?php echo $assignFormClass; ?>">
<form method="post" class="border rounded-3 p-1 text-start cc-callcenter-assign-form<?php echo $assignFormIsUnassigned ? ' is-unassigned' : ''; ?>" style="<?php echo htmlspecialchars($assignFormStyle); ?>">
<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 elseif ($assignedAssessorKey === 'CARMEN'): ?>
<span class="badge bg-warning-subtle text-warning-emphasis border">CARMEN</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary-emphasis border">Asignado</span>
<?php endif; ?>
<span class="badge border cc-callcenter-assessor-badge">
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<span class="cc-callcenter-assessor-label"><?php echo htmlspecialchars($badgeLabel); ?></span>
</span>
</div>
<div class="cc-callcenter-assign-row">
<select name="target_assessor" class="form-select form-select-sm">
<select name="target_assessor" class="form-select form-select-sm js-assessor-select">
<option value=""<?php echo ($selectedAssessorKeyForSelect === '') ? ' selected' : ''; ?>>Sin asignar</option>
<?php if ($unassigned): ?>
@ -1227,7 +1422,13 @@ require_once 'layout_header.php';
<?php endif; ?>
</select>
<button type="submit" class="btn btn-sm btn-primary cc-callcenter-assign-submit cc-callcenter-action-btn"><?php echo htmlspecialchars((string) $buttonLabel); ?></button>
<button type="button" class="btn btn-sm cc-callcenter-color-trigger js-assessor-color-trigger" title="Selecciona una asesora para cambiar su color" aria-label="Selecciona una asesora para cambiar su color"<?php echo $selectedAssessorKeyForSelect === '' ? ' disabled' : ''; ?>>
<span class="cc-callcenter-color-dot" aria-hidden="true"></span>
<i class="bi bi-palette"></i>
<span>Color</span>
</button>
<button type="submit" class="btn btn-sm cc-callcenter-assign-submit cc-callcenter-action-btn">Asignar pedido</button>
</div>
</form>
<?php endif; ?>
@ -1748,6 +1949,50 @@ require_once 'layout_header.php';
", $modalsHtml); ?>
<?php endif; ?>
<div class="modal fade" id="ccAssessorColorModal" tabindex="-1" aria-labelledby="ccAssessorColorModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form method="post" id="ccAssessorColorForm">
<div class="modal-header align-items-start gap-2">
<div>
<div class="small text-uppercase text-muted fw-semibold">Color de asesora</div>
<h3 class="modal-title h5 mb-0" id="ccAssessorColorModalLabel">Configurar color</h3>
</div>
<button type="button" class="btn-close mt-1" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<div class="modal-body">
<input type="hidden" name="action" value="update_assessor_color">
<input type="hidden" name="assessor_key" id="ccAssessorColorAssessorKey">
<div class="mb-3">
<label for="ccAssessorColorAssessorLabel" class="form-label">Asesora</label>
<input type="text" class="form-control" id="ccAssessorColorAssessorLabel" readonly>
</div>
<div class="mb-3">
<label for="ccAssessorColorInput" class="form-label">Color</label>
<div class="d-flex align-items-center gap-3 flex-wrap">
<input type="color" class="form-control form-control-color" id="ccAssessorColorInput" name="color_hex" value="#0D6EFD" title="Elegir color">
<code class="small" id="ccAssessorColorHexText">#0D6EFD</code>
</div>
<div class="form-text">Este color se aplicará a la franja, al badge y al panel de rendimiento de la asesora.</div>
</div>
<div class="rounded-4 border p-3" id="ccAssessorColorPreview">
<div class="small text-muted">Vista previa</div>
<div class="fw-semibold mt-1" id="ccAssessorColorPreviewLabel">Asesora</div>
<code class="small" id="ccAssessorColorPreviewHex">#0D6EFD</code>
</div>
</div>
<div class="modal-footer d-flex justify-content-between flex-wrap gap-2">
<div class="small text-muted">Guarda un color diferente para reconocer rápido cada asesora.</div>
<div class="d-flex gap-2 ms-auto flex-wrap">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancelar</button>
<button type="submit" class="btn btn-primary">Guardar color</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="position-fixed bottom-0 end-0 p-3" id="airDroidAssistPanel" style="z-index: 1080; max-width: 26rem; width: min(26rem, calc(100vw - 1.5rem));">
<div class="card border-primary shadow-lg d-none" id="airDroidAssistCard">
<div class="card-header bg-primary text-white d-flex align-items-start justify-content-between gap-3">
@ -1786,6 +2031,7 @@ require_once 'layout_header.php';
<script>
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 ccAssessorCatalog = <?php echo json_encode($assessorUiCatalog, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
function getLocationControls(sourceKey) {
return {
@ -2454,6 +2700,159 @@ const clearSelectedOrders = document.getElementById('clearSelectedOrders');
const bulkOrderCheckboxes = Array.from(document.querySelectorAll('.js-bulk-order-checkbox'));
const quickSelectButtons = Array.from(document.querySelectorAll('.js-quick-select-btn'));
const bulkAssessorSelect = bulkAssignForm ? bulkAssignForm.querySelector('select[name="target_assessor"]') : null;
const ccAssessorColorModalElement = document.getElementById('ccAssessorColorModal');
const ccAssessorColorModalInstance = ccAssessorColorModalElement && window.bootstrap && window.bootstrap.Modal
? window.bootstrap.Modal.getOrCreateInstance(ccAssessorColorModalElement)
: null;
const ccAssessorColorAssessorKey = document.getElementById('ccAssessorColorAssessorKey');
const ccAssessorColorAssessorLabel = document.getElementById('ccAssessorColorAssessorLabel');
const ccAssessorColorInput = document.getElementById('ccAssessorColorInput');
const ccAssessorColorHexText = document.getElementById('ccAssessorColorHexText');
const ccAssessorColorPreview = document.getElementById('ccAssessorColorPreview');
const ccAssessorColorPreviewLabel = document.getElementById('ccAssessorColorPreviewLabel');
const ccAssessorColorPreviewHex = document.getElementById('ccAssessorColorPreviewHex');
function normalizeAssessorHexColor(value) {
value = String(value || '').trim().toUpperCase();
if (!value) {
return '';
}
if (value.charAt(0) !== '#') {
value = '#' + value;
}
return /^#[0-9A-F]{6}$/.test(value) ? value : '';
}
function assessorHexToRgbTriplet(hex) {
const normalized = normalizeAssessorHexColor(hex);
if (!normalized) {
return null;
}
const intValue = parseInt(normalized.slice(1), 16);
return [
(intValue >> 16) & 255,
(intValue >> 8) & 255,
intValue & 255
];
}
function syncAssessorColorPreview(colorHex, assessorLabel) {
const resolvedHex = normalizeAssessorHexColor(colorHex) || '#ADB5BD';
const rgb = assessorHexToRgbTriplet(resolvedHex) || [173, 181, 189];
const backgroundColor = 'rgba(' + rgb.join(', ') + ', .16)';
const borderColor = 'rgba(' + rgb.join(', ') + ', .28)';
if (ccAssessorColorPreview) {
ccAssessorColorPreview.style.background = backgroundColor;
ccAssessorColorPreview.style.borderColor = borderColor;
}
if (ccAssessorColorPreviewLabel) {
ccAssessorColorPreviewLabel.textContent = assessorLabel || 'Asesora';
}
if (ccAssessorColorPreviewHex) {
ccAssessorColorPreviewHex.textContent = resolvedHex;
}
if (ccAssessorColorHexText) {
ccAssessorColorHexText.textContent = resolvedHex;
}
}
function syncAssessorFormAccent(form) {
if (!form) {
return;
}
const select = form.querySelector('.js-assessor-select');
const colorButton = form.querySelector('.js-assessor-color-trigger');
const assessorKey = select ? (select.value || '') : '';
const config = assessorKey && ccAssessorCatalog[assessorKey] ? ccAssessorCatalog[assessorKey] : null;
const isUnassigned = !assessorKey || !config;
const colorHex = isUnassigned
? '#FFFFFF'
: (normalizeAssessorHexColor(config ? config.color_hex : '') || '#ADB5BD');
const rgb = assessorHexToRgbTriplet(colorHex) || [255, 255, 255];
const contrastHex = isUnassigned
? '#212529'
: (normalizeAssessorHexColor(config ? config.contrast_hex : '') || '#212529');
form.classList.toggle('is-unassigned', isUnassigned);
form.style.setProperty('--cc-assessor-accent', colorHex);
form.style.setProperty('--cc-assessor-accent-rgb', rgb.join(', '));
form.style.setProperty('--cc-assessor-accent-contrast', contrastHex);
if (colorButton) {
colorButton.disabled = !assessorKey;
const buttonTitle = assessorKey && config
? 'Cambiar color de ' + config.label
: 'Selecciona una asesora para cambiar su color';
colorButton.title = buttonTitle;
colorButton.setAttribute('aria-label', buttonTitle);
const dot = colorButton.querySelector('.cc-callcenter-color-dot');
if (dot) {
dot.style.backgroundColor = isUnassigned ? '#ADB5BD' : colorHex;
}
}
}
function openAssessorColorModal(trigger) {
const form = trigger ? trigger.closest('form') : null;
const select = form ? form.querySelector('.js-assessor-select') : null;
const assessorKey = select ? (select.value || '') : '';
const config = assessorKey && ccAssessorCatalog[assessorKey] ? ccAssessorCatalog[assessorKey] : null;
if (!assessorKey || !config) {
alert('Selecciona una asesora para cambiar su color.');
return;
}
const colorHex = normalizeAssessorHexColor(config.color_hex) || '#ADB5BD';
if (ccAssessorColorAssessorKey) {
ccAssessorColorAssessorKey.value = assessorKey;
}
if (ccAssessorColorAssessorLabel) {
ccAssessorColorAssessorLabel.value = config.label || assessorKey;
}
if (ccAssessorColorInput) {
ccAssessorColorInput.value = colorHex.toLowerCase();
}
syncAssessorColorPreview(colorHex, config.label || assessorKey);
if (ccAssessorColorModalInstance) {
ccAssessorColorModalInstance.show();
}
}
document.querySelectorAll('.cc-callcenter-assign-form').forEach(function (form) {
syncAssessorFormAccent(form);
const select = form.querySelector('.js-assessor-select');
if (select) {
select.addEventListener('change', function () {
syncAssessorFormAccent(form);
});
}
const colorButton = form.querySelector('.js-assessor-color-trigger');
if (colorButton) {
colorButton.addEventListener('click', function () {
openAssessorColorModal(this);
});
}
});
if (ccAssessorColorInput) {
ccAssessorColorInput.addEventListener('input', function () {
const label = ccAssessorColorAssessorLabel ? (ccAssessorColorAssessorLabel.value || 'Asesora') : 'Asesora';
syncAssessorColorPreview(this.value, label);
});
}
function getSelectedBulkOrderCheckboxes() {
return bulkOrderCheckboxes.filter(function (checkbox) {

View File

@ -17,6 +17,110 @@ function cc_test_ensure_column(PDO $pdo, string $table, string $column, string $
$cache[$key] = true;
}
function cc_test_column_exists(PDO $pdo, string $table, string $column): bool
{
static $cache = [];
$key = $table . '.' . $column;
if (array_key_exists($key, $cache)) {
return $cache[$key];
}
try {
$stmt = $pdo->prepare("SHOW COLUMNS FROM `{$table}` LIKE ?");
$stmt->execute([$column]);
$cache[$key] = (bool) $stmt->fetch(PDO::FETCH_ASSOC);
} catch (Throwable $exception) {
$cache[$key] = false;
}
return $cache[$key];
}
function cc_test_normalize_hex_color(?string $value): ?string
{
$value = strtoupper(trim((string) $value));
if ($value === '') {
return null;
}
if ($value[0] !== '#') {
$value = '#' . $value;
}
return preg_match('/^#[0-9A-F]{6}$/', $value) ? $value : null;
}
function cc_test_hex_to_rgb_triplet(string $hex): ?array
{
$normalized = cc_test_normalize_hex_color($hex);
if ($normalized === null) {
return null;
}
$normalized = ltrim($normalized, '#');
return [
hexdec(substr($normalized, 0, 2)),
hexdec(substr($normalized, 2, 2)),
hexdec(substr($normalized, 4, 2)),
];
}
function cc_test_assessor_default_color_hex(string $assessorKey): string
{
return match (cc_test_normalize_user_key($assessorKey)) {
'KARINA' => '#0D6EFD',
'ESTEFANYA' => '#198754',
'CARMEN' => '#FF00FF',
default => '#6C757D',
};
}
function cc_test_assessor_effective_color_hex(string $assessorKey, ?string $storedColorHex = null): string
{
return cc_test_normalize_hex_color($storedColorHex) ?? cc_test_assessor_default_color_hex($assessorKey);
}
function cc_test_hex_relative_luminance(string $hex): float
{
$rgb = cc_test_hex_to_rgb_triplet($hex) ?? [108, 117, 125];
$linear = array_map(
static function (int $channel): float {
$value = $channel / 255;
return $value <= 0.03928
? $value / 12.92
: pow(($value + 0.055) / 1.055, 2.4);
},
$rgb
);
return (0.2126 * $linear[0]) + (0.7152 * $linear[1]) + (0.0722 * $linear[2]);
}
function cc_test_assessor_contrast_text_hex(string $hex): string
{
static $darkText = '#212529';
static $darkLuminance = null;
if ($darkLuminance === null) {
$darkLuminance = cc_test_hex_relative_luminance($darkText);
}
$luminance = cc_test_hex_relative_luminance($hex);
$contrastWithWhite = 1.05 / ($luminance + 0.05);
$contrastWithDark = ($luminance + 0.05) / ($darkLuminance + 0.05);
return $contrastWithDark >= $contrastWithWhite ? $darkText : '#FFFFFF';
}
function cc_test_assessor_css_vars(string $assessorKey, ?string $storedColorHex = null): string
{
$hex = cc_test_assessor_effective_color_hex($assessorKey, $storedColorHex);
$rgb = cc_test_hex_to_rgb_triplet($hex) ?? [108, 117, 125];
return '--cc-assessor-accent:' . $hex . '; --cc-assessor-accent-rgb:' . implode(', ', $rgb) . '; --cc-assessor-accent-contrast:' . cc_test_assessor_contrast_text_hex($hex) . ';';
}
function cc_test_ensure_tracking_table(PDO $pdo): void
{
static $checked = false;
@ -140,7 +244,9 @@ function cc_test_fetch_assessors(PDO $pdo, array $allowedNames = []): array
$allowedNames = cc_test_default_assessor_keys();
}
$stmt = $pdo->prepare("SELECT id, username, nombre_asesor FROM users WHERE role = 'Asesor'");
$hasColorColumn = cc_test_column_exists($pdo, 'users', 'color_hex');
$selectColorColumn = $hasColorColumn ? ', color_hex' : ', NULL AS color_hex';
$stmt = $pdo->prepare("SELECT id, username, nombre_asesor{$selectColorColumn} FROM users WHERE role = 'Asesor'");
$stmt->execute();
$allowedLookup = array_fill_keys(array_map('strtoupper', $allowedNames), true);
@ -155,6 +261,7 @@ function cc_test_fetch_assessors(PDO $pdo, array $allowedNames = []): array
$byKey[$key] = [
'id' => (int) $row['id'],
'label' => trim((string) ($row['nombre_asesor'] ?: $row['username'] ?: ('Asesor #' . (int) $row['id']))),
'color_hex' => cc_test_normalize_hex_color($row['color_hex'] ?? null),
];
}
@ -627,22 +734,22 @@ function cc_test_row_background_style(string $estado): string
'POR LLAMAR', 'DEVOLVER LLAMADA' => '#ffffff',
// Amarillo
'OBSERVADO' => '#fff3cd',
'OBSERVADO' => '#FFFF00',
// Verde claro
'SE ENVIO NUMERO DE CUENTA' => '#d1e7dd',
// Verde brillante
'SE ENVIO NUMERO DE CUENTA' => '#00FF00',
// Naranja
'CONFIRMADO CONTRAENTREGA' => '#ffb74d',
// Verde
'CONFIRMADO ENVIO' => '#b6f1c7',
// Verde Google
'CONFIRMADO ENVIO' => '#34A853',
// Rojo
'CANCELADO' => '#f8d7da',
'CANCELADO' => '#FF0000',
// Plomo
'REPETIDO' => '#d6d8db',
'REPETIDO' => '#CCCCCC',
default => '#ffffff',
};