feat: gestion directe des banques enfants et attribution des coquillages par activite

This commit is contained in:
2026-11-11 21:53:45 +01:00
parent 7051681531
commit 9c48217c36
2 changed files with 849 additions and 61 deletions

View File

@@ -290,6 +290,71 @@ app.delete('/api/activities/:id', (req, res) => {
res.json({ message: 'Activité supprimée.' });
});
// 6b. POST /api/activities/:id/direct-contribute (Attribution directe de coquillages par les parents pour un enfant)
app.post('/api/activities/:id/direct-contribute', (req, res) => {
const activityId = Number(req.params.id);
const activity = db.prepare('SELECT * FROM activities WHERE id = ?').get(activityId);
if (!activity) return res.status(404).json({ error: 'Activité introuvable.' });
const { child_id, shells, source_type = 'PHYSICAL' } = req.body;
const child = db.prepare('SELECT * FROM children WHERE id = ?').get(Number(child_id));
if (!child) return res.status(404).json({ error: 'Enfant introuvable.' });
const count = Number(shells);
if (!count || count <= 0) {
return res.status(400).json({ error: 'Veuillez saisir un nombre de coquillages supérieur à 0.' });
}
const isVirtual = source_type === 'VIRTUAL';
if (isVirtual && child.available_shells < count) {
return res.status(400).json({
error: `Solde virtuel insuffisant : ${child.name} n'a que ${child.available_shells} coquillages disponibles.`
});
}
const now = new Date().toISOString();
// Déduire si c'est virtuel
if (isVirtual) {
db.prepare('UPDATE children SET available_shells = available_shells - ? WHERE id = ?').run(count, child.id);
}
// Créditer l'activité
const newTotal = activity.current_shells + count;
const newStatus = newTotal >= activity.cost_shells ? 'UNLOCKED' : activity.status;
db.prepare('UPDATE activities SET current_shells = ?, status = ? WHERE id = ?').run(newTotal, newStatus, activity.id);
// Enregistrer la contribution
db.prepare(`
INSERT INTO activity_contributions (activity_id, child_id, shells, created_at)
VALUES (?, ?, ?, ?)
`).run(activity.id, child.id, count, now);
// Résoudre d'éventuelles requêtes Alexa PENDING pour cet enfant sur cette activité afin de déverrouiller son bouton
db.prepare(`
UPDATE pending_requests
SET status = 'APPROVED', resolved_at = ?
WHERE activity_id = ? AND child_id = ? AND status = 'PENDING'
`).run(now, activity.id, child.id);
// Enregistrer la transaction
const sourceLabel = isVirtual ? 'déduits du compte virtuel' : 'coquillages physiques remis';
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'CONTRIBUTION', ?, 0, ?, ?)
`).run(
child.id,
isVirtual ? -count : 0,
`${child.name} a mis ${count} 🐚 (${sourceLabel}) pour "${activity.title}" (${newTotal}/${activity.cost_shells})`,
now
);
res.json({
message: `${count} 🐚 attribués avec succès à "${activity.title}" pour ${child.name} !`,
activity: getEnrichedActivities().find((a) => a.id === activityId)
});
});
// 7. POST /api/requests
app.post('/api/requests', (req, res) => {
const {
@@ -701,6 +766,125 @@ app.post('/api/children/:id/adjust', (req, res) => {
res.json({ message: 'Compte mis à jour.', child: getEnrichedChildren().find((c) => c.id === id) });
});
// 13b. PUT /api/children/:id (Édition directe des soldes de l'enfant par les parents)
app.put('/api/children/:id', (req, res) => {
const id = Number(req.params.id);
const child = db.prepare('SELECT * FROM children WHERE id = ?').get(id);
if (!child) return res.status(404).json({ error: 'Enfant introuvable.' });
const { available_shells, locked_shells, euros_balance } = req.body;
const newAvailable = available_shells !== undefined ? Math.max(0, Number(available_shells)) : child.available_shells;
const newLocked = locked_shells !== undefined ? Math.max(0, Number(locked_shells)) : child.locked_shells;
const newEuros = euros_balance !== undefined ? Math.max(0, Number(euros_balance)) : child.euros_balance;
const now = new Date().toISOString();
db.prepare(`
UPDATE children
SET available_shells = ?, locked_shells = ?, euros_balance = ?
WHERE id = ?
`).run(newAvailable, newLocked, newEuros, id);
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'ADMIN_EDIT', ?, ?, ?, ?)
`).run(
id,
newAvailable - child.available_shells,
newEuros - child.euros_balance,
`Mise à jour des soldes de ${child.name} : ${newAvailable} 🐚 dispo, ${newLocked} 🔒 bloqués, ${newEuros.toFixed(2)} €`,
now
);
res.json({
message: `Soldes de ${child.name} enregistrés avec succès !`,
child: getEnrichedChildren().find((c) => c.id === id)
});
});
// 13c. POST /api/children/:id/transfer (Transfert réel <-> virtuel, blocage/déblocage rapide)
app.post('/api/children/:id/transfer', (req, res) => {
const id = Number(req.params.id);
const child = db.prepare('SELECT * FROM children WHERE id = ?').get(id);
if (!child) return res.status(404).json({ error: 'Enfant introuvable.' });
const { type, shells, reason = '' } = req.body;
const count = Number(shells);
if (!count || count <= 0) {
return res.status(400).json({ error: 'Le nombre de coquillages doit être supérieur à 0.' });
}
const now = new Date().toISOString();
switch (type) {
case 'REAL_TO_AVAILABLE': {
// Coquillages physiques reçus -> crédités sur le compte disponible
db.prepare('UPDATE children SET available_shells = available_shells + ? WHERE id = ?').run(count, id);
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'TRANSFER_REAL_AVAILABLE', ?, 0, ?, ?)
`).run(id, count, `Dépôt réel : +${count} 🐚 physiques ajoutés au compte disponible de ${child.name} (${reason || 'Remis en main propre'})`, now);
break;
}
case 'REAL_TO_LOCKED': {
// Coquillages physiques reçus -> directement bloqués sur le livret (+intérêts)
db.prepare('UPDATE children SET locked_shells = locked_shells + ? WHERE id = ?').run(count, id);
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'TRANSFER_REAL_LOCKED', ?, 0, ?, ?)
`).run(id, count, `Dépôt réel bloqué : +${count} 🔒 bloqués pour ${child.name} (${reason || 'Livret épargne'})`, now);
break;
}
case 'AVAILABLE_TO_REAL': {
// L'enfant reprend/utilise des coquillages réels -> débités du compte virtuel disponible
if (child.available_shells < count) {
return res.status(400).json({ error: `Solde insuffisant : ${child.name} n'a que ${child.available_shells} coquillages disponibles.` });
}
db.prepare('UPDATE children SET available_shells = available_shells - ? WHERE id = ?').run(count, id);
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'TRANSFER_AVAILABLE_REAL', ?, 0, ?, ?)
`).run(id, -count, `Retrait réel : -${count} 🐚 utilisés dans la vraie vie par ${child.name} (${reason || 'Utilisation réelle'})`, now);
break;
}
case 'LOCK': {
// Transfert interne : disponible -> bloqué
if (child.available_shells < count) {
return res.status(400).json({ error: `Solde disponible insuffisant (${child.available_shells} 🐚 dispo).` });
}
db.prepare('UPDATE children SET available_shells = available_shells - ?, locked_shells = locked_shells + ? WHERE id = ?').run(count, count, id);
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'LOCK_VIRTUAL', 0, 0, ?, ?)
`).run(id, `${child.name} a bloqué ${count} 🐚 vers son livret épargne`, now);
break;
}
case 'UNLOCK': {
// Transfert interne : bloqué -> disponible
if (child.locked_shells < count) {
return res.status(400).json({ error: `Solde bloqué insuffisant (${child.locked_shells} 🔒 bloqués).` });
}
db.prepare('UPDATE children SET available_shells = available_shells + ?, locked_shells = locked_shells - ? WHERE id = ?').run(count, count, id);
db.prepare(`
INSERT INTO transactions (child_id, type, shells_delta, euros_delta, description, created_at)
VALUES (?, 'UNLOCK_VIRTUAL', 0, 0, ?, ?)
`).run(id, `${count} 🐚 bloqués reversés sur le compte disponible de ${child.name}`, now);
break;
}
default:
return res.status(400).json({ error: `Type de transfert inconnu: ${type}` });
}
res.json({
message: `Opération enregistrée pour ${child.name} !`,
child: getEnrichedChildren().find((c) => c.id === id)
});
});
// 14. POST /api/alexa/voice-intent (Avec support Personalisation Voice ID, cartes illustrées & APL Echo Show)
app.post('/api/alexa/voice-intent', (req, res) => {
const { intent, slots = {}, personId = null, personGivenName = null } = req.body;

View File

@@ -104,6 +104,28 @@ export default function App() {
const [unlockAfterInterest, setUnlockAfterInterest] = useState(false);
const [approvalShells, setApprovalShells] = useState({});
// Modal for Parents Direct Bank Management & Real/Virtual Transfer
const [editBankModal, setEditBankModal] = useState({
open: false,
child: null,
activeMode: 'transfer', // 'transfer' | 'manual'
transferType: 'REAL_TO_AVAILABLE',
shells: 5,
reason: '',
manualAvailable: 0,
manualLocked: 0,
manualEuros: 0
});
// Modal for Parents Direct Contribution to an Activity
const [directContributeModal, setDirectContributeModal] = useState({
open: false,
activity: null,
childId: 1,
shells: 5,
sourceType: 'PHYSICAL' // 'PHYSICAL' | 'VIRTUAL'
});
// Alexa simulator state (Voice + Echo Show visual card)
const [alexaResponse, setAlexaResponse] = useState(
'Cliquez sur une commande vocale ci-dessous pour entendre Alexa répondre et voir l\'écran Echo Show !'
@@ -249,6 +271,81 @@ export default function App() {
}
}
// Direct Parent attribution of shells to an activity
async function handleDirectContribute(e) {
e.preventDefault();
if (!directContributeModal.activity) return;
try {
const res = await fetch(`${API_URL}/activities/${directContributeModal.activity.id}/direct-contribute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
child_id: Number(directContributeModal.childId),
shells: Number(directContributeModal.shells),
source_type: directContributeModal.sourceType
})
});
const json = await res.json();
if (!res.ok) {
showToast(json.error || 'Erreur lors de l\'attribution', true);
return;
}
showToast(json.message);
setDirectContributeModal({ ...directContributeModal, open: false });
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Direct Parent edit or transfer of child's bank
async function handleSaveBankEdit(e) {
e.preventDefault();
const { child, activeMode, transferType, shells, reason, manualAvailable, manualLocked, manualEuros } = editBankModal;
if (!child) return;
try {
if (activeMode === 'manual') {
const res = await fetch(`${API_URL}/children/${child.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
available_shells: Number(manualAvailable),
locked_shells: Number(manualLocked),
euros_balance: Number(manualEuros)
})
});
const json = await res.json();
if (!res.ok) {
showToast(json.error || 'Erreur lors de la modification', true);
return;
}
showToast(json.message);
} else {
const res = await fetch(`${API_URL}/children/${child.id}/transfer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: transferType,
shells: Number(shells),
reason
})
});
const json = await res.json();
if (!res.ok) {
showToast(json.error || 'Erreur lors du transfert', true);
return;
}
showToast(json.message);
}
setEditBankModal({ ...editBankModal, open: false });
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Apply monthly interest engine
async function handleApplyMonthlyInterest() {
try {
@@ -547,6 +644,25 @@ export default function App() {
</span>
</div>
</div>
<button
onClick={() => {
setEditBankModal({
open: true,
child,
activeMode: 'transfer',
transferType: 'REAL_TO_AVAILABLE',
shells: 5,
reason: '',
manualAvailable: child.available_shells,
manualLocked: child.locked_shells,
manualEuros: child.euros_balance
});
}}
className="px-3 py-1.5 rounded-xl bg-amber-400/20 hover:bg-amber-400/35 border border-amber-400/40 text-amber-300 text-xs font-bold flex items-center gap-1.5 transition cursor-pointer shadow-sm"
title="Gérer la banque : transfert réel/virtuel ou modification des soldes"
>
<Coins className="w-3.5 h-3.5" /> Gérer / Modifier
</button>
</div>
<div className="space-y-3 mb-6">
@@ -752,39 +868,47 @@ export default function App() {
</div>
</div>
{/* Contribute Buttons for each Child (Locked if pending!) */}
<div className="px-5 py-3.5 bg-slate-950/50 border-t border-white/10 flex flex-wrap items-center justify-between gap-2">
<span className="text-xs text-slate-400">Ajouter des 🐚 :</span>
<div className="flex items-center gap-2">
{/* Attribution Directe des Coquillages (Parents & Enfants) */}
<div className="px-5 py-3.5 bg-slate-950/50 border-t border-white/10 flex flex-wrap items-center justify-between gap-3">
<button
onClick={() => {
setDirectContributeModal({
open: true,
activity: act,
childId: children[0]?.id || 1,
shells: Math.min(5, Math.max(1, act.remaining_shells || 5)),
sourceType: 'PHYSICAL'
});
}}
className="px-3.5 py-1.5 rounded-xl bg-gradient-to-r from-amber-400 to-orange-400 text-slate-950 text-xs font-black flex items-center gap-1.5 hover:from-amber-300 hover:to-orange-300 transition cursor-pointer shadow-md shadow-amber-400/20"
title="Attribuer des coquillages à cette activité pour un enfant"
>
<Plus className="w-3.5 h-3.5" /> Attribuer des 🐚
</button>
<div className="flex items-center gap-1.5">
{children.map((child) => {
const isLocked = Array.isArray(act.locked_child_ids) && act.locked_child_ids.includes(child.id);
if (isLocked) {
return (
<span
key={child.id}
className="px-3 py-1.5 rounded-xl bg-slate-800/80 border border-slate-600/50 text-slate-400 text-xs font-bold flex items-center gap-1 cursor-not-allowed"
title="Demande en attente de validation par les parents"
>
🔒 {child.name} (En attente)
</span>
);
}
return (
<button
key={child.id}
onClick={() => {
setModalConfig({
setDirectContributeModal({
open: true,
activity: act,
childId: child.id,
childName: child.name,
type: 'CONTRIBUTE_PHYSICAL',
shells: Math.min(5, Math.max(1, act.remaining_shells || 5)),
activityId: act.id
sourceType: 'PHYSICAL'
});
setModalOpen(true);
}}
className="px-3 py-1.5 rounded-xl bg-amber-400/15 hover:bg-amber-400/30 border border-amber-400/30 text-amber-200 text-xs font-bold transition cursor-pointer flex items-center gap-1"
className={`px-2.5 py-1.5 rounded-xl text-xs font-bold transition cursor-pointer flex items-center gap-1 ${
isLocked
? 'bg-amber-500/20 border border-amber-400/50 text-amber-300 hover:bg-amber-500/30'
: 'bg-white/5 hover:bg-amber-400/20 border border-white/10 hover:border-amber-400/30 text-slate-200'
}`}
title={isLocked ? "Demande Alexa en attente : cliquer pour valider et attribuer" : `Attribuer des coquillages pour ${child.name}`}
>
<span>{child.avatar}</span> {child.name}
<span>{isLocked ? '🔒' : child.avatar}</span> {child.name}
</button>
);
})}
@@ -1264,56 +1388,127 @@ export default function App() {
</section>
</div>
{/* 4. Ajustement Rapide des Comptes & Journal */}
{/* 4. Gestion Complète & Ajustement des Comptes (Parents) */}
<section className="bg-slate-900/90 border border-white/10 rounded-3xl p-6">
<h2 className="text-lg font-bold mb-4">⚙️ Ajustement Manuel des Comptes (Parents)</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-4">
<div>
<h2 className="text-lg font-bold flex items-center gap-2 text-white">
<Coins className="w-5 h-5 text-amber-400" /> Gestion & Édition de la Banque des Enfants
</h2>
<p className="text-xs text-slate-400">
Transférez les coquillages réels reçus ou utilisés, ou modifiez directement les soldes de Hailey, Violette et Andreas.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mb-6">
{children.map((c) => (
<div key={c.id} className="bg-slate-950/80 border border-white/10 rounded-2xl p-4 space-y-3">
<div key={c.id} className="bg-slate-950/90 border border-white/10 rounded-3xl p-5 space-y-4 shadow-md">
<div className="flex items-center justify-between">
<span className="font-bold flex items-center gap-2">
<span>{c.avatar}</span> {c.name}
</span>
<span className="text-xs text-slate-400">
{c.available_shells} dispo • {c.locked_shells} bloqués • {c.euros_balance.toFixed(2)}€
</span>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-3xl">
{c.avatar}
</div>
<div>
<h3 className="font-extrabold text-lg text-white">{c.name}</h3>
<span className="text-xs text-slate-400">
Total : {c.total_shells} 🐚
</span>
</div>
</div>
</div>
<div className="flex flex-wrap gap-1.5">
<button
onClick={() => handleAdjustChild(c.id, 'available_shells', 1, 'Bonus parent (+1)')}
className="px-2.5 py-1.5 rounded-lg bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-xs font-bold cursor-pointer"
>
+1 🐚 Dispo
</button>
<button
onClick={() => handleAdjustChild(c.id, 'available_shells', 5, 'Bonus parent (+5)')}
className="px-2.5 py-1.5 rounded-lg bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-xs font-bold cursor-pointer"
>
+5 🐚 Dispo
</button>
<button
onClick={() => handleAdjustChild(c.id, 'available_shells', -1, 'Retrait parent (-1)')}
className="px-2.5 py-1.5 rounded-lg bg-rose-500/20 hover:bg-rose-500/30 text-rose-300 text-xs font-bold cursor-pointer"
>
-1 🐚 Dispo
</button>
{c.locked_shells > 0 && (
<div className="grid grid-cols-3 gap-2 text-center text-xs">
<div className="bg-amber-500/10 border border-amber-400/20 rounded-xl p-2">
<span className="text-[10px] text-amber-200/80 block">Dispo</span>
<strong className="text-amber-300 text-sm font-extrabold">{c.available_shells} 🐚</strong>
</div>
<div className="bg-cyan-500/10 border border-cyan-400/20 rounded-xl p-2">
<span className="text-[10px] text-cyan-200/80 block">Bloqué</span>
<strong className="text-cyan-300 text-sm font-extrabold">{c.locked_shells} 🔒</strong>
</div>
<div className="bg-emerald-500/10 border border-emerald-400/20 rounded-xl p-2">
<span className="text-[10px] text-emerald-200/80 block">Tirelire</span>
<strong className="text-emerald-300 text-sm font-extrabold">{c.euros_balance.toFixed(2)} €</strong>
</div>
</div>
<div className="space-y-2 pt-1 border-t border-white/10">
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => handleAdjustChild(c.id, 'unlock_all', 0, 'Déblocage épargne')}
className="px-2.5 py-1.5 rounded-lg bg-cyan-500/20 hover:bg-cyan-500/30 text-cyan-300 text-xs font-bold cursor-pointer"
onClick={() => {
setEditBankModal({
open: true,
child: c,
activeMode: 'transfer',
transferType: 'REAL_TO_AVAILABLE',
shells: 5,
reason: '',
manualAvailable: c.available_shells,
manualLocked: c.locked_shells,
manualEuros: c.euros_balance
});
}}
className="px-3 py-2 rounded-xl bg-amber-400/15 hover:bg-amber-400/25 border border-amber-400/30 text-amber-300 text-xs font-bold transition cursor-pointer flex items-center justify-center gap-1"
>
🔓 Tout débloquer ({c.locked_shells})
<span>🔄</span> Transfert Réel/Virtuel
</button>
)}
{c.euros_balance > 0 && (
<button
onClick={() => handleAdjustChild(c.id, 'reset_euros', 0, 'Billet donné en vrai')}
className="px-2.5 py-1.5 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 text-xs font-bold cursor-pointer"
onClick={() => {
setEditBankModal({
open: true,
child: c,
activeMode: 'manual',
transferType: 'REAL_TO_AVAILABLE',
shells: 5,
reason: '',
manualAvailable: c.available_shells,
manualLocked: c.locked_shells,
manualEuros: c.euros_balance
});
}}
className="px-3 py-2 rounded-xl bg-cyan-500/15 hover:bg-cyan-500/25 border border-cyan-400/30 text-cyan-300 text-xs font-bold transition cursor-pointer flex items-center justify-center gap-1"
>
💶 Donner les {c.euros_balance.toFixed(2)}€ en vrai
<span>✏️</span> Éditer les soldes
</button>
)}
</div>
<div className="flex flex-wrap gap-1.5 pt-1">
<button
onClick={() => handleAdjustChild(c.id, 'available_shells', 1, 'Bonus parent (+1)')}
className="px-2 py-1 rounded-lg bg-white/5 hover:bg-white/15 text-slate-300 text-[11px] font-bold cursor-pointer"
>
+1 🐚
</button>
<button
onClick={() => handleAdjustChild(c.id, 'available_shells', 5, 'Bonus parent (+5)')}
className="px-2 py-1 rounded-lg bg-white/5 hover:bg-white/15 text-slate-300 text-[11px] font-bold cursor-pointer"
>
+5 🐚
</button>
<button
onClick={() => handleAdjustChild(c.id, 'available_shells', -1, 'Retrait parent (-1)')}
className="px-2 py-1 rounded-lg bg-white/5 hover:bg-rose-500/20 text-rose-300 text-[11px] font-bold cursor-pointer"
>
-1 🐚
</button>
{c.locked_shells > 0 && (
<button
onClick={() => handleAdjustChild(c.id, 'unlock_all', 0, 'Déblocage épargne')}
className="px-2 py-1 rounded-lg bg-cyan-500/20 hover:bg-cyan-500/30 text-cyan-300 text-[11px] font-bold cursor-pointer"
>
🔓 Tout débloquer ({c.locked_shells})
</button>
)}
{c.euros_balance > 0 && (
<button
onClick={() => handleAdjustChild(c.id, 'reset_euros', 0, 'Billet donné en vrai')}
className="px-2 py-1 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 text-[11px] font-bold cursor-pointer"
>
💶 Donner {c.euros_balance.toFixed(2)}€
</button>
)}
</div>
</div>
</div>
))}
@@ -1757,6 +1952,415 @@ export default function App() {
</div>
</div>
)}
{/* Modal 1 : Édition & Transfert Banque Enfant (Parents) */}
{editBankModal.open && editBankModal.child && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4">
<div className="bg-slate-900 border border-white/15 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-3xl">
{editBankModal.child.avatar}
</div>
<div>
<h3 className="text-xl font-extrabold text-white">
Gérer la banque de {editBankModal.child.name}
</h3>
<span className="text-xs text-slate-400">
Solde actuel : {editBankModal.child.available_shells} 🐚 dispo • {editBankModal.child.locked_shells} 🔒 bloqués • {editBankModal.child.euros_balance.toFixed(2)} €
</span>
</div>
</div>
<button
onClick={() => setEditBankModal({ ...editBankModal, open: false })}
className="text-slate-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
</button>
</div>
{/* Mode Selector Tabs: Transfert Réel/Virtuel vs Édition Manuelle Directe */}
<div className="grid grid-cols-2 gap-2 bg-slate-950 p-1.5 rounded-2xl border border-white/10">
<button
type="button"
onClick={() => setEditBankModal({ ...editBankModal, activeMode: 'transfer' })}
className={`py-2 px-3 rounded-xl text-xs font-bold transition cursor-pointer flex items-center justify-center gap-1.5 ${
editBankModal.activeMode === 'transfer'
? 'bg-amber-400 text-slate-950 shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
<span>🔄</span> Transferts Réel ⇄ Virtuel
</button>
<button
type="button"
onClick={() => setEditBankModal({ ...editBankModal, activeMode: 'manual' })}
className={`py-2 px-3 rounded-xl text-xs font-bold transition cursor-pointer flex items-center justify-center gap-1.5 ${
editBankModal.activeMode === 'manual'
? 'bg-cyan-400 text-slate-950 shadow-md'
: 'text-slate-400 hover:text-white'
}`}
>
<span>✏️</span> Modifier les Soldes Directement
</button>
</div>
<form onSubmit={handleSaveBankEdit} className="space-y-4">
{editBankModal.activeMode === 'transfer' ? (
<>
<div>
<label className="block text-xs font-bold text-slate-300 mb-2">
Type d'opération :
</label>
<div className="grid grid-cols-1 gap-2">
{[
{
id: 'REAL_TO_AVAILABLE',
icon: '📥',
title: 'Dépôt Réel → Virtuel (+🐚 dispo)',
desc: 'L\'enfant donne des vrais coquillages pour les mettre sur son compte disponible.'
},
{
id: 'REAL_TO_LOCKED',
icon: '🔒',
title: 'Dépôt Réel → Livret Bloqué (+🔒)',
desc: 'L\'enfant donne des vrais coquillages à placer directement à +20% d\'intérêts.'
},
{
id: 'AVAILABLE_TO_REAL',
icon: '📤',
title: 'Utilisation Réelle (Virtuel → Réel -🐚)',
desc: 'L\'enfant utilise des coquillages dans la vraie vie (déduits de son solde dispo).'
},
{
id: 'LOCK',
icon: '📈',
title: 'Bloquer : Disponible → Livret',
desc: 'Transférer des coquillages du compte disponible vers le livret épargne.'
},
{
id: 'UNLOCK',
icon: '🔓',
title: 'Débloquer : Livret → Disponible',
desc: 'Rapatrier des coquillages du livret vers le compte disponible.'
}
].map((op) => (
<label
key={op.id}
className={`flex items-start gap-3 p-3 rounded-2xl border cursor-pointer transition ${
editBankModal.transferType === op.id
? 'bg-amber-500/15 border-amber-400 text-white shadow-sm'
: 'bg-slate-950/60 border-white/5 text-slate-300 hover:border-white/20'
}`}
>
<input
type="radio"
name="transferType"
value={op.id}
checked={editBankModal.transferType === op.id}
onChange={(e) => setEditBankModal({ ...editBankModal, transferType: e.target.value })}
className="mt-1 accent-amber-400"
/>
<div className="flex-1">
<span className="font-bold text-xs block text-amber-200">
{op.icon} {op.title}
</span>
<span className="text-[11px] text-slate-400 block mt-0.5">{op.desc}</span>
</div>
</label>
))}
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">
Nombre de coquillages :
</label>
<div className="flex items-center gap-2">
<input
type="number"
min="1"
max="200"
required
value={editBankModal.shells}
onChange={(e) => setEditBankModal({ ...editBankModal, shells: e.target.value })}
className="w-24 bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-center text-lg font-black text-amber-300"
/>
<div className="flex items-center gap-1.5">
{[1, 2, 5, 10, 20].map((n) => (
<button
type="button"
key={n}
onClick={() => setEditBankModal({ ...editBankModal, shells: n })}
className="px-2.5 py-2 rounded-xl bg-white/5 hover:bg-white/15 border border-white/10 text-xs font-bold cursor-pointer text-slate-200"
>
+{n}
</button>
))}
</div>
</div>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">
Motif / Raison (optionnel) :
</label>
<input
type="text"
placeholder="Ex: Récompense devoirs, Achat réel, Rangement..."
value={editBankModal.reason}
onChange={(e) => setEditBankModal({ ...editBankModal, reason: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-xs"
/>
</div>
</>
) : (
<div className="space-y-4">
<p className="text-xs text-slate-300 bg-cyan-950/40 border border-cyan-400/30 p-3 rounded-2xl">
✏️ <strong>Édition directe des soldes :</strong> saisissez ici le montant exact que vous souhaitez attribuer à {editBankModal.child.name}.
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="bg-slate-950 border border-amber-400/30 rounded-2xl p-3">
<label className="block text-xs font-bold text-amber-300 mb-1">
🐚 Disponibles
</label>
<input
type="number"
min="0"
value={editBankModal.manualAvailable}
onChange={(e) => setEditBankModal({ ...editBankModal, manualAvailable: e.target.value })}
className="w-full bg-slate-900 border border-white/15 rounded-xl px-3 py-2 text-center font-black text-amber-300 text-lg"
/>
</div>
<div className="bg-slate-950 border border-cyan-400/30 rounded-2xl p-3">
<label className="block text-xs font-bold text-cyan-300 mb-1">
🔒 Bloqués (+20%)
</label>
<input
type="number"
min="0"
value={editBankModal.manualLocked}
onChange={(e) => setEditBankModal({ ...editBankModal, manualLocked: e.target.value })}
className="w-full bg-slate-900 border border-white/15 rounded-xl px-3 py-2 text-center font-black text-cyan-300 text-lg"
/>
</div>
<div className="bg-slate-950 border border-emerald-400/30 rounded-2xl p-3">
<label className="block text-xs font-bold text-emerald-300 mb-1">
💶 Tirelire (€)
</label>
<input
type="number"
step="0.5"
min="0"
value={editBankModal.manualEuros}
onChange={(e) => setEditBankModal({ ...editBankModal, manualEuros: e.target.value })}
className="w-full bg-slate-900 border border-white/15 rounded-xl px-3 py-2 text-center font-black text-emerald-300 text-lg"
/>
</div>
</div>
</div>
)}
<div className="pt-2 flex items-center justify-end gap-3">
<button
type="button"
onClick={() => setEditBankModal({ ...editBankModal, open: false })}
className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-semibold cursor-pointer"
>
Annuler
</button>
<button
type="submit"
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-amber-400 to-orange-500 hover:from-amber-300 hover:to-orange-400 text-slate-950 text-xs font-black transition cursor-pointer shadow-lg shadow-amber-400/20"
>
{editBankModal.activeMode === 'manual' ? 'Enregistrer les soldes' : 'Valider l\'opération'}
</button>
</div>
</form>
</div>
</div>
)}
{/* Modal 2 : Attribution Directe de Coquillages à une Activité (Parents) */}
{directContributeModal.open && directContributeModal.activity && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4">
<div className="bg-slate-900 border border-white/15 rounded-3xl max-w-lg w-full p-6 space-y-5 shadow-2xl">
<div className="flex items-center justify-between border-b border-white/10 pb-4">
<div>
<span className="text-xs text-amber-300 font-extrabold uppercase tracking-wider block">
Attribution de Coquillages (Parents)
</span>
<h3 className="text-xl font-extrabold text-white flex items-center gap-2 mt-0.5">
<span>{directContributeModal.activity.emoji}</span> {directContributeModal.activity.title}
</h3>
<span className="text-xs text-slate-400">
Pot actuel : {directContributeModal.activity.current_shells} / {directContributeModal.activity.cost_shells} 🐚 (Il manque {directContributeModal.activity.remaining_shells} 🐚)
</span>
</div>
<button
onClick={() => setDirectContributeModal({ ...directContributeModal, open: false })}
className="text-slate-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
</button>
</div>
<form onSubmit={handleDirectContribute} className="space-y-5">
{/* Étape 1 : Quel enfant participe */}
<div>
<label className="block text-xs font-bold text-slate-300 mb-2">
1. Quel enfant met des coquillages ?
</label>
<div className="grid grid-cols-3 gap-2.5">
{children.map((c) => {
const isSelected = Number(directContributeModal.childId) === c.id;
const alreadyPut = directContributeModal.activity.contributions?.find((cb) => cb.child_id === c.id)?.total_shells || 0;
return (
<button
type="button"
key={c.id}
onClick={() => setDirectContributeModal({ ...directContributeModal, childId: c.id })}
className={`p-3 rounded-2xl border text-center transition cursor-pointer flex flex-col items-center justify-between ${
isSelected
? 'bg-amber-500/20 border-amber-400 shadow-md shadow-amber-500/20'
: 'bg-slate-950/70 border-white/10 hover:border-white/20'
}`}
>
<span className="text-3xl mb-1">{c.avatar}</span>
<span className="font-extrabold text-sm text-white">{c.name}</span>
<span className="text-[11px] text-amber-300 mt-0.5">
{c.available_shells} 🐚 dispo
</span>
{alreadyPut > 0 && (
<span className="text-[10px] text-emerald-400 font-bold mt-1 bg-emerald-500/10 px-1.5 py-0.5 rounded-full">
Déjà mis : {alreadyPut} 🐚
</span>
)}
</button>
);
})}
</div>
</div>
{/* Étape 2 : Combien de coquillages */}
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">
2. Nombre de coquillages à ajouter sur cette activité :
</label>
<div className="flex items-center gap-3">
<input
type="number"
min="1"
max={directContributeModal.activity.remaining_shells || 100}
required
value={directContributeModal.shells}
onChange={(e) => setDirectContributeModal({ ...directContributeModal, shells: e.target.value })}
className="w-28 bg-slate-950 border border-white/15 rounded-xl px-4 py-2.5 text-center text-xl font-black text-amber-300"
/>
<div className="flex flex-wrap items-center gap-1.5">
{[1, 2, 5, 10].map((n) => (
<button
type="button"
key={n}
onClick={() => setDirectContributeModal({ ...directContributeModal, shells: n })}
className="px-3 py-2 rounded-xl bg-white/5 hover:bg-white/15 border border-white/10 text-xs font-bold cursor-pointer text-slate-200"
>
+{n}
</button>
))}
{directContributeModal.activity.remaining_shells > 0 && (
<button
type="button"
onClick={() => setDirectContributeModal({ ...directContributeModal, shells: directContributeModal.activity.remaining_shells })}
className="px-3 py-2 rounded-xl bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-400/40 text-emerald-300 text-xs font-bold cursor-pointer"
>
Solder ({directContributeModal.activity.remaining_shells} 🐚)
</button>
)}
</div>
</div>
</div>
{/* Étape 3 : Mode de prélèvement */}
<div>
<label className="block text-xs font-bold text-slate-300 mb-2">
3. Source des coquillages :
</label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<label
className={`p-3 rounded-2xl border cursor-pointer transition flex items-start gap-2.5 ${
directContributeModal.sourceType === 'PHYSICAL'
? 'bg-amber-500/15 border-amber-400 text-white'
: 'bg-slate-950/60 border-white/5 text-slate-300 hover:border-white/20'
}`}
>
<input
type="radio"
name="contribSource"
value="PHYSICAL"
checked={directContributeModal.sourceType === 'PHYSICAL'}
onChange={(e) => setDirectContributeModal({ ...directContributeModal, sourceType: e.target.value })}
className="mt-1 accent-amber-400"
/>
<div>
<span className="font-bold text-xs block text-amber-200">
🪙 Coquillages physiques reçus
</span>
<span className="text-[11px] text-slate-400 block mt-0.5">
L'enfant vous donne de vrais coquillages. Son solde virtuel n'est pas débité.
</span>
</div>
</label>
<label
className={`p-3 rounded-2xl border cursor-pointer transition flex items-start gap-2.5 ${
directContributeModal.sourceType === 'VIRTUAL'
? 'bg-cyan-500/15 border-cyan-400 text-white'
: 'bg-slate-950/60 border-white/5 text-slate-300 hover:border-white/20'
}`}
>
<input
type="radio"
name="contribSource"
value="VIRTUAL"
checked={directContributeModal.sourceType === 'VIRTUAL'}
onChange={(e) => setDirectContributeModal({ ...directContributeModal, sourceType: e.target.value })}
className="mt-1 accent-cyan-400"
/>
<div>
<span className="font-bold text-xs block text-cyan-200">
💻 Déduire du compte virtuel
</span>
<span className="text-[11px] text-slate-400 block mt-0.5">
Retire directement {directContributeModal.shells} 🐚 de son compte disponible.
</span>
</div>
</label>
</div>
</div>
<div className="pt-2 flex items-center justify-end gap-3">
<button
type="button"
onClick={() => setDirectContributeModal({ ...directContributeModal, open: false })}
className="px-4 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 text-xs font-semibold cursor-pointer"
>
Annuler
</button>
<button
type="submit"
className="px-6 py-2.5 rounded-xl bg-gradient-to-r from-emerald-400 to-teal-500 hover:from-emerald-300 hover:to-teal-400 text-slate-950 text-xs font-black transition cursor-pointer shadow-lg shadow-emerald-400/20"
>
Valider l'attribution ({directContributeModal.shells} 🐚)
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}