feat: v8 - skill itinova-user-management (3 profils admin/standard/readonly, logo FEHAP, login/email)
This commit is contained in:
@@ -121,19 +121,27 @@ function UsersPanel() {
|
||||
|
||||
// Formulaire de création
|
||||
const [createForm, setCreateForm] = useState({
|
||||
name: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
login: "",
|
||||
email: "",
|
||||
sonumRole: "referent" as SonumRole,
|
||||
role: "standard" as "admin" | "standard" | "readonly",
|
||||
isActive: true,
|
||||
password: "",
|
||||
showPassword: false,
|
||||
});
|
||||
|
||||
// Formulaire d'édition
|
||||
const [editForm, setEditForm] = useState<{
|
||||
name: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
login: string;
|
||||
email: string;
|
||||
sonumRole: SonumRole;
|
||||
}>({ name: "", email: "", sonumRole: "referent" });
|
||||
role: "admin" | "standard" | "readonly";
|
||||
isActive: boolean;
|
||||
}>({ firstName: "", lastName: "", login: "", email: "", sonumRole: "referent", role: "standard", isActive: true });
|
||||
|
||||
// Formulaire de réinitialisation de mot de passe
|
||||
const [resetPasswordForm, setResetPasswordForm] = useState({ userId: 0, password: "", show: false });
|
||||
@@ -146,7 +154,7 @@ function UsersPanel() {
|
||||
onSuccess: () => {
|
||||
toast.success("Utilisateur créé avec succès");
|
||||
setShowCreate(false);
|
||||
setCreateForm({ name: "", email: "", sonumRole: "referent", password: "", showPassword: false });
|
||||
setCreateForm({ firstName: "", lastName: "", login: "", email: "", sonumRole: "referent", role: "standard", isActive: true, password: "", showPassword: false });
|
||||
refetchAll();
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
@@ -187,7 +195,15 @@ function UsersPanel() {
|
||||
|
||||
const startEdit = (u: any) => {
|
||||
setEditingId(u.id);
|
||||
setEditForm({ name: u.name ?? "", email: u.email ?? "", sonumRole: u.sonumRole ?? "referent" });
|
||||
setEditForm({
|
||||
firstName: u.firstName ?? "",
|
||||
lastName: u.lastName ?? "",
|
||||
login: u.login ?? "",
|
||||
email: u.email ?? "",
|
||||
sonumRole: u.sonumRole ?? "referent",
|
||||
role: (u.role ?? "standard") as "admin" | "standard" | "readonly",
|
||||
isActive: u.isActive !== false,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleAffectation = (userId: number, etablissementId: number, currentIds: number[]) => {
|
||||
@@ -219,12 +235,30 @@ function UsersPanel() {
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1.5">Nom complet *</label>
|
||||
<label className="block text-xs font-medium text-foreground mb-1.5">Prénom *</label>
|
||||
<input
|
||||
value={createForm.name}
|
||||
onChange={(e) => setCreateForm((f) => ({ ...f, name: e.target.value }))}
|
||||
value={createForm.firstName}
|
||||
onChange={(e) => setCreateForm((f) => ({ ...f, firstName: e.target.value }))}
|
||||
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||
placeholder="Prénom Nom"
|
||||
placeholder="Prénom"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1.5">Nom *</label>
|
||||
<input
|
||||
value={createForm.lastName}
|
||||
onChange={(e) => setCreateForm((f) => ({ ...f, lastName: e.target.value }))}
|
||||
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||
placeholder="Nom de famille"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-foreground mb-1.5">Login (optionnel)</label>
|
||||
<input
|
||||
value={createForm.login}
|
||||
onChange={(e) => setCreateForm((f) => ({ ...f, login: e.target.value }))}
|
||||
className="w-full px-3 py-2 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||
placeholder="identifiant court (ex: jdupont)"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -279,8 +313,8 @@ function UsersPanel() {
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
onClick={() => createMutation.mutate(createForm)}
|
||||
disabled={!createForm.name.trim() || !createForm.email.trim() || createForm.password.length < 8 || createMutation.isPending}
|
||||
onClick={() => createMutation.mutate({ ...createForm, login: createForm.login || undefined })}
|
||||
disabled={!createForm.firstName.trim() || !createForm.lastName.trim() || !createForm.email.trim() || createForm.password.length < 8 || createMutation.isPending}
|
||||
className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium bg-primary text-white rounded-lg hover:bg-primary/90 disabled:opacity-50 shadow-sm"
|
||||
>
|
||||
<Check size={13} />
|
||||
@@ -328,17 +362,29 @@ function UsersPanel() {
|
||||
<tr className="hover:bg-muted/20 transition-colors">
|
||||
<td className="px-5 py-3.5">
|
||||
{isEditing ? (
|
||||
<input
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, name: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm bg-background border border-primary/40 rounded-md focus:outline-none focus:ring-1 focus:ring-primary/30"
|
||||
/>
|
||||
<div className="flex gap-1">
|
||||
<input
|
||||
value={editForm.firstName}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, firstName: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm bg-background border border-primary/40 rounded-md focus:outline-none focus:ring-1 focus:ring-primary/30"
|
||||
placeholder="Prénom"
|
||||
/>
|
||||
<input
|
||||
value={editForm.lastName}
|
||||
onChange={(e) => setEditForm((f) => ({ ...f, lastName: e.target.value }))}
|
||||
className="w-full px-2 py-1 text-sm bg-background border border-primary/40 rounded-md focus:outline-none focus:ring-1 focus:ring-primary/30"
|
||||
placeholder="Nom"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center text-xs font-semibold text-primary flex-shrink-0">
|
||||
{u.name?.charAt(0)?.toUpperCase() ?? "U"}
|
||||
</div>
|
||||
<span className="font-medium text-foreground">{u.name ?? "—"}</span>
|
||||
<div>
|
||||
<span className="font-medium text-foreground">{u.name ?? "—"}</span>
|
||||
{u.login && <span className="block text-xs text-muted-foreground">@{u.login}</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
@@ -395,7 +441,7 @@ function UsersPanel() {
|
||||
{isEditing ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => updateMutation.mutate({ userId: u.id, ...editForm })}
|
||||
onClick={() => updateMutation.mutate({ userId: u.id, ...editForm, login: editForm.login || undefined })}
|
||||
disabled={updateMutation.isPending}
|
||||
className="p-1.5 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
|
||||
title="Enregistrer"
|
||||
@@ -1116,8 +1162,13 @@ function ImportContactsPanel() {
|
||||
const errs: string[] = [];
|
||||
for (const row of rows) {
|
||||
try {
|
||||
// Décomposer nom en prénom/nom
|
||||
const parts = (row.nom ?? "").trim().split(" ");
|
||||
const firstName = parts[0] ?? "Inconnu";
|
||||
const lastName = parts.slice(1).join(" ") || "";
|
||||
await createUserMutation.mutateAsync({
|
||||
name: row.nom,
|
||||
firstName,
|
||||
lastName: lastName || firstName,
|
||||
email: row.email,
|
||||
sonumRole: row.sonumRole,
|
||||
password: row.password ?? "Sonum2024!",
|
||||
|
||||
@@ -1,87 +1,135 @@
|
||||
import { getLoginUrl } from "@/const";
|
||||
import { useLocation } from "wouter";
|
||||
import { Building2, KeyRound, ExternalLink } from "lucide-react";
|
||||
import { KeyRound, ExternalLink } from "lucide-react";
|
||||
|
||||
const FEHAP_LOGO = "/manus-storage/logoFEHAP_69ddd0ee.PNG";
|
||||
const SANTINOVA_LOGO_TEXT = "Santinova Soft";
|
||||
|
||||
export default function Login() {
|
||||
const [, navigate] = useLocation();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* En-tête */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="inline-flex items-center gap-3 mb-5">
|
||||
<div className="w-14 h-14 rounded-2xl bg-primary flex items-center justify-center shadow-lg">
|
||||
<Building2 size={26} className="text-white" />
|
||||
<div className="min-h-screen flex bg-background">
|
||||
{/* ── Colonne gauche : branding SONUM ── */}
|
||||
<div className="hidden lg:flex flex-col justify-between w-1/2 bg-primary px-14 py-12">
|
||||
<div>
|
||||
{/* Logo FEHAP */}
|
||||
<img
|
||||
src={FEHAP_LOGO}
|
||||
alt="FEHAP – Santé Social, Privé Solidaire"
|
||||
className="h-16 object-contain bg-white rounded-xl px-3 py-2 shadow"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-white">
|
||||
<h1 className="text-5xl font-bold mb-4" style={{ fontFamily: "'Playfair Display', serif" }}>
|
||||
SONUM
|
||||
</h1>
|
||||
<p className="text-lg text-white/80 max-w-sm leading-relaxed">
|
||||
Cartographie des Solutions Numériques des établissements FEHAP
|
||||
</p>
|
||||
<div className="mt-8 space-y-3 text-sm text-white/70">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white/50" />
|
||||
Référencement des logiciels métiers
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-widest">FEHAP</div>
|
||||
<div
|
||||
className="text-3xl font-bold text-primary leading-tight"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}
|
||||
>
|
||||
SONUM
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white/50" />
|
||||
Cartographie par établissement et région
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white/50" />
|
||||
Mise en relation entre adhérents
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Bienvenue</h1>
|
||||
<p className="text-sm text-muted-foreground mt-2 max-w-xs mx-auto">
|
||||
Cartographie des Solutions Numériques des établissements FEHAP
|
||||
</div>
|
||||
|
||||
<div className="text-white/40 text-xs">
|
||||
© {new Date().getFullYear()} FEHAP — Tous droits réservés
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Colonne droite : formulaire de connexion ── */}
|
||||
<div className="flex flex-col justify-between flex-1 px-8 py-12 lg:px-16">
|
||||
{/* Logo FEHAP en haut (mobile + desktop) */}
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<img
|
||||
src={FEHAP_LOGO}
|
||||
alt="FEHAP"
|
||||
className="h-12 object-contain bg-white rounded-lg px-2 py-1 shadow-sm border border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Formulaire centré */}
|
||||
<div className="w-full max-w-sm mx-auto">
|
||||
{/* Titre mobile */}
|
||||
<div className="text-center mb-8 lg:hidden">
|
||||
<h1 className="text-3xl font-bold text-primary" style={{ fontFamily: "'Playfair Display', serif" }}>
|
||||
SONUM
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Cartographie des Solutions Numériques
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-2">Connexion</h2>
|
||||
<p className="text-sm text-muted-foreground mb-8">
|
||||
Choisissez votre mode d'authentification
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Connexion via espace adhérent FEHAP */}
|
||||
<a
|
||||
href={getLoginUrl()}
|
||||
className="group flex items-center gap-4 p-5 bg-primary text-white rounded-2xl shadow-md hover:bg-primary/90 transition-all hover:shadow-lg hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center flex-shrink-0">
|
||||
<ExternalLink size={20} className="text-white" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-semibold text-base">Espace adhérent FEHAP</div>
|
||||
<div className="text-sm text-white/75 mt-0.5">
|
||||
Connexion via votre compte FEHAP existant
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-white/50 group-hover:text-white/80 transition-colors text-lg">→</span>
|
||||
</a>
|
||||
|
||||
{/* Séparateur */}
|
||||
<div className="flex items-center gap-3 py-1">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-xs text-muted-foreground font-medium">ou</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
{/* Connexion locale */}
|
||||
<button
|
||||
onClick={() => navigate("/login/local")}
|
||||
className="group w-full flex items-center gap-4 p-5 bg-card border border-border rounded-2xl shadow-sm hover:border-primary/40 hover:shadow-md transition-all hover:-translate-y-0.5 text-left"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<KeyRound size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-base text-foreground">Connexion locale</div>
|
||||
<div className="text-sm text-muted-foreground mt-0.5">
|
||||
Identifiant / email et mot de passe
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-muted-foreground group-hover:text-primary transition-colors text-lg">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-muted-foreground mt-8">
|
||||
En vous connectant, vous acceptez les conditions générales d'utilisation de la plateforme SONUM.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Options de connexion */}
|
||||
<div className="space-y-4">
|
||||
{/* Connexion via espace adhérent FEHAP */}
|
||||
<a
|
||||
href={getLoginUrl()}
|
||||
className="group flex items-center gap-4 p-5 bg-primary text-white rounded-2xl shadow-md hover:bg-primary/90 transition-all hover:shadow-lg hover:-translate-y-0.5"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-white/20 flex items-center justify-center flex-shrink-0">
|
||||
<ExternalLink size={20} className="text-white" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-semibold text-base">Espace adhérent FEHAP</div>
|
||||
<div className="text-sm text-white/75 mt-0.5">
|
||||
Connexion via votre compte FEHAP existant
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white/50 group-hover:text-white/80 transition-colors">
|
||||
→
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{/* Séparateur */}
|
||||
<div className="flex items-center gap-3 py-1">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-xs text-muted-foreground font-medium">ou</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
{/* Connexion locale */}
|
||||
<button
|
||||
onClick={() => navigate("/login/local")}
|
||||
className="group w-full flex items-center gap-4 p-5 bg-card border border-border rounded-2xl shadow-sm hover:border-primary/40 hover:shadow-md transition-all hover:-translate-y-0.5 text-left"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-xl bg-muted flex items-center justify-center flex-shrink-0">
|
||||
<KeyRound size={20} className="text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-base text-foreground">Connexion locale</div>
|
||||
<div className="text-sm text-muted-foreground mt-0.5">
|
||||
Email et mot de passe fournis par un gestionnaire SONUM
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground group-hover:text-primary transition-colors">
|
||||
→
|
||||
</div>
|
||||
</button>
|
||||
{/* Pied de page : powered by Santinova */}
|
||||
<div className="flex justify-center lg:justify-end items-center gap-2 mt-8">
|
||||
<span className="text-xs text-muted-foreground">powered by</span>
|
||||
<span className="text-xs font-semibold text-foreground/70">{SANTINOVA_LOGO_TEXT}</span>
|
||||
</div>
|
||||
|
||||
{/* Pied de page */}
|
||||
<p className="text-center text-xs text-muted-foreground mt-8">
|
||||
En vous connectant, vous acceptez les conditions générales d'utilisation de la plateforme SONUM.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,14 @@ import { getLoginUrl } from "@/const";
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { toast } from "sonner";
|
||||
import { Eye, EyeOff, Lock, Mail, ArrowLeft, ExternalLink } from "lucide-react";
|
||||
import { Eye, EyeOff, Lock, User, ArrowLeft, ExternalLink } from "lucide-react";
|
||||
|
||||
const FEHAP_LOGO = "/manus-storage/logoFEHAP_69ddd0ee.PNG";
|
||||
const SANTINOVA_LOGO_TEXT = "Santinova Soft";
|
||||
|
||||
export default function LoginLocal() {
|
||||
const [, navigate] = useLocation();
|
||||
const [email, setEmail] = useState("");
|
||||
const [loginOrEmail, setLoginOrEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
@@ -17,133 +20,163 @@ export default function LoginLocal() {
|
||||
window.location.href = "/";
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message || "Email ou mot de passe incorrect");
|
||||
toast.error(err.message || "Identifiant ou mot de passe incorrect");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!email || !password) {
|
||||
toast.error("Veuillez renseigner votre email et votre mot de passe");
|
||||
if (!loginOrEmail || !password) {
|
||||
toast.error("Veuillez renseigner votre identifiant et votre mot de passe");
|
||||
return;
|
||||
}
|
||||
loginMutation.mutate({ email, password });
|
||||
loginMutation.mutate({ email: loginOrEmail, password });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center gap-3 mb-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-primary flex items-center justify-center shadow-md">
|
||||
<Lock size={22} className="text-white" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-widest">FEHAP</div>
|
||||
<div
|
||||
className="text-2xl font-bold text-primary leading-tight"
|
||||
style={{ fontFamily: "'Playfair Display', serif" }}
|
||||
>
|
||||
SONUM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-xl font-semibold text-foreground">Connexion locale</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Connectez-vous avec votre email et votre mot de passe
|
||||
<div className="min-h-screen flex bg-background">
|
||||
{/* ── Colonne gauche : branding SONUM ── */}
|
||||
<div className="hidden lg:flex flex-col justify-between w-1/2 bg-primary px-14 py-12">
|
||||
<div>
|
||||
<img
|
||||
src={FEHAP_LOGO}
|
||||
alt="FEHAP – Santé Social, Privé Solidaire"
|
||||
className="h-16 object-contain bg-white rounded-xl px-3 py-2 shadow"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-white">
|
||||
<h1 className="text-5xl font-bold mb-4" style={{ fontFamily: "'Playfair Display', serif" }}>
|
||||
SONUM
|
||||
</h1>
|
||||
<p className="text-lg text-white/80 max-w-sm leading-relaxed">
|
||||
Cartographie des Solutions Numériques des établissements FEHAP
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-white/40 text-xs">
|
||||
© {new Date().getFullYear()} FEHAP — Tous droits réservés
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulaire */}
|
||||
<div className="bg-card rounded-2xl border border-border shadow-sm p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
Adresse email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="prenom.nom@etablissement.fr"
|
||||
autoComplete="email"
|
||||
className="w-full pl-10 pr-4 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mot de passe */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
className="w-full pl-10 pr-10 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bouton connexion */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending}
|
||||
className="w-full py-2.5 px-4 bg-primary text-white rounded-lg font-medium text-sm hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{loginMutation.isPending ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
Connexion en cours...
|
||||
</>
|
||||
) : (
|
||||
"Se connecter"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
{/* ── Colonne droite : formulaire ── */}
|
||||
<div className="flex flex-col justify-between flex-1 px-8 py-12 lg:px-16">
|
||||
{/* Logo FEHAP en haut */}
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<img
|
||||
src={FEHAP_LOGO}
|
||||
alt="FEHAP"
|
||||
className="h-12 object-contain bg-white rounded-lg px-2 py-1 shadow-sm border border-border"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Liens */}
|
||||
<div className="mt-6 space-y-3 text-center">
|
||||
<button
|
||||
onClick={() => navigate("/login")}
|
||||
className="flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mx-auto"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Retour aux options de connexion
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-xs text-muted-foreground">ou</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
{/* Formulaire centré */}
|
||||
<div className="w-full max-w-sm mx-auto">
|
||||
{/* Titre mobile */}
|
||||
<div className="text-center mb-8 lg:hidden">
|
||||
<h1 className="text-3xl font-bold text-primary" style={{ fontFamily: "'Playfair Display', serif" }}>
|
||||
SONUM
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={getLoginUrl()}
|
||||
className="flex items-center justify-center gap-2 text-sm text-primary hover:text-primary/80 font-medium transition-colors"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
Se connecter via l'espace adhérent FEHAP
|
||||
</a>
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-2">Connexion locale</h2>
|
||||
<p className="text-sm text-muted-foreground mb-8">
|
||||
Connectez-vous avec votre identifiant (login ou email) et votre mot de passe
|
||||
</p>
|
||||
|
||||
<div className="bg-card rounded-2xl border border-border shadow-sm p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* Login ou email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
Identifiant ou email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={loginOrEmail}
|
||||
onChange={(e) => setLoginOrEmail(e.target.value)}
|
||||
placeholder="jdupont ou prenom.nom@etablissement.fr"
|
||||
autoComplete="username"
|
||||
className="w-full pl-10 pr-4 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mot de passe */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-1.5">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock size={16} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
className="w-full pl-10 pr-10 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-all"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bouton connexion */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending}
|
||||
className="w-full py-2.5 px-4 bg-primary text-white rounded-lg font-medium text-sm hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{loginMutation.isPending ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
Connexion en cours...
|
||||
</>
|
||||
) : (
|
||||
"Se connecter"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Liens */}
|
||||
<div className="mt-6 space-y-3 text-center">
|
||||
<button
|
||||
onClick={() => navigate("/login")}
|
||||
className="flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mx-auto"
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Retour aux options de connexion
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-xs text-muted-foreground">ou</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={getLoginUrl()}
|
||||
className="flex items-center justify-center gap-2 text-sm text-primary hover:text-primary/80 font-medium transition-colors"
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
Se connecter via l'espace adhérent FEHAP
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pied de page : powered by Santinova */}
|
||||
<div className="flex justify-center lg:justify-end items-center gap-2 mt-8">
|
||||
<span className="text-xs text-muted-foreground">powered by</span>
|
||||
<span className="text-xs font-semibold text-foreground/70">{SANTINOVA_LOGO_TEXT}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user