Mise à jour des pages d'accueil
This commit is contained in:
@@ -1870,6 +1870,13 @@ console.log('[DB] Migrations 2FA OK');
|
|||||||
db.exec("ALTER TABLE smtp_config ADD COLUMN min_password_length INTEGER NOT NULL DEFAULT 8");
|
db.exec("ALTER TABLE smtp_config ADD COLUMN min_password_length INTEGER NOT NULL DEFAULT 8");
|
||||||
console.log('[DB] Colonne smtp_config.min_password_length ajoutée');
|
console.log('[DB] Colonne smtp_config.min_password_length ajoutée');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Migration : email sur investisseurs ───────────────────────────────
|
||||||
|
const invColsEmail = db.prepare('PRAGMA table_info(investisseurs)').all().map(c => c.name);
|
||||||
|
if (!invColsEmail.includes('email')) {
|
||||||
|
db.exec('ALTER TABLE investisseurs ADD COLUMN email TEXT');
|
||||||
|
console.log('[DB] Colonne investisseurs.email ajoutée');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ const Schema = z.object({
|
|||||||
type: z.enum(['famille', 'entreprise']).default('famille'),
|
type: z.enum(['famille', 'entreprise']).default('famille'),
|
||||||
type_fiscal: z.string().optional(),
|
type_fiscal: z.string().optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
|
email: z.string().email().optional().or(z.literal('')),
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const rows = db
|
const rows = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT id, nom, prenom, type, type_fiscal, is_principal, notes, created_at
|
`SELECT id, nom, prenom, type, type_fiscal, is_principal, notes, email, created_at
|
||||||
FROM investisseurs WHERE user_id = ?
|
FROM investisseurs WHERE user_id = ?
|
||||||
ORDER BY is_principal DESC, type, nom`
|
ORDER BY is_principal DESC, type, nom`
|
||||||
)
|
)
|
||||||
@@ -29,7 +30,7 @@ router.post('/', (req, res, next) => {
|
|||||||
const body = Schema.parse(req.body);
|
const body = Schema.parse(req.body);
|
||||||
const r = db
|
const r = db
|
||||||
.prepare(
|
.prepare(
|
||||||
'INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes) VALUES (?,?,?,?,?,?)'
|
'INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes, email) VALUES (?,?,?,?,?,?,?)'
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
req.user.id,
|
req.user.id,
|
||||||
@@ -38,6 +39,7 @@ router.post('/', (req, res, next) => {
|
|||||||
body.type,
|
body.type,
|
||||||
body.type_fiscal || null,
|
body.type_fiscal || null,
|
||||||
body.notes || null,
|
body.notes || null,
|
||||||
|
body.email || null,
|
||||||
);
|
);
|
||||||
const invId = r.lastInsertRowid;
|
const invId = r.lastInsertRowid;
|
||||||
// Auto-créer un compte courant pour ce nouveau profil
|
// Auto-créer un compte courant pour ce nouveau profil
|
||||||
@@ -56,7 +58,7 @@ router.put('/:id', (req, res, next) => {
|
|||||||
const body = Schema.parse(req.body);
|
const body = Schema.parse(req.body);
|
||||||
const r = db
|
const r = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`UPDATE investisseurs SET nom=?, prenom=?, type=?, type_fiscal=?, notes=?, updated_at=datetime('now')
|
`UPDATE investisseurs SET nom=?, prenom=?, type=?, type_fiscal=?, notes=?, email=?, updated_at=datetime('now')
|
||||||
WHERE id=? AND user_id=?`
|
WHERE id=? AND user_id=?`
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
@@ -65,6 +67,7 @@ router.put('/:id', (req, res, next) => {
|
|||||||
body.type,
|
body.type,
|
||||||
body.type_fiscal || null,
|
body.type_fiscal || null,
|
||||||
body.notes || null,
|
body.notes || null,
|
||||||
|
body.email || null,
|
||||||
req.params.id,
|
req.params.id,
|
||||||
req.user.id,
|
req.user.id,
|
||||||
);
|
);
|
||||||
@@ -115,10 +118,25 @@ router.delete('/:id', (req, res, next) => {
|
|||||||
.get(req.user.id).n;
|
.get(req.user.id).n;
|
||||||
if (count <= 1) throw new HttpError(400, 'Impossible de supprimer le dernier profil.');
|
if (count <= 1) throw new HttpError(400, 'Impossible de supprimer le dernier profil.');
|
||||||
|
|
||||||
const r = db
|
// Récupérer le compte principal pour la réassignation
|
||||||
.prepare('DELETE FROM investisseurs WHERE id=? AND user_id=?')
|
const principal = db
|
||||||
.run(req.params.id, req.user.id);
|
.prepare('SELECT id FROM investisseurs WHERE user_id=? AND is_principal=1')
|
||||||
if (r.changes === 0) throw new HttpError(404, 'Not found');
|
.get(req.user.id);
|
||||||
|
if (!principal) throw new HttpError(500, 'Aucun compte principal trouvé pour la réassignation.');
|
||||||
|
|
||||||
|
const principalId = principal.id;
|
||||||
|
const targetId = Number(req.params.id);
|
||||||
|
|
||||||
|
// Réassigner toutes les données liées au compte principal avant suppression
|
||||||
|
const deleteWithReassign = db.transaction(() => {
|
||||||
|
db.prepare('UPDATE investissements SET investisseur_id=? WHERE investisseur_id=?').run(principalId, targetId);
|
||||||
|
db.prepare('UPDATE depots_retraits SET investisseur_id=? WHERE investisseur_id=?').run(principalId, targetId);
|
||||||
|
db.prepare('UPDATE plateformes SET investisseur_id=? WHERE investisseur_id=? AND user_id=?').run(principalId, targetId, req.user.id);
|
||||||
|
db.prepare('UPDATE comptes SET investisseur_id=? WHERE investisseur_id=? AND user_id=?').run(principalId, targetId, req.user.id);
|
||||||
|
db.prepare('DELETE FROM investisseurs WHERE id=? AND user_id=?').run(targetId, req.user.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteWithReassign();
|
||||||
res.status(204).end();
|
res.status(204).end();
|
||||||
} catch (e) { next(e); }
|
} catch (e) { next(e); }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ const zipUpload = multer({
|
|||||||
router.get('/export', (req, res, next) => {
|
router.get('/export', (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
let rows = db.prepare(`
|
let rows = db.prepare(`
|
||||||
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
|
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom, inv.email AS investisseur_email
|
||||||
FROM plateformes p
|
FROM plateformes p
|
||||||
LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
|
LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
|
||||||
WHERE p.user_id = ?
|
WHERE p.user_id = ?
|
||||||
@@ -358,6 +358,7 @@ router.get('/export', (req, res, next) => {
|
|||||||
icone_filename: r.icone_filename,
|
icone_filename: r.icone_filename,
|
||||||
investisseur_nom: r.investisseur_nom,
|
investisseur_nom: r.investisseur_nom,
|
||||||
investisseur_prenom: r.investisseur_prenom,
|
investisseur_prenom: r.investisseur_prenom,
|
||||||
|
investisseur_email: r.investisseur_email,
|
||||||
categories: legacyMap[r.id] || [],
|
categories: legacyMap[r.id] || [],
|
||||||
categories_inv: (r.categories_inv || []).map(c => c.nom),
|
categories_inv: (r.categories_inv || []).map(c => c.nom),
|
||||||
secteurs_inv: (r.secteurs_inv || []).map(s => s.nom),
|
secteurs_inv: (r.secteurs_inv || []).map(s => s.nom),
|
||||||
@@ -368,7 +369,7 @@ router.get('/export', (req, res, next) => {
|
|||||||
// Export des investisseurs (détenteurs) liés aux plateformes
|
// Export des investisseurs (détenteurs) liés aux plateformes
|
||||||
const invIds = [...new Set(rows.map(r => r.investisseur_id).filter(Boolean))];
|
const invIds = [...new Set(rows.map(r => r.investisseur_id).filter(Boolean))];
|
||||||
const invRows = invIds.length > 0
|
const invRows = invIds.length > 0
|
||||||
? db.prepare(`SELECT nom, prenom, type, type_fiscal, notes FROM investisseurs WHERE id IN (${invIds.map(() => '?').join(',')}) AND user_id = ?`).all(...invIds, req.user.id)
|
? db.prepare(`SELECT nom, prenom, type, type_fiscal, notes, email FROM investisseurs WHERE id IN (${invIds.map(() => '?').join(',')}) AND user_id = ?`).all(...invIds, req.user.id)
|
||||||
: [];
|
: [];
|
||||||
if (invRows.length > 0) {
|
if (invRows.length > 0) {
|
||||||
entries.push({ name: 'investisseurs.json', data: JSON.stringify(invRows, null, 2) });
|
entries.push({ name: 'investisseurs.json', data: JSON.stringify(invRows, null, 2) });
|
||||||
@@ -394,7 +395,7 @@ router.get('/export', (req, res, next) => {
|
|||||||
router.get('/:id/export', (req, res, next) => {
|
router.get('/:id/export', (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const p = db.prepare(`
|
const p = db.prepare(`
|
||||||
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
|
SELECT p.*, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom, inv.email AS investisseur_email
|
||||||
FROM plateformes p LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
|
FROM plateformes p LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
|
||||||
WHERE p.id = ? AND p.user_id = ?
|
WHERE p.id = ? AND p.user_id = ?
|
||||||
`).get(req.params.id, req.user.id);
|
`).get(req.params.id, req.user.id);
|
||||||
@@ -421,7 +422,7 @@ router.get('/:id/export', (req, res, next) => {
|
|||||||
date_ouverture: withAll.date_ouverture,
|
date_ouverture: withAll.date_ouverture,
|
||||||
type_pret_defaut: withAll.type_pret_defaut, freq_interets_defaut: withAll.freq_interets_defaut,
|
type_pret_defaut: withAll.type_pret_defaut, freq_interets_defaut: withAll.freq_interets_defaut,
|
||||||
logo_filename: withAll.logo_filename, icone_filename: withAll.icone_filename,
|
logo_filename: withAll.logo_filename, icone_filename: withAll.icone_filename,
|
||||||
investisseur_nom: withAll.investisseur_nom, investisseur_prenom: withAll.investisseur_prenom,
|
investisseur_nom: withAll.investisseur_nom, investisseur_prenom: withAll.investisseur_prenom, investisseur_email: withAll.investisseur_email,
|
||||||
categories: catsLegacy,
|
categories: catsLegacy,
|
||||||
categories_inv: (withAll.categories_inv || []).map(c => c.nom),
|
categories_inv: (withAll.categories_inv || []).map(c => c.nom),
|
||||||
secteurs_inv: (withAll.secteurs_inv || []).map(s => s.nom),
|
secteurs_inv: (withAll.secteurs_inv || []).map(s => s.nom),
|
||||||
@@ -430,7 +431,7 @@ router.get('/:id/export', (req, res, next) => {
|
|||||||
|
|
||||||
// Export du détenteur lié à la plateforme
|
// Export du détenteur lié à la plateforme
|
||||||
if (withAll.investisseur_id) {
|
if (withAll.investisseur_id) {
|
||||||
const inv = db.prepare('SELECT nom, prenom, type, type_fiscal, notes FROM investisseurs WHERE id = ? AND user_id = ?').get(withAll.investisseur_id, req.user.id);
|
const inv = db.prepare('SELECT nom, prenom, type, type_fiscal, notes, email FROM investisseurs WHERE id = ? AND user_id = ?').get(withAll.investisseur_id, req.user.id);
|
||||||
if (inv) entries.push({ name: 'investisseurs.json', data: JSON.stringify([inv], null, 2) });
|
if (inv) entries.push({ name: 'investisseurs.json', data: JSON.stringify([inv], null, 2) });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,26 +460,34 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
|||||||
const platforms = JSON.parse(dataEntry.data.toString('utf8'));
|
const platforms = JSON.parse(dataEntry.data.toString('utf8'));
|
||||||
if (!Array.isArray(platforms)) throw new HttpError(400, 'data.json doit être un tableau');
|
if (!Array.isArray(platforms)) throw new HttpError(400, 'data.json doit être un tableau');
|
||||||
|
|
||||||
// Importer les investisseurs du ZIP (créer les manquants)
|
// Importer les investisseurs du ZIP (créer les manquants, dédupliquer par email puis nom)
|
||||||
const invEntry = zipEntries.find(e => e.name === 'investisseurs.json');
|
const invEntry = zipEntries.find(e => e.name === 'investisseurs.json');
|
||||||
if (invEntry) {
|
if (invEntry) {
|
||||||
const invList = JSON.parse(invEntry.data.toString('utf8'));
|
const invList = JSON.parse(invEntry.data.toString('utf8'));
|
||||||
for (const inv of (Array.isArray(invList) ? invList : [])) {
|
for (const inv of (Array.isArray(invList) ? invList : [])) {
|
||||||
if (!inv.nom) continue;
|
if (!inv.nom) continue;
|
||||||
const exists = db.prepare('SELECT id FROM investisseurs WHERE nom = ? AND user_id = ?').get(inv.nom, req.user.id);
|
// Déduplication : priorité à l'email, puis correspondance insensible à la casse sur le nom
|
||||||
if (!exists) {
|
const existsByEmail = inv.email
|
||||||
db.prepare('INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes) VALUES (?,?,?,?,?,?)')
|
? db.prepare('SELECT id FROM investisseurs WHERE LOWER(email) = LOWER(?) AND user_id = ?').get(inv.email, req.user.id)
|
||||||
.run(req.user.id, inv.nom, inv.prenom ?? null, inv.type || 'famille', inv.type_fiscal ?? null, inv.notes ?? null);
|
: null;
|
||||||
|
const existsByNom = db.prepare('SELECT id FROM investisseurs WHERE LOWER(nom) = LOWER(?) AND user_id = ?').get(inv.nom, req.user.id);
|
||||||
|
if (!existsByEmail && !existsByNom) {
|
||||||
|
db.prepare('INSERT INTO investisseurs (user_id, nom, prenom, type, type_fiscal, notes, email) VALUES (?,?,?,?,?,?,?)')
|
||||||
|
.run(req.user.id, inv.nom, inv.prenom ?? null, inv.type || 'famille', inv.type_fiscal ?? null, inv.notes ?? null, inv.email ?? null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Résoudre investisseur par nom+prénom (après création éventuelle)
|
// Résoudre investisseur — priorité email, puis nom+prénom insensible à la casse
|
||||||
const userInvestisseurs = db.prepare('SELECT * FROM investisseurs WHERE user_id = ?').all(req.user.id);
|
const userInvestisseurs = db.prepare('SELECT * FROM investisseurs WHERE user_id = ?').all(req.user.id);
|
||||||
function resolveInvestisseur(nom, prenom) {
|
function resolveInvestisseur(nom, prenom, email) {
|
||||||
if (!nom) return userInvestisseurs[0]?.id ?? null;
|
if (!nom && !email) return userInvestisseurs[0]?.id ?? null;
|
||||||
|
if (email) {
|
||||||
|
const byEmail = userInvestisseurs.find(i => i.email?.toLowerCase() === email.toLowerCase());
|
||||||
|
if (byEmail) return byEmail.id;
|
||||||
|
}
|
||||||
const match = userInvestisseurs.find(i =>
|
const match = userInvestisseurs.find(i =>
|
||||||
i.nom?.toLowerCase() === nom?.toLowerCase() && i.prenom?.toLowerCase() === prenom?.toLowerCase()
|
i.nom?.toLowerCase() === nom?.toLowerCase() && i.prenom?.toLowerCase() === (prenom || '').toLowerCase()
|
||||||
) || userInvestisseurs.find(i => i.nom?.toLowerCase() === nom?.toLowerCase());
|
) || userInvestisseurs.find(i => i.nom?.toLowerCase() === nom?.toLowerCase());
|
||||||
return match?.id ?? userInvestisseurs[0]?.id ?? null;
|
return match?.id ?? userInvestisseurs[0]?.id ?? null;
|
||||||
}
|
}
|
||||||
@@ -527,7 +536,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
|
|||||||
if (!p.nom) continue;
|
if (!p.nom) continue;
|
||||||
const fiscalite = p.domiciliation === 'FR' ? 'flat_tax' : (p.fiscalite || 'flat_tax');
|
const fiscalite = p.domiciliation === 'FR' ? 'flat_tax' : (p.fiscalite || 'flat_tax');
|
||||||
const taux = fiscalite === 'avec_fiscalite_locale' ? (p.taux_fiscalite_locale ?? null) : null;
|
const taux = fiscalite === 'avec_fiscalite_locale' ? (p.taux_fiscalite_locale ?? null) : null;
|
||||||
const investisseurId = resolveInvestisseur(p.investisseur_nom, p.investisseur_prenom);
|
const investisseurId = resolveInvestisseur(p.investisseur_nom, p.investisseur_prenom, p.investisseur_email);
|
||||||
const catsInvIds = resolveTagIds(p.categories_inv, 'categories_inv');
|
const catsInvIds = resolveTagIds(p.categories_inv, 'categories_inv');
|
||||||
const sectsInvIds = resolveTagIds(p.secteurs_inv, 'secteurs_inv');
|
const sectsInvIds = resolveTagIds(p.secteurs_inv, 'secteurs_inv');
|
||||||
const legacyCatIds = resolveLegacyCatIds(p.categories);
|
const legacyCatIds = resolveLegacyCatIds(p.categories);
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* AuthBgCol — colonne image gauche partagée entre toutes les pages d'authentification.
|
||||||
|
*
|
||||||
|
* L'image /login-bg.jpg contient les deux thèmes côte à côte (dark à gauche, light à droite).
|
||||||
|
* La classe CSS .login-bg-col utilise background-position pour focaliser sur la bonne moitié
|
||||||
|
* selon le thème actif ([data-theme="dark"] sur <html>).
|
||||||
|
*/
|
||||||
|
export default function AuthBgCol({ appInfo = {} }) {
|
||||||
|
return (
|
||||||
|
<div className="login-bg-col">
|
||||||
|
<div className="login-bg-overlay" aria-hidden="true" />
|
||||||
|
<div className="login-bg-brand">
|
||||||
|
{appInfo.iconUrl && (
|
||||||
|
<img src={appInfo.iconUrl} alt="" width={36} height={36}
|
||||||
|
style={{ borderRadius: 8, flexShrink: 0 }} />
|
||||||
|
)}
|
||||||
|
<span>{appInfo.appName || 'Crowdlending Tracker'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -93,8 +93,8 @@ export default function FamilleEntreprises() {
|
|||||||
const [editTarget, setEditTarget] = useState(null); // membre à éditer
|
const [editTarget, setEditTarget] = useState(null); // membre à éditer
|
||||||
|
|
||||||
/* Formulaires */
|
/* Formulaires */
|
||||||
const emptyFam = { prenom: '', nom_famille: '' };
|
const emptyFam = { prenom: '', nom_famille: '', email: '' };
|
||||||
const emptyEnt = { nom: '', type_fiscal: 'PM' };
|
const emptyEnt = { nom: '', type_fiscal: 'PM', email: '' };
|
||||||
const [famForm, setFamForm] = useState(emptyFam);
|
const [famForm, setFamForm] = useState(emptyFam);
|
||||||
const [entForm, setEntForm] = useState(emptyEnt);
|
const [entForm, setEntForm] = useState(emptyEnt);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -116,10 +116,10 @@ export default function FamilleEntreprises() {
|
|||||||
setEditTarget(m);
|
setEditTarget(m);
|
||||||
if (m.type === 'famille') {
|
if (m.type === 'famille') {
|
||||||
const restNom = m.prenom ? m.nom.replace(m.prenom, '').trim() : m.nom;
|
const restNom = m.prenom ? m.nom.replace(m.prenom, '').trim() : m.nom;
|
||||||
setFamForm({ prenom: m.prenom || '', nom_famille: restNom });
|
setFamForm({ prenom: m.prenom || '', nom_famille: restNom, email: m.email || '' });
|
||||||
setModalFamille(true);
|
setModalFamille(true);
|
||||||
} else {
|
} else {
|
||||||
setEntForm({ nom: m.nom, type_fiscal: m.type_fiscal || 'PM' });
|
setEntForm({ nom: m.nom, type_fiscal: m.type_fiscal || 'PM', email: m.email || '' });
|
||||||
setModalEntreprise(true);
|
setModalEntreprise(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -142,6 +142,7 @@ export default function FamilleEntreprises() {
|
|||||||
prenom: famForm.prenom.trim() || null,
|
prenom: famForm.prenom.trim() || null,
|
||||||
type: 'famille',
|
type: 'famille',
|
||||||
type_fiscal: 'PP',
|
type_fiscal: 'PP',
|
||||||
|
email: famForm.email.trim() || null,
|
||||||
};
|
};
|
||||||
if (editTarget) {
|
if (editTarget) {
|
||||||
await api.put(`/investisseurs/${editTarget.id}`, payload);
|
await api.put(`/investisseurs/${editTarget.id}`, payload);
|
||||||
@@ -163,6 +164,7 @@ export default function FamilleEntreprises() {
|
|||||||
prenom: null,
|
prenom: null,
|
||||||
type: 'entreprise',
|
type: 'entreprise',
|
||||||
type_fiscal: entForm.type_fiscal,
|
type_fiscal: entForm.type_fiscal,
|
||||||
|
email: entForm.email.trim() || null,
|
||||||
};
|
};
|
||||||
if (editTarget) {
|
if (editTarget) {
|
||||||
await api.put(`/investisseurs/${editTarget.id}`, payload);
|
await api.put(`/investisseurs/${editTarget.id}`, payload);
|
||||||
@@ -279,6 +281,12 @@ export default function FamilleEntreprises() {
|
|||||||
onChange={e => setFamForm({ ...famForm, nom_famille: e.target.value })}
|
onChange={e => setFamForm({ ...famForm, nom_famille: e.target.value })}
|
||||||
placeholder="CROGUENNEC" />
|
placeholder="CROGUENNEC" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="modal-field">
|
||||||
|
<label>Adresse e-mail <span className="text-muted" style={{ fontWeight: 400 }}>(optionnel)</span></label>
|
||||||
|
<input type="email" value={famForm.email}
|
||||||
|
onChange={e => setFamForm({ ...famForm, email: e.target.value })}
|
||||||
|
placeholder="olivier@example.com" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -317,6 +325,12 @@ export default function FamilleEntreprises() {
|
|||||||
<option value="SA">SA</option>
|
<option value="SA">SA</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="modal-field">
|
||||||
|
<label>Adresse e-mail <span className="text-muted" style={{ fontWeight: 400 }}>(optionnel)</span></label>
|
||||||
|
<input type="email" value={entForm.email}
|
||||||
|
onChange={e => setEntForm({ ...entForm, email: e.target.value })}
|
||||||
|
placeholder="contact@entreprise.com" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
|
||||||
export default function ForgotPassword() {
|
export default function ForgotPassword() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@@ -35,39 +36,8 @@ export default function ForgotPassword() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
||||||
|
|
||||||
{/* ── Colonne gauche — image ───────────────────────────── */}
|
<AuthBgCol appInfo={appInfo} />
|
||||||
<div className="login-bg-col" style={{
|
|
||||||
flex: '1 1 50%',
|
|
||||||
display: 'none',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
background: '#0d0d0d',
|
|
||||||
}}>
|
|
||||||
<img
|
|
||||||
src="/login-bg.jpg"
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
onError={e => { e.target.style.display = 'none'; }}
|
|
||||||
style={{
|
|
||||||
position: 'absolute', inset: 0,
|
|
||||||
width: '100%', height: '100%',
|
|
||||||
objectFit: 'cover', opacity: 0.85,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute', bottom: 40, left: 40,
|
|
||||||
color: '#fff',
|
|
||||||
display: 'flex', alignItems: 'center', gap: 12,
|
|
||||||
}}>
|
|
||||||
{appInfo.iconUrl && (
|
|
||||||
<img src={appInfo.iconUrl} alt="" width={36} height={36}
|
|
||||||
style={{ borderRadius: 8, flexShrink: 0 }} />
|
|
||||||
)}
|
|
||||||
<span style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.3px' }}>
|
|
||||||
{appInfo.appName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Colonne droite — formulaire ──────────────────────── */}
|
{/* ── Colonne droite — formulaire ──────────────────────── */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -212,12 +182,6 @@ export default function ForgotPassword() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.login-bg-col { display: block !important; }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
const DEVICE_KEY = 'cl_device_token';
|
const DEVICE_KEY = 'cl_device_token';
|
||||||
@@ -12,25 +13,6 @@ function fmtCountdown(sec) {
|
|||||||
return `${m}:${s}`;
|
return `${m}:${s}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Colonne image gauche (partagée) ───────────────────────────────────────
|
|
||||||
function BgCol({ appInfo }) {
|
|
||||||
return (
|
|
||||||
<div className="login-bg-col" style={{
|
|
||||||
flex: '1 1 50%', display: 'none',
|
|
||||||
position: 'relative', overflow: 'hidden', background: '#0d0d0d',
|
|
||||||
}}>
|
|
||||||
<img src="/login-bg.jpg" alt="" aria-hidden="true"
|
|
||||||
onError={e => { e.target.style.display = 'none'; }}
|
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', opacity: 0.85 }}
|
|
||||||
/>
|
|
||||||
<div style={{ position: 'absolute', bottom: 40, left: 40, color: '#fff', display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
||||||
{appInfo.iconUrl && <img src={appInfo.iconUrl} alt="" width={36} height={36} style={{ borderRadius: 8, flexShrink: 0 }} />}
|
|
||||||
<span style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.3px' }}>{appInfo.appName}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Header logo (colonne droite) ──────────────────────────────────────────
|
// ── Header logo (colonne droite) ──────────────────────────────────────────
|
||||||
function AppHeader({ appInfo }) {
|
function AppHeader({ appInfo }) {
|
||||||
return (
|
return (
|
||||||
@@ -188,7 +170,7 @@ export default function Login() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
||||||
<BgCol appInfo={appInfo} />
|
<AuthBgCol appInfo={appInfo} />
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: '1 1 50%', display: 'flex', flexDirection: 'column',
|
flex: '1 1 50%', display: 'flex', flexDirection: 'column',
|
||||||
@@ -381,9 +363,6 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
|
||||||
@media (min-width: 768px) { .login-bg-col { display: block !important; } }
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext.jsx';
|
import { useAuth } from '../context/AuthContext.jsx';
|
||||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
const { register } = useAuth();
|
const { register } = useAuth();
|
||||||
@@ -39,39 +40,8 @@ export default function Register() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
||||||
|
|
||||||
{/* ── Colonne gauche — image ───────────────────────────── */}
|
<AuthBgCol appInfo={appInfo} />
|
||||||
<div className="login-bg-col" style={{
|
|
||||||
flex: '1 1 50%',
|
|
||||||
display: 'none',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
background: '#0d0d0d',
|
|
||||||
}}>
|
|
||||||
<img
|
|
||||||
src="/login-bg.jpg"
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
onError={e => { e.target.style.display = 'none'; }}
|
|
||||||
style={{
|
|
||||||
position: 'absolute', inset: 0,
|
|
||||||
width: '100%', height: '100%',
|
|
||||||
objectFit: 'cover', opacity: 0.85,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute', bottom: 40, left: 40,
|
|
||||||
color: '#fff',
|
|
||||||
display: 'flex', alignItems: 'center', gap: 12,
|
|
||||||
}}>
|
|
||||||
{appInfo.iconUrl && (
|
|
||||||
<img src={appInfo.iconUrl} alt="" width={36} height={36}
|
|
||||||
style={{ borderRadius: 8, flexShrink: 0 }} />
|
|
||||||
)}
|
|
||||||
<span style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.3px' }}>
|
|
||||||
{appInfo.appName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Colonne droite — formulaire ──────────────────────── */}
|
{/* ── Colonne droite — formulaire ──────────────────────── */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -255,12 +225,6 @@ export default function Register() {
|
|||||||
</>)} {/* fin verifyEmail ternaire */}
|
</>)} {/* fin verifyEmail ternaire */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.login-bg-col { display: block !important; }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import PasswordStrength from '../components/PasswordStrength.jsx';
|
import PasswordStrength from '../components/PasswordStrength.jsx';
|
||||||
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
|
||||||
export default function ResetPassword() {
|
export default function ResetPassword() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
@@ -42,39 +43,8 @@ export default function ResetPassword() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
||||||
|
|
||||||
{/* ── Colonne gauche — image ───────────────────────────── */}
|
<AuthBgCol appInfo={appInfo} />
|
||||||
<div className="login-bg-col" style={{
|
|
||||||
flex: '1 1 50%',
|
|
||||||
display: 'none',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
background: '#0d0d0d',
|
|
||||||
}}>
|
|
||||||
<img
|
|
||||||
src="/login-bg.jpg"
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
onError={e => { e.target.style.display = 'none'; }}
|
|
||||||
style={{
|
|
||||||
position: 'absolute', inset: 0,
|
|
||||||
width: '100%', height: '100%',
|
|
||||||
objectFit: 'cover', opacity: 0.85,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute', bottom: 40, left: 40,
|
|
||||||
color: '#fff',
|
|
||||||
display: 'flex', alignItems: 'center', gap: 12,
|
|
||||||
}}>
|
|
||||||
{appInfo.iconUrl && (
|
|
||||||
<img src={appInfo.iconUrl} alt="" width={36} height={36}
|
|
||||||
style={{ borderRadius: 8, flexShrink: 0 }} />
|
|
||||||
)}
|
|
||||||
<span style={{ fontSize: 18, fontWeight: 600, letterSpacing: '-0.3px' }}>
|
|
||||||
{appInfo.appName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── Colonne droite — formulaire ──────────────────────── */}
|
{/* ── Colonne droite — formulaire ──────────────────────── */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -245,12 +215,6 @@ export default function ResetPassword() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.login-bg-col { display: block !important; }
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import AuthBgCol from '../components/AuthBgCol.jsx';
|
||||||
|
|
||||||
export default function VerifyEmail() {
|
export default function VerifyEmail() {
|
||||||
const [params] = useSearchParams();
|
const [params] = useSearchParams();
|
||||||
@@ -26,19 +27,8 @@ export default function VerifyEmail() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
<div style={{ display: 'flex', minHeight: '100dvh' }}>
|
||||||
|
|
||||||
<div className="login-bg-col" style={{
|
<AuthBgCol appInfo={appInfo} />
|
||||||
flex: '1 1 50%', display: 'none', position: 'relative',
|
|
||||||
overflow: 'hidden', background: '#0d0d0d',
|
|
||||||
}}>
|
|
||||||
<img src="/login-bg.jpg" alt="" aria-hidden="true"
|
|
||||||
onError={e => { e.target.style.display = 'none'; }}
|
|
||||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', opacity: 0.85 }}
|
|
||||||
/>
|
|
||||||
<div style={{ position: 'absolute', bottom: 40, left: 40, color: '#fff', display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
||||||
{appInfo.iconUrl && <img src={appInfo.iconUrl} alt="" width={36} height={36} style={{ borderRadius: 8 }} />}
|
|
||||||
<span style={{ fontSize: 18, fontWeight: 600 }}>{appInfo.appName}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: '1 1 50%', display: 'flex', flexDirection: 'column',
|
flex: '1 1 50%', display: 'flex', flexDirection: 'column',
|
||||||
@@ -118,10 +108,6 @@ export default function VerifyEmail() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{`
|
|
||||||
@media (min-width: 768px) { .login-bg-col { display: block !important; } }
|
|
||||||
`}</style>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-34
@@ -1899,7 +1899,10 @@ tr:hover td { background: var(--surface-2); }
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
bord
|
border-radius: 4px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── DrillCellPanel table ── */
|
/* ── DrillCellPanel table ── */
|
||||||
.drill-table {
|
.drill-table {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -1973,42 +1976,64 @@ tr:hover td { background: var(--surface-2); }
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
.pagination-btn:disabled {
|
|
||||||
opacity: .35;
|
/* ── Pages d'authentification (login, register, forgot, reset, verify) ───── */
|
||||||
cursor: default;
|
.login-bg-col {
|
||||||
}
|
flex: 1 1 50%;
|
||||||
.pagination-pages {
|
display: none;
|
||||||
font-size: var(--fs-xs);
|
position: relative;
|
||||||
color: var(--text-muted);
|
overflow: hidden;
|
||||||
padding: 0 8px;
|
|
||||||
white-space: nowrap;
|
/* Image split dark/light : dark à gauche, light à droite.
|
||||||
|
background-size: 200% auto → zoom sur une seule moitié.
|
||||||
|
Par défaut (thème clair) : moitié droite. */
|
||||||
|
background-image: url('/login-bg.png');
|
||||||
|
background-size: 200% auto;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 100% 50%;
|
||||||
|
background-color: #f0f2f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Barre de filtres admin users ─────────────────────────────────────────── */
|
[data-theme="dark"] .login-bg-col {
|
||||||
.admin-filters {
|
/* Thème sombre : moitié gauche de l'image. */
|
||||||
|
background-position: 0% 50%;
|
||||||
|
background-color: #0d0e1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.login-bg-col { display: block !important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-bg-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
/* léger dégradé pour améliorer la lisibilité du badge app en bas */
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,.35) 0%, transparent 40%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .login-bg-overlay {
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,.55) 0%, transparent 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-bg-brand {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 36px;
|
||||||
|
left: 36px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -.3px;
|
||||||
|
text-shadow: 0 1px 4px rgba(0,0,0,.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-mobile-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 32px;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.admin-filters input[type=search] {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 180px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
}
|
|
||||||
.admin-filters input[type=search]::placeholder { color: var(--text-muted); }
|
|
||||||
.admin-filters select {
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
min-width: 160px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user