feat: gestion directe des banques enfants et attribution des coquillages par activite
This commit is contained in:
@@ -290,6 +290,71 @@ app.delete('/api/activities/:id', (req, res) => {
|
|||||||
res.json({ message: 'Activité supprimée.' });
|
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
|
// 7. POST /api/requests
|
||||||
app.post('/api/requests', (req, res) => {
|
app.post('/api/requests', (req, res) => {
|
||||||
const {
|
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) });
|
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)
|
// 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) => {
|
app.post('/api/alexa/voice-intent', (req, res) => {
|
||||||
const { intent, slots = {}, personId = null, personGivenName = null } = req.body;
|
const { intent, slots = {}, personId = null, personGivenName = null } = req.body;
|
||||||
|
|||||||
@@ -104,6 +104,28 @@ export default function App() {
|
|||||||
const [unlockAfterInterest, setUnlockAfterInterest] = useState(false);
|
const [unlockAfterInterest, setUnlockAfterInterest] = useState(false);
|
||||||
const [approvalShells, setApprovalShells] = useState({});
|
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)
|
// Alexa simulator state (Voice + Echo Show visual card)
|
||||||
const [alexaResponse, setAlexaResponse] = useState(
|
const [alexaResponse, setAlexaResponse] = useState(
|
||||||
'Cliquez sur une commande vocale ci-dessous pour entendre Alexa répondre et voir l\'écran Echo Show !'
|
'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
|
// Apply monthly interest engine
|
||||||
async function handleApplyMonthlyInterest() {
|
async function handleApplyMonthlyInterest() {
|
||||||
try {
|
try {
|
||||||
@@ -547,6 +644,25 @@ export default function App() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div className="space-y-3 mb-6">
|
<div className="space-y-3 mb-6">
|
||||||
@@ -752,39 +868,47 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contribute Buttons for each Child (Locked if pending!) */}
|
{/* 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-2">
|
<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">
|
||||||
<span className="text-xs text-slate-400">Ajouter des 🐚 :</span>
|
<button
|
||||||
<div className="flex items-center gap-2">
|
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) => {
|
{children.map((child) => {
|
||||||
const isLocked = Array.isArray(act.locked_child_ids) && act.locked_child_ids.includes(child.id);
|
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 (
|
return (
|
||||||
<button
|
<button
|
||||||
key={child.id}
|
key={child.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setModalConfig({
|
setDirectContributeModal({
|
||||||
|
open: true,
|
||||||
|
activity: act,
|
||||||
childId: child.id,
|
childId: child.id,
|
||||||
childName: child.name,
|
|
||||||
type: 'CONTRIBUTE_PHYSICAL',
|
|
||||||
shells: Math.min(5, Math.max(1, act.remaining_shells || 5)),
|
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>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -1264,44 +1388,114 @@ export default function App() {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</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">
|
<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="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
<div>
|
||||||
{children.map((c) => (
|
<h2 className="text-lg font-bold flex items-center gap-2 text-white">
|
||||||
<div key={c.id} className="bg-slate-950/80 border border-white/10 rounded-2xl p-4 space-y-3">
|
<Coins className="w-5 h-5 text-amber-400" /> Gestion & Édition de la Banque des Enfants
|
||||||
<div className="flex items-center justify-between">
|
</h2>
|
||||||
<span className="font-bold flex items-center gap-2">
|
<p className="text-xs text-slate-400">
|
||||||
<span>{c.avatar}</span> {c.name}
|
Transférez les coquillages réels reçus ou utilisés, ou modifiez directement les soldes de Hailey, Violette et Andreas.
|
||||||
</span>
|
</p>
|
||||||
<span className="text-xs text-slate-400">
|
</div>
|
||||||
{c.available_shells} dispo • {c.locked_shells} bloqués • {c.euros_balance.toFixed(2)}€
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<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/90 border border-white/10 rounded-3xl p-5 space-y-4 shadow-md">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<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="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={() => {
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<span>🔄</span> Transfert Réel/Virtuel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<span>✏️</span> Éditer les soldes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAdjustChild(c.id, 'available_shells', 1, 'Bonus parent (+1)')}
|
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"
|
className="px-2 py-1 rounded-lg bg-white/5 hover:bg-white/15 text-slate-300 text-[11px] font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
+1 🐚 Dispo
|
+1 🐚
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAdjustChild(c.id, 'available_shells', 5, 'Bonus parent (+5)')}
|
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"
|
className="px-2 py-1 rounded-lg bg-white/5 hover:bg-white/15 text-slate-300 text-[11px] font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
+5 🐚 Dispo
|
+5 🐚
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAdjustChild(c.id, 'available_shells', -1, 'Retrait parent (-1)')}
|
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"
|
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 🐚 Dispo
|
-1 🐚
|
||||||
</button>
|
</button>
|
||||||
{c.locked_shells > 0 && (
|
{c.locked_shells > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAdjustChild(c.id, 'unlock_all', 0, 'Déblocage épargne')}
|
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"
|
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})
|
🔓 Tout débloquer ({c.locked_shells})
|
||||||
</button>
|
</button>
|
||||||
@@ -1309,13 +1503,14 @@ export default function App() {
|
|||||||
{c.euros_balance > 0 && (
|
{c.euros_balance > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAdjustChild(c.id, 'reset_euros', 0, 'Billet donné en vrai')}
|
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"
|
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 les {c.euros_balance.toFixed(2)}€ en vrai
|
💶 Donner {c.euros_balance.toFixed(2)}€
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1757,6 +1952,415 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user