<div class="data-list-container">
<!-- Header Section -->
<div class="data-list-header">
<div class="header-content">
<h2 class="list-title">Directorio de Empleados</h2>
<p class="list-description">Gestiona y visualiza información de empleados con filtrado y ordenamiento avanzado</p>
</div>
<!-- Action Buttons -->
<div class="header-actions">
<button class="btn btn-secondary" id="columnToggle">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="9" x2="15" y2="9"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
Columnas
</button>
<button class="btn btn-secondary" id="exportData">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7,10 12,15 17,10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Exportar
</button>
<button class="btn btn-primary" id="addNew">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Agregar Empleado
</button>
</div>
</div>
<!-- Filters Section -->
<div class="filters-section">
<!-- Search Bar -->
<div class="search-container">
<div class="search-input-wrapper">
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<input
type="text"
id="globalSearch"
class="search-input"
placeholder="Buscar empleados..."
aria-label="Buscar empleados"
>
<button class="search-clear" id="searchClear" aria-label="Limpiar búsqueda">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<!-- Advanced Filters -->
<div class="advanced-filters">
<div class="filter-group">
<label for="departmentFilter">Departamento</label>
<select id="departmentFilter" class="filter-select">
<option value="">Todos los Departamentos</option>
<option value="engineering">Ingeniería</option>
<option value="marketing">Marketing</option>
<option value="sales">Ventas</option>
<option value="hr">Recursos Humanos</option>
<option value="finance">Finanzas</option>
</select>
</div>
<div class="filter-group">
<label for="statusFilter">Estado</label>
<select id="statusFilter" class="filter-select">
<option value="">Todos los Estados</option>
<option value="active">Activo</option>
<option value="inactive">Inactivo</option>
<option value="pending">Pendiente</option>
</select>
</div>
<div class="filter-group">
<label for="locationFilter">Ubicación</label>
<select id="locationFilter" class="filter-select">
<option value="">Todas las Ubicaciones</option>
<option value="madrid">Madrid</option>
<option value="barcelona">Barcelona</option>
<option value="valencia">Valencia</option>
<option value="remoto">Remoto</option>
</select>
</div>
<div class="filter-group">
<label for="hireDateFrom">Fecha de Contratación Desde</label>
<input type="date" id="hireDateFrom" class="filter-input">
</div>
<div class="filter-group">
<label for="hireDateTo">Fecha de Contratación Hasta</label>
<input type="date" id="hireDateTo" class="filter-input">
</div>
<button class="btn btn-secondary" id="clearFilters">
Limpiar Filtros
</button>
</div>
</div>
<!-- Results Info -->
<div class="results-info">
<div class="results-count">
<span id="resultsText">Mostrando 1-10 de 150 empleados</span>
</div>
<div class="results-actions">
<div class="page-size-selector">
<label for="pageSize">Mostrar:</label>
<select id="pageSize" class="page-size-select">
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span>por página</span>
</div>
<div class="bulk-actions" id="bulkActions" style="display: none;">
<span class="selected-count" id="selectedCount">2 seleccionados</span>
<button class="btn btn-sm btn-secondary" id="bulkEdit">Editar</button>
<button class="btn btn-sm btn-danger" id="bulkDelete">Eliminar</button>
</div>
</div>
</div>
<!-- Data Table -->
<div class="table-container">
<div class="table-wrapper">
<table class="data-table" id="dataTable">
<thead>
<tr>
<th class="select-column">
<input type="checkbox" id="selectAll" aria-label="Seleccionar todas las filas">
</th>
<th class="sortable" data-column="name" data-type="string">
<div class="th-content">
<span>Nombre</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="sortable" data-column="email" data-type="string">
<div class="th-content">
<span>Email</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="sortable" data-column="department" data-type="string">
<div class="th-content">
<span>Departamento</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="sortable" data-column="position" data-type="string">
<div class="th-content">
<span>Posición</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="sortable" data-column="location" data-type="string">
<div class="th-content">
<span>Ubicación</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="sortable" data-column="hireDate" data-type="date">
<div class="th-content">
<span>Fecha de Contratación</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="sortable" data-column="status" data-type="string">
<div class="th-content">
<span>Estado</span>
<div class="sort-indicators">
<svg class="sort-icon sort-asc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="18,15 12,9 6,15"></polyline>
</svg>
<svg class="sort-icon sort-desc" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</div>
</div>
</th>
<th class="actions-column">Acciones</th>
</tr>
</thead>
<tbody id="tableBody">
<!-- Data rows will be populated by JavaScript -->
</tbody>
</table>
</div>
<!-- Loading State -->
<div class="loading-state" id="loadingState" style="display: none;">
<div class="loading-spinner"></div>
<p>Cargando datos...</p>
</div>
<!-- Empty State -->
<div class="empty-state" id="emptyState" style="display: none;">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<h3>No se encontraron empleados</h3>
<p>Intenta ajustar tus criterios de búsqueda o filtros</p>
<button class="btn btn-primary" onclick="clearAllFilters()">Limpiar Todos los Filtros</button>
</div>
</div>
<!-- Pagination -->
<div class="pagination-container">
<div class="pagination-info">
<span id="paginationInfo">Mostrando 1 a 25 de 150 entradas</span>
</div>
<nav class="pagination" aria-label="Paginación de tabla de datos">
<button class="pagination-btn" id="firstPage" aria-label="Primera página">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="11,17 6,12 11,7"></polyline>
<polyline points="18,17 13,12 18,7"></polyline>
</svg>
</button>
<button class="pagination-btn" id="prevPage" aria-label="Página anterior">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="15,18 9,12 15,6"></polyline>
</svg>
</button>
<div class="pagination-numbers" id="paginationNumbers">
<!-- Page numbers will be populated by JavaScript -->
</div>
<button class="pagination-btn" id="nextPage" aria-label="Página siguiente">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="9,18 15,12 9,6"></polyline>
</svg>
</button>
<button class="pagination-btn" id="lastPage" aria-label="Última página">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<polyline points="13,17 18,12 13,7"></polyline>
<polyline points="6,17 11,12 6,7"></polyline>
</svg>
</button>
</nav>
</div>
<!-- Column Toggle Modal -->
<div class="modal" id="columnModal">
<div class="modal-content">
<div class="modal-header">
<h3>Gestionar Columnas</h3>
<button class="modal-close" id="closeColumnModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<p>Selecciona qué columnas mostrar en la tabla:</p>
<div class="column-toggles" id="columnToggles">
<!-- Column toggles will be populated by JavaScript -->
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="resetColumns">Restablecer por Defecto</button>
<button class="btn btn-primary" id="applyColumns">Aplicar Cambios</button>
</div>
</div>
</div>
<!-- Export Modal -->
<div class="modal" id="exportModal">
<div class="modal-content">
<div class="modal-header">
<h3>Exportar Datos</h3>
<button class="modal-close" id="closeExportModal">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<p>Elige el formato de exportación y opciones:</p>
<div class="export-options">
<div class="export-format">
<label>Formato de Exportación:</label>
<div class="radio-group">
<label class="radio-option">
<input type="radio" name="exportFormat" value="csv" checked>
<span>CSV</span>
</label>
<label class="radio-option">
<input type="radio" name="exportFormat" value="json">
<span>JSON</span>
</label>
<label class="radio-option">
<input type="radio" name="exportFormat" value="print">
<span>Imprimir</span>
</label>
</div>
</div>
<div class="export-scope">
<label>Alcance de Exportación:</label>
<div class="radio-group">
<label class="radio-option">
<input type="radio" name="exportScope" value="current" checked>
<span>Página Actual</span>
</label>
<label class="radio-option">
<input type="radio" name="exportScope" value="filtered">
<span>Todos los Resultados Filtrados</span>
</label>
<label class="radio-option">
<input type="radio" name="exportScope" value="all">
<span>Todos los Datos</span>
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancelExport">Cancelar</button>
<button class="btn btn-primary" id="confirmExport">Exportar</button>
</div>
</div>
</div>
</div>
/* Base Styles */
.data-list-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8fafc;
min-height: 100vh;
}
/* Header */
.data-list-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
gap: 20px;
}
.header-content {
flex: 1;
}
.list-title {
font-size: 2rem;
font-weight: 700;
color: #1a202c;
margin: 0 0 8px 0;
}
.list-description {
color: #718096;
margin: 0;
font-size: 1rem;
}
.header-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
white-space: nowrap;
}
.btn-primary {
background: #4f46e5;
color: white;
}
.btn-primary:hover {
background: #4338ca;
transform: translateY(-1px);
}
.btn-secondary {
background: white;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-secondary:hover {
background: #f9fafb;
border-color: #9ca3af;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.75rem;
}
.icon {
width: 16px;
height: 16px;
stroke-width: 2;
}
/* Filters Section */
.filters-section {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-container {
margin-bottom: 20px;
}
.search-input-wrapper {
position: relative;
max-width: 400px;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
color: #9ca3af;
pointer-events: none;
}
.search-input {
width: 100%;
padding: 12px 40px 12px 40px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 0.875rem;
transition: border-color 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: #4f46e5;
}
.search-clear {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
width: 20px;
height: 20px;
color: #9ca3af;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
}
.search-clear.visible {
opacity: 1;
}
.advanced-filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
align-items: end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.filter-group label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}
.filter-select,
.filter-input {
padding: 8px 12px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.875rem;
background: white;
}
.filter-select:focus,
.filter-input:focus {
outline: none;
border-color: #4f46e5;
}
/* Results Info */
.results-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 20px;
}
.results-count {
color: #6b7280;
font-size: 0.875rem;
}
.results-actions {
display: flex;
align-items: center;
gap: 20px;
}
.page-size-selector {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
color: #6b7280;
}
.page-size-select {
padding: 4px 8px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 0.875rem;
}
.bulk-actions {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
}
.selected-count {
font-size: 0.875rem;
color: #1e40af;
font-weight: 500;
}
/* Table */
.table-container {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
.table-wrapper {
overflow-x: auto;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.data-table th {
background: #f8fafc;
padding: 16px 12px;
text-align: left;
font-weight: 600;
color: #374151;
border-bottom: 1px solid #e5e7eb;
white-space: nowrap;
}
.data-table td {
padding: 16px 12px;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.data-table tbody tr:hover {
background: #f8fafc;
}
.data-table tbody tr.selected {
background: #eff6ff;
}
/* Sortable Headers */
.sortable {
cursor: pointer;
user-select: none;
position: relative;
}
.sortable:hover {
background: #f1f5f9;
}
.th-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.sort-indicators {
display: flex;
flex-direction: column;
gap: 1px;
}
.sort-icon {
width: 12px;
height: 12px;
color: #d1d5db;
transition: color 0.2s ease;
}
.sortable.sort-asc .sort-asc,
.sortable.sort-desc .sort-desc {
color: #4f46e5;
}
/* Column Types */
.select-column {
width: 40px;
text-align: center;
}
.actions-column {
width: 120px;
text-align: center;
}
/* Status Badges */
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: capitalize;
}
.status-active {
background: #dcfce7;
color: #166534;
}
.status-inactive {
background: #fee2e2;
color: #991b1b;
}
.status-pending {
background: #fef3c7;
color: #92400e;
}
/* Action Buttons */
.action-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 0.75rem;
cursor: pointer;
margin: 0 2px;
transition: all 0.2s ease;
}
.action-btn.edit {
background: #dbeafe;
color: #1e40af;
}
.action-btn.edit:hover {
background: #bfdbfe;
}
.action-btn.delete {
background: #fee2e2;
color: #991b1b;
}
.action-btn.delete:hover {
background: #fecaca;
}
/* Loading State */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
color: #6b7280;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f4f6;
border-top: 3px solid #4f46e5;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.empty-icon {
width: 64px;
height: 64px;
color: #d1d5db;
margin-bottom: 16px;
}
.empty-state h3 {
font-size: 1.25rem;
font-weight: 600;
color: #374151;
margin: 0 0 8px 0;
}
.empty-state p {
color: #6b7280;
margin: 0 0 24px 0;
}
/* Pagination */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.pagination-info {
color: #6b7280;
font-size: 0.875rem;
}
.pagination {
display: flex;
align-items: center;
gap: 4px;
}
.pagination-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: 1px solid #d1d5db;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.pagination-btn:hover:not(:disabled) {
background: #f9fafb;
border-color: #9ca3af;
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-btn svg {
width: 16px;
height: 16px;
}
.pagination-numbers {
display: flex;
gap: 4px;
margin: 0 8px;
}
.page-number {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: 1px solid #d1d5db;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
}
.page-number:hover {
background: #f9fafb;
border-color: #9ca3af;
}
.page-number.active {
background: #4f46e5;
color: white;
border-color: #4f46e5;
}
.page-ellipsis {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
color: #9ca3af;
font-weight: 500;
}
/* Modals */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.modal.active {
opacity: 1;
visibility: visible;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow: hidden;
transform: scale(0.9);
transition: transform 0.2s ease;
}
.modal.active .modal-content {
transform: scale(1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid #e5e7eb;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1a202c;
}
.modal-close {
background: none;
border: none;
width: 24px;
height: 24px;
cursor: pointer;
color: #6b7280;
}
.modal-close:hover {
color: #374151;
}
.modal-body {
padding: 24px;
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid #e5e7eb;
background: #f8fafc;
}
/* Column Toggles */
.column-toggles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 16px;
}
.column-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border: 1px solid #e5e7eb;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.column-toggle:hover {
background: #f8fafc;
}
.column-toggle input[type="checkbox"] {
margin: 0;
}
/* Export Options */
.export-options {
margin-top: 16px;
}
.export-format,
.export-scope {
margin-bottom: 20px;
}
.export-format label,
.export-scope label {
display: block;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.radio-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.radio-option {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-weight: normal !important;
}
.radio-option input[type="radio"] {
margin: 0;
}
/* Search Highlighting */
.highlight {
background: #fef3c7;
padding: 1px 2px;
border-radius: 2px;
font-weight: 600;
}
/* Responsive Design */
@media (max-width: 1024px) {
.data-list-header {
flex-direction: column;
align-items: stretch;
}
.header-actions {
justify-content: flex-start;
}
.advanced-filters {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
}
@media (max-width: 768px) {
.data-list-container {
padding: 16px;
}
.list-title {
font-size: 1.5rem;
}
.filters-section {
padding: 16px;
}
.advanced-filters {
grid-template-columns: 1fr;
}
.results-info {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.results-actions {
justify-content: space-between;
}
.pagination-container {
flex-direction: column;
gap: 12px;
}
.pagination {
justify-content: center;
}
/* Hide less important columns on mobile */
.data-table th:nth-child(n+6),
.data-table td:nth-child(n+6) {
display: none;
}
}
@media (max-width: 480px) {
.header-actions {
flex-direction: column;
}
.btn {
justify-content: center;
}
.data-table th:nth-child(n+5),
.data-table td:nth-child(n+5) {
display: none;
}
.pagination-numbers {
display: none;
}
}
// Sample data for demonstration
const sampleData = [
{
id: 1,
name: "Ana García",
email: "ana.garcia@empresa.com",
department: "engineering",
position: "Desarrolladora Senior",
location: "madrid",
hireDate: "2022-03-15",
status: "active"
},
{
id: 2,
name: "Carlos Rodríguez",
email: "carlos.rodriguez@empresa.com",
department: "marketing",
position: "Gerente de Marketing",
location: "barcelona",
hireDate: "2021-08-22",
status: "active"
},
{
id: 3,
name: "María López",
email: "maria.lopez@empresa.com",
department: "sales",
position: "Representante de Ventas",
location: "valencia",
hireDate: "2023-01-10",
status: "pending"
},
{
id: 4,
name: "David Martín",
email: "david.martin@empresa.com",
department: "hr",
position: "Especialista en RRHH",
location: "remoto",
hireDate: "2020-11-05",
status: "active"
},
{
id: 5,
name: "Laura Sánchez",
email: "laura.sanchez@empresa.com",
department: "finance",
position: "Analista Financiera",
location: "madrid",
hireDate: "2022-07-18",
status: "inactive"
}
];
// Data List Manager Class
class AdvancedDataList {
constructor(containerId, data = []) {
this.container = document.getElementById(containerId);
this.originalData = [...data];
this.filteredData = [...data];
this.currentPage = 1;
this.pageSize = 25;
this.sortColumn = null;
this.sortDirection = 'asc';
this.selectedRows = new Set();
this.visibleColumns = new Set(['name', 'email', 'department', 'position', 'location', 'hireDate', 'status']);
this.init();
}
init() {
this.bindEvents();
this.populateTable();
this.updateResultsInfo();
this.updatePagination();
this.setupColumnToggles();
}
bindEvents() {
// Search functionality
const searchInput = document.getElementById('globalSearch');
const searchClear = document.getElementById('searchClear');
searchInput.addEventListener('input', (e) => {
this.handleSearch(e.target.value);
this.toggleSearchClear(e.target.value);
});
searchClear.addEventListener('click', () => {
searchInput.value = '';
this.handleSearch('');
this.toggleSearchClear('');
});
// Filter functionality
const filters = ['departmentFilter', 'statusFilter', 'locationFilter', 'hireDateFrom', 'hireDateTo'];
filters.forEach(filterId => {
const element = document.getElementById(filterId);
if (element) {
element.addEventListener('change', () => this.applyFilters());
}
});
// Clear filters
document.getElementById('clearFilters').addEventListener('click', () => {
this.clearAllFilters();
});
// Page size change
document.getElementById('pageSize').addEventListener('change', (e) => {
this.pageSize = parseInt(e.target.value);
this.currentPage = 1;
this.populateTable();
this.updatePagination();
});
// Select all checkbox
document.getElementById('selectAll').addEventListener('change', (e) => {
this.handleSelectAll(e.target.checked);
});
// Sorting
document.querySelectorAll('.sortable').forEach(header => {
header.addEventListener('click', () => {
const column = header.dataset.column;
const type = header.dataset.type;
this.handleSort(column, type);
});
});
// Pagination
document.getElementById('firstPage').addEventListener('click', () => this.goToPage(1));
document.getElementById('prevPage').addEventListener('click', () => this.goToPage(this.currentPage - 1));
document.getElementById('nextPage').addEventListener('click', () => this.goToPage(this.currentPage + 1));
document.getElementById('lastPage').addEventListener('click', () => this.goToPage(this.getTotalPages()));
// Modal functionality
this.bindModalEvents();
// Bulk actions
document.getElementById('bulkEdit').addEventListener('click', () => this.handleBulkEdit());
document.getElementById('bulkDelete').addEventListener('click', () => this.handleBulkDelete());
}
bindModalEvents() {
// Column toggle modal
document.getElementById('columnToggle').addEventListener('click', () => {
this.openModal('columnModal');
});
document.getElementById('closeColumnModal').addEventListener('click', () => {
this.closeModal('columnModal');
});
document.getElementById('applyColumns').addEventListener('click', () => {
this.applyColumnChanges();
});
document.getElementById('resetColumns').addEventListener('click', () => {
this.resetColumns();
});
// Export modal
document.getElementById('exportData').addEventListener('click', () => {
this.openModal('exportModal');
});
document.getElementById('closeExportModal').addEventListener('click', () => {
this.closeModal('exportModal');
});
document.getElementById('confirmExport').addEventListener('click', () => {
this.handleExport();
});
document.getElementById('cancelExport').addEventListener('click', () => {
this.closeModal('exportModal');
});
// Close modals on backdrop click
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeModal(modal.id);
}
});
});
}
handleSearch(query) {
if (!query.trim()) {
this.applyFilters();
return;
}
const searchTerm = query.toLowerCase();
this.filteredData = this.originalData.filter(item => {
return Object.values(item).some(value =>
value.toString().toLowerCase().includes(searchTerm)
);
});
this.currentPage = 1;
this.populateTable();
this.updateResultsInfo();
this.updatePagination();
}
toggleSearchClear(value) {
const clearBtn = document.getElementById('searchClear');
if (value) {
clearBtn.classList.add('visible');
} else {
clearBtn.classList.remove('visible');
}
}
applyFilters() {
let filtered = [...this.originalData];
// Department filter
const department = document.getElementById('departmentFilter').value;
if (department) {
filtered = filtered.filter(item => item.department === department);
}
// Status filter
const status = document.getElementById('statusFilter').value;
if (status) {
filtered = filtered.filter(item => item.status === status);
}
// Location filter
const location = document.getElementById('locationFilter').value;
if (location) {
filtered = filtered.filter(item => item.location === location);
}
// Date range filter
const dateFrom = document.getElementById('hireDateFrom').value;
const dateTo = document.getElementById('hireDateTo').value;
if (dateFrom) {
filtered = filtered.filter(item => new Date(item.hireDate) >= new Date(dateFrom));
}
if (dateTo) {
filtered = filtered.filter(item => new Date(item.hireDate) <= new Date(dateTo));
}
this.filteredData = filtered;
this.currentPage = 1;
this.populateTable();
this.updateResultsInfo();
this.updatePagination();
}
clearAllFilters() {
document.getElementById('globalSearch').value = '';
document.getElementById('departmentFilter').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('locationFilter').value = '';
document.getElementById('hireDateFrom').value = '';
document.getElementById('hireDateTo').value = '';
this.toggleSearchClear('');
this.filteredData = [...this.originalData];
this.currentPage = 1;
this.populateTable();
this.updateResultsInfo();
this.updatePagination();
}
handleSort(column, type) {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
this.filteredData.sort((a, b) => {
let aVal = a[column];
let bVal = b[column];
if (type === 'date') {
aVal = new Date(aVal);
bVal = new Date(bVal);
} else if (type === 'number') {
aVal = parseFloat(aVal) || 0;
bVal = parseFloat(bVal) || 0;
} else {
aVal = aVal.toString().toLowerCase();
bVal = bVal.toString().toLowerCase();
}
if (aVal < bVal) return this.sortDirection === 'asc' ? -1 : 1;
if (aVal > bVal) return this.sortDirection === 'asc' ? 1 : -1;
return 0;
});
this.updateSortIndicators();
this.populateTable();
}
updateSortIndicators() {
document.querySelectorAll('.sortable').forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
});
if (this.sortColumn) {
const activeHeader = document.querySelector(`[data-column="${this.sortColumn}"]`);
if (activeHeader) {
activeHeader.classList.add(`sort-${this.sortDirection}`);
}
}
}
populateTable() {
const tbody = document.getElementById('tableBody');
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
const pageData = this.filteredData.slice(startIndex, endIndex);
if (pageData.length === 0) {
this.showEmptyState();
return;
}
this.hideEmptyState();
tbody.innerHTML = pageData.map(item => {
const isSelected = this.selectedRows.has(item.id);
return `
<tr class="${isSelected ? 'selected' : ''}">
<td class="select-column">
<input type="checkbox" ${isSelected ? 'checked' : ''}
onchange="dataList.handleRowSelect(${item.id}, this.checked)">
</td>
<td>${this.highlightSearchTerm(item.name)}</td>
<td>${this.highlightSearchTerm(item.email)}</td>
<td>${this.highlightSearchTerm(this.formatDepartment(item.department))}</td>
<td>${this.highlightSearchTerm(item.position)}</td>
<td>${this.highlightSearchTerm(this.formatLocation(item.location))}</td>
<td>${this.formatDate(item.hireDate)}</td>
<td><span class="status-badge status-${item.status}">${this.formatStatus(item.status)}</span></td>
<td class="actions-column">
<button class="action-btn edit" onclick="dataList.editRow(${item.id})">Editar</button>
<button class="action-btn delete" onclick="dataList.deleteRow(${item.id})">Eliminar</button>
</td>
</tr>
`;
}).join('');
this.updateSelectAllState();
}
highlightSearchTerm(text) {
const searchTerm = document.getElementById('globalSearch').value.trim();
if (!searchTerm) return text;
const regex = new RegExp(`(${searchTerm})`, 'gi');
return text.toString().replace(regex, '<span class="highlight">$1</span>');
}
formatDepartment(dept) {
const departments = {
engineering: 'Ingeniería',
marketing: 'Marketing',
sales: 'Ventas',
hr: 'Recursos Humanos',
finance: 'Finanzas'
};
return departments[dept] || dept;
}
formatLocation(loc) {
const locations = {
madrid: 'Madrid',
barcelona: 'Barcelona',
valencia: 'Valencia',
remoto: 'Remoto'
};
return locations[loc] || loc;
}
formatStatus(status) {
const statuses = {
active: 'Activo',
inactive: 'Inactivo',
pending: 'Pendiente'
};
return statuses[status] || status;
}
formatDate(dateString) {
return new Date(dateString).toLocaleDateString('es-ES');
}
showEmptyState() {
document.getElementById('emptyState').style.display = 'flex';
document.querySelector('.table-wrapper').style.display = 'none';
}
hideEmptyState() {
document.getElementById('emptyState').style.display = 'none';
document.querySelector('.table-wrapper').style.display = 'block';
}
handleRowSelect(id, checked) {
if (checked) {
this.selectedRows.add(id);
} else {
this.selectedRows.delete(id);
}
this.updateBulkActions();
this.updateSelectAllState();
}
handleSelectAll(checked) {
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
const pageData = this.filteredData.slice(startIndex, endIndex);
pageData.forEach(item => {
if (checked) {
this.selectedRows.add(item.id);
} else {
this.selectedRows.delete(item.id);
}
});
this.populateTable();
this.updateBulkActions();
}
updateSelectAllState() {
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
const pageData = this.filteredData.slice(startIndex, endIndex);
const selectAllCheckbox = document.getElementById('selectAll');
const selectedOnPage = pageData.filter(item => this.selectedRows.has(item.id)).length;
if (selectedOnPage === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (selectedOnPage === pageData.length) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
updateBulkActions() {
const bulkActions = document.getElementById('bulkActions');
const selectedCount = document.getElementById('selectedCount');
if (this.selectedRows.size > 0) {
bulkActions.style.display = 'flex';
selectedCount.textContent = `${this.selectedRows.size} seleccionados`;
} else {
bulkActions.style.display = 'none';
}
}
updateResultsInfo() {
const total = this.filteredData.length;
const startIndex = (this.currentPage - 1) * this.pageSize + 1;
const endIndex = Math.min(this.currentPage * this.pageSize, total);
const resultsText = document.getElementById('resultsText');
if (total === 0) {
resultsText.textContent = 'No se encontraron empleados';
} else {
resultsText.textContent = `Mostrando ${startIndex}-${endIndex} de ${total} empleados`;
}
}
updatePagination() {
const totalPages = this.getTotalPages();
const paginationNumbers = document.getElementById('paginationNumbers');
const paginationInfo = document.getElementById('paginationInfo');
// Update pagination info
const total = this.filteredData.length;
const startIndex = (this.currentPage - 1) * this.pageSize + 1;
const endIndex = Math.min(this.currentPage * this.pageSize, total);
paginationInfo.textContent = `Mostrando ${startIndex} a ${endIndex} de ${total} entradas`;
// Update pagination buttons
document.getElementById('firstPage').disabled = this.currentPage === 1;
document.getElementById('prevPage').disabled = this.currentPage === 1;
document.getElementById('nextPage').disabled = this.currentPage === totalPages;
document.getElementById('lastPage').disabled = this.currentPage === totalPages;
// Generate page numbers
this.generatePageNumbers(totalPages);
}
generatePageNumbers(totalPages) {
const paginationNumbers = document.getElementById('paginationNumbers');
const maxVisiblePages = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
let html = '';
if (startPage > 1) {
html += '<button class="page-number" onclick="dataList.goToPage(1)">1</button>';
if (startPage > 2) {
html += '<span class="page-ellipsis">...</span>';
}
}
for (let i = startPage; i <= endPage; i++) {
const isActive = i === this.currentPage ? 'active' : '';
html += `<button class="page-number ${isActive}" onclick="dataList.goToPage(${i})">${i}</button>`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += '<span class="page-ellipsis">...</span>';
}
html += `<button class="page-number" onclick="dataList.goToPage(${totalPages})">${totalPages}</button>`;
}
paginationNumbers.innerHTML = html;
}
getTotalPages() {
return Math.ceil(this.filteredData.length / this.pageSize);
}
goToPage(page) {
const totalPages = this.getTotalPages();
if (page >= 1 && page <= totalPages) {
this.currentPage = page;
this.populateTable();
this.updatePagination();
}
}
openModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
closeModal(modalId) {
const modal = document.getElementById(modalId);
modal.classList.remove('active');
document.body.style.overflow = '';
}
setupColumnToggles() {
const columnToggles = document.getElementById('columnToggles');
const columns = [
{ key: 'name', label: 'Nombre' },
{ key: 'email', label: 'Email' },
{ key: 'department', label: 'Departamento' },
{ key: 'position', label: 'Posición' },
{ key: 'location', label: 'Ubicación' },
{ key: 'hireDate', label: 'Fecha de Contratación' },
{ key: 'status', label: 'Estado' }
];
columnToggles.innerHTML = columns.map(col => `
<label class="column-toggle">
<input type="checkbox" value="${col.key}"
${this.visibleColumns.has(col.key) ? 'checked' : ''}>
<span>${col.label}</span>
</label>
`).join('');
}
applyColumnChanges() {
const checkboxes = document.querySelectorAll('#columnToggles input[type="checkbox"]');
this.visibleColumns.clear();
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
this.visibleColumns.add(checkbox.value);
}
});
this.updateTableColumns();
this.closeModal('columnModal');
}
resetColumns() {
this.visibleColumns = new Set(['name', 'email', 'department', 'position', 'location', 'hireDate', 'status']);
this.setupColumnToggles();
this.updateTableColumns();
}
updateTableColumns() {
// This would typically update the table structure
// For this demo, we'll just repopulate the table
this.populateTable();
}
handleExport() {
const format = document.querySelector('input[name="exportFormat"]:checked').value;
const scope = document.querySelector('input[name="exportScope"]:checked').value;
let dataToExport = [];
switch (scope) {
case 'current':
const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = startIndex + this.pageSize;
dataToExport = this.filteredData.slice(startIndex, endIndex);
break;
case 'filtered':
dataToExport = this.filteredData;
break;
case 'all':
dataToExport = this.originalData;
break;
}
switch (format) {
case 'csv':
this.exportToCSV(dataToExport);
break;
case 'json':
this.exportToJSON(dataToExport);
break;
case 'print':
this.printData(dataToExport);
break;
}
this.closeModal('exportModal');
}
exportToCSV(data) {
const headers = ['Nombre', 'Email', 'Departamento', 'Posición', 'Ubicación', 'Fecha de Contratación', 'Estado'];
const csvContent = [
headers.join(','),
...data.map(row => [
row.name,
row.email,
this.formatDepartment(row.department),
row.position,
this.formatLocation(row.location),
row.hireDate,
this.formatStatus(row.status)
].join(','))
].join('\n');
this.downloadFile(csvContent, 'empleados.csv', 'text/csv');
}
exportToJSON(data) {
const jsonContent = JSON.stringify(data, null, 2);
this.downloadFile(jsonContent, 'empleados.json', 'application/json');
}
downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
printData(data) {
const printWindow = window.open('', '_blank');
const printContent = `
<html>
<head>
<title>Lista de Empleados</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Lista de Empleados</h1>
<table>
<thead>
<tr>
<th>Nombre</th>
<th>Email</th>
<th>Departamento</th>
<th>Posición</th>
<th>Ubicación</th>
<th>Fecha de Contratación</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
${data.map(row => `
<tr>
<td>${row.name}</td>
<td>${row.email}</td>
<td>${this.formatDepartment(row.department)}</td>
<td>${row.position}</td>
<td>${this.formatLocation(row.location)}</td>
<td>${this.formatDate(row.hireDate)}</td>
<td>${this.formatStatus(row.status)}</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
printWindow.print();
}
editRow(id) {
const item = this.originalData.find(item => item.id === id);
if (item) {
alert(`Editando empleado: ${item.name}`);
// Implement edit functionality here
}
}
deleteRow(id) {
if (confirm('¿Estás seguro de que quieres eliminar este empleado?')) {
this.originalData = this.originalData.filter(item => item.id !== id);
this.applyFilters();
this.selectedRows.delete(id);
this.updateBulkActions();
}
}
handleBulkEdit() {
if (this.selectedRows.size > 0) {
alert(`Editando ${this.selectedRows.size} empleados seleccionados`);
// Implement bulk edit functionality here
}
}
handleBulkDelete() {
if (this.selectedRows.size > 0 && confirm(`¿Estás seguro de que quieres eliminar ${this.selectedRows.size} empleados?`)) {
this.originalData = this.originalData.filter(item => !this.selectedRows.has(item.id));
this.selectedRows.clear();
this.applyFilters();
this.updateBulkActions();
}
}
}
// Global function for clearing filters (used in empty state)
function clearAllFilters() {
if (window.dataList) {
window.dataList.clearAllFilters();
}
}
// Initialize the data list when the page loads
document.addEventListener('DOMContentLoaded', function() {
// Create global instance
window.dataList = new AdvancedDataList('dataListContainer', sampleData);
});