feat: Version 1.10.0 - Refactorizacion, optimizacion UI y mejoras de seguridad

- Extraccion de helpers en backend: audit_helpers.py, helpers.py
- Modularizacion de schemas en archivos individuales por dominio
- Reduccion de audit.py en 953 lineas (74% del archivo)
- Reduccion de tickets.py en 655 lineas (60% del archivo)
- Expansion de auth.py con recuperacion de contrasenia y tokens
- Nuevos modulos: core/email.py, core/cache.py
- Reorganizacion de scripts a backend/scripts/
- Frontend: refactorizacion de audit page con array-driven components
- Frontend: correccion de 11 errores ortograficos en tickets page
- Frontend: proxy Docker corregido en vite.config.js
- Frontend: nuevas rutas forgot-password, reset-password, organization, profile
- Nuevas utilidades TS: colorUtils.ts, dateFormats.ts
- 5 nuevos archivos de tests unitarios en backend/tests/unit/
- Eliminacion de 3 scripts temporales de prueba
- Documentacion tecnica: CAMBIOS_v1.10.0.md, OPTIMIZACIONES_RENDIMIENTO.md
This commit is contained in:
2026-02-19 13:48:21 -07:00
parent 16d795e8bd
commit 517297e89a
57 changed files with 8022 additions and 3660 deletions

View File

@@ -160,14 +160,17 @@
<!-- User info -->
<div class="px-4 py-4 border-t border-gray-200">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<a
href="/profile"
class="flex items-center space-x-3 rounded-lg p-1 -m-1 hover:bg-gray-100 transition-colors group"
>
<div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center shrink-0">
<span class="text-primary-600 text-sm font-medium">
{$auth.user?.first_name?.[0]}{$auth.user?.last_name?.[0]}
</span>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-gray-900 truncate">
<p class="text-sm font-medium text-gray-900 truncate group-hover:text-primary-600">
{$auth.user?.first_name}
{$auth.user?.last_name}
</p>
@@ -181,12 +184,20 @@
: 'Auditor'}
</p>
</div>
</div>
<svg
class="w-4 h-4 text-gray-400 group-hover:text-primary-500 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
<!-- Version info -->
<div class="px-4 py-2 border-t border-gray-100">
<p class="text-xs text-gray-400 text-center">v1.6.0</p>
<p class="text-xs text-gray-400 text-center">v1.9.0</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,73 @@
// Utilidades de colores para diferentes estados y severidades
type ColorType = 'severity' | 'status' | 'action' | 'priority';
const COLOR_MAPS = {
severity: {
'critical': 'bg-red-600 text-white',
'high': 'bg-orange-600 text-white',
'medium': 'bg-yellow-500 text-white',
'low': 'bg-blue-600 text-white'
},
status: {
'active': 'bg-blue-600 text-white',
'open': 'bg-blue-600 text-white',
'resolved': 'bg-green-600 text-white',
'closed': 'bg-green-600 text-white',
'investigating': 'bg-yellow-500 text-white',
'new': 'bg-blue-500 text-white',
'in_progress': 'bg-purple-600 text-white',
'waiting_customer': 'bg-orange-500 text-white',
'reopened': 'bg-red-500 text-white'
},
action: {
'delete': 'bg-red-600 text-white',
'update': 'bg-blue-600 text-white',
'login': 'bg-indigo-600 text-white',
'logout': 'bg-indigo-600 text-white',
'create': 'bg-green-600 text-white',
'failed': 'bg-red-500 text-white'
},
priority: {
'urgent': 'bg-red-600 text-white',
'high': 'bg-orange-500 text-white',
'medium': 'bg-yellow-500 text-white',
'low': 'bg-blue-500 text-white'
}
};
export function getColorClass(value: string, type: ColorType = 'status'): string {
const map = COLOR_MAPS[type];
const key = value?.toLowerCase();
if (type === 'action') {
const matchKey = Object.keys(map).find(k => key?.includes(k));
return map[matchKey as keyof typeof map] || 'bg-gray-600 text-white';
}
return map[key as keyof typeof map] || 'bg-gray-600 text-white';
}
export function getStatusIcon(status: string): string {
const icons = {
'active': '🔴',
'open': '📂',
'resolved': '✅',
'closed': '🔒',
'investigating': '🔍',
'new': '🆕',
'in_progress': '⚙️',
'waiting_customer': '⏳',
'reopened': '🔄'
};
return icons[status?.toLowerCase() as keyof typeof icons] || '📋';
}
export function getSeverityIcon(severity: string): string {
const icons = {
'critical': '🚨',
'high': '⚠️',
'medium': '⚡',
'low': ''
};
return icons[severity?.toLowerCase() as keyof typeof icons] || '📊';
}

View File

@@ -0,0 +1,77 @@
// Utilidades de formato de fecha
export type DateFormat = 'full' | 'short' | 'simple' | 'time';
export function formatDate(dateString: string, format: DateFormat = 'full'): string {
const date = new Date(dateString);
const today = new Date();
const isToday = date.toDateString() === today.toDateString();
const formats = {
full: () => date.toLocaleString('es-MX', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
}),
short: () => isToday
? date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' })
: date.toLocaleDateString('es-MX', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
simple: () => date.toLocaleDateString('es-MX', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
}),
time: () => date.toLocaleTimeString('es-MX', { hour: '2-digit', minute: '2-digit' })
};
return formats[format]();
}
export function getRelativeTime(dateString: string): string {
const now = new Date().getTime();
const then = new Date(dateString).getTime();
const diffMs = now - then;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Hace un momento';
if (diffMins < 60) return `Hace ${diffMins} minuto${diffMins > 1 ? 's' : ''}`;
if (diffHours < 24) return `Hace ${diffHours} hora${diffHours > 1 ? 's' : ''}`;
if (diffDays < 7) return `Hace ${diffDays} día${diffDays > 1 ? 's' : ''}`;
return formatDate(dateString, 'short');
}
export function getDateRangeForPeriod(periodFilter: string, customDateFrom?: string, customDateTo?: string): { from: string; to: string } {
const now = new Date();
let from: Date;
let to: Date = new Date();
switch (periodFilter) {
case 'today':
from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0));
break;
case 'yesterday':
from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 1, 0, 0, 0));
to = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0));
break;
case 'last7days':
from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 7, 0, 0, 0));
break;
case 'last30days':
from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() - 30, 0, 0, 0));
break;
case 'custom':
if (!customDateFrom || !customDateTo) {
from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0));
} else {
from = new Date(customDateFrom + 'T00:00:00Z');
to = new Date(customDateTo + 'T23:59:59Z');
}
break;
default:
from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0, 0));
}
return {
from: from.toISOString(),
to: to.toISOString()
};
}