feat: ajout images:delete dans scopes, édition client dans ClientDetail
This commit is contained in:
@@ -1,18 +1,49 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { PageLayout } from '@/components/layout/PageLayout';
|
import { PageLayout } from '@/components/layout/PageLayout';
|
||||||
import { PlanBadge } from '@/components/shared/PlanBadge';
|
import { PlanBadge } from '@/components/shared/PlanBadge';
|
||||||
import { QuotaBar } from '@/components/shared/QuotaBar';
|
import { QuotaBar } from '@/components/shared/QuotaBar';
|
||||||
import { StatusBadge } from '@/components/shared/StatusBadge';
|
import { StatusBadge } from '@/components/shared/StatusBadge';
|
||||||
import { CardSkeleton } from '@/components/shared/Skeleton';
|
import { CardSkeleton } from '@/components/shared/Skeleton';
|
||||||
import { useClient } from '@/hooks/useClients';
|
import { toast } from '@/components/shared/Toast';
|
||||||
|
import { useClient, useUpdateClient } from '@/hooks/useClients';
|
||||||
import { useImages } from '@/hooks/useImages';
|
import { useImages } from '@/hooks/useImages';
|
||||||
import { formatDate, formatTokens, formatMB } from '@/utils/formatters';
|
import { formatDate, formatTokens, formatMB } from '@/utils/formatters';
|
||||||
import { ArrowLeft, Images, ExternalLink } from 'lucide-react';
|
import { PLANS, SCOPES } from '@/utils/constants';
|
||||||
|
import { ArrowLeft, Images, ExternalLink, Edit3, X, Loader2, Save } from 'lucide-react';
|
||||||
|
import type { Plan, APIClientUpdate } from '@/types/api.types';
|
||||||
|
|
||||||
export default function ClientDetail() {
|
export default function ClientDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const { data: client, isLoading } = useClient(id);
|
const { data: client, isLoading } = useClient(id);
|
||||||
const { data: clientImages } = useImages({ client_id: id, page_size: 20, sort_order: 'desc' });
|
const { data: clientImages } = useImages({ client_id: id, page_size: 20, sort_order: 'desc' });
|
||||||
|
const updateClientMut = useUpdateClient();
|
||||||
|
|
||||||
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
|
const [editScopes, setEditScopes] = useState<string[]>([]);
|
||||||
|
const [editPlan, setEditPlan] = useState<Plan>('free');
|
||||||
|
|
||||||
|
const openEdit = () => {
|
||||||
|
if (!client) return;
|
||||||
|
setEditScopes([...client.scopes]);
|
||||||
|
setEditPlan(client.plan);
|
||||||
|
setShowEdit(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!client) return;
|
||||||
|
const data: APIClientUpdate = {
|
||||||
|
scopes: editScopes,
|
||||||
|
plan: editPlan,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await updateClientMut.mutateAsync({ id: client.id, data });
|
||||||
|
toast('success', 'Client mis à jour');
|
||||||
|
setShowEdit(false);
|
||||||
|
} catch {
|
||||||
|
toast('error', 'Erreur lors de la mise à jour');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +77,9 @@ export default function ClientDetail() {
|
|||||||
<span className={`badge ${client.is_active ? 'bg-status-ok/20 text-status-ok' : 'bg-content-muted/20 text-content-muted'}`}>
|
<span className={`badge ${client.is_active ? 'bg-status-ok/20 text-status-ok' : 'bg-content-muted/20 text-content-muted'}`}>
|
||||||
{client.is_active ? 'Actif' : 'Inactif'}
|
{client.is_active ? 'Actif' : 'Inactif'}
|
||||||
</span>
|
</span>
|
||||||
|
<button onClick={openEdit} className="btn-ghost text-sm flex items-center gap-1 text-brand-400 hover:text-brand-300">
|
||||||
|
<Edit3 size={14} /> Éditer
|
||||||
|
</button>
|
||||||
<Link to="/admin/clients" className="btn-ghost text-sm flex items-center gap-1">
|
<Link to="/admin/clients" className="btn-ghost text-sm flex items-center gap-1">
|
||||||
<ArrowLeft size={14} /> Retour
|
<ArrowLeft size={14} /> Retour
|
||||||
</Link>
|
</Link>
|
||||||
@@ -82,7 +116,12 @@ export default function ClientDetail() {
|
|||||||
|
|
||||||
{/* Scopes */}
|
{/* Scopes */}
|
||||||
<div className="card mb-8">
|
<div className="card mb-8">
|
||||||
<h3 className="text-sm font-semibold text-content-secondary mb-3">Scopes accordés</h3>
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-sm font-semibold text-content-secondary">Scopes accordés</h3>
|
||||||
|
<button onClick={openEdit} className="text-xs text-brand-400 hover:text-brand-300 flex items-center gap-1">
|
||||||
|
<Edit3 size={12} /> Modifier
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{client.scopes.map((scope) => (
|
{client.scopes.map((scope) => (
|
||||||
<span key={scope} className="badge bg-brand-500/10 text-brand-400 font-mono text-xs">
|
<span key={scope} className="badge bg-brand-500/10 text-brand-400 font-mono text-xs">
|
||||||
@@ -137,6 +176,65 @@ export default function ClientDetail() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Edit modal */}
|
||||||
|
{showEdit && (
|
||||||
|
<div className="fixed inset-0 z-50 flex justify-end">
|
||||||
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowEdit(false)} />
|
||||||
|
<div className="relative w-full max-w-md bg-surface-raised border-l border-border h-full overflow-y-auto animate-slide-in">
|
||||||
|
<div className="sticky top-0 bg-surface-raised border-b border-border p-4 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold">Éditer {client.name}</h2>
|
||||||
|
<button onClick={() => setShowEdit(false)} className="p-1 rounded hover:bg-surface-overlay text-content-muted">
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-content-secondary mb-1">Plan</label>
|
||||||
|
<select
|
||||||
|
value={editPlan}
|
||||||
|
onChange={(e) => setEditPlan(e.target.value as Plan)}
|
||||||
|
className="input-field w-full"
|
||||||
|
>
|
||||||
|
<option value="free">Free</option>
|
||||||
|
<option value="standard">Standard</option>
|
||||||
|
<option value="premium">Premium</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-content-muted mt-1">
|
||||||
|
{PLANS[editPlan].images === Infinity ? '∞' : PLANS[editPlan].images} images · {formatMB(PLANS[editPlan].storageMb)} · {formatTokens(PLANS[editPlan].aiTokens)} tokens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-content-secondary mb-1">Scopes</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{SCOPES.map((scope) => (
|
||||||
|
<label key={scope} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editScopes.includes(scope)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) setEditScopes([...editScopes, scope]);
|
||||||
|
else setEditScopes(editScopes.filter((s) => s !== scope));
|
||||||
|
}}
|
||||||
|
className="rounded border-border bg-surface text-brand-500 focus:ring-brand-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-content-secondary font-mono">{scope}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={updateClientMut.isPending}
|
||||||
|
className="btn-primary w-full flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{updateClientMut.isPending ? <Loader2 size={16} className="animate-spin" /> : <Save size={16} />}
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const PLANS = {
|
|||||||
export const SCOPES = [
|
export const SCOPES = [
|
||||||
'images:read',
|
'images:read',
|
||||||
'images:write',
|
'images:write',
|
||||||
|
'images:delete',
|
||||||
'ai:use',
|
'ai:use',
|
||||||
'admin',
|
'admin',
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user