feat: Funcion de sistema tenants
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user