xpeditis2.0/apps/frontend/src/components/organization/LicensesTab.tsx
2026-02-10 17:16:35 +01:00

367 lines
14 KiB
TypeScript

/**
* Licenses Tab Component
*
* Manages user licenses within the organization
*/
'use client';
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getSubscriptionOverview } from '@/lib/api/subscriptions';
import Link from 'next/link';
import { UserPlus } from 'lucide-react';
export default function LicensesTab() {
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const { data: subscription, isLoading } = useQuery({
queryKey: ['subscription'],
queryFn: getSubscriptionOverview,
});
if (isLoading) {
return (
<div className="space-y-4">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
<div className="h-64 bg-gray-200 rounded"></div>
</div>
</div>
);
}
const licenses = subscription?.licenses || [];
const activeLicenses = licenses.filter((l) => l.status === 'ACTIVE');
const revokedLicenses = licenses.filter((l) => l.status === 'REVOKED');
const usagePercentage = subscription
? subscription.maxLicenses === -1
? 0
: (subscription.usedLicenses / subscription.maxLicenses) * 100
: 0;
return (
<div className="space-y-6">
{/* Alerts */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
{error}
</div>
)}
{success && (
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-lg">
{success}
</div>
)}
{/* License Summary */}
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Résumé des licences</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-white rounded-lg p-4 border border-gray-200">
<p className="text-sm text-gray-500">Licences utilisées</p>
<p className="text-2xl font-bold text-gray-900">
{subscription?.usedLicenses || 0}
</p>
<p className="text-xs text-gray-400 mt-1">Hors ADMIN (illimité)</p>
</div>
<div className="bg-white rounded-lg p-4 border border-gray-200">
<p className="text-sm text-gray-500">Licences disponibles</p>
<p className="text-2xl font-bold text-gray-900">
{subscription?.availableLicenses === -1
? 'Illimité'
: subscription?.availableLicenses || 0}
</p>
</div>
<div className="bg-white rounded-lg p-4 border border-gray-200">
<p className="text-sm text-gray-500">Licences totales</p>
<p className="text-2xl font-bold text-gray-900">
{subscription?.maxLicenses === -1
? 'Illimité'
: subscription?.maxLicenses || 0}
</p>
</div>
</div>
{/* Usage Bar */}
{subscription && subscription.maxLicenses !== -1 && (
<div className="mt-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700">Utilisation</span>
<span className="text-sm text-gray-500">
{Math.round(usagePercentage)}%
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5">
<div
className={`h-2.5 rounded-full ${
usagePercentage >= 90
? 'bg-red-600'
: usagePercentage >= 70
? 'bg-yellow-500'
: 'bg-blue-600'
}`}
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
></div>
</div>
</div>
)}
</div>
{/* Active Licenses */}
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50 flex items-center justify-between">
<h3 className="text-lg font-medium text-gray-900">
Licences actives ({activeLicenses.length})
</h3>
<Link
href="/dashboard/settings/users"
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
<UserPlus className="w-4 h-4 mr-2" />
Inviter un utilisateur
</Link>
</div>
{activeLicenses.length === 0 ? (
<div className="px-6 py-8 text-center text-gray-500">
Aucune licence active
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Utilisateur
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Email
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Rôle
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Assignée le
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Licence
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{activeLicenses.map((license) => {
const isAdmin = license.userRole === 'ADMIN';
return (
<tr key={license.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{license.userName}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">{license.userEmail}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
isAdmin
? 'bg-purple-100 text-purple-800'
: 'bg-gray-100 text-gray-600'
}`}>
{license.userRole}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">
{new Date(license.assignedAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{isAdmin ? (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-amber-100 text-amber-800">
Illimité
</span>
) : (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
Active
</span>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
{/* Revoked Licenses (History) */}
{revokedLicenses.length > 0 && (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 className="text-lg font-medium text-gray-900">
Historique des licences révoquées ({revokedLicenses.length})
</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Utilisateur
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Email
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Rôle
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Assignée le
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Révoquée le
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Statut
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{revokedLicenses.map((license) => (
<tr key={license.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{license.userName}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">{license.userEmail}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`px-2 py-1 text-xs font-medium rounded-full ${
license.userRole === 'ADMIN'
? 'bg-purple-100 text-purple-800'
: 'bg-gray-100 text-gray-600'
}`}>
{license.userRole}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">
{new Date(license.assignedAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">
{license.revokedAt
? new Date(license.revokedAt).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})
: '-'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600">
Révoquée
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex">
<div className="flex-shrink-0">
<svg
className="h-5 w-5 text-blue-400"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-blue-800">
Comment fonctionnent les licences ?
</h3>
<div className="mt-2 text-sm text-blue-700">
<ul className="list-disc list-inside space-y-1">
<li>
Chaque utilisateur actif de votre organisation consomme une licence
</li>
<li>
Les licences sont automatiquement assignées lors de l&apos;ajout d&apos;un
utilisateur
</li>
<li>
Les licences sont libérées lorsqu&apos;un utilisateur est désactivé ou
supprimé
</li>
<li>
Pour ajouter plus d&apos;utilisateurs, passez à un plan supérieur dans
l&apos;onglet Abonnement
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
}