Le super admin (table super_admins, master DB) ne pouvait pas se connecter via /api/login car ce firewall n'utilisait que le provider tenant. De même, le JWT n'était pas enrichi pour les super admins, l'endpoint /api/me/roles les rejetait, et le frontend redirigeait systématiquement vers /dashboard. Un chain provider (super_admin + tenant) résout l'authentification, le JwtPayloadEnricher et MyRolesProvider gèrent désormais les deux types d'utilisateurs, et le frontend redirige selon le rôle après login.
228 lines
4.2 KiB
Svelte
228 lines
4.2 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import {
|
|
getEstablishments,
|
|
switchTenant,
|
|
type EstablishmentData
|
|
} from '$lib/features/super-admin/api/super-admin';
|
|
|
|
let establishments = $state<EstablishmentData[]>([]);
|
|
let isLoading = $state(true);
|
|
let error = $state<string | null>(null);
|
|
|
|
onMount(async () => {
|
|
try {
|
|
establishments = await getEstablishments();
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : 'Erreur inconnue';
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
});
|
|
|
|
async function handleSwitch(tenantId: string) {
|
|
try {
|
|
await switchTenant(tenantId);
|
|
window.location.href = '/dashboard';
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : 'Erreur lors du basculement';
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="establishments-page">
|
|
<div class="page-header">
|
|
<h2>Établissements</h2>
|
|
<a href="/super-admin/establishments/new" class="btn-create">
|
|
Nouvel établissement
|
|
</a>
|
|
</div>
|
|
|
|
{#if error}
|
|
<div class="error-banner">{error}</div>
|
|
{/if}
|
|
|
|
{#if isLoading}
|
|
<div class="loading">Chargement...</div>
|
|
{:else if establishments.length === 0}
|
|
<div class="empty-state">
|
|
<p>Aucun établissement configuré.</p>
|
|
<a href="/super-admin/establishments/new" class="btn-create">Créer le premier établissement</a>
|
|
</div>
|
|
{:else}
|
|
<div class="table-container">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Nom</th>
|
|
<th>Sous-domaine</th>
|
|
<th>Statut</th>
|
|
<th>Dernière activité</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each establishments as establishment}
|
|
<tr>
|
|
<td class="name-cell">
|
|
<a href="/super-admin/establishments/{establishment.id}">
|
|
{establishment.name}
|
|
</a>
|
|
</td>
|
|
<td class="subdomain-cell">{establishment.subdomain}</td>
|
|
<td>
|
|
<span class="badge" class:active={establishment.status === 'active'}>
|
|
{establishment.status === 'active' ? 'Actif' : 'Inactif'}
|
|
</span>
|
|
</td>
|
|
<td class="date-cell">
|
|
{#if establishment.lastActivityAt}
|
|
{new Date(establishment.lastActivityAt).toLocaleDateString('fr-FR')}
|
|
{:else}
|
|
—
|
|
{/if}
|
|
</td>
|
|
<td class="actions-cell">
|
|
<button
|
|
class="btn-sm"
|
|
onclick={() => handleSwitch(establishment.tenantId)}
|
|
>
|
|
Accéder
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.establishments-page h2 {
|
|
margin: 0;
|
|
font-size: 1.5rem;
|
|
color: #1a1a2e;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.btn-create {
|
|
background: #1a1a2e;
|
|
color: white;
|
|
padding: 0.5rem 1.25rem;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.btn-create:hover {
|
|
background: #16213e;
|
|
}
|
|
|
|
.error-banner {
|
|
background: #fee2e2;
|
|
color: #dc2626;
|
|
padding: 0.75rem 1rem;
|
|
border-radius: 8px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
color: #666;
|
|
padding: 3rem;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: #666;
|
|
}
|
|
|
|
.table-container {
|
|
background: white;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
th {
|
|
text-align: left;
|
|
padding: 0.75rem 1rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
color: #666;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
td {
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #f5f5f5;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.name-cell a {
|
|
color: #1a1a2e;
|
|
font-weight: 500;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.name-cell a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.subdomain-cell {
|
|
color: #666;
|
|
font-family: monospace;
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.date-cell {
|
|
color: #888;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
padding: 0.125rem 0.5rem;
|
|
border-radius: 12px;
|
|
background: #fee2e2;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.badge.active {
|
|
background: #dcfce7;
|
|
color: #16a34a;
|
|
}
|
|
|
|
.actions-cell {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 0.375rem 0.75rem;
|
|
background: #1a1a2e;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.btn-sm:hover {
|
|
background: #16213e;
|
|
}
|
|
</style>
|