feat: Funcion de sistema tenants

This commit is contained in:
2026-02-23 13:01:24 -07:00
parent ceea67eb2b
commit 1ccc39732b
58 changed files with 1889 additions and 315 deletions

View File

@@ -17,7 +17,14 @@
eye: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z',
clock: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
check: 'M5 13l4 4L19 7',
chevronDown: 'M19 9l-7 7-7-7'
chevronDown: 'M19 9l-7 7-7-7',
'building-2': 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
'loader-2': 'M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83',
'alert-circle': 'M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
'shield-check': 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
mail: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
lock: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z',
'eye-off': 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21'
};
$: path = icons[name] || icons.home;

View File

@@ -2,22 +2,25 @@
export let ticket: import('$lib/stores/tickets').Ticket;
// Status mapping
const statusConfig = {
const statusConfig: Record<string, { label: string; class: string }> = {
NEW: { label: 'Nuevo', class: 'badge-new' },
IN_PROGRESS: { label: 'En Progreso', class: 'badge-in-progress' },
WAITING_CUSTOMER: { label: 'Esperando Cliente', class: 'badge-waiting' },
WAITING_FOR_CLIENT: { label: 'Esperando Cliente', class: 'badge-waiting' },
RESOLVED: { label: 'Resuelto', class: 'badge-resolved' },
CLOSED: { label: 'Cerrado', class: 'badge-closed' },
REOPENED: { label: 'Reabierto', class: 'badge-reopened' }
};
const fallbackStatus = { label: 'Desconocido', class: 'badge-new' };
// Priority mapping
const priorityConfig = {
const priorityConfig: Record<string, { label: string; class: string }> = {
LOW: { label: 'Baja', class: 'badge-priority-low' },
MEDIUM: { label: 'Media', class: 'badge-priority-medium' },
HIGH: { label: 'Alta', class: 'badge-priority-high' },
URGENT: { label: 'Urgente', class: 'badge-priority-urgent' }
};
const fallbackPriority = { label: 'Normal', class: 'badge-priority-medium' };
// Format date
function formatDate(dateString: string): string {
@@ -58,11 +61,11 @@
</a>
</h3>
<div class="flex items-center space-x-2 ml-4">
<span class={`${statusConfig[ticket.status].class}`}>
{statusConfig[ticket.status].label}
<span class={(statusConfig[ticket.status] ?? fallbackStatus).class}>
{(statusConfig[ticket.status] ?? fallbackStatus).label}
</span>
<span class={`${priorityConfig[ticket.priority].class}`}>
{priorityConfig[ticket.priority].label}
<span class={(priorityConfig[ticket.priority] ?? fallbackPriority).class}>
{(priorityConfig[ticket.priority] ?? fallbackPriority).label}
</span>
</div>
</div>

View File

@@ -35,6 +35,7 @@ async function apiCall(endpoint: string, options: RequestInit = {}) {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authState.token}`,
...(authState.user?.tenant_id ? { 'X-Tenant-ID': authState.user.tenant_id } : {}),
...options.headers
}
});

View File

@@ -135,7 +135,9 @@ function createTicketsStore() {
update((state: TicketsState) => ({ ...state, isLoading: true, error: null }));
try {
const tickets = await apiCall('/tickets/');
const raw = await apiCall('/tickets/');
// El backend devuelve 'subject', el tipo Ticket usa 'title'
const tickets = raw.map((t: any) => ({ ...t, title: t.subject ?? t.title }));
update((state: TicketsState) => ({ ...state, tickets, isLoading: false }));
} catch (error) {
update((state: TicketsState) => ({
@@ -151,11 +153,13 @@ function createTicketsStore() {
update((state: TicketsState) => ({ ...state, isLoading: true, error: null }));
try {
const [ticket, comments, attachments] = await Promise.all([
const [ticketRaw, comments, attachments] = await Promise.all([
apiCall(`/tickets/${ticketId}`),
apiCall(`/tickets/${ticketId}/comments`),
apiCall(`/tickets/${ticketId}/attachments`)
]);
// El backend devuelve 'subject', el tipo Ticket usa 'title'
const ticket = { ...ticketRaw, title: ticketRaw.subject ?? ticketRaw.title };
update((state: TicketsState) => ({
...state,

View File

@@ -7,6 +7,7 @@
let email = '';
let password = '';
let tenantSlug = 'aduanasoft-demo';
let totpCode = '';
let isLoading = false;
let showTwoFactor = false;
@@ -33,7 +34,7 @@
await auth.login({
email,
password,
tenant_slug: 'aduanasoft', // Default tenant for now
tenant_slug: tenantSlug.trim() || 'aduanasoft-demo',
totp_code: totpCode || undefined
});

View File

@@ -63,22 +63,25 @@
}
// Status mapping
const statusConfig = {
const statusConfig: Record<string, { label: string; class: string }> = {
NEW: { label: 'Nuevo', class: 'badge-new' },
IN_PROGRESS: { label: 'En Progreso', class: 'badge-in-progress' },
WAITING_FOR_CLIENT: { label: 'Esperando Cliente', class: 'badge-waiting' },
WAITING_CUSTOMER: { label: 'Esperando Cliente', class: 'badge-waiting' },
RESOLVED: { label: 'Resuelto', class: 'badge-resolved' },
CLOSED: { label: 'Cerrado', class: 'badge-closed' },
REOPENED: { label: 'Reabierto', class: 'badge-reopened' }
};
const fallbackStatus = { label: 'Desconocido', class: 'badge-new' };
// Priority mapping
const priorityConfig = {
const priorityConfig: Record<string, { label: string; class: string }> = {
LOW: { label: 'Baja', class: 'badge-priority-low' },
MEDIUM: { label: 'Media', class: 'badge-priority-medium' },
HIGH: { label: 'Alta', class: 'badge-priority-high' },
URGENT: { label: 'Urgente', class: 'badge-priority-urgent' }
};
const fallbackPriority = { label: 'Normal', class: 'badge-priority-medium' };
async function handleAddComment() {
if (!newComment.trim()) return;
@@ -175,7 +178,7 @@
// Check if user can close ticket
$: canClose =
$tickets.currentTicket &&
['RESOLVED', 'WAITING_FOR_CLIENT'].includes($tickets.currentTicket.status);
['RESOLVED', 'WAITING_CUSTOMER'].includes($tickets.currentTicket.status);
</script>
<svelte:head>
@@ -230,11 +233,11 @@
{$tickets.currentTicket.title}
</h1>
<div class="flex items-center space-x-3">
<span class={statusConfig[$tickets.currentTicket.status].class}>
{statusConfig[$tickets.currentTicket.status].label}
<span class={(statusConfig[$tickets.currentTicket.status] ?? fallbackStatus).class}>
{(statusConfig[$tickets.currentTicket.status] ?? fallbackStatus).label}
</span>
<span class={priorityConfig[$tickets.currentTicket.priority].class}>
{priorityConfig[$tickets.currentTicket.priority].label}
<span class={(priorityConfig[$tickets.currentTicket.priority] ?? fallbackPriority).class}>
{(priorityConfig[$tickets.currentTicket.priority] ?? fallbackPriority).label}
</span>
<span class="text-sm text-gray-500">
Creado {formatDate($tickets.currentTicket.created_at)}
@@ -505,6 +508,45 @@
<dd class="text-sm text-gray-900">{formatDate($tickets.currentTicket.updated_at)}</dd>
</div>
<!-- SLA -->
{#if $tickets.currentTicket.sla_response_due || $tickets.currentTicket.sla_resolution_due}
<div class="pt-3 border-t border-gray-100">
<dt class="text-sm font-medium text-gray-500 mb-2">Tiempos de SLA</dt>
{#if $tickets.currentTicket.sla_response_due}
{@const respDue = new Date($tickets.currentTicket.sla_response_due)}
{@const respVencido = respDue < new Date() && !$tickets.currentTicket.first_response_at}
<div class="mb-2">
<dt class="text-xs text-gray-400">Respuesta límite</dt>
<dd class="text-sm {respVencido ? 'text-red-600 font-medium' : 'text-gray-900'}">
{formatDate($tickets.currentTicket.sla_response_due)}
{#if respVencido}
<span class="block text-xs text-red-500">¡Vencido!</span>
{:else if $tickets.currentTicket.first_response_at}
<span class="block text-xs text-green-600">✓ Respondido</span>
{/if}
</dd>
</div>
{/if}
{#if $tickets.currentTicket.sla_resolution_due}
{@const resDue = new Date($tickets.currentTicket.sla_resolution_due)}
{@const resVencido = resDue < new Date() && !$tickets.currentTicket.resolved_at}
<div>
<dt class="text-xs text-gray-400">Resolución límite</dt>
<dd class="text-sm {resVencido ? 'text-red-600 font-medium' : 'text-gray-900'}">
{formatDate($tickets.currentTicket.sla_resolution_due)}
{#if resVencido}
<span class="block text-xs text-red-500">¡Vencido!</span>
{:else if $tickets.currentTicket.resolved_at}
<span class="block text-xs text-green-600">✓ Resuelto</span>
{/if}
</dd>
</div>
{/if}
</div>
{/if}
{#if $tickets.currentTicket.due_date}
<div>
<dt class="text-sm font-medium text-gray-500">Fecha límite</dt>

View File

@@ -12,7 +12,7 @@ export default defineConfig({
},
proxy: {
'/api': {
target: process.env.PUBLIC_API_URL || 'http://backend:8000',
target: process.env.PUBLIC_API_URL || 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}