Compare commits

..

3 Commits

Author SHA1 Message Date
ocroguennec b481c4cc1d MAJ 2026-07-03 18:33:58 +02:00
ocroguennec 563063ba17 Ajout de la possibilité de voir le mot de passe 2026-07-03 18:31:59 +02:00
ocroguennec 3c5aa635c4 Ajout direct pour nouveau user 2026-07-03 18:21:24 +02:00
14 changed files with 186 additions and 23 deletions
+27 -1
View File
@@ -1,5 +1,5 @@
# MEMORY.md — Crowdlending Tracker
*Dernière mise à jour: 2026-06-15 (session 7)*
*Dernière mise à jour: 2026-07-03 (session 8)*
---
@@ -425,3 +425,29 @@ const isBonus = BONUS_VALUES.includes(form.investissement_id);
- `useState(8)` comme valeur initiale sur tous les écrans → jamais bloquant si l'API est lente
- Register/ResetPassword/InvitationRegister : via `appInfo.minPasswordLength || 8` (appInfo déjà fetché)
- MonCompte (SecurityForm) et UsersSection (CreateUserModal) : state local `minPasswordLength` + useEffect fetch dédié
---
## Session 8 — UX empty state + visibilité mot de passe (2026-07-03)
### EmptyState → ouverture directe de l'ajout de plateforme
- `EmptyState.jsx` : prop `to` par défaut passée de `/settings?section=plateformes` à `/settings?section=plateformes&openAdd=1`
- `PlateformesSection.jsx` : `useSearchParams` + `useEffect` détecte `openAdd=1` au montage → ouvre directement `showAddPicker` (modale "Ajouter une plateforme") → nettoie le paramètre de l'URL (`replace: true`) pour éviter la réouverture au refresh
- Évite l'étape intermédiaire où l'utilisateur devait cliquer une seconde fois sur "+ Ajouter" après avoir été redirigé depuis un état vide (Dashboard, Investissements, Remboursements, DepotsRetraits)
- **Pattern réutilisable** : pour toute redirection "action directe" similaire depuis un état vide, ajouter un query param dédié + `useEffect` de consommation/nettoyage dans la section cible
### Composant PasswordInput — afficher/masquer mot de passe
- Nouveau composant `frontend/src/components/PasswordInput.jsx` : wrapper autour d'un `<input type="password">` avec bouton œil (SVG inline, pas de lib externe) togglant `type` entre `password`/`text`
- API : toutes les props (`className`, `value`, `onChange`, `required`, `autoComplete`, `minLength`, `style`, etc.) sont transmises telles quelles à l'`<input>` interne ; `wrapperStyle` optionnel pour le `<div style="position:relative">` englobant
- **Déployé sur les 8 écrans contenant un champ mot de passe** : Login, Register, ResetPassword (×2 champs), InvitationRegister (×2 champs), MonCompte (SecurityForm ×3 champs + changement email ×1 + désactivation 2FA ×1), admin/CreateUserSection, admin/UsersSection
- **Règle à respecter** : tout nouveau champ mot de passe doit utiliser `<PasswordInput>` plutôt que `<input type="password">` brut, pour garder l'UX cohérente sur toute l'app
### Bug — profil principal / compte courant non créés hors /auth/register
- **Constat** : seul `/api/auth/register` (auto-inscription) créait le profil investisseur principal (`is_principal=1`) ET le compte courant associé. Les deux autres parcours de création de compte en étaient dépourvus :
- `POST /api/admin/users` (admin.js, `CreateUserSection.jsx`) : créait l'investisseur mais **sans `is_principal=1`** et **sans compte courant**
- `POST /api/invitations/:token/register` (invitations.js, `InvitationRegister.jsx`) : ne créait **aucun investisseur ni compte courant**
- **Cas réel trouvé en base** : `marine@croguennec.net` (user #2, créée par invitation le 2026-06-18) n'avait **aucun** investisseur ; `newargus@gmail.com` (user #3, créé par l'admin le 2026-07-03) avait un investisseur mais `is_principal=0` et zéro compte
- **Fix appliqué** :
1. `admin.js` (`POST /users`) et `invitations.js` (`POST /:token/register`) répliquent maintenant exactement la logique de `auth.js` : `INSERT INTO investisseurs (..., is_principal) VALUES (..., 1)` + `INSERT INTO comptes (user_id, nom, type, investisseur_id)` avec `nom = 'Compte courant — ' + fullName`
2. Backfill idempotent ajouté en fin de `backend/src/db/index.js` (avant `export default db`) qui tourne à chaque démarrage : (1) crée un investisseur principal pour tout user qui n'en a aucun, (2) marque principal le plus ancien investisseur `famille` pour tout user qui n'a pas de principal, (3) crée le compte courant manquant pour tout investisseur principal qui n'en a pas. Toutes les requêtes utilisent `NOT EXISTS` → sans effet une fois les données corrigées.
- **Règle à retenir** : toute nouvelle voie de création de compte utilisateur doit répliquer les 2 inserts de `auth.js` (`investisseurs` avec `is_principal=1` + `comptes` type `compte_courant`) — ne pas dupliquer seulement l'insert `investisseurs`.
+50
View File
@@ -2012,4 +2012,54 @@ console.log('[DB] Migrations 2FA OK');
}
}
// ── Backfill : garantir un profil principal + un compte courant par utilisateur ──
// Certains parcours de création de compte (admin, invitation) ne créaient pas
// systématiquement le profil investisseur principal et/ou son compte courant,
// contrairement à /auth/register. Ce backfill est idempotent (NOT EXISTS partout)
// et s'exécute à chaque démarrage pour rattraper les comptes existants.
{
// 1) Utilisateurs sans aucun investisseur (ex : comptes créés via invitation)
const usersWithoutInvestisseur = db.prepare(`
SELECT id, email, display_name FROM users u
WHERE NOT EXISTS (SELECT 1 FROM investisseurs i WHERE i.user_id = u.id)
`).all();
for (const u of usersWithoutInvestisseur) {
const fullName = u.display_name || u.email.split('@')[0];
const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
db.prepare(
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
).run(u.id, fullName, prenom);
console.log(`[DB] backfill: profil investisseur principal créé pour user #${u.id} (${u.email})`);
}
// 2) Utilisateurs ayant des investisseurs mais aucun marqué principal (ex : comptes créés par un admin)
const usersWithoutPrincipal = db.prepare(`
SELECT DISTINCT user_id FROM investisseurs i
WHERE NOT EXISTS (SELECT 1 FROM investisseurs p WHERE p.user_id = i.user_id AND p.is_principal = 1)
`).all();
for (const { user_id } of usersWithoutPrincipal) {
const candidate = db.prepare(`
SELECT id FROM investisseurs WHERE user_id = ? ORDER BY (type = 'famille') DESC, id ASC LIMIT 1
`).get(user_id);
if (candidate) {
db.prepare('UPDATE investisseurs SET is_principal = 1 WHERE id = ?').run(candidate.id);
console.log(`[DB] backfill: investisseur #${candidate.id} marqué principal pour user #${user_id}`);
}
}
// 3) Investisseurs principaux sans compte courant
const principalsWithoutCompte = db.prepare(`
SELECT i.id AS investisseur_id, i.user_id, i.nom
FROM investisseurs i
WHERE i.is_principal = 1
AND NOT EXISTS (SELECT 1 FROM comptes c WHERE c.investisseur_id = i.id)
`).all();
for (const inv of principalsWithoutCompte) {
db.prepare(
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
).run(inv.user_id, `Compte courant — ${inv.nom}`, 'compte_courant', inv.investisseur_id);
console.log(`[DB] backfill: compte courant créé pour l'investisseur principal #${inv.investisseur_id} (user #${inv.user_id})`);
}
}
export default db;
+9 -2
View File
@@ -107,10 +107,17 @@ router.post('/users', (req, res, next) => {
const userId = result.lastInsertRowid;
const fullName = body.displayName || body.email.split('@')[0];
const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
db.prepare(
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal) VALUES (?, ?, ?, 'famille', 'PP')`
// Auto-créer le profil principal (= l'utilisateur lui-même), comme dans /auth/register
const invResult = db.prepare(
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
).run(userId, fullName, prenom);
// Auto-créer un compte courant pour le profil principal, comme dans /auth/register
db.prepare(
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
).run(userId, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
audit(req, { action: 'user_created', category: 'account', actorId: req.user.id, targetUserId: userId, details: { email: body.email, role: body.role, created_by_admin: true } });
res.status(201).json({ id: userId, email: body.email, display_name: body.displayName || null, role: body.role });
} catch (e) { next(e); }
+14 -2
View File
@@ -167,7 +167,8 @@ router.post('/:token/register', async (req, res, next) => {
const pendingUser = db.prepare('SELECT id FROM users WHERE LOWER(email) = LOWER(?) AND email_verified = 0').get(inv.email);
if (!pendingUser) throw new HttpError(409, 'Ce compte a déjà été activé ou supprimé.');
const hash = await bcrypt.hash(password, 12);
const hash = await bcrypt.hash(password, 12);
const fullName = displayName || nameFromEmail(inv.email);
// Finaliser le compte : mot de passe, nom, vérification
db.prepare(`
@@ -177,7 +178,18 @@ router.post('/:token/register', async (req, res, next) => {
email_verified = 1,
updated_at = datetime('now')
WHERE id = ?
`).run(hash, displayName || nameFromEmail(inv.email), pendingUser.id);
`).run(hash, fullName, pendingUser.id);
// Auto-créer le profil principal (= l'utilisateur lui-même) + son compte courant,
// comme dans /auth/register — sans quoi l'utilisateur invité se retrouve sans
// aucun investisseur ni compte courant à sa première connexion.
const prenom = fullName.includes(' ') ? fullName.split(' ')[0] : null;
const invResult = db.prepare(
`INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, is_principal) VALUES (?, ?, ?, 'famille', 'PP', 1)`
).run(pendingUser.id, fullName, prenom);
db.prepare(
'INSERT INTO comptes (user_id, nom, type, investisseur_id) VALUES (?,?,?,?)'
).run(pendingUser.id, `Compte courant — ${fullName}`, 'compte_courant', invResult.lastInsertRowid);
// Marquer l'invitation comme utilisée
db.prepare("UPDATE invitations SET used_at = datetime('now') WHERE id = ?").run(inv.id);
+1 -1
View File
@@ -14,7 +14,7 @@ export default function EmptyState({
title = 'Aucune plateforme configurée',
message = "Vous n'avez pas encore configuré de plateforme de crowdfunding sur votre compte. Associez au moins une plateforme pour commencer à suivre vos investissements.",
cta = 'Configurer mes plateformes',
to = '/settings?section=plateformes',
to = '/settings?section=plateformes&openAdd=1',
}) {
const navigate = useNavigate();
+48
View File
@@ -0,0 +1,48 @@
import { useState } from 'react';
/**
* Champ mot de passe avec bouton "afficher/masquer".
* Utilisation : remplacer <input type="password" .../> par <PasswordInput .../>
* Toutes les props (className, value, onChange, required, autoComplete, style, etc.)
* sont transmises telles quelles à l'<input> sous-jacent.
*/
function EyeIcon({ open }) {
return open ? (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="var(--text-muted)" strokeWidth="2"/>
</svg>
) : (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M17.94 17.94A10.94 10.94 0 0112 19c-7 0-11-7-11-7a20.6 20.6 0 015.06-5.94M9.9 4.24A10.9 10.9 0 0112 4c7 0 11 7 11 7a20.6 20.6 0 01-2.66 3.78M14.12 14.12a3 3 0 11-4.24-4.24" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<line x1="1" y1="1" x2="23" y2="23" stroke="var(--text-muted)" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
}
export default function PasswordInput({ style, wrapperStyle, ...props }) {
const [show, setShow] = useState(false);
return (
<div style={{ position: 'relative', ...wrapperStyle }}>
<input
{...props}
type={show ? 'text' : 'password'}
style={{ ...style, paddingRight: 40 }}
/>
<button
type="button"
tabIndex={-1}
onClick={() => setShow(v => !v)}
aria-label={show ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
style={{
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', padding: 4, display: 'flex',
alignItems: 'center', justifyContent: 'center', cursor: 'pointer', lineHeight: 0,
}}
>
<EyeIcon open={show} />
</button>
</div>
);
}
+3 -2
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import PasswordStrength from '../components/PasswordStrength.jsx';
import PasswordInput from '../components/PasswordInput.jsx';
import { useParams, useNavigate, Link } from 'react-router-dom';
// Wrap défini en dehors du composant pour éviter le remontage à chaque frappe
@@ -173,13 +174,13 @@ export default function InvitationRegister() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe <span style={{ color: 'var(--text-muted)', fontWeight: 400 }}>(8 car. min.)</span></label>
<input className="form-input" type="password" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.password} onChange={e => set('password', e.target.value)} style={{ width: '100%' }} />
<PasswordInput className="form-input" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.password} onChange={e => set('password', e.target.value)} style={{ width: '100%' }} />
<PasswordStrength password={form.password} minLength={appInfo.minPasswordLength || 8} />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Confirmer le mot de passe</label>
<input className="form-input" type="password" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.confirm} onChange={e => set('confirm', e.target.value)} style={{ width: '100%' }} />
<PasswordInput className="form-input" required minLength={appInfo.minPasswordLength || 8} autoComplete="new-password" placeholder="••••••••" value={form.confirm} onChange={e => set('confirm', e.target.value)} style={{ width: '100%' }} />
</div>
<button type="submit" disabled={busy} style={{ marginTop: 4, width: '100%', padding: '11px 0', background: busy ? 'var(--text-muted)' : 'var(--primary, #1e40af)', color: '#fff', border: 'none', borderRadius: 8, fontSize: 15, fontWeight: 600, cursor: busy ? 'not-allowed' : 'pointer', transition: 'background 0.15s' }}>
+2 -1
View File
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext.jsx';
import { api } from '../api.js';
import AuthBgCol from '../components/AuthBgCol.jsx';
import PasswordInput from '../components/PasswordInput.jsx';
// ── Helpers ────────────────────────────────────────────────────────────────
const DEVICE_KEY = 'cl_device_token';
@@ -217,7 +218,7 @@ export default function Login() {
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>Mot de passe</label>
<Link to="/forgot-password" style={{ fontSize: 13, color: 'var(--text-muted)', textDecoration: 'underline' }}>Mot de passe oublié ?</Link>
</div>
<input className="form-input" type="password" required autoComplete="current-password" placeholder="••••••••"
<PasswordInput className="form-input" required autoComplete="current-password" placeholder="••••••••"
value={password} onChange={e => setPassword(e.target.value)} style={{ width: '100%' }} />
</div>
+6 -5
View File
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import PasswordStrength from '../components/PasswordStrength.jsx';
import PasswordInput from '../components/PasswordInput.jsx';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext.jsx';
import { useUi } from '../context/UiContext.jsx';
@@ -143,7 +144,7 @@ function EmailChangeForm({ onDone }) {
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label className="profile-label">Mot de passe actuel (confirmation)</label>
<input className="profile-input" type="password" required value={pwd} onChange={e => setPwd(e.target.value)} placeholder="••••••••" />
<PasswordInput className="profile-input" 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 }}>
@@ -389,7 +390,7 @@ function TwoFASection({ user }) {
<div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
<div style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 13, fontWeight: 500 }}>Mot de passe actuel</label>
<input type="password" required value={disablePwd}
<PasswordInput required value={disablePwd}
onChange={e => setDisablePwd(e.target.value)}
autoComplete="current-password" placeholder="••••••••" />
</div>
@@ -469,20 +470,20 @@ function SecurityForm() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 400 }}>
<div>
<label>Mot de passe actuel</label>
<input type="password" required autoComplete="current-password"
<PasswordInput required autoComplete="current-password"
value={pwdForm.currentPassword}
onChange={e => setPwdForm({ ...pwdForm, currentPassword: e.target.value })} />
</div>
<div>
<label>Nouveau mot de passe</label>
<input type="password" required autoComplete="new-password"
<PasswordInput required autoComplete="new-password"
value={pwdForm.newPassword}
onChange={e => setPwdForm({ ...pwdForm, newPassword: e.target.value })} />
<PasswordStrength password={pwdForm.newPassword} minLength={minPasswordLength} />
</div>
<div>
<label>Confirmer le nouveau mot de passe</label>
<input type="password" required autoComplete="new-password"
<PasswordInput required autoComplete="new-password"
value={pwdForm.confirm}
onChange={e => setPwdForm({ ...pwdForm, confirm: e.target.value })} />
</div>
+3 -2
View File
@@ -3,6 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext.jsx';
import PasswordStrength from '../components/PasswordStrength.jsx';
import AuthBgCol from '../components/AuthBgCol.jsx';
import PasswordInput from '../components/PasswordInput.jsx';
export default function Register() {
const { register } = useAuth();
@@ -178,9 +179,9 @@ export default function Register() {
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
Mot de passe
</label>
<input
<PasswordInput
className="form-input"
type="password" required
required
minLength={appInfo.minPasswordLength || 8}
autoComplete="new-password"
placeholder="••••••••"
+5 -4
View File
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import PasswordStrength from '../components/PasswordStrength.jsx';
import AuthBgCol from '../components/AuthBgCol.jsx';
import PasswordInput from '../components/PasswordInput.jsx';
export default function ResetPassword() {
const [params] = useSearchParams();
@@ -154,9 +155,9 @@ export default function ResetPassword() {
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
Nouveau mot de passe
</label>
<input
<PasswordInput
className="form-input"
type="password" required minLength={appInfo.minPasswordLength || 8}
required minLength={appInfo.minPasswordLength || 8}
autoComplete="new-password"
placeholder="••••••••"
value={password}
@@ -170,9 +171,9 @@ export default function ResetPassword() {
<label style={{ fontSize: 14, fontWeight: 500, color: 'var(--text)' }}>
Confirmer le mot de passe
</label>
<input
<PasswordInput
className="form-input"
type="password" required minLength={appInfo.minPasswordLength || 8}
required minLength={appInfo.minPasswordLength || 8}
autoComplete="new-password"
placeholder="••••••••"
value={password2}
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { api } from '../../api.js';
import PasswordInput from '../../components/PasswordInput.jsx';
export default function CreateUserSection({ onCreated }) {
const [form, setForm] = useState({ email: '', password: '', displayName: '', role: 'user' });
@@ -48,7 +49,7 @@ export default function CreateUserSection({ onCreated }) {
</div>
<div>
<label>Mot de passe *</label>
<input type="password" required minLength={8} value={form.password} onChange={e => set('password', e.target.value)} placeholder="8 caractères minimum" />
<PasswordInput required minLength={8} value={form.password} onChange={e => set('password', e.target.value)} placeholder="8 caractères minimum" />
</div>
<div>
<label>Rôle</label>
+2 -1
View File
@@ -5,6 +5,7 @@ import ConfirmModal from '../../components/ConfirmModal.jsx';
import Modal from '../../components/Modal.jsx';
import ResultBanner from '../../components/ResultBanner.jsx';
import PasswordStrength from '../../components/PasswordStrength.jsx';
import PasswordInput from '../../components/PasswordInput.jsx';
import { fmt, Badge, UserStatusBadge } from './adminHelpers.jsx';
// ── Helpers ────────────────────────────────────────────────────────────────
@@ -363,7 +364,7 @@ function CreateUserModal({ open, onClose, onCreated }) {
</div>
<div>
<label>Mot de passe *</label>
<input type="password" required minLength={minPasswordLength} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder={`${minPasswordLength} caractères minimum`} />
<PasswordInput required minLength={minPasswordLength} autoComplete="new-password" value={form.password} onChange={e => set('password', e.target.value)} placeholder={`${minPasswordLength} caractères minimum`} />
<PasswordStrength password={form.password} minLength={minPasswordLength} />
</div>
<div>
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { api } from '../../api.js';
import InvSelect from '../../components/InvSelect.jsx';
import Modal from '../../components/Modal.jsx';
@@ -629,6 +629,8 @@ function PlatForm({ state, setter, logoFile, setLogoFile, logoPreview, setLogoPr
}
export default function PlateformesSection() {
const [searchParams, setSearchParams] = useSearchParams();
// ── State ──────────────────────────────────────────────────────
const [plats, setPlats] = useState([]);
const [investisseurs, setInvestisseurs] = useState([]);
@@ -693,6 +695,17 @@ export default function PlateformesSection() {
setSelectedPlat(prev => prev ? prev : plats[0]);
}, [plats]); // eslint-disable-line
// Ouverture automatique du picker d'ajout (venant de l'état vide "Aucune plateforme configurée")
useEffect(() => {
if (searchParams.get('openAdd') === '1') {
setErr(null);
setShowAddPicker(true);
const next = new URLSearchParams(searchParams);
next.delete('openAdd');
setSearchParams(next, { replace: true });
}
}, [searchParams]); // eslint-disable-line
const handleLogoFile = (file, setFile, setPreview) => {
if (!file) { setFile(null); setPreview(null); return; }
setFile(file);