feat(ui): rediseño integral de UI/UX de la aplicación (navbar, sidebar offcanvas, KPIs, tablas y chat de tickets)
This commit is contained in:
+218
-51
@@ -1,68 +1,235 @@
|
||||
<%- include('../partials/header') %>
|
||||
<% title = 'Mis Tickets'; %>
|
||||
|
||||
<%
|
||||
const totalTickets = tickets.length;
|
||||
const openTickets = tickets.filter(t => t.status === 'abierto').length;
|
||||
const inProgressTickets = tickets.filter(t => t.status === 'en_progreso').length;
|
||||
const closedTickets = tickets.filter(t => t.status === 'cerrado').length;
|
||||
%>
|
||||
|
||||
<!-- Alertas Flash -->
|
||||
<% if (success_msg && success_msg.length > 0) { %>
|
||||
<% success_msg.forEach(msg => { %>
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3"><%= msg %><button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle me-2 fs-5 text-success"></i>
|
||||
<div class="fw-medium"><%= msg %></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<% if (error_msg && error_msg.length > 0) { %>
|
||||
<% error_msg.forEach(msg => { %>
|
||||
<div class="alert alert-danger alert-dismissible fade show mt-3"><%= msg %><button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
<div class="alert alert-danger alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-exclamation-triangle me-2 fs-5 text-danger"></i>
|
||||
<div class="fw-medium"><%= msg %></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Mis Tickets</h1>
|
||||
<a href="/client/new-ticket" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Nuevo Ticket
|
||||
</a>
|
||||
<!-- Cabecera de Página -->
|
||||
<div class="page-header d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<h1 class="page-title h3 mb-0">Mis Solicitudes y Tickets</h1>
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25 rounded-pill px-2.5 py-1" style="font-size: 0.75rem;">
|
||||
Portal Clientes
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-muted small mb-0">Monitorea en tiempo real el progreso de atención y despacho de tus incidencias.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/client/new-ticket" class="btn btn-primary btn-cta d-inline-flex align-items-center gap-2 shadow-sm">
|
||||
<i class="fas fa-plus-circle"></i>
|
||||
<span>Nuevo Ticket</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Asunto</th>
|
||||
<th>Departamento</th>
|
||||
<th>Prioridad</th>
|
||||
<th>Estado</th>
|
||||
<th>Asignado</th>
|
||||
<th>Fecha</th>
|
||||
<th>Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (tickets.length === 0) { %>
|
||||
<tr><td colspan="8" class="text-center">No tienes tickets aún</td></tr>
|
||||
<% } %>
|
||||
<% tickets.forEach(ticket => { %>
|
||||
<tr>
|
||||
<td>#<%= ticket.id %></td>
|
||||
<td><%= ticket.asunto %></td>
|
||||
<td><%= ticket.departamento_nombre || 'Sin departamento' %></td>
|
||||
<td>
|
||||
<span class="badge bg-<%= ticket.prioridad === 'alta' ? 'danger' : ticket.prioridad === 'critica' ? 'dark' : ticket.prioridad === 'media' ? 'warning' : 'info' %>">
|
||||
<%= ticket.prioridad %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-<%= ticket.status === 'abierto' ? 'success' : ticket.status === 'en_progreso' ? 'primary' : 'secondary' %>">
|
||||
<%= ticket.status %>
|
||||
</span>
|
||||
</td>
|
||||
<td><%= ticket.asignado_nombre || 'Sin asignar' %></td>
|
||||
<td><%= ticket.created_at %></td>
|
||||
<td>
|
||||
<a href="/client/ticket/<%= ticket.id %>" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Tarjetas KPI de Resumen -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="kpi-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="kpi-label">Total Solicitudes</span>
|
||||
<div class="kpi-icon-box bg-primary-subtle text-primary">
|
||||
<i class="fas fa-ticket-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-value"><%= totalTickets %></div>
|
||||
<div class="kpi-subtext text-muted">Historial acumulado</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="kpi-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="kpi-label">Abiertos</span>
|
||||
<div class="kpi-icon-box bg-success-subtle text-success">
|
||||
<i class="fas fa-envelope-open-text"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-value text-success"><%= openTickets %></div>
|
||||
<div class="kpi-subtext text-muted">Pendientes de atención</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="kpi-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="kpi-label">En Progreso / FSM</span>
|
||||
<div class="kpi-icon-box bg-info-subtle text-info">
|
||||
<i class="fas fa-tools"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-value text-info"><%= inProgressTickets %></div>
|
||||
<div class="kpi-subtext text-muted">Con técnico asignado</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="kpi-card">
|
||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||
<span class="kpi-label">Resueltos</span>
|
||||
<div class="kpi-icon-box bg-secondary-subtle text-secondary">
|
||||
<i class="fas fa-check-double"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-value text-secondary"><%= closedTickets %></div>
|
||||
<div class="kpi-subtext text-muted">Casos cerrados</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Tickets con Filtro Rápido -->
|
||||
<div class="app-card mb-4">
|
||||
<div class="app-card-header d-flex flex-wrap justify-content-between align-items-center gap-2 p-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="fas fa-stream text-primary"></i>
|
||||
<h5 class="mb-0 fw-bold" style="font-size: 0.95rem;">Bandeja de Tickets</h5>
|
||||
<span class="badge bg-light text-dark border ms-1"><%= tickets.length %></span>
|
||||
</div>
|
||||
|
||||
<!-- Búsqueda rápida en navegador -->
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="input-group input-group-sm search-input-group">
|
||||
<span class="input-group-text bg-white border-end-0"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" id="ticketSearchInput" class="form-control border-start-0 ps-0" placeholder="Buscar por asunto, ID o área..." onkeyup="filterTicketsTable()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table mb-0" id="ticketsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 75px;">ID</th>
|
||||
<th>Asunto & Descripción</th>
|
||||
<th>Departamento</th>
|
||||
<th>Prioridad</th>
|
||||
<th>Estado</th>
|
||||
<th>Técnico / FSM</th>
|
||||
<th>Fecha</th>
|
||||
<th class="text-end" style="width: 110px;">Acción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (tickets.length === 0) { %>
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5">
|
||||
<div class="empty-state-wrapper text-center">
|
||||
<img src="/img/bot-soporte.svg" alt="Sin tickets" width="72" height="72" class="mb-3 opacity-75">
|
||||
<h5 class="fw-bold text-dark">No tienes tickets abiertos actualmente</h5>
|
||||
<p class="text-muted small mb-3">Si tienes algún problema o requieres asistencia técnica, genera un ticket aquí.</p>
|
||||
<a href="/client/new-ticket" class="btn btn-primary btn-sm px-3 rounded-pill">
|
||||
<i class="fas fa-plus me-1"></i> Crear mi primer ticket
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
|
||||
<% tickets.forEach(ticket => { %>
|
||||
<tr class="ticket-row" data-status="<%= ticket.status %>">
|
||||
<td>
|
||||
<span class="badge bg-light text-dark border fw-bold">#<%= ticket.id %></span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold text-dark text-truncate" style="max-width: 280px;">
|
||||
<%= ticket.asunto %>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-secondary small d-inline-flex align-items-center gap-1">
|
||||
<i class="fas fa-folder-open text-muted"></i>
|
||||
<%= ticket.departamento_nombre || 'General' %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (ticket.prioridad === 'critica') { %>
|
||||
<span class="priority-pill priority-critica"><i class="fas fa-fire me-1"></i>Crítica</span>
|
||||
<% } else if (ticket.prioridad === 'alta') { %>
|
||||
<span class="priority-pill priority-alta"><i class="fas fa-exclamation-circle me-1"></i>Alta</span>
|
||||
<% } else if (ticket.prioridad === 'media') { %>
|
||||
<span class="priority-pill priority-media"><i class="fas fa-minus-circle me-1"></i>Media</span>
|
||||
<% } else { %>
|
||||
<span class="priority-pill priority-baja"><i class="fas fa-arrow-down me-1"></i>Baja</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (ticket.status === 'abierto') { %>
|
||||
<span class="status-pill status-abierto"><span class="status-dot"></span>Abierto</span>
|
||||
<% } else if (ticket.status === 'en_progreso') { %>
|
||||
<span class="status-pill status-en-progreso"><span class="status-dot"></span>En Progreso</span>
|
||||
<% } else { %>
|
||||
<span class="status-pill status-cerrado"><span class="status-dot"></span>Cerrado</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (ticket.asignado_nombre) { %>
|
||||
<div class="d-flex align-items-center gap-1.5 text-secondary small">
|
||||
<i class="fas fa-user-check text-success"></i>
|
||||
<span><%= ticket.asignado_nombre %></span>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<span class="badge bg-light text-muted border" style="font-size: 0.72rem;">Sin asignar</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted small" style="font-size: 0.75rem;">
|
||||
<%= new Date(ticket.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' }) %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="/client/ticket/<%= ticket.id %>" class="btn btn-action-view btn-sm d-inline-flex align-items-center gap-1">
|
||||
<span>Ver</span>
|
||||
<i class="fas fa-chevron-right" style="font-size: 0.7rem;"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Filtro interactivo de la tabla de tickets
|
||||
function filterTicketsTable() {
|
||||
const query = document.getElementById('ticketSearchInput').value.toLowerCase();
|
||||
const rows = document.querySelectorAll('#ticketsTable tbody tr.ticket-row');
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = row.innerText.toLowerCase();
|
||||
if (text.includes(query)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<%- include('../partials/footer') %>
|
||||
|
||||
+128
-32
@@ -1,59 +1,155 @@
|
||||
<%- include('../partials/header') %>
|
||||
<% title = 'Nuevo Ticket'; %>
|
||||
|
||||
<!-- Alertas Flash -->
|
||||
<% if (success_msg && success_msg.length > 0) { %>
|
||||
<% success_msg.forEach(msg => { %>
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3"><%= msg %><button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle me-2 fs-5 text-success"></i>
|
||||
<div><%= msg %></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<% if (error_msg && error_msg.length > 0) { %>
|
||||
<% error_msg.forEach(msg => { %>
|
||||
<div class="alert alert-danger alert-dismissible fade show mt-3"><%= msg %><button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
<div class="alert alert-danger alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-exclamation-triangle me-2 fs-5 text-danger"></i>
|
||||
<div><%= msg %></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Nuevo Ticket</h1>
|
||||
<!-- Cabecera -->
|
||||
<div class="page-header d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<h1 class="page-title h3 mb-0">Generar Solicitud de Soporte</h1>
|
||||
</div>
|
||||
<p class="text-muted small mb-0">Describe el problema o requerimiento para canalizarlo al equipo técnico correspondiente.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/client/dashboard" class="btn btn-outline-secondary btn-sm d-inline-flex align-items-center gap-1.5 rounded-pill px-3">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span>Volver a mis tickets</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<!-- Formulario Principal -->
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card">
|
||||
<div class="app-card-header p-3 border-bottom d-flex align-items-center gap-2">
|
||||
<div class="app-card-icon-header">
|
||||
<i class="fas fa-edit text-primary"></i>
|
||||
</div>
|
||||
<h5 class="mb-0 fw-bold" style="font-size: 0.95rem;">Detalles de la Incidencia</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<form action="/client/new-ticket" method="POST">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Asunto</label>
|
||||
<input type="text" name="asunto" class="form-control" required>
|
||||
<label class="form-label text-dark fw-semibold small">Asunto o Título Resumido <span class="text-danger">*</span></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light text-muted border-end-0"><i class="fas fa-heading"></i></span>
|
||||
<input type="text" name="asunto" class="form-control border-start-0" placeholder="Ej: Falla de conectividad en enlace principal de fibra" required autocomplete="off">
|
||||
</div>
|
||||
<small class="text-muted" style="font-size: 0.74rem;">Sé lo más específico posible para acelerar el triaje técnico.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Departamento</label>
|
||||
<select name="department_id" class="form-select">
|
||||
<option value="">Sin departamento</option>
|
||||
<% departments.forEach(dept => { %>
|
||||
<option value="<%= dept.id %>"><%= dept.nombre %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-dark fw-semibold small">Departamento / Especialidad</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light text-muted border-end-0"><i class="fas fa-network-wired"></i></span>
|
||||
<select name="department_id" class="form-select border-start-0">
|
||||
<option value="">Soporte General</option>
|
||||
<% departments.forEach(dept => { %>
|
||||
<option value="<%= dept.id %>"><%= dept.nombre %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label text-dark fw-semibold small">Nivel de Prioridad Estimado</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light text-muted border-end-0"><i class="fas fa-exclamation-triangle"></i></span>
|
||||
<select name="prioridad" class="form-select border-start-0">
|
||||
<option value="baja">Baja — Consultas o tareas menores</option>
|
||||
<option value="media" selected>Media — Afectación parcial normal</option>
|
||||
<option value="alta">Alta — Impacto operativo significativo</option>
|
||||
<option value="critica">Crítica — Caída total de servicio</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Prioridad</label>
|
||||
<select name="prioridad" class="form-select">
|
||||
<option value="baja">Baja</option>
|
||||
<option value="media" selected>Media</option>
|
||||
<option value="alta">Alta</option>
|
||||
<option value="critica">Crítica</option>
|
||||
</select>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label text-dark fw-semibold small">Descripción Detallada y Evidencias <span class="text-danger">*</span></label>
|
||||
<textarea name="mensaje" class="form-control" rows="7" placeholder="Por favor detalla: 1. ¿Qué suceso o anomalía observas? 2. ¿A qué equipos, sedes o usuarios afecta? 3. ¿Qué pasos ya se han intentado para mitigar?" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mensaje</label>
|
||||
<textarea name="mensaje" class="form-control" rows="6" required></textarea>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-2 border-top">
|
||||
<a href="/client/dashboard" class="btn btn-light border text-muted btn-sm px-3">
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-cta d-inline-flex align-items-center gap-2 shadow-sm">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
<span>Registrar y Despachar Ticket</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-paper-plane"></i> Enviar Ticket
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panel Lateral Informativo -->
|
||||
<div class="col-lg-4">
|
||||
|
||||
<!-- Tarjeta del Bot de Soporte -->
|
||||
<div class="app-card mb-3 p-3 text-center bg-gradient-light border">
|
||||
<img src="/img/bot-soporte.svg" alt="Bot Soporte" width="68" height="68" class="mb-2 bot-avatar-img">
|
||||
<h6 class="fw-bold text-dark mb-1">Mesa de Ayuda SIGMA</h6>
|
||||
<p class="text-muted small mb-2" style="font-size: 0.78rem;">
|
||||
Tu ticket entrará inmediatamente en nuestra cola de triaje automatizada.
|
||||
</p>
|
||||
<div class="d-inline-flex align-items-center gap-1.5 bot-status-pill" style="font-size: 0.72rem; padding: 0.25rem 0.65rem;">
|
||||
<span class="bot-status-dot"></span>
|
||||
<span>Tiempo estimado de respuesta: < 15 min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarjeta de Buenas Prácticas -->
|
||||
<div class="app-card p-3">
|
||||
<h6 class="fw-bold text-dark mb-2" style="font-size: 0.85rem;">
|
||||
<i class="fas fa-lightbulb text-warning me-1.5"></i> Consejos para agilizar
|
||||
</h6>
|
||||
<ul class="list-unstyled small text-muted mb-0 d-flex flex-column gap-2" style="font-size: 0.78rem;">
|
||||
<li class="d-flex align-items-start gap-2">
|
||||
<i class="fas fa-check text-success mt-1"></i>
|
||||
<span>Indica marcas, modelos o números de serie si corresponde a hardware.</span>
|
||||
</li>
|
||||
<li class="d-flex align-items-start gap-2">
|
||||
<i class="fas fa-check text-success mt-1"></i>
|
||||
<span>Si es una sede remota, especifica dirección o persona de contacto en sitio.</span>
|
||||
</li>
|
||||
<li class="d-flex align-items-start gap-2">
|
||||
<i class="fas fa-check text-success mt-1"></i>
|
||||
<span>Podrás adjuntar respuestas o consultar avances en tiempo real desde tu panel.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/footer') %>
|
||||
|
||||
+181
-49
@@ -1,87 +1,219 @@
|
||||
<%- include('../partials/header') %>
|
||||
<% title = 'Ticket #' + ticket.id; %>
|
||||
<% title = 'Ticket #' + ticket.id + ' — ' + ticket.asunto; %>
|
||||
|
||||
<!-- Alertas Flash -->
|
||||
<% if (success_msg && success_msg.length > 0) { %>
|
||||
<% success_msg.forEach(msg => { %>
|
||||
<div class="alert alert-success alert-dismissible fade show mt-3"><%= msg %><button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
<div class="alert alert-success alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle me-2 fs-5 text-success"></i>
|
||||
<div><%= msg %></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
<% if (error_msg && error_msg.length > 0) { %>
|
||||
<% error_msg.forEach(msg => { %>
|
||||
<div class="alert alert-danger alert-dismissible fade show mt-3"><%= msg %><button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
|
||||
<div class="alert alert-danger alert-dismissible fade show rounded-3 border-0 shadow-sm mb-3" role="alert">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-exclamation-triangle me-2 fs-5 text-danger"></i>
|
||||
<div><%= msg %></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">Ticket #<%= ticket.id %>: <%= ticket.asunto %></h1>
|
||||
<!-- Cabecera del Ticket -->
|
||||
<div class="page-header d-flex flex-wrap justify-content-between align-items-center gap-3 mb-4">
|
||||
<div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-1">
|
||||
<span class="badge bg-light text-dark border px-2.5 py-1.5 fw-bold" style="font-size: 0.85rem;">Ticket #<%= ticket.id %></span>
|
||||
<h1 class="page-title h3 mb-0"><%= ticket.asunto %></h1>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 text-muted small">
|
||||
<span><i class="far fa-calendar-alt me-1"></i><%= new Date(ticket.created_at).toLocaleString('es-ES') %></span>
|
||||
<span>•</span>
|
||||
<span><i class="fas fa-folder me-1"></i><%= ticket.departamento_nombre || 'Soporte General' %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<% if (ticket.status !== 'cerrado') { %>
|
||||
<form action="/client/ticket/<%= ticket.id %>/close" method="POST" style="display:inline">
|
||||
<button type="submit" class="btn btn-warning" onclick="return confirm('¿Cerrar este ticket?')">
|
||||
<i class="fas fa-times"></i> Cerrar Ticket
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm d-inline-flex align-items-center gap-1.5 rounded-pill px-3" onclick="return confirm('¿Confirmas dar por resuelto y cerrar este ticket?')">
|
||||
<i class="fas fa-check"></i>
|
||||
<span>Cerrar Ticket</span>
|
||||
</button>
|
||||
</form>
|
||||
<a href="/client/dashboard" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Volver
|
||||
<% } %>
|
||||
<a href="/client/dashboard" class="btn btn-outline-secondary btn-sm d-inline-flex align-items-center gap-1.5 rounded-pill px-3">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<span>Volver</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header"><h6 class="mb-0">Información</h6></div>
|
||||
<div class="card-body">
|
||||
<p><strong>Estado:</strong>
|
||||
<span class="badge bg-<%= ticket.status === 'abierto' ? 'success' : ticket.status === 'en_progreso' ? 'primary' : 'secondary' %>"><%= ticket.status %></span>
|
||||
</p>
|
||||
<p><strong>Prioridad:</strong>
|
||||
<span class="badge bg-<%= ticket.prioridad === 'alta' ? 'danger' : ticket.prioridad === 'critica' ? 'dark' : ticket.prioridad === 'media' ? 'warning' : 'info' %>"><%= ticket.prioridad %></span>
|
||||
</p>
|
||||
<p><strong>Departamento:</strong> <%= ticket.departamento_nombre || 'Sin departamento' %></p>
|
||||
<p><strong>Asignado:</strong> <%= ticket.asignado_nombre || 'Sin asignar' %></p>
|
||||
<p><strong>Creado:</strong> <%= ticket.created_at %></p>
|
||||
<div class="row g-4">
|
||||
<!-- Columna Izquierda: Información de Ficha -->
|
||||
<div class="col-lg-4 order-lg-2">
|
||||
<div class="app-card mb-3">
|
||||
<div class="app-card-header p-3 border-bottom d-flex align-items-center justify-content-between">
|
||||
<h6 class="mb-0 fw-bold" style="font-size: 0.88rem;">
|
||||
<i class="fas fa-info-circle text-primary me-1.5"></i> Ficha del Ticket
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-3">
|
||||
<div class="ticket-meta-item mb-2.5">
|
||||
<span class="text-muted small d-block">Estado de Atención:</span>
|
||||
<% if (ticket.status === 'abierto') { %>
|
||||
<span class="status-pill status-abierto mt-1"><span class="status-dot"></span>Abierto</span>
|
||||
<% } else if (ticket.status === 'en_progreso') { %>
|
||||
<span class="status-pill status-en-progreso mt-1"><span class="status-dot"></span>En Progreso</span>
|
||||
<% } else { %>
|
||||
<span class="status-pill status-cerrado mt-1"><span class="status-dot"></span>Cerrado</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="ticket-meta-item mb-2.5">
|
||||
<span class="text-muted small d-block">Prioridad:</span>
|
||||
<% if (ticket.prioridad === 'critica') { %>
|
||||
<span class="priority-pill priority-critica mt-1"><i class="fas fa-fire me-1"></i>Crítica</span>
|
||||
<% } else if (ticket.prioridad === 'alta') { %>
|
||||
<span class="priority-pill priority-alta mt-1"><i class="fas fa-exclamation-circle me-1"></i>Alta</span>
|
||||
<% } else if (ticket.prioridad === 'media') { %>
|
||||
<span class="priority-pill priority-media mt-1"><i class="fas fa-minus-circle me-1"></i>Media</span>
|
||||
<% } else { %>
|
||||
<span class="priority-pill priority-baja mt-1"><i class="fas fa-arrow-down me-1"></i>Baja</span>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="ticket-meta-item mb-2.5">
|
||||
<span class="text-muted small d-block">Especialista Asignado:</span>
|
||||
<div class="fw-semibold text-dark mt-1 d-flex align-items-center gap-1.5" style="font-size: 0.84rem;">
|
||||
<i class="fas fa-user-shield text-indigo" style="color: #4f46e5;"></i>
|
||||
<span><%= ticket.asignado_nombre || 'En asignación de guardia' %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ticket-meta-item">
|
||||
<span class="text-muted small d-block">Solicitante:</span>
|
||||
<div class="fw-medium text-dark mt-1" style="font-size: 0.84rem;">
|
||||
<%= ticket.usuario_nombre %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asistencia Rápida -->
|
||||
<div class="app-card p-3 text-center bg-light border">
|
||||
<img src="/img/bot-soporte.svg" alt="Bot Soporte" width="46" height="46" class="mb-2">
|
||||
<div class="fw-bold text-dark" style="font-size: 0.8rem;">¿Requieres añadir más detalles?</div>
|
||||
<small class="text-muted d-block" style="font-size: 0.72rem;">Usa la caja de comentarios abajo para enviar mensajes directamente al técnico.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<strong><%= ticket.usuario_nombre %></strong>
|
||||
<span class="text-muted">- <%= ticket.created_at %></span>
|
||||
|
||||
<!-- Columna Derecha: Hilo de Conversación -->
|
||||
<div class="col-lg-8 order-lg-1">
|
||||
|
||||
<!-- Mensaje Original del Cliente -->
|
||||
<div class="app-card mb-4 border-start border-4 border-primary">
|
||||
<div class="app-card-header p-3 bg-light border-bottom d-flex align-items-center justify-content-between">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="user-avatar-initials-sm bg-primary text-white">
|
||||
<%= (ticket.usuario_nombre || 'C').charAt(0).toUpperCase() %>
|
||||
</div>
|
||||
<div>
|
||||
<span class="fw-bold text-dark" style="font-size: 0.85rem;"><%= ticket.usuario_nombre %></span>
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle ms-1" style="font-size: 0.68rem;">Autor del Ticket</span>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size: 0.72rem;"><%= new Date(ticket.created_at).toLocaleString('es-ES') %></small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><%= ticket.mensaje %></p>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<div class="ticket-description-body" style="white-space: pre-line; font-size: 0.92rem; line-height: 1.6;">
|
||||
<%= ticket.mensaje %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Respuestas</h5>
|
||||
<!-- Hilo de Respuestas -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h5 class="fw-bold mb-0 text-dark" style="font-size: 0.95rem;">
|
||||
<i class="fas fa-comments text-primary me-1.5"></i> Hilo de Conversación (<%= respuestas.length %>)
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<% if (respuestas.length === 0) { %>
|
||||
<p class="text-muted">No hay respuestas aún</p>
|
||||
<% } %>
|
||||
<% respuestas.forEach(r => { %>
|
||||
<div class="card mb-2 <%= r.usuario_rol === 'agent' || r.usuario_rol === 'admin' ? 'border-primary' : '' %>">
|
||||
<div class="card-header">
|
||||
<strong><%= r.usuario_nombre %></strong>
|
||||
<span class="badge bg-<%= r.usuario_rol === 'admin' ? 'danger' : r.usuario_rol === 'agent' ? 'info' : 'secondary' %> ms-1"><%= r.usuario_rol %></span>
|
||||
<span class="text-muted float-end"><%= r.created_at %></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0"><%= r.mensaje %></p>
|
||||
</div>
|
||||
<div class="app-card p-4 text-center mb-4 bg-light border">
|
||||
<i class="far fa-clock text-muted fs-3 mb-2"></i>
|
||||
<h6 class="fw-semibold text-secondary">Aún no hay respuestas en este ticket</h6>
|
||||
<small class="text-muted">Un agente técnico revisará tu solicitud a la brevedad posible.</small>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><strong>Responder</strong></div>
|
||||
<div class="card-body">
|
||||
<div class="conversation-thread mb-4 d-flex flex-column gap-3">
|
||||
<% respuestas.forEach(r => { %>
|
||||
<% const isStaff = ['agent', 'admin', 'superadmin'].includes(r.usuario_rol); %>
|
||||
<div class="ticket-message-card <%= isStaff ? 'message-staff' : 'message-client' %>">
|
||||
<div class="message-card-header d-flex align-items-center justify-content-between p-2.5 px-3 border-bottom">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="user-avatar-initials-sm <%= isStaff ? 'bg-indigo text-white' : 'bg-secondary text-white' %>" style="<%= isStaff ? 'background: #4f46e5;' : '' %>">
|
||||
<%= (r.usuario_nombre || 'U').charAt(0).toUpperCase() %>
|
||||
</div>
|
||||
<div>
|
||||
<span class="fw-bold text-dark" style="font-size: 0.84rem;"><%= r.usuario_nombre %></span>
|
||||
<% if (isStaff) { %>
|
||||
<span class="badge bg-indigo-subtle text-indigo border ms-1" style="font-size: 0.68rem; color: #4f46e5; border-color: rgba(79,70,229,0.2) !important;">
|
||||
<i class="fas fa-shield-alt me-0.5"></i> Soporte Oficial
|
||||
</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-secondary-subtle text-secondary border ms-1" style="font-size: 0.68rem;">Cliente</span>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size: 0.72rem;">
|
||||
<%= new Date(r.created_at).toLocaleString('es-ES') %>
|
||||
</small>
|
||||
</div>
|
||||
<div class="message-card-body p-3">
|
||||
<div style="white-space: pre-line; font-size: 0.90rem; line-height: 1.55;">
|
||||
<%= r.mensaje %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
</div>
|
||||
|
||||
<!-- Caja de Respuesta -->
|
||||
<% if (ticket.status !== 'cerrado') { %>
|
||||
<div class="app-card border shadow-sm">
|
||||
<div class="app-card-header p-3 border-bottom d-flex align-items-center gap-2">
|
||||
<i class="fas fa-reply text-primary"></i>
|
||||
<h6 class="mb-0 fw-bold" style="font-size: 0.88rem;">Añadir Respuesta o Información Adicional</h6>
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<form action="/client/ticket/<%= ticket.id %>/respond" method="POST">
|
||||
<div class="mb-3">
|
||||
<textarea name="mensaje" class="form-control" rows="3" required></textarea>
|
||||
<textarea name="mensaje" class="form-control" rows="4" placeholder="Escribe aquí tu respuesta, confirmación o detalle adicional para el equipo técnico..." required></textarea>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary btn-cta d-inline-flex align-items-center gap-2 shadow-sm">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
<span>Enviar Mensaje</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-reply"></i> Responder</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="alert alert-secondary d-flex align-items-center gap-2 rounded-3 border">
|
||||
<i class="fas fa-lock fs-5"></i>
|
||||
<div>Este ticket se encuentra cerrado. Si experimentas una nueva incidencia, por favor abre un nuevo ticket.</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user