758 lines
27 KiB
TypeScript
758 lines
27 KiB
TypeScript
"use client";
|
||
|
||
import { GraphQLClient, gql } from "graphql-request";
|
||
import { useEffect, useState } from "react";
|
||
|
||
const graphQLClient = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql");
|
||
|
||
const GET_ORGANIZATIONS_QUERY = gql`
|
||
query GetOrganizations($page: Int, $pageSize: Int) {
|
||
organizations(page: $page, pageSize: $pageSize) {
|
||
success
|
||
message
|
||
totalCount
|
||
organizations {
|
||
id
|
||
name
|
||
description
|
||
customData
|
||
isMfaRequired
|
||
createdAt
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
const GET_MY_ORGANIZATIONS_QUERY = gql`
|
||
query GetMyOrganizations($page: Int, $pageSize: Int) {
|
||
myOrganizations(page: $page, pageSize: $pageSize) {
|
||
success
|
||
message
|
||
totalCount
|
||
organizations {
|
||
id
|
||
name
|
||
description
|
||
customData
|
||
isMfaRequired
|
||
createdAt
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
const CREATE_ORGANIZATION_MUTATION = gql`
|
||
mutation CreateOrganization($input: CreateOrganizationInput!) {
|
||
createOrganization(input: $input) {
|
||
success
|
||
message
|
||
organization {
|
||
id
|
||
name
|
||
description
|
||
customData
|
||
isMfaRequired
|
||
createdAt
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
const GET_ORGANIZATION_USERS_QUERY = gql`
|
||
query GetOrganizationUsers($organizationId: ID!, $page: Int, $pageSize: Int) {
|
||
organizationUsers(organizationId: $organizationId, page: $page, pageSize: $pageSize) {
|
||
success
|
||
message
|
||
totalCount
|
||
users {
|
||
id
|
||
username
|
||
primaryEmail
|
||
name
|
||
avatar
|
||
organizationRoles {
|
||
id
|
||
name
|
||
description
|
||
type
|
||
}
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
const GET_ORGANIZATION_ROLES_QUERY = gql`
|
||
query GetOrganizationRoles($organizationId: ID!, $page: Int, $pageSize: Int) {
|
||
organizationRoles(organizationId: $organizationId, page: $page, pageSize: $pageSize) {
|
||
success
|
||
message
|
||
totalCount
|
||
roles {
|
||
id
|
||
name
|
||
description
|
||
type
|
||
}
|
||
}
|
||
}
|
||
`;
|
||
|
||
const TEST_ORGANIZATIONS_API_QUERY = gql`
|
||
query TestOrganizationsAPI {
|
||
testOrganizationsAPI {
|
||
available
|
||
message
|
||
}
|
||
}
|
||
`;
|
||
|
||
interface Organization {
|
||
id: string;
|
||
name: string;
|
||
description?: string;
|
||
customData?: Record<string, unknown>;
|
||
isMfaRequired?: boolean;
|
||
createdAt: string;
|
||
}
|
||
|
||
interface OrganizationUser {
|
||
id: string;
|
||
username?: string;
|
||
primaryEmail?: string;
|
||
name?: string;
|
||
avatar?: string;
|
||
organizationRoles: Array<{
|
||
id: string;
|
||
name: string;
|
||
description?: string;
|
||
type: string;
|
||
}>;
|
||
}
|
||
|
||
interface OrganizationRole {
|
||
id: string;
|
||
name: string;
|
||
description?: string;
|
||
type: string;
|
||
}
|
||
|
||
interface OrganizationsResponse {
|
||
organizations: {
|
||
success: boolean;
|
||
message?: string;
|
||
totalCount?: number;
|
||
organizations?: Organization[];
|
||
};
|
||
}
|
||
|
||
interface MyOrganizationsResponse {
|
||
myOrganizations: {
|
||
success: boolean;
|
||
message?: string;
|
||
totalCount?: number;
|
||
organizations?: Organization[];
|
||
};
|
||
}
|
||
|
||
interface CreateOrganizationResponse {
|
||
createOrganization: {
|
||
success: boolean;
|
||
message?: string;
|
||
organization?: Organization;
|
||
};
|
||
}
|
||
|
||
interface OrganizationUsersResponse {
|
||
organizationUsers: {
|
||
success: boolean;
|
||
message?: string;
|
||
totalCount?: number;
|
||
users?: OrganizationUser[];
|
||
};
|
||
}
|
||
|
||
interface OrganizationRolesResponse {
|
||
organizationRoles: {
|
||
success: boolean;
|
||
message?: string;
|
||
totalCount?: number;
|
||
roles?: OrganizationRole[];
|
||
};
|
||
}
|
||
|
||
interface TestOrganizationsAPIResponse {
|
||
testOrganizationsAPI: {
|
||
available: boolean;
|
||
message: string;
|
||
};
|
||
}
|
||
|
||
export default function OrganizationsTestPage() {
|
||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||
const [myOrganizations, setMyOrganizations] = useState<Organization[]>([]);
|
||
const [selectedOrganization, setSelectedOrganization] = useState<Organization | null>(null);
|
||
const [organizationUsers, setOrganizationUsers] = useState<OrganizationUser[]>([]);
|
||
const [organizationRoles, setOrganizationRoles] = useState<OrganizationRole[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [createLoading, setCreateLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [activeTab, setActiveTab] = useState<"all" | "mine" | "details">("all");
|
||
|
||
// Form data for creating organization
|
||
const [createFormData, setCreateFormData] = useState({
|
||
name: "",
|
||
description: "",
|
||
isMfaRequired: false,
|
||
});
|
||
|
||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||
|
||
const fetchAllOrganizations = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const response = await graphQLClient.request<OrganizationsResponse>(GET_ORGANIZATIONS_QUERY, {
|
||
page: 1,
|
||
pageSize: 20,
|
||
});
|
||
|
||
if (response.organizations.success) {
|
||
const orgs = response.organizations.organizations || [];
|
||
setOrganizations(orgs);
|
||
} else {
|
||
setError(response.organizations.message || "Erreur lors du chargement des organisations");
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
// Récupérer le token depuis localStorage
|
||
const token = localStorage.getItem("accessToken");
|
||
setAccessToken(token);
|
||
|
||
// Auto-loading des organisations au démarrage de la page
|
||
const loadOrganizations = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const response = await graphQLClient.request<OrganizationsResponse>(GET_ORGANIZATIONS_QUERY, {
|
||
page: 1,
|
||
pageSize: 20,
|
||
});
|
||
|
||
if (response.organizations.success) {
|
||
const orgs = response.organizations.organizations || [];
|
||
setOrganizations(orgs);
|
||
} else {
|
||
setError(response.organizations.message || "Erreur lors du chargement des organisations");
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
loadOrganizations();
|
||
}, []);
|
||
|
||
const testOrganizationsAPI = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const response = await graphQLClient.request<TestOrganizationsAPIResponse>(TEST_ORGANIZATIONS_API_QUERY);
|
||
|
||
if (response.testOrganizationsAPI.available) {
|
||
// Si l'API est disponible, on peut essayer de charger les organisations
|
||
setError(null);
|
||
// On ne recharge pas automatiquement, on laisse l'utilisateur choisir
|
||
alert(
|
||
`✅ ${response.testOrganizationsAPI.message}\n\nVous pouvez maintenant utiliser les fonctionnalités d'organisations.`
|
||
);
|
||
} else {
|
||
setError(response.testOrganizationsAPI.message);
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Erreur inconnue lors du test");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const fetchMyOrganizations = async (token: string) => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const clientWithAuth = new GraphQLClient(process.env.NEXT_PUBLIC_APP_URL + "/api/graphql", {
|
||
headers: {
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
});
|
||
|
||
const response = await clientWithAuth.request<MyOrganizationsResponse>(GET_MY_ORGANIZATIONS_QUERY, {
|
||
page: 1,
|
||
pageSize: 20,
|
||
});
|
||
|
||
if (response.myOrganizations.success) {
|
||
setMyOrganizations(response.myOrganizations.organizations || []);
|
||
} else {
|
||
setError(response.myOrganizations.message || "Erreur lors du chargement de vos organisations");
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const fetchOrganizationDetails = async (organization: Organization) => {
|
||
setSelectedOrganization(organization);
|
||
setActiveTab("details");
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
// Récupérer les utilisateurs
|
||
const usersResponse = await graphQLClient.request<OrganizationUsersResponse>(GET_ORGANIZATION_USERS_QUERY, {
|
||
organizationId: organization.id,
|
||
page: 1,
|
||
pageSize: 20,
|
||
});
|
||
|
||
if (usersResponse.organizationUsers.success) {
|
||
setOrganizationUsers(usersResponse.organizationUsers.users || []);
|
||
}
|
||
|
||
// Récupérer les rôles
|
||
const rolesResponse = await graphQLClient.request<OrganizationRolesResponse>(GET_ORGANIZATION_ROLES_QUERY, {
|
||
organizationId: organization.id,
|
||
page: 1,
|
||
pageSize: 20,
|
||
});
|
||
|
||
if (rolesResponse.organizationRoles.success) {
|
||
setOrganizationRoles(rolesResponse.organizationRoles.roles || []);
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleCreateOrganization = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
// Validation simple côté client
|
||
if (!createFormData.name.trim()) {
|
||
setError("Le nom de l'organisation est requis");
|
||
return;
|
||
}
|
||
|
||
setCreateLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const response = await graphQLClient.request<CreateOrganizationResponse>(CREATE_ORGANIZATION_MUTATION, {
|
||
input: {
|
||
name: createFormData.name,
|
||
description: createFormData.description || undefined,
|
||
isMfaRequired: createFormData.isMfaRequired,
|
||
},
|
||
});
|
||
|
||
if (response.createOrganization.success) {
|
||
alert("Organisation créée avec succès!");
|
||
setCreateFormData({ name: "", description: "", isMfaRequired: false });
|
||
// Recharger la liste si on est sur l'onglet "all"
|
||
if (activeTab === "all") {
|
||
await fetchAllOrganizations();
|
||
}
|
||
} else {
|
||
setError(response.createOrganization.message || "Erreur lors de la création");
|
||
}
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Erreur inconnue");
|
||
} finally {
|
||
setCreateLoading(false);
|
||
}
|
||
};
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||
<div className="mx-auto max-w-6xl">
|
||
<div className="rounded-lg bg-white p-8 shadow-lg">
|
||
<h1 className="mb-6 text-center text-3xl font-bold text-gray-900">🏢 Test des Organisations Logto</h1>
|
||
|
||
{/* Info Section */}
|
||
<div className="mb-6 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||
<h3 className="text-sm font-medium text-blue-800">ℹ️ À propos des organisations Logto</h3>
|
||
<div className="mt-2 text-sm text-blue-700">
|
||
<p>
|
||
Les organisations permettent de gérer des environnements multi-tenant (B2B/SaaS). Cette fonctionnalité
|
||
peut nécessiter :
|
||
</p>
|
||
<ul className="mt-2 ml-4 list-disc space-y-1">
|
||
<li>Un plan Logto qui inclut les organisations</li>
|
||
<li>L'activation dans votre console Logto</li>
|
||
<li>La configuration d'un template d'organisation</li>
|
||
</ul>
|
||
<p className="mt-2">
|
||
<a
|
||
href="https://docs.logto.io/docs/recipes/organizations/"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="underline hover:no-underline"
|
||
>
|
||
📚 Documentation Logto sur les organisations
|
||
</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Navigation Tabs */}
|
||
<div className="mb-6 border-b border-gray-200">
|
||
<nav className="-mb-px flex space-x-8">
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("all");
|
||
fetchAllOrganizations();
|
||
}}
|
||
className={`border-b-2 px-1 py-2 text-sm font-medium ${
|
||
activeTab === "all"
|
||
? "border-blue-500 text-blue-600"
|
||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||
}`}
|
||
disabled={loading}
|
||
>
|
||
Toutes les organisations
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setActiveTab("mine");
|
||
if (accessToken) {
|
||
fetchMyOrganizations(accessToken);
|
||
}
|
||
}}
|
||
className={`border-b-2 px-1 py-2 text-sm font-medium ${
|
||
activeTab === "mine"
|
||
? "border-blue-500 text-blue-600"
|
||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||
}`}
|
||
disabled={!accessToken || loading}
|
||
>
|
||
Mes organisations
|
||
{!accessToken && " (Connexion requise)"}
|
||
</button>
|
||
{selectedOrganization && (
|
||
<button
|
||
onClick={() => setActiveTab("details")}
|
||
className={`border-b-2 px-1 py-2 text-sm font-medium ${
|
||
activeTab === "details"
|
||
? "border-blue-500 text-blue-600"
|
||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
|
||
}`}
|
||
>
|
||
Détails: {selectedOrganization.name}
|
||
</button>
|
||
)}
|
||
</nav>
|
||
</div>
|
||
|
||
{/* API Test Section */}
|
||
<div className="mb-8 rounded-md border border-gray-200 bg-gray-50 p-4">
|
||
<h3 className="mb-4 text-lg font-medium text-gray-800">🔬 Test de l'API des organisations</h3>
|
||
<div className="flex items-center gap-4">
|
||
<button
|
||
onClick={testOrganizationsAPI}
|
||
disabled={loading}
|
||
className="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
{loading ? "⏳ Test..." : "Tester l'API des organisations"}
|
||
</button>
|
||
<p className="text-sm text-gray-600">
|
||
Cliquez pour vérifier si l'API des organisations est disponible sur votre instance Logto.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Create Organization Form */}
|
||
<div className="mb-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||
<h3 className="mb-4 text-lg font-medium text-blue-800">Créer une nouvelle organisation</h3>
|
||
|
||
<form onSubmit={handleCreateOrganization} className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||
<div>
|
||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||
Nom (requis)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="name"
|
||
value={createFormData.name}
|
||
onChange={e => setCreateFormData(prev => ({ ...prev, name: e.target.value }))}
|
||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||
placeholder="Nom de l'organisation"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||
Description
|
||
</label>
|
||
<input
|
||
type="text"
|
||
id="description"
|
||
value={createFormData.description}
|
||
onChange={e => setCreateFormData(prev => ({ ...prev, description: e.target.value }))}
|
||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 focus:outline-none"
|
||
placeholder="Description de l'organisation"
|
||
/>
|
||
</div>
|
||
<div className="flex items-end">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="checkbox"
|
||
checked={createFormData.isMfaRequired}
|
||
onChange={e => setCreateFormData(prev => ({ ...prev, isMfaRequired: e.target.checked }))}
|
||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||
/>
|
||
<span className="ml-2 text-sm text-gray-700">MFA requis</span>
|
||
</label>
|
||
<button
|
||
type="submit"
|
||
disabled={createLoading}
|
||
className="ml-4 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
{createLoading ? "⏳ Création..." : "Créer"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Error Display */}
|
||
{error && (
|
||
<div className="mb-6 rounded-md border border-red-200 bg-red-50 p-4">
|
||
<h3 className="text-sm font-medium text-red-800">Erreur</h3>
|
||
<p className="mt-2 text-sm text-red-700">{error}</p>
|
||
|
||
{error.includes("n'est pas disponible") && (
|
||
<div className="mt-4 rounded border border-yellow-200 bg-yellow-50 p-3">
|
||
<h4 className="text-sm font-medium text-yellow-800">💡 Comment activer les organisations</h4>
|
||
<div className="mt-2 text-sm text-yellow-700">
|
||
<p>Les organisations Logto peuvent nécessiter :</p>
|
||
<ul className="mt-1 ml-4 list-disc space-y-1">
|
||
<li>Un plan Logto qui inclut cette fonctionnalité</li>
|
||
<li>L'activation des organisations dans votre console Logto</li>
|
||
<li>La configuration d'un template d'organisation</li>
|
||
</ul>
|
||
<p className="mt-2">
|
||
Consultez la{" "}
|
||
<a
|
||
href="https://docs.logto.io/docs/recipes/organizations/"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="underline hover:no-underline"
|
||
>
|
||
documentation Logto sur les organisations
|
||
</a>{" "}
|
||
pour plus d'informations.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Loading */}
|
||
{loading && (
|
||
<div className="mb-6 text-center">
|
||
<p className="text-gray-600">Chargement...</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Content based on active tab */}
|
||
{activeTab === "all" && (
|
||
<div>
|
||
<h2 className="mb-4 text-xl font-semibold text-gray-900">
|
||
Toutes les organisations ({organizations.length})
|
||
</h2>
|
||
{organizations.length === 0 ? (
|
||
<p className="text-gray-600">Aucune organisation trouvée.</p>
|
||
) : (
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{organizations.map(org => (
|
||
<div
|
||
key={org.id}
|
||
className="cursor-pointer rounded-lg border border-gray-200 p-4 hover:bg-gray-50"
|
||
onClick={() => fetchOrganizationDetails(org)}
|
||
>
|
||
<h3 className="font-medium text-gray-900">{org.name}</h3>
|
||
{org.description && <p className="mt-1 text-sm text-gray-600">{org.description}</p>}
|
||
<div className="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||
<span>ID: {org.id.substring(0, 8)}...</span>
|
||
{org.isMfaRequired && (
|
||
<span className="rounded bg-yellow-100 px-2 py-1 text-yellow-800">MFA</span>
|
||
)}
|
||
</div>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
Créée: {new Date(parseInt(org.createdAt)).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "mine" && (
|
||
<div>
|
||
<h2 className="mb-4 text-xl font-semibold text-gray-900">Mes organisations ({myOrganizations.length})</h2>
|
||
{!accessToken ? (
|
||
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-4">
|
||
<p className="text-sm text-yellow-700">
|
||
Veuillez vous connecter pour voir vos organisations.{" "}
|
||
<a href="/login" className="underline hover:no-underline">
|
||
Se connecter
|
||
</a>
|
||
</p>
|
||
</div>
|
||
) : myOrganizations.length === 0 ? (
|
||
<p className="text-gray-600">Vous n'êtes membre d'aucune organisation.</p>
|
||
) : (
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{myOrganizations.map(org => (
|
||
<div
|
||
key={org.id}
|
||
className="cursor-pointer rounded-lg border border-gray-200 p-4 hover:bg-gray-50"
|
||
onClick={() => fetchOrganizationDetails(org)}
|
||
>
|
||
<h3 className="font-medium text-gray-900">{org.name}</h3>
|
||
{org.description && <p className="mt-1 text-sm text-gray-600">{org.description}</p>}
|
||
<div className="mt-2 flex items-center justify-between text-xs text-gray-500">
|
||
<span>ID: {org.id.substring(0, 8)}...</span>
|
||
{org.isMfaRequired && (
|
||
<span className="rounded bg-yellow-100 px-2 py-1 text-yellow-800">MFA</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === "details" && selectedOrganization && (
|
||
<div>
|
||
<h2 className="mb-4 text-xl font-semibold text-gray-900">Détails: {selectedOrganization.name}</h2>
|
||
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
{/* Organization Users */}
|
||
<div className="rounded-lg border border-gray-200 p-4">
|
||
<h3 className="mb-3 font-medium text-gray-900">Utilisateurs ({organizationUsers.length})</h3>
|
||
{organizationUsers.length === 0 ? (
|
||
<p className="text-sm text-gray-600">Aucun utilisateur.</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{organizationUsers.map(user => (
|
||
<div key={user.id} className="rounded bg-gray-50 p-3">
|
||
<p className="text-sm font-medium">{user.name || user.username || "Utilisateur"}</p>
|
||
{user.primaryEmail && <p className="text-xs text-gray-600">{user.primaryEmail}</p>}
|
||
{user.organizationRoles.length > 0 && (
|
||
<div className="mt-1 flex flex-wrap gap-1">
|
||
{user.organizationRoles.map(role => (
|
||
<span key={role.id} className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800">
|
||
{role.name}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Organization Roles */}
|
||
<div className="rounded-lg border border-gray-200 p-4">
|
||
<h3 className="mb-3 font-medium text-gray-900">Rôles ({organizationRoles.length})</h3>
|
||
{organizationRoles.length === 0 ? (
|
||
<p className="text-sm text-gray-600">Aucun rôle défini.</p>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{organizationRoles.map(role => (
|
||
<div key={role.id} className="rounded bg-gray-50 p-3">
|
||
<p className="text-sm font-medium">{role.name}</p>
|
||
{role.description && <p className="text-xs text-gray-600">{role.description}</p>}
|
||
<span className="mt-1 inline-block rounded bg-gray-200 px-2 py-1 text-xs text-gray-700">
|
||
{role.type}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Organization Info */}
|
||
<div className="mt-6 rounded-lg border border-gray-200 p-4">
|
||
<h3 className="mb-3 font-medium text-gray-900">Informations</h3>
|
||
<dl className="grid grid-cols-1 gap-2 text-sm md:grid-cols-2">
|
||
<div>
|
||
<dt className="font-medium text-gray-700">ID:</dt>
|
||
<dd className="text-gray-600">{selectedOrganization.id}</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="font-medium text-gray-700">Nom:</dt>
|
||
<dd className="text-gray-600">{selectedOrganization.name}</dd>
|
||
</div>
|
||
{selectedOrganization.description && (
|
||
<div className="md:col-span-2">
|
||
<dt className="font-medium text-gray-700">Description:</dt>
|
||
<dd className="text-gray-600">{selectedOrganization.description}</dd>
|
||
</div>
|
||
)}
|
||
<div>
|
||
<dt className="font-medium text-gray-700">MFA requis:</dt>
|
||
<dd className="text-gray-600">{selectedOrganization.isMfaRequired ? "Oui" : "Non"}</dd>
|
||
</div>
|
||
<div>
|
||
<dt className="font-medium text-gray-700">Créée le:</dt>
|
||
<dd className="text-gray-600">
|
||
{new Date(parseInt(selectedOrganization.createdAt)).toLocaleString()}
|
||
</dd>
|
||
</div>
|
||
</dl>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Navigation Links */}
|
||
<div className="mt-8 rounded-md border border-blue-200 bg-blue-50 p-4">
|
||
<h3 className="text-sm font-medium text-blue-800">Navigation</h3>
|
||
<div className="mt-2 space-y-1">
|
||
<div>
|
||
<a href="/login" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||
🔐 Se connecter
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<a href="/profile" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||
👤 Profil utilisateur
|
||
</a>
|
||
</div>
|
||
<div>
|
||
<a href="/test-users" className="text-sm text-blue-600 underline hover:text-blue-500">
|
||
👥 Liste des utilisateurs
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|