Files
gestion_projet/src/pages/Admin.tsx
T
Mathew b054e36df0 Panel admin, planning, tags, suivi du temps, thèmes clair/sombre
- Admin: création/suppression de comptes + rôles via Edge Function sécurisée
- Config Supabase intégrée au build (.env), écran Setup supprimé
- Page Planning (vue mois/semaine)
- Tags "jobs" sur les tâches + filtres (membre / tag)
- Suivi du temps par tâche, total par jalon
- Toasts + confirmations in-app (fini alert/confirm natifs)
- Thème Notion clair + mode sombre (accent monochrome, bascule persistée)
- .env suivi par git (public uniquement) pour synchro multi-machines

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:51:40 +02:00

275 lines
9.2 KiB
TypeScript

import { useEffect, useState } from 'react'
import { ShieldCheck, UserPlus, Trash2, Loader2, Crown, Mail } from 'lucide-react'
import { useApp } from '../lib/AppContext'
import { useFeedback } from '../lib/feedback'
import { MemberRole, ROLE_LABELS } from '../lib/types'
import {
AdminUser,
adminListUsers,
adminCreateUser,
adminDeleteUser,
adminUpdateUser
} from '../lib/admin'
import { Modal, Select, EmptyState } from '../components/ui'
import PageHeader from '../components/PageHeader'
export default function Admin(): JSX.Element {
const { profile } = useApp()
const { toast, confirm } = useFeedback()
const [users, setUsers] = useState<AdminUser[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [creating, setCreating] = useState(false)
const load = async (): Promise<void> => {
setLoading(true)
setError('')
try {
setUsers(await adminListUsers())
} catch (e) {
setError((e as Error).message)
} finally {
setLoading(false)
}
}
useEffect(() => {
load()
}, [])
const setRole = async (u: AdminUser, role: MemberRole): Promise<void> => {
setUsers((list) => list.map((x) => (x.id === u.id ? { ...x, role } : x)))
try {
await adminUpdateUser(u.id, { role })
} catch (e) {
setError((e as Error).message)
load()
}
}
const toggleAdmin = async (u: AdminUser): Promise<void> => {
try {
await adminUpdateUser(u.id, { is_admin: !u.is_admin })
setUsers((list) => list.map((x) => (x.id === u.id ? { ...x, is_admin: !u.is_admin } : x)))
} catch (e) {
setError((e as Error).message)
}
}
const remove = async (u: AdminUser): Promise<void> => {
const ok = await confirm({
title: 'Supprimer ce compte ?',
message: `${u.full_name} (${u.email}) sera définitivement supprimé.`,
danger: true,
confirmLabel: 'Supprimer'
})
if (!ok) return
try {
await adminDeleteUser(u.id)
setUsers((list) => list.filter((x) => x.id !== u.id))
toast('Compte supprimé.', 'success')
} catch (e) {
setError((e as Error).message)
}
}
return (
<div className="flex h-full flex-col">
<PageHeader title="Administration" subtitle={`${users.length} compte(s)`}>
<button className="btn-primary" onClick={() => setCreating(true)}>
<UserPlus size={16} /> Créer un compte
</button>
</PageHeader>
<div className="flex-1 overflow-y-auto p-6">
{error && (
<div className="mb-4 rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-sm text-rose-600">
{error}
</div>
)}
{loading ? (
<div className="flex justify-center py-16 text-muted">
<Loader2 className="animate-spin" size={28} />
</div>
) : users.length === 0 ? (
<EmptyState icon={<ShieldCheck size={40} />} text="Aucun compte." />
) : (
<div className="overflow-hidden rounded-xl border border-border">
<table className="w-full text-sm">
<thead className="bg-panel2 text-left text-xs uppercase text-subtle">
<tr>
<th className="px-4 py-3">Membre</th>
<th className="px-4 py-3">Rôle</th>
<th className="px-4 py-3">Admin</th>
<th className="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody>
{users.map((u) => {
const isMe = u.id === profile?.id
return (
<tr key={u.id} className="border-t border-border">
<td className="px-4 py-3">
<div className="font-medium text-ink">
{u.full_name}
{isMe && <span className="ml-1 text-xs text-accent2">(moi)</span>}
</div>
<div className="flex items-center gap-1 text-xs text-subtle">
<Mail size={11} /> {u.email}
</div>
</td>
<td className="px-4 py-3">
<Select
className="!py-1 text-xs"
value={u.role}
onChange={(e) => setRole(u, e.target.value as MemberRole)}
>
{Object.entries(ROLE_LABELS).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</Select>
</td>
<td className="px-4 py-3">
<button
onClick={() => toggleAdmin(u)}
disabled={isMe}
title={isMe ? 'Tu ne peux pas retirer ton propre statut' : 'Basculer admin'}
className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs ${
u.is_admin
? 'bg-amber-100 text-amber-700'
: 'bg-panel2 text-muted hover:text-ink'
} ${isMe ? 'cursor-not-allowed opacity-60' : ''}`}
>
<Crown size={12} /> {u.is_admin ? 'Admin' : 'Membre'}
</button>
</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => remove(u)}
disabled={isMe}
title={isMe ? 'Impossible de supprimer son propre compte' : 'Supprimer'}
className={`text-muted hover:text-rose-600 ${
isMe ? 'cursor-not-allowed opacity-40' : ''
}`}
>
<Trash2 size={16} />
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
{creating && (
<CreateUserModal
onClose={() => setCreating(false)}
onCreated={async () => {
setCreating(false)
await load()
toast('Compte créé.', 'success')
}}
/>
)}
</div>
)
}
function CreateUserModal({
onClose,
onCreated
}: {
onClose: () => void
onCreated: () => Promise<void>
}): JSX.Element {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [fullName, setFullName] = useState('')
const [role, setRole] = useState<MemberRole>('programmer')
const [isAdmin, setIsAdmin] = useState(false)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const submit = async (): Promise<void> => {
setError('')
if (!email.trim() || password.length < 6) {
setError('Email valide et mot de passe (6+ caractères) requis.')
return
}
setBusy(true)
try {
await adminCreateUser({
email: email.trim(),
password,
full_name: fullName.trim(),
role,
is_admin: isAdmin
})
await onCreated()
} catch (e) {
setError((e as Error).message)
setBusy(false)
}
}
return (
<Modal title="Créer un compte" onClose={onClose}>
<label className="label">Nom complet</label>
<input
className="input mb-3"
placeholder="Alex Martin"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<label className="label">Email</label>
<input
className="input mb-3"
type="email"
placeholder="alex@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label className="label">Mot de passe provisoire</label>
<input
className="input mb-3"
type="text"
placeholder="au moins 6 caractères"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<label className="label">Rôle</label>
<Select className="mb-3" value={role} onChange={(e) => setRole(e.target.value as MemberRole)}>
{Object.entries(ROLE_LABELS).map(([v, l]) => (
<option key={v} value={v}>
{l}
</option>
))}
</Select>
<label className="mb-4 flex cursor-pointer items-center gap-2 text-sm text-gray-600">
<input
type="checkbox"
checked={isAdmin}
onChange={(e) => setIsAdmin(e.target.checked)}
className="h-4 w-4 accent-accent"
/>
Donner les droits administrateur
</label>
{error && <p className="mb-3 text-sm text-rose-600">{error}</p>}
<button className="btn-primary w-full" onClick={submit} disabled={busy}>
{busy && <Loader2 className="animate-spin" size={16} />}
Créer le compte
</button>
<p className="mt-3 text-center text-xs text-subtle">
Communique l'email et le mot de passe provisoire au membre.
</p>
</Modal>
)
}