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

@@ -26,10 +26,22 @@ async function request<T>(endpoint: string, options: RequestOptions = {}): Promi
const authState = get(auth);
const token = authState.token || (typeof window !== 'undefined' ? localStorage.getItem('internal_auth_token') : null);
// Resolve tenant_id from store or from the persisted user object in localStorage
let tenantId = authState.user?.tenant_id ?? null;
if (!tenantId && typeof window !== 'undefined') {
try {
const stored = localStorage.getItem('internal_auth_user');
if (stored) tenantId = JSON.parse(stored)?.tenant_id ?? null;
} catch { /* ignore */ }
}
const headers = new Headers(init.headers);
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
if (tenantId && !headers.has('X-Tenant-ID')) {
headers.set('X-Tenant-ID', tenantId);
}
if (!headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
@@ -66,10 +78,21 @@ async function downloadFile(endpoint: string, filename: string): Promise<void> {
const authState = get(auth);
const token = authState.token || (typeof window !== 'undefined' ? localStorage.getItem('internal_auth_token') : null);
let tenantId = authState.user?.tenant_id ?? null;
if (!tenantId && typeof window !== 'undefined') {
try {
const stored = localStorage.getItem('internal_auth_user');
if (stored) tenantId = JSON.parse(stored)?.tenant_id ?? null;
} catch { /* ignore */ }
}
const headers = new Headers();
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
if (tenantId) {
headers.set('X-Tenant-ID', tenantId);
}
const response = await fetch(`${API_BASE}${endpoint}`, {
method: 'GET',

View File

@@ -48,7 +48,8 @@
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth.token}`
Authorization: `Bearer ${$auth.token}`,
...($auth.user?.tenant_id ? { 'X-Tenant-ID': $auth.user.tenant_id } : {})
},
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword })
});
@@ -78,7 +79,7 @@
try {
const response = await fetch('/api/v1/auth/2fa/setup', {
method: 'POST',
headers: { Authorization: `Bearer ${$auth.token}` }
headers: { Authorization: `Bearer ${$auth.token}`, ...($auth.user?.tenant_id ? { 'X-Tenant-ID': $auth.user.tenant_id } : {}) }
});
if (!response.ok) throw new Error((await response.json()).detail);
const data = await response.json();
@@ -101,7 +102,7 @@
try {
const response = await fetch('/api/v1/auth/2fa/enable', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$auth.token}` },
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$auth.token}`, ...($auth.user?.tenant_id ? { 'X-Tenant-ID': $auth.user.tenant_id } : {}) },
body: JSON.stringify({ totp_code: totpSetupCode })
});
if (!response.ok) throw new Error((await response.json()).detail);
@@ -127,7 +128,7 @@
try {
const response = await fetch('/api/v1/auth/2fa/disable', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$auth.token}` },
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${$auth.token}`, ...($auth.user?.tenant_id ? { 'X-Tenant-ID': $auth.user.tenant_id } : {}) },
body: JSON.stringify({ totp_code: disableTotpCode })
});
if (!response.ok) throw new Error((await response.json()).detail);

View File

@@ -188,12 +188,11 @@
<!-- Stats Summary -->
<div class="mt-6 bg-red-50 border-l-4 border-red-500 p-4 rounded-lg">
<p class="text-sm text-red-800">
<strong class="font-bold">{total}</strong> {total === 1 ? 'violación activa' : 'violaciones activas'} encontradas
{#if total > 0}
- Requieren atención inmediata
{/if}
</p>
</div>
<strong class="font-bold">{total}</strong> {total === 1 ? 'violación activa' : 'violaciones activas'} encontradas
{#if total > 0}
- Requieren atención inmediata
{/if}
</p>
</div>
<!-- Violations Table -->
@@ -282,12 +281,9 @@
<td class="whitespace-nowrap px-3 py-4 text-sm">
<div>
<span class="font-bold text-red-600 text-base">
{formatHours(violation.hours_overdue)}
</span>
<div class="text-xs text-gray-600">
vencido
</div>
</div>
{formatHours(violation.hours_overdue)}
</span>
<div class="text-xs text-gray-600">vencido</div>
</div>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">

View File

@@ -86,7 +86,7 @@
$: filteredTickets = allTickets.filter(t => {
if (filterStatus && t.status !== filterStatus) return false;
if (filterPriority && t.priority !== filterPriority) return false;
if (filterTenantId && t.tenant?.id !== filterTenantId) return false;
if (filterTenantId && (t.tenant_id ?? t.tenant?.id) !== filterTenantId) return false;
if (searchQuery) {
const q = searchQuery.toLowerCase();
const haystack =
@@ -155,7 +155,7 @@
$: tenantCounts = (() => {
const counts: Record<string, number> = {};
for (const t of allTickets) {
const id = t.tenant?.id;
const id = t.tenant_id ?? t.tenant?.id;
if (id) counts[id] = (counts[id] || 0) + 1;
}
return counts;
@@ -311,56 +311,25 @@
------------------------------------------------------------------------------ -->
<div class="flex gap-5 px-4 py-5 mx-auto max-w-full sm:px-6 lg:px-8">
<aside class="w-60 flex-shrink-0 space-y-4">
<div class="bg-white rounded-lg shadow border border-gray-200 overflow-hidden">
<div class="bg-blue-700 px-4 py-3">
<h2 class="text-sm font-semibold text-white tracking-wide">Organización</h2>
</div>
<ul class="divide-y divide-gray-100">
<li>
<button
class="w-full text-left px-4 py-2.5 flex items-center justify-between text-sm transition-colors
{filterTenantId === ''
? 'bg-blue-50 text-blue-700 font-semibold'
: 'text-gray-700 hover:bg-gray-50'}"
on:click={() => {
filterTenantId = '';
}}
>
<span>Todas</span>
<span class="text-xs bg-gray-100 text-gray-600 rounded-full px-2 py-0.5 font-medium">
{allTickets.length}
</span>
</button>
</li>
{#each tenants as tenant}
{#if (tenantCounts[tenant.id] ?? 0) > 0}
<li>
<button
class="w-full text-left px-4 py-2.5 flex items-center justify-between text-sm transition-colors
{filterTenantId === tenant.id
? 'bg-blue-50 text-blue-700 font-semibold'
: 'text-gray-700 hover:bg-gray-50'}"
on:click={() => {
filterTenantId = tenant.id;
categoryPages = {};
}}
>
<span class="truncate pr-1">{tenant.name}</span>
<span
class="text-xs bg-gray-100 text-blue-700 rounded-full px-2 py-0.5 font-medium flex-shrink-0 border border-gray-200"
>
{tenantCounts[tenant.id] ?? 0}
</span>
</button>
</li>
{/if}
{/each}
</ul>
</div>
<div class="bg-white rounded-lg shadow border border-gray-200 p-4 space-y-3">
<h2 class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Filtros</h2>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Organización</label>
<select
bind:value={filterTenantId}
on:change={() => (categoryPages = {})}
class="block w-full rounded-md border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500 border px-2 py-1.5"
>
<option value="">Todas ({allTickets.length})</option>
{#each tenants as tenant}
{#if (tenantCounts[tenant.id] ?? 0) > 0}
<option value={tenant.id}>{tenant.name} ({tenantCounts[tenant.id] ?? 0})</option>
{/if}
{/each}
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Estado</label>
<select