Amélioration de la notion de workspace
This commit is contained in:
1 parent
1d5737703b
commit
c918de1e7d
11 files changed
+202
-58
No files matched your search
@@ -83,14 +83,15 @@ function RefModal({ row, onClose, onSaved }) {
|
||||
setCatsInv(cats);
|
||||
setSecteursInv(sects);
|
||||
setWorkspaces(ws);
|
||||
// À la création, présélectionne le workspace "crowdlending" (seul
|
||||
// workspace réel aujourd'hui) — l'utilisateur reste libre d'ajuster ;
|
||||
// au moins une affectation reste exigée à l'enregistrement.
|
||||
// À la création, présélectionne le workspace de type "crowdlending"
|
||||
// (19/09/26 : testé sur le type, plus sur le slug — cf. db/index.js) —
|
||||
// l'utilisateur reste libre d'ajuster ; au moins une affectation reste
|
||||
// exigée à l'enregistrement.
|
||||
if (isNew) {
|
||||
setForm(f => (f.workspace_ids && f.workspace_ids.length > 0) ? f : {
|
||||
...f,
|
||||
workspace_ids: (() => {
|
||||
const cl = ws.find(w => w.slug === 'crowdlending');
|
||||
const cl = ws.find(w => w.type === 'crowdlending');
|
||||
return cl ? [cl.id] : ws.map(w => w.id);
|
||||
})(),
|
||||
});
|
||||
|
||||
@@ -40,7 +40,29 @@ function Toggle({ checked, onClick, title }) {
|
||||
);
|
||||
}
|
||||
|
||||
const emptyForm = { slug: '', nom: '', libelleMenu: '', description: '', actifGlobal: true, ordre: 0 };
|
||||
// Types reconnus par l'app (19/09/26 — cf. db/index.js pour le contexte complet de cette
|
||||
// migration : c'est ce champ, et non plus le slug, qui détermine quel modèle de pages
|
||||
// crowdlending/PE l'app affiche pour ce workspace). Choisi une seule fois à la création, comme
|
||||
// le slug — jamais modifiable ensuite (changer le type d'un workspace déjà peuplé de données
|
||||
// changerait silencieusement le modèle de page/API qui s'applique à ces données).
|
||||
const WORKSPACE_TYPE_LABELS = { crowdlending: 'Crowdlending', private_equity: 'Private Equity' };
|
||||
|
||||
// Valeurs pré-remplies suggérées selon le type choisi (retour d'Olivier, 20/09/26) — appliquées
|
||||
// uniquement à la création et uniquement sur les champs encore vides (ne jamais écraser ce que
|
||||
// l'admin a déjà saisi), pour lui faire gagner du temps sans lui imposer les valeurs : il reste
|
||||
// libre de tout changer avant d'enregistrer.
|
||||
const WORKSPACE_TYPE_SUGGESTIONS = {
|
||||
crowdlending: {
|
||||
slug: 'crowdlending', nom: 'Crowdlending', libelleMenu: 'Crowdlending',
|
||||
description: 'Suivi de portefeuille crowdlending',
|
||||
},
|
||||
private_equity: {
|
||||
slug: 'private-equity', nom: 'Private Equity', libelleMenu: 'Private Equity',
|
||||
description: 'Suivi des investissements de type Private Equity',
|
||||
},
|
||||
};
|
||||
|
||||
const emptyForm = { slug: '', nom: '', libelleMenu: '', description: '', type: '', actifGlobal: true, ordre: 0 };
|
||||
|
||||
function WorkspaceFormModal({ open, onClose, onSaved, workspace }) {
|
||||
const isEdit = !!workspace;
|
||||
@@ -57,6 +79,7 @@ function WorkspaceFormModal({ open, onClose, onSaved, workspace }) {
|
||||
nom: workspace.nom,
|
||||
libelleMenu: workspace.libelle_menu,
|
||||
description: workspace.description || '',
|
||||
type: workspace.type || '',
|
||||
actifGlobal: !!workspace.actif_global,
|
||||
ordre: workspace.ordre ?? 0,
|
||||
}
|
||||
@@ -66,7 +89,7 @@ function WorkspaceFormModal({ open, onClose, onSaved, workspace }) {
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setErr(null);
|
||||
if (!form.nom.trim() || !form.libelleMenu.trim() || (!isEdit && !form.slug.trim())) {
|
||||
if (!form.nom.trim() || !form.libelleMenu.trim() || (!isEdit && !form.slug.trim()) || (!isEdit && !form.type)) {
|
||||
setErr('Merci de renseigner les champs obligatoires (*)');
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +105,7 @@ function WorkspaceFormModal({ open, onClose, onSaved, workspace }) {
|
||||
if (isEdit) {
|
||||
await api.put(`/admin/workspaces/${workspace.id}`, payload);
|
||||
} else {
|
||||
await api.post('/admin/workspaces', { ...payload, slug: form.slug.trim() });
|
||||
await api.post('/admin/workspaces', { ...payload, slug: form.slug.trim(), type: form.type });
|
||||
}
|
||||
onSaved();
|
||||
} catch (e2) {
|
||||
@@ -92,6 +115,24 @@ function WorkspaceFormModal({ open, onClose, onSaved, workspace }) {
|
||||
}
|
||||
}
|
||||
|
||||
// N'a d'effet qu'à la création (le type n'est de toute façon pas modifiable à l'édition,
|
||||
// cf. rendu du champ Type ci-dessous) : pré-remplit les champs encore vides avec les valeurs
|
||||
// suggérées pour le type choisi, sans jamais écraser une saisie déjà faite par l'admin.
|
||||
function handleTypeChange(newType) {
|
||||
setForm(f => {
|
||||
const suggestion = WORKSPACE_TYPE_SUGGESTIONS[newType];
|
||||
if (!suggestion) return { ...f, type: newType };
|
||||
return {
|
||||
...f,
|
||||
type: newType,
|
||||
slug: f.slug.trim() ? f.slug : suggestion.slug,
|
||||
nom: f.nom.trim() ? f.nom : suggestion.nom,
|
||||
libelleMenu: f.libelleMenu.trim() ? f.libelleMenu : suggestion.libelleMenu,
|
||||
description: f.description.trim() ? f.description : suggestion.description,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@@ -108,6 +149,25 @@ function WorkspaceFormModal({ open, onClose, onSaved, workspace }) {
|
||||
}
|
||||
>
|
||||
<form id="workspace-form" onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 4 }}>
|
||||
Type *
|
||||
</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={form.type}
|
||||
disabled={isEdit}
|
||||
onChange={e => handleTypeChange(e.target.value)}
|
||||
>
|
||||
<option value="">— Choisir —</option>
|
||||
<option value="crowdlending">Crowdlending</option>
|
||||
<option value="private_equity">Private Equity</option>
|
||||
</select>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>
|
||||
Détermine les pages affichées pour cet espace (Crowdlending ou Private Equity) — non
|
||||
modifiable après création. Un seul espace de type Crowdlending peut exister.
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label style={{ display: 'block', fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 4 }}>
|
||||
Identifiant (slug) *
|
||||
@@ -256,6 +316,7 @@ export default function WorkspacesSection() {
|
||||
<th style={{ paddingLeft: 20 }}>Nom</th>
|
||||
<th>Libellé menu</th>
|
||||
<th>Slug</th>
|
||||
<th>Type</th>
|
||||
<th style={{ textAlign: 'center' }}>Disponible</th>
|
||||
<th style={{ textAlign: 'right' }}>Ordre</th>
|
||||
<th style={{ textAlign: 'right' }}>Utilisateurs</th>
|
||||
@@ -268,6 +329,7 @@ export default function WorkspacesSection() {
|
||||
<td style={{ paddingLeft: 20, fontWeight: 600 }}>{w.nom}</td>
|
||||
<td>{w.libelle_menu}</td>
|
||||
<td style={{ color: 'var(--text-muted)', fontFamily: 'monospace', fontSize: 12 }}>{w.slug}</td>
|
||||
<td style={{ color: 'var(--text-muted)' }}>{WORKSPACE_TYPE_LABELS[w.type] || w.type}</td>
|
||||
<td style={{ textAlign: 'center' }}>
|
||||
{w.actif_global
|
||||
? <span className="badge badge-success">Oui</span>
|
||||
@@ -286,8 +348,8 @@ export default function WorkspacesSection() {
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-ghost"
|
||||
title={w.slug === 'crowdlending' ? 'Le workspace Crowdlending ne peut pas être supprimé' : 'Supprimer'}
|
||||
disabled={w.slug === 'crowdlending'}
|
||||
title={w.type === 'crowdlending' ? 'Le workspace Crowdlending ne peut pas être supprimé' : 'Supprimer'}
|
||||
disabled={w.type === 'crowdlending'}
|
||||
onClick={() => setToDelete(w)}
|
||||
>
|
||||
<IconTrash />
|
||||
|
||||
Reference in new issue
Block a user