Autosave: 20260725-171839

This commit is contained in:
Flatlogic Bot 2026-07-25 17:17:12 +00:00
parent 8633846941
commit 7a6edd7faf
8 changed files with 263 additions and 57 deletions

View File

@ -1382,7 +1382,7 @@ require_once 'layout_header.php';
<?php $rowHtml = ob_get_clean(); echo $rowHtml; ?>
<?php ob_start(); ?>
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header align-items-start gap-2">
@ -1601,7 +1601,7 @@ require_once 'layout_header.php';
<div class="col-md-6 js-numero-cuenta-group <?php echo $mostrarCamposNumeroCuenta ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="numero_cuenta_sede_id-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">SEDE / ID</label>
<input type="text" class="form-control" id="numero_cuenta_sede_id-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars($numeroCuentaSedeIdSeleccionado); ?>" placeholder="Ej.: SJL / 1542">
<div class="form-text">Solo aparece en <strong>SE ENVIO NUMERO DE CUENTA</strong>.</div>
<div class="form-text">Se muestra en <strong>SE ENVIO NUMERO DE CUENTA</strong> y se mantiene en <strong>CONFIRMADO ENVIO</strong>.</div>
</div>
<div class="col-md-6 js-numero-cuenta-group <?php echo $mostrarCamposNumeroCuenta ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="numero_cuenta_dni-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label"> DNI</label>
@ -1783,7 +1783,6 @@ require_once 'layout_header.php';
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<button type="button" class="btn btn-outline-danger" onclick="eliminarPedido('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Eliminar del panel</button>
<?php endif; ?>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
<button type="button" class="btn btn-primary" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Guardar gestión</button>
</div>
</div>
@ -2061,7 +2060,7 @@ function toggleAgendaFields(sourceKey) {
const deliveryInput = document.getElementById('fecha-entrega-' + sourceKey);
const needsDeliveryDate = estado === 'CONFIRMADO CONTRAENTREGA';
const needsShippingDetails = estado === 'CONFIRMADO ENVIO';
const needsAccountNumberDetails = estado === 'SE ENVIO NUMERO DE CUENTA';
const needsAccountNumberDetails = ['SE ENVIO NUMERO DE CUENTA', 'CONFIRMADO ENVIO'].includes(estado);
const needsNextCall = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO'].includes(estado);
if (nextCallGroup) {
@ -2378,7 +2377,7 @@ function showAirDroidHelper(details) {
if (modalId) {
const modalElement = document.getElementById(modalId);
if (modalElement && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalElement).show();
window.bootstrap.Modal.getOrCreateInstance(modalElement, { backdrop: 'static', keyboard: false }).show();
}
}
@ -2422,7 +2421,7 @@ function registrarLlamada(event, trigger) {
if (modalId) {
const modalElement = document.getElementById(modalId);
if (modalElement && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalElement).show();
window.bootstrap.Modal.getOrCreateInstance(modalElement, { backdrop: 'static', keyboard: false }).show();
}
}

View File

@ -0,0 +1,3 @@
-- Agrega la columna de resultados para Producción de Video
ALTER TABLE marketing_videos
ADD COLUMN IF NOT EXISTS resultados ENUM('DESCONTINUADO', 'REDIMIENTO MEDIUM', 'BUEN RENDIMIENTO') NULL AFTER estado;

View File

@ -0,0 +1,3 @@
-- Amplía el ENUM de resultados para incluir REDIMIENTO MEDIUM.
ALTER TABLE marketing_videos
MODIFY COLUMN resultados ENUM('DESCONTINUADO', 'REDIMIENTO MEDIUM', 'BUEN RENDIMIENTO') NULL AFTER estado;

View File

@ -2066,7 +2066,7 @@ require_once 'layout_header.php';
<?php $rowHtml = ob_get_clean(); echo $rowHtml; ?>
<?php ob_start(); ?>
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="<?php echo htmlspecialchars($modalId); ?>" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header align-items-start gap-2">
@ -2285,7 +2285,7 @@ require_once 'layout_header.php';
<div class="col-md-6 js-numero-cuenta-group <?php echo $mostrarCamposNumeroCuenta ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="numero_cuenta_sede_id-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label">SEDE / ID</label>
<input type="text" class="form-control" id="numero_cuenta_sede_id-<?php echo htmlspecialchars($order['source_key']); ?>" value="<?php echo htmlspecialchars($numeroCuentaSedeIdSeleccionado); ?>" placeholder="Ej.: SJL / 1542">
<div class="form-text">Solo aparece en <strong>SE ENVIO NUMERO DE CUENTA</strong>.</div>
<div class="form-text">Se muestra en <strong>SE ENVIO NUMERO DE CUENTA</strong> y se mantiene en <strong>CONFIRMADO ENVIO</strong>.</div>
</div>
<div class="col-md-6 js-numero-cuenta-group <?php echo $mostrarCamposNumeroCuenta ? '' : 'd-none'; ?>" data-source-key="<?php echo htmlspecialchars($order['source_key']); ?>">
<label for="numero_cuenta_dni-<?php echo htmlspecialchars($order['source_key']); ?>" class="form-label"> DNI</label>
@ -2467,7 +2467,6 @@ require_once 'layout_header.php';
<?php if (in_array($_SESSION['user_role'] ?? '', ['Administrador', 'admin'], true)): ?>
<button type="button" class="btn btn-outline-danger" onclick="eliminarPedido('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Eliminar del panel</button>
<?php endif; ?>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
<button type="button" class="btn btn-primary" onclick="guardarGestion('<?php echo htmlspecialchars($order['source_key']); ?>', this)">Guardar gestión</button>
</div>
</div>
@ -2790,7 +2789,7 @@ function toggleAgendaFields(sourceKey) {
const deliveryInput = document.getElementById('fecha-entrega-' + sourceKey);
const needsDeliveryDate = estado === 'CONFIRMADO CONTRAENTREGA';
const needsShippingDetails = estado === 'CONFIRMADO ENVIO';
const needsAccountNumberDetails = estado === 'SE ENVIO NUMERO DE CUENTA';
const needsAccountNumberDetails = ['SE ENVIO NUMERO DE CUENTA', 'CONFIRMADO ENVIO'].includes(estado);
const needsNextCall = ['POR LLAMAR', 'DEVOLVER LLAMADA', 'OBSERVADO'].includes(estado);
if (nextCallGroup) {
@ -3107,7 +3106,7 @@ function showAirDroidHelper(details) {
if (modalId) {
const modalElement = document.getElementById(modalId);
if (modalElement && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalElement).show();
window.bootstrap.Modal.getOrCreateInstance(modalElement, { backdrop: 'static', keyboard: false }).show();
}
}
@ -3151,7 +3150,7 @@ function registrarLlamada(event, trigger) {
if (modalId) {
const modalElement = document.getElementById(modalId);
if (modalElement && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalElement).show();
window.bootstrap.Modal.getOrCreateInstance(modalElement, { backdrop: 'static', keyboard: false }).show();
}
}

View File

@ -1199,7 +1199,7 @@ function cc_test_requires_shipping_details(string $estado): bool
function cc_test_requires_account_number_fields(string $estado): bool
{
return cc_test_normalize_state($estado) === 'SE ENVIO NUMERO DE CUENTA';
return in_array(cc_test_normalize_state($estado), ['SE ENVIO NUMERO DE CUENTA', 'CONFIRMADO ENVIO'], true);
}
function cc_test_pending_logistica_destination(array $order): ?string

View File

@ -2,6 +2,62 @@
// Auto-inicializa pequeños ajustes del esquema de Marketing cuando faltan columnas.
// Así evitamos errores al abrir la pantalla o guardar datos nuevos.
function marketing_apply_migration_if_missing(PDO $pdo, string $columnName, string $migrationFile): void
{
$checkColumnStmt = $pdo->prepare(
'SELECT COUNT(*) AS c '
. 'FROM information_schema.columns '
. 'WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?'
);
$checkColumnStmt->execute(['marketing_videos', $columnName]);
$row = $checkColumnStmt->fetch(PDO::FETCH_ASSOC);
$count = (int)($row['c'] ?? 0);
if ($count > 0) {
return;
}
if (!is_file($migrationFile)) {
throw new RuntimeException('No se encontró la migración: ' . basename($migrationFile));
}
$sql = file_get_contents($migrationFile);
if ($sql === false) {
throw new RuntimeException('No se pudo leer la migración: ' . basename($migrationFile));
}
$pdo->exec($sql);
}
function marketing_apply_migration_if_column_type_missing(PDO $pdo, string $columnName, string $needle, string $migrationFile): void
{
$checkColumnStmt = $pdo->prepare(
'SELECT COLUMN_TYPE AS column_type '
. 'FROM information_schema.columns '
. 'WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?'
);
$checkColumnStmt->execute(['marketing_videos', $columnName]);
$row = $checkColumnStmt->fetch(PDO::FETCH_ASSOC);
$columnType = (string)($row['column_type'] ?? '');
if ($columnType !== '' && stripos($columnType, $needle) !== false) {
return;
}
if (!is_file($migrationFile)) {
throw new RuntimeException('No se encontró la migración: ' . basename($migrationFile));
}
$sql = file_get_contents($migrationFile);
if ($sql === false) {
throw new RuntimeException('No se pudo leer la migración: ' . basename($migrationFile));
}
$pdo->exec($sql);
}
function marketing_ensure_schema(PDO $pdo): void
{
static $attempted = false;
@ -10,27 +66,22 @@ function marketing_ensure_schema(PDO $pdo): void
}
$attempted = true;
$checkColumnStmt = $pdo->prepare(
'SELECT COUNT(*) AS c '
. 'FROM information_schema.columns '
. 'WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?'
marketing_apply_migration_if_missing(
$pdo,
'observacion',
__DIR__ . '/../db/migrations/086_add_observacion_to_marketing_videos.sql'
);
$checkColumnStmt->execute(['marketing_videos', 'observacion']);
$row = $checkColumnStmt->fetch(PDO::FETCH_ASSOC);
$count = (int)($row['c'] ?? 0);
marketing_apply_migration_if_missing(
$pdo,
'resultados',
__DIR__ . '/../db/migrations/087_add_resultados_to_marketing_videos.sql'
);
if ($count === 0) {
$file = __DIR__ . '/../db/migrations/086_add_observacion_to_marketing_videos.sql';
if (!is_file($file)) {
throw new RuntimeException('No se encontró la migración: ' . basename($file));
}
$sql = file_get_contents($file);
if ($sql === false) {
throw new RuntimeException('No se pudo leer la migración: ' . basename($file));
}
$pdo->exec($sql);
}
marketing_apply_migration_if_column_type_missing(
$pdo,
'resultados',
'REDIMIENTO MEDIUM',
__DIR__ . '/../db/migrations/088_update_resultados_enum_to_include_redimiento_medium.sql'
);
}

View File

@ -19,6 +19,8 @@ $stmt_videos = $db->query("SELECT mv.*, p.nombre as nombre_producto
$videos = $stmt_videos->fetchAll(PDO::FETCH_ASSOC);
$statusOptions = ['PENDIENTE', 'EN PROCESO', 'TERMINADO', 'OBSERVADO', 'APROBADO', 'PUBLICADO'];
$resultOptions = ['DESCONTINUADO', 'REDIMIENTO MEDIUM', 'BUEN RENDIMIENTO'];
$isAdminUser = in_array($_SESSION['user_role'] ?? ($_SESSION['role'] ?? ''), ['Administrador', 'admin'], true);
function marketing_status_badge_class(?string $estado): string
{
@ -33,6 +35,16 @@ function marketing_status_badge_class(?string $estado): string
};
}
function marketing_result_row_class(?string $resultados): string
{
return match (strtoupper(trim((string)$resultados))) {
'DESCONTINUADO' => 'video-row-result-descontinuado',
'REDIMIENTO MEDIUM' => 'video-row-result-medium',
'BUEN RENDIMIENTO' => 'video-row-result-buen-rendimiento',
default => '',
};
}
// Contadores de estado
$stats = [
'TOTAL' => count($videos),
@ -193,7 +205,8 @@ foreach ($videos as $v) {
font-weight: 600;
letter-spacing: 0.3px;
}
.status-select {
.status-select,
.result-select {
min-width: 160px;
border: 0;
border-radius: 999px;
@ -206,18 +219,26 @@ foreach ($videos as $v) {
padding: 0.55rem 2.25rem 0.55rem 0.9rem;
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.06);
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
background-color: #f8fafc;
color: #1f2937;
}
.status-select:hover {
.status-select:hover,
.result-select:hover {
transform: translateY(-1px);
box-shadow: 0 8px 16px rgba(15, 23, 42, 0.08);
}
.status-select:focus {
.status-select:focus,
.result-select:focus {
box-shadow: 0 0 0 0.2rem rgba(67, 97, 238, 0.16);
}
.status-select:disabled {
.status-select:disabled,
.result-select:disabled {
opacity: 0.8;
cursor: wait;
}
.result-select[data-readonly="1"]:disabled {
cursor: not-allowed;
}
.status-select[data-status="PENDIENTE"] {
background-color: #ffe082;
color: #5f4b00;
@ -242,9 +263,46 @@ foreach ($videos as $v) {
background-color: #dfe4ea;
color: #111827;
}
.status-feedback {
.result-select[data-resultados="DESCONTINUADO"] {
background-color: #ffe0e6;
color: #b42318;
}
.result-select[data-resultados="REDIMIENTO MEDIUM"] {
background-color: #fff3cd;
color: #8a6d00;
}
.result-select[data-resultados="BUEN RENDIMIENTO"] {
background-color: #dcfce7;
color: #166534;
}
.status-feedback,
.resultados-feedback {
min-height: 18px;
}
.video-row-result-descontinuado > td,
.video-row-result-descontinuado > th {
background-color: rgba(239, 68, 68, 0.11) !important;
}
.video-row-result-medium > td,
.video-row-result-medium > th {
background-color: rgba(255, 243, 205, 0.26) !important;
}
.video-row-result-buen-rendimiento > td,
.video-row-result-buen-rendimiento > th {
background-color: rgba(34, 197, 94, 0.11) !important;
}
.video-row-result-descontinuado:hover > td,
.video-row-result-descontinuado:hover > th {
background-color: rgba(239, 68, 68, 0.16) !important;
}
.video-row-result-medium:hover > td,
.video-row-result-medium:hover > th {
background-color: rgba(255, 243, 205, 0.34) !important;
}
.video-row-result-buen-rendimiento:hover > td,
.video-row-result-buen-rendimiento:hover > th {
background-color: rgba(34, 197, 94, 0.16) !important;
}
.editable:hover {
background-color: #f8f9fa;
cursor: pointer;
@ -374,7 +432,7 @@ foreach ($videos as $v) {
<div>
<h2 class="mb-0 fw-bold" style="color: #2b2d42;">Producción de Video</h2>
<p class="text-muted small mb-1">Gestión de activos y flujo de trabajo creativo</p>
<p class="text-muted small mb-0">La columna <strong>Observación</strong> se guarda al salir del campo y <strong>Estado</strong> al cambiar la opción.</p>
<p class="text-muted small mb-0">Las columnas <strong>Observación</strong> y <strong>Estado</strong> se guardan al salir del campo o al cambiar la opción. <strong>Resultados</strong> solo puede editarlo el rol <strong>Administrador</strong> y puedes volver a <strong>Sin definir</strong> para limpiar la selección.</p>
</div>
<div class="d-flex align-items-center gap-3">
<div class="bg-white p-1 rounded-3 shadow-sm d-flex border">
@ -421,20 +479,22 @@ foreach ($videos as $v) {
<th class="text-center"><i class="fas fa-file-image me-1"></i> Link Flyer</th>
<th><i class="fas fa-comment-dots me-1"></i> Observación</th>
<th class="text-center"><i class="fas fa-tasks me-1"></i> Estado</th>
<th class="text-center"><i class="fas fa-chart-line me-1"></i> Resultados<?php if (!$isAdminUser): ?> <span class="badge bg-secondary ms-1">Solo admin</span><?php endif; ?></th>
<th class="text-center"><i class="fas fa-cog me-1"></i> Acción</th>
</tr>
</thead>
<tbody>
<?php if (empty($videos)): ?>
<tr>
<td colspan="14" class="text-center py-5 text-muted">
<td colspan="15" class="text-center py-5 text-muted">
<i class="fas fa-video-slash fa-3x mb-3 d-block opacity-25"></i>
No hay pedidos registrados.
</td>
</tr>
<?php else: ?>
<?php foreach ($videos as $v): ?>
<tr>
<?php $resultadoValue = strtoupper(trim((string)($v['resultados'] ?? ''))); ?>
<tr id="video-row-<?php echo $v['id']; ?>" class="<?php echo marketing_result_row_class($resultadoValue); ?>">
<td class="text-center fw-bold text-primary"><?php echo $v['orden']; ?></td>
<td>
<div class="d-flex align-items-center">
@ -557,6 +617,7 @@ foreach ($videos as $v) {
<select
class="form-select form-select-sm status-select"
data-id="<?php echo $v['id']; ?>"
data-field="estado"
data-status="<?php echo htmlspecialchars($v['estado'] ?? ''); ?>"
aria-label="Estado del pedido <?php echo (int)$v['id']; ?>">
<?php foreach ($statusOptions as $statusOption): ?>
@ -567,6 +628,23 @@ foreach ($videos as $v) {
</select>
<div class="small mt-1 status-feedback" id="status-feedback-<?php echo $v['id']; ?>"></div>
</td>
<td class="text-center">
<select
class="form-select form-select-sm result-select"
data-id="<?php echo $v['id']; ?>"
data-field="resultados"
data-resultados="<?php echo htmlspecialchars($resultadoValue); ?>"
aria-label="Resultados del pedido <?php echo (int)$v['id']; ?>"
<?php echo $isAdminUser ? '' : 'data-readonly="1" disabled aria-disabled="true" title="Solo un administrador puede cambiar este campo"'; ?>>
<option value="" <?php echo ($resultadoValue === '') ? 'selected' : ''; ?>>Sin definir</option>
<?php foreach ($resultOptions as $resultOption): ?>
<option value="<?php echo htmlspecialchars($resultOption); ?>" <?php echo ($resultadoValue === $resultOption) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($resultOption); ?>
</option>
<?php endforeach; ?>
</select>
<div class="small mt-1 resultados-feedback" id="resultados-feedback-<?php echo $v['id']; ?>"></div>
</td>
<td class="text-center">
<div class="d-flex justify-content-center gap-1">
<button class="btn btn-sm btn-light border shadow-sm" data-video-id="<?php echo $v['id']; ?>" data-video-payload="<?php echo htmlspecialchars(json_encode($v, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), ENT_QUOTES, 'UTF-8'); ?>" onclick="gestionarVideoFromButton(this)" title="Editar">
@ -787,7 +865,7 @@ foreach ($videos as $v) {
</div>
<!-- Modal Gestionar Video -->
<div class="modal fade" id="gestionarVideoModal" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="gestionarVideoModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form action="save_marketing_video.php" method="POST">
@ -1065,17 +1143,26 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
const statusSelects = document.querySelectorAll('.status-select');
statusSelects.forEach(select => {
const workflowSelects = document.querySelectorAll('.status-select, .result-select');
workflowSelects.forEach(select => {
const field = select.dataset.field || 'estado';
const feedbackBaseClass = field === 'estado' ? 'status-feedback' : 'resultados-feedback';
const feedback = getWorkflowFeedbackElement(field, select.dataset.id);
let lastSavedValue = select.value;
const feedback = document.getElementById(`status-feedback-${select.dataset.id}`);
const setFeedback = (message, className = 'text-muted') => {
if (!feedback) return;
feedback.className = `small mt-1 status-feedback ${className}`;
feedback.className = `small mt-1 ${feedbackBaseClass} ${className}`;
feedback.textContent = message;
};
if (field === 'estado') {
select.dataset.status = select.value;
} else if (field === 'resultados') {
select.dataset.resultados = select.value;
updateVideoResultRow(select.dataset.id, select.value);
}
select.addEventListener('change', function() {
const newValue = select.value;
const previousValue = lastSavedValue;
@ -1085,7 +1172,11 @@ document.addEventListener('DOMContentLoaded', function() {
}
select.disabled = true;
select.dataset.status = newValue;
if (field === 'estado') {
select.dataset.status = newValue;
} else if (field === 'resultados') {
select.dataset.resultados = newValue;
}
setFeedback('Guardando...', 'text-primary');
fetch('update_marketing_video_field.php', {
@ -1093,7 +1184,7 @@ document.addEventListener('DOMContentLoaded', function() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: select.dataset.id,
field: 'estado',
field,
value: newValue
})
})
@ -1101,10 +1192,16 @@ document.addEventListener('DOMContentLoaded', function() {
.then(data => {
if (data.success) {
lastSavedValue = newValue;
syncVideoPayload(select.dataset.id, 'estado', newValue);
updateCardStatusBadge(select.dataset.id, newValue);
updateStatusCounter(previousValue, -1);
updateStatusCounter(newValue, 1);
syncVideoPayload(select.dataset.id, field, newValue);
if (field === 'estado') {
updateCardStatusBadge(select.dataset.id, newValue);
updateStatusCounter(previousValue, -1);
updateStatusCounter(newValue, 1);
} else if (field === 'resultados') {
updateVideoResultRow(select.dataset.id, newValue);
}
setFeedback('Guardado', 'text-success');
setTimeout(() => {
if (feedback && feedback.textContent === 'Guardado') {
@ -1113,14 +1210,22 @@ document.addEventListener('DOMContentLoaded', function() {
}, 2000);
} else {
select.value = previousValue;
select.dataset.status = previousValue;
if (field === 'estado') {
select.dataset.status = previousValue;
} else if (field === 'resultados') {
select.dataset.resultados = previousValue;
}
setFeedback('Error al guardar', 'text-danger');
alert('Error: ' + data.error);
}
})
.catch(() => {
select.value = previousValue;
select.dataset.status = previousValue;
if (field === 'estado') {
select.dataset.status = previousValue;
} else if (field === 'resultados') {
select.dataset.resultados = previousValue;
}
setFeedback('Error al guardar', 'text-danger');
})
.finally(() => {
@ -1195,7 +1300,11 @@ function gestionarVideo(video) {
fillLinks('edit_links_landing_container', 'link_landing[]', video.link_landing);
fillLinks('edit_links_flyer_container', 'link_flyer[]', video.link_flyer);
var myModal = new bootstrap.Modal(document.getElementById('gestionarVideoModal'));
var modalElement = document.getElementById('gestionarVideoModal');
var myModal = bootstrap.Modal.getOrCreateInstance(modalElement, {
backdrop: 'static',
keyboard: false
});
myModal.show();
}
@ -1205,6 +1314,27 @@ function eliminarVideo(id) {
}
}
function updateVideoResultRow(videoId, value) {
const row = document.getElementById(`video-row-${videoId}`);
if (!row) {
return;
}
row.classList.remove('video-row-result-descontinuado', 'video-row-result-medium', 'video-row-result-buen-rendimiento');
if (value === 'DESCONTINUADO') {
row.classList.add('video-row-result-descontinuado');
} else if (value === 'REDIMIENTO MEDIUM') {
row.classList.add('video-row-result-medium');
} else if (value === 'BUEN RENDIMIENTO') {
row.classList.add('video-row-result-buen-rendimiento');
}
}
function getWorkflowFeedbackElement(field, videoId) {
const prefix = field === 'estado' ? 'status' : field;
return document.getElementById(`${prefix}-feedback-${videoId}`);
}
document.addEventListener('DOMContentLoaded', function() {
const editableCells = document.querySelectorAll('.editable');

View File

@ -35,9 +35,12 @@ if (is_array($value) || is_object($value)) {
}
$value = is_string($value) ? trim($value) : (string)$value;
if ($field === 'resultados' && $value === '') {
$value = null;
}
// Whitelist allowed fields for security
$allowed_fields = ['material', 'estado', 'angulo_video', 'observacion'];
$allowed_fields = ['material', 'estado', 'angulo_video', 'observacion', 'resultados'];
if (!in_array($field, $allowed_fields, true)) {
http_response_code(400);
echo json_encode(['error' => 'Campo no permitido.']);
@ -53,6 +56,24 @@ if ($field === 'estado') {
}
}
if ($field === 'resultados') {
$userRole = $_SESSION['user_role'] ?? ($_SESSION['role'] ?? '');
$isAdminUser = in_array($userRole, ['Administrador', 'admin'], true);
if (!$isAdminUser) {
http_response_code(403);
echo json_encode(['error' => 'Solo un administrador puede cambiar resultados.']);
exit;
}
$allowedResults = ['DESCONTINUADO', 'REDIMIENTO MEDIUM', 'BUEN RENDIMIENTO'];
if ($value !== null && !in_array($value, $allowedResults, true)) {
http_response_code(400);
echo json_encode(['error' => 'Resultado no permitido.']);
exit;
}
}
try {
$pdo = db();
marketing_ensure_schema($pdo);
@ -69,4 +90,4 @@ try {
http_response_code(500);
echo json_encode(['error' => 'Error al actualizar la base de datos: ' . $e->getMessage()]);
}
?>
?>