UX améliorations
This commit is contained in:
@@ -43,7 +43,7 @@ function ProfileSelect({ label, options, value, onChange }) {
|
||||
|
||||
return (
|
||||
<div ref={ref} className="profile-field">
|
||||
<span className="profile-label">{label}</span>
|
||||
<label>{label}</label>
|
||||
<div className={`profile-select-trigger${open ? ' open' : ''}`}
|
||||
onClick={() => setOpen(o => !o)} role="button" tabIndex={0}
|
||||
onKeyDown={e => e.key === 'Enter' && setOpen(o => !o)}>
|
||||
@@ -132,25 +132,31 @@ function EmailChangeForm({ onDone }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={submit} style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<form onSubmit={submit} style={{
|
||||
marginTop: 16, paddingTop: 16, borderTop: '1px solid var(--border)',
|
||||
display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400,
|
||||
}}>
|
||||
<p style={{ margin: 0, fontSize: 13, color: 'var(--text-muted)' }}>
|
||||
Après le changement, un email de vérification sera envoyé à la nouvelle adresse.
|
||||
</p>
|
||||
{err && <div className="error">{err}</div>}
|
||||
{msg && <div className="success-msg">{msg}</div>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label className="profile-label">Nouvelle adresse email</label>
|
||||
<input className="profile-input" type="email" required value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="nouvelle@email.com" />
|
||||
<div>
|
||||
<label>Nouvelle adresse email</label>
|
||||
<input type="email" required value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="nouvelle@email.com" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<label className="profile-label">Mot de passe actuel (confirmation)</label>
|
||||
<PasswordInput className="profile-input" required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
|
||||
<div>
|
||||
<label>Mot de passe actuel (confirmation)</label>
|
||||
<PasswordInput required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button type="submit" className="btn btn-primary" disabled={busy} style={{ fontSize: 13 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 12 }}>
|
||||
<button type="submit" className="primary" disabled={busy}>
|
||||
{busy ? 'Enregistrement…' : 'Confirmer le changement'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-outline" onClick={() => onDone()} style={{ fontSize: 13 }}>Annuler</button>
|
||||
<button type="button" onClick={() => onDone()}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -191,64 +197,73 @@ function AccountForm() {
|
||||
const handleBlur = () => save();
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
|
||||
<>
|
||||
{/* ── Mon profil ──────────────────────────────────────── */}
|
||||
<section className="profile-section">
|
||||
<h2 className="profile-section-title">Mon profil</h2>
|
||||
<div className="card">
|
||||
<h3 style={{ margin: '0 0 4px' }}>Mon profil</h3>
|
||||
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
|
||||
Vos informations personnelles et votre adresse email de connexion.
|
||||
</p>
|
||||
|
||||
{infoErr && <div className="error" style={{ marginBottom: 12 }}>{infoErr}</div>}
|
||||
{infoMsg && <div className="success-msg" style={{ marginBottom: 12 }}>{infoMsg}</div>}
|
||||
|
||||
<div className="profile-grid-2">
|
||||
<div className="profile-field">
|
||||
<span className="profile-label">Prénom</span>
|
||||
<input className="profile-input" value={prenom}
|
||||
onChange={e => setPrenom(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder="Prénom" />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
||||
<div>
|
||||
<label>Prénom</label>
|
||||
<input value={prenom}
|
||||
onChange={e => setPrenom(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder="Prénom" />
|
||||
</div>
|
||||
<div>
|
||||
<label>Nom</label>
|
||||
<input value={nom}
|
||||
onChange={e => setNom(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder="NOM" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="profile-field">
|
||||
<span className="profile-label">Nom</span>
|
||||
<input className="profile-input" value={nom}
|
||||
onChange={e => setNom(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
placeholder="NOM" />
|
||||
|
||||
<div>
|
||||
<label>Mon email</label>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 'var(--fs-base)', color: 'var(--text)' }}>{user?.email}</span>
|
||||
{user?.email_verified
|
||||
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)', border: '1px solid #bbf7d0' }}>✓ Vérifié</span>
|
||||
: <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid #fcd34d' }}>Non vérifié</span>
|
||||
}
|
||||
</div>
|
||||
{!user?.email_verified && <EmailResendBlock email={user?.email} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profile-field profile-field-full">
|
||||
<span className="profile-label">Mon email</span>
|
||||
<div className="profile-email-row" style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
<span className="profile-email-value">{user?.email}</span>
|
||||
{user?.email_verified
|
||||
? <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--success-bg, #f0fdf4)', color: 'var(--success, #16a34a)', border: '1px solid #bbf7d0' }}>✓ Vérifié</span>
|
||||
: <span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 10, background: 'var(--warning-bg, #fffbeb)', color: 'var(--warning-text, #92400e)', border: '1px solid #fcd34d' }}>Non vérifié</span>
|
||||
}
|
||||
{!changingEmail && (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
||||
<button className="primary" type="button" onClick={() => setChangingEmail(true)}>
|
||||
Changer mon email
|
||||
</button>
|
||||
</div>
|
||||
{!user?.email_verified && <EmailResendBlock email={user?.email} />}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button className="profile-manage-btn" type="button" onClick={() => setChangingEmail(v => !v)}>
|
||||
{changingEmail ? 'Annuler' : 'Changer mon email'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{changingEmail && <EmailChangeForm onDone={() => setChangingEmail(false)} />}
|
||||
</section>
|
||||
|
||||
{loading && <p className="text-muted" style={{ fontSize: 'var(--fs-sm)', marginTop: 12 }}>Enregistrement…</p>}
|
||||
</div>
|
||||
|
||||
{/* ── Préférences ─────────────────────────────────────── */}
|
||||
<section className="profile-section">
|
||||
<h2 className="profile-section-title">Préférences</h2>
|
||||
<div className="profile-grid-2">
|
||||
<div className="card" style={{ marginTop: 20 }}>
|
||||
<h3 style={{ margin: '0 0 4px' }}>Préférences</h3>
|
||||
<p className="text-muted" style={{ margin: '0 0 16px', fontSize: 'var(--fs-sm)' }}>
|
||||
Langue et devise utilisées dans l'application.
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, maxWidth: 400 }}>
|
||||
<ProfileSelect label="Langue" options={LANGUES} value={langue} onChange={setLangue} />
|
||||
<ProfileSelect label="Devise" options={DEVISES} value={devise} onChange={setDevise} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{loading && <p className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>Enregistrement…</p>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -322,15 +337,19 @@ function TwoFASection({ user }) {
|
||||
|
||||
{/* ── État normal ── */}
|
||||
{status === 'idle' && !enabled && (
|
||||
<button className="primary" onClick={() => startSetup()} disabled={busy}>
|
||||
{busy ? 'Chargement…' : 'Activer le 2FA'}
|
||||
</button>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button className="primary" onClick={() => startSetup()} disabled={busy}>
|
||||
{busy ? 'Chargement…' : 'Activer le 2FA'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{status === 'idle' && enabled && (
|
||||
<button className="btn btn-outline" style={{ color: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }}
|
||||
onClick={() => { setStatus('disabling'); setErr(null); setMsg(null); }}>
|
||||
Désactiver le 2FA
|
||||
</button>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<button className="btn btn-outline" style={{ color: 'var(--danger,#dc2626)', borderColor: 'var(--danger,#dc2626)' }}
|
||||
onClick={() => { setStatus('disabling'); setErr(null); setMsg(null); }}>
|
||||
Désactiver le 2FA
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Setup : QR code + vérification ── */}
|
||||
@@ -374,10 +393,12 @@ function TwoFASection({ user }) {
|
||||
{busy ? '…' : 'Confirmer'}
|
||||
</button>
|
||||
</form>
|
||||
<button type="button" onClick={() => { setStatus('idle'); setSetupData(null); setCode(''); setErr(null); }}
|
||||
style={{ marginTop: 12, background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
|
||||
Annuler
|
||||
</button>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', maxWidth: 320, marginTop: 12 }}>
|
||||
<button type="button" onClick={() => { setStatus('idle'); setSetupData(null); setCode(''); setErr(null); }}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -399,10 +420,12 @@ function TwoFASection({ user }) {
|
||||
{busy ? '…' : 'Désactiver'}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" onClick={() => { setStatus('idle'); setErr(null); setDisablePwd(''); }}
|
||||
style={{ marginTop: 10, background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
|
||||
Annuler
|
||||
</button>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 10 }}>
|
||||
<button type="button" onClick={() => { setStatus('idle'); setErr(null); setDisablePwd(''); }}
|
||||
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', fontSize: 13, cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
@@ -438,26 +461,6 @@ function SecurityForm() {
|
||||
finally { setPwdLoading(false); }
|
||||
};
|
||||
|
||||
const handleBackfillComptes = async () => {
|
||||
setLoadingBackfill(true);
|
||||
setErrorMsg(null);
|
||||
setSuccessMsg(null);
|
||||
try {
|
||||
const { updated, total } = await api.post('/remboursements/backfill-comptes', {});
|
||||
if (updated === 0) {
|
||||
setSuccessMsg(`Aucun remboursement à corriger (${total} vérifié${total > 1 ? 's' : ''}).`);
|
||||
} else {
|
||||
setSuccessMsg(`${updated} remboursement${updated > 1 ? 's' : ''} mis à jour sur ${total} vérifié${total > 1 ? 's' : ''}.`);
|
||||
}
|
||||
setShowBackfillModal(false);
|
||||
} catch (err) {
|
||||
setErrorMsg(err.message || 'Une erreur est survenue.');
|
||||
setShowBackfillModal(false);
|
||||
} finally {
|
||||
setLoadingBackfill(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h3 style={{ margin: '0 0 4px' }}>Mot de passe</h3>
|
||||
@@ -742,9 +745,11 @@ export default function MonCompte() {
|
||||
))}
|
||||
</aside>
|
||||
{/* ── Contenu ─────────────────────────────────────── */}
|
||||
<div className="account-content">
|
||||
{section === 'profil' && <AccountForm />}
|
||||
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
|
||||
<div className="account-content account-content-center">
|
||||
<div className="account-content-narrow">
|
||||
{section === 'profil' && <AccountForm />}
|
||||
{section === 'securite' && <><SecurityForm /><TwoFASection user={user} /><TrustedDevicesSection /></>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1338,6 +1338,17 @@ tr:hover td { background: var(--surface-2); }
|
||||
padding: 24px 0 32px 28px;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Variante pour les pages "compte" à faible densité de contenu (MonCompte) :
|
||||
évite que les cards s'étirent sur toute la largeur d'un grand écran.
|
||||
Centrage via flex (plus fiable qu'un margin:auto sur un enfant de grid). */
|
||||
.account-content-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.account-content-narrow {
|
||||
width: 100%;
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
/* AccountForm interne — profil + préférences côte-à-côte */
|
||||
.profile-page {
|
||||
|
||||
Reference in New Issue
Block a user