import React, { useState, useEffect } from 'react';
import {
Lock,
PiggyBank,
Sparkles,
CheckCircle2,
XCircle,
Plus,
Trash2,
Mic,
Volume2,
ShieldCheck,
TrendingUp,
Gift,
RefreshCw,
History,
Settings,
Coins,
Image as ImageIcon,
Upload,
Tv
} from 'lucide-react';
const API_URL =
import.meta.env.VITE_API_URL ||
(typeof window !== 'undefined' && window.location.hostname !== 'localhost'
? '/api'
: 'http://localhost:4050/api');
const EMOJI_OPTIONS = ['🍕', '🎳', '🌊', '🧗', '🎢', '🎬', '🍦', '🏕️', '🎮', '🚲', '🎨', '⚽'];
const PRESET_GALLERY = [
{
label: 'Pizza & Cinéma',
emoji: '🍕',
url: 'https://images.unsplash.com/photo-1513104890138-7c749659a591?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Bowling',
emoji: '🎳',
url: 'https://images.unsplash.com/photo-1538511059256-06459b067f41?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Parc Aquatique',
emoji: '🌊',
url: 'https://images.unsplash.com/photo-1582653291997-079a1c04e5a1?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Accrobranche',
emoji: '🧗',
url: 'https://images.unsplash.com/photo-1542273917363-3b1817f69a2d?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Parc Attractions',
emoji: '🎢',
url: 'https://images.unsplash.com/photo-1513889961551-628c1e5e2ee9?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Zoo & Animaux',
emoji: '🦁',
url: 'https://images.unsplash.com/photo-1534567153574-2b12153a87f0?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Soirée Glaces',
emoji: '🍦',
url: 'https://images.unsplash.com/photo-1497034825429-c343d7c6a68f?auto=format&fit=crop&w=1000&q=80'
},
{
label: 'Laser Game / Jeux',
emoji: '🎮',
url: 'https://images.unsplash.com/photo-1511512578047-dfb367046420?auto=format&fit=crop&w=1000&q=80'
}
];
export default function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('kids'); // 'kids' | 'parents' | 'alexa'
const [toast, setToast] = useState(null);
// Quick operation modal for kids/family
const [modalOpen, setModalOpen] = useState(false);
const [modalConfig, setModalConfig] = useState({
childId: 1,
childName: 'Hailey',
type: 'DEPOSIT_LOCKED',
shells: 4,
activityId: ''
});
// New activity form in Parent tab (with image support for Alexa)
const [newActivity, setNewActivity] = useState({
title: '',
description: '',
emoji: '🎢',
image_url: PRESET_GALLERY[4].url,
cost_shells: 30
});
const [uploadingImage, setUploadingImage] = useState(false);
const [editingActivity, setEditingActivity] = useState(null);
// Bank interest settings form
const [interestInput, setInterestInput] = useState(20);
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 !'
);
const [alexaCard, setAlexaCard] = useState(null);
const [alexaCardsList, setAlexaCardsList] = useState([]);
const [alexaLoading, setAlexaLoading] = useState(false);
const [customAlexa, setCustomAlexa] = useState({
intent: 'ContributeActivityIntent',
ChildName: 'Hailey',
ShellCount: 6,
AccountType: 'bloqué',
ActivityName: 'Bowling en famille'
});
function showToast(msg, isError = false) {
setToast({ msg, isError });
setTimeout(() => setToast(null), 4500);
}
async function fetchDashboard() {
try {
const res = await fetch(`${API_URL}/dashboard`);
const json = await res.json();
setData(json);
if (json.settings) {
setInterestInput(json.settings.interest_rate_percent);
}
if (!alexaCard && json.activities?.length > 0) {
const first = json.activities[0];
setAlexaCard({
title: `${first.emoji} ${first.title}`,
subtitle: `${first.current_shells} / ${first.cost_shells} coquillages (${first.progress_percent}%)`,
text: first.description,
imageUrl: first.image_url,
progressPercent: first.progress_percent
});
}
} catch (err) {
showToast('Impossible de contacter le serveur Backend (port 4050).', true);
} finally {
setLoading(false);
}
}
useEffect(() => {
fetchDashboard();
}, []);
function speakText(text) {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'fr-FR';
utterance.rate = 1.02;
window.speechSynthesis.speak(utterance);
}
}
// Upload a local image file from the user's computer/phone
async function handleFileUpload(e, isEditing = false) {
const file = e.target.files?.[0];
if (!file) return;
setUploadingImage(true);
const reader = new FileReader();
reader.onload = async () => {
try {
const res = await fetch(`${API_URL}/upload-image`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
dataUrl: reader.result,
filename: file.name
})
});
const json = await res.json();
if (!res.ok) {
showToast(json.error || 'Erreur upload image', true);
return;
}
if (isEditing && editingActivity) {
setEditingActivity({ ...editingActivity, image_url: json.image_url });
} else {
setNewActivity((prev) => ({ ...prev, image_url: json.image_url }));
}
showToast('Image téléchargée et prête pour le Skill Alexa ! 🖼️');
} catch (err) {
showToast('Erreur lors de l\'envoi de l\'image', true);
} finally {
setUploadingImage(false);
}
};
reader.readAsDataURL(file);
}
// Create a request (Deposit, Lock, Contribute to Activity, Convert to Euros)
async function handleSubmitRequest(e) {
e.preventDefault();
try {
const res = await fetch(`${API_URL}/requests`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
child_id: modalConfig.childId,
type: modalConfig.type,
shells: Number(modalConfig.shells),
activity_id: modalConfig.activityId ? Number(modalConfig.activityId) : null,
source: 'WEB'
})
});
const json = await res.json();
if (!res.ok) {
showToast(json.error || 'Erreur lors de la demande', true);
return;
}
showToast(`Demande enregistrée ! Les parents peuvent maintenant valider la réception des coquillages.`);
setModalOpen(false);
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Approve or Reject a pending physical request (with parent-entered shells count and optional extra metadata)
async function handleResolveRequest(reqId, action, defaultShells = 5, extraBody = {}) {
try {
const shellsCount = Number(approvalShells[reqId] ?? (defaultShells > 0 ? defaultShells : 5));
const res = await fetch(`${API_URL}/requests/${reqId}/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ shells: shellsCount, ...extraBody })
});
const json = await res.json();
if (!res.ok) {
showToast(json.error || 'Erreur lors de la validation', true);
return;
}
showToast(json.message);
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// 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 {
const res = await fetch(`${API_URL}/bank/apply-interest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unlock_after_interest: unlockAfterInterest })
});
const json = await res.json();
showToast(json.message);
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Start a new month (reset 5€ cap)
async function handleNewMonth(unlockSavings) {
try {
const res = await fetch(`${API_URL}/bank/new-month`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ unlock_savings: unlockSavings })
});
const json = await res.json();
showToast(json.message);
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Update interest rate setting
async function handleSaveRate(e) {
e.preventDefault();
try {
const res = await fetch(`${API_URL}/bank/settings`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interest_rate_percent: Number(interestInput) })
});
const json = await res.json();
showToast(json.message);
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Create new illustrated activity
async function handleCreateActivity(e) {
e.preventDefault();
try {
const res = await fetch(`${API_URL}/activities`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newActivity)
});
const json = await res.json();
if (!res.ok) {
showToast(json.error, true);
return;
}
setNewActivity({
title: '',
description: '',
emoji: '🎢',
image_url: PRESET_GALLERY[4].url,
cost_shells: 30
});
showToast('Nouvelle activité illustrée ajoutée pour le site et Alexa !');
fetchDashboard();
} catch (err) {
showToast(err.message, true);
}
}
// Save edited activity (image/title/cost)
async function handleSaveEditedActivity(e) {
e.preventDefault();
if (!editingActivity) return;
try {
const res = await fetch(`${API_URL}/activities/${editingActivity.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editingActivity)
});
if (res.ok) {
showToast('Activité et illustration Alexa mises à jour !');
setEditingActivity(null);
fetchDashboard();
}
} catch (err) {
showToast(err.message, true);
}
}
async function handleDeleteActivity(id) {
await fetch(`${API_URL}/activities/${id}`, { method: 'DELETE' });
showToast('Activité supprimée.');
fetchDashboard();
}
async function handleAdjustChild(childId, field, delta, reason) {
await fetch(`${API_URL}/children/${childId}/adjust`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ field, delta, reason })
});
showToast('Compte mis à jour !');
fetchDashboard();
}
async function triggerAlexaVoice(intent, slots = {}) {
setAlexaLoading(true);
try {
const res = await fetch(`${API_URL}/alexa/voice-intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ intent, slots })
});
const json = await res.json();
setAlexaResponse(json.speech);
if (json.card) setAlexaCard(json.card);
if (json.cards) setAlexaCardsList(json.cards);
else setAlexaCardsList([]);
speakText(json.speech);
fetchDashboard();
} catch (err) {
setAlexaResponse('Erreur de connexion au serveur Backend.');
} finally {
setAlexaLoading(false);
}
}
if (loading || !data) {
return (
🐚
Chargement de la Banque des Coquillages...
);
}
const { settings, summary, children, activities, pending_requests, transactions } = data;
return (
{/* Toast Notification */}
{toast && (
{toast.isError ? : }
{toast.msg}
)}
{/* Header */}
{/* Top Banner if pending requests exist */}
{pending_requests.length > 0 && activeTab !== 'parents' && (
🧺
{pending_requests.length} remise(s) de coquillages physiques en attente de validation parentale !
{pending_requests[0].note}
setActiveTab('parents')}
className="px-4 py-2 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-bold text-xs shrink-0 cursor-pointer transition"
>
Valider dans le Guichet Parents →
)}
{/* ==================== TAB 1: ESPACE ENFANTS & ACTIVITÉS ==================== */}
{activeTab === 'kids' && (
{/* Children Cards: Hailey, Violette, Andreas */}
🧜♀️🐬🦈 Les Comptes de Hailey, Violette & Andreas
Dépose tes vrais coquillages auprès des parents pour les placer, les bloquer à +{settings.interest_rate_percent}% ou choisir une activité !
Dispo famille : {summary.total_available_shells} 🐚
•
Bloqués : {summary.total_locked_shells} 🔒
•
Intérêts fin de mois : +{summary.total_projected_interest} 🐚
{children.map((child) => (
{child.avatar}
{child.name}
Total : {child.total_shells} coquillages en banque
{
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"
>
Gérer / Modifier
Coquillages Disponibles
Prêts pour activités ou euros
{child.available_shells}
🐚
Livret Bloqué (+{settings.interest_rate_percent}%/mois)
✨ Rapporte +{child.projected_interest_shells} 🐚 gratuits ce mois-ci !
{child.locked_shells}
🔒
Tirelire Euros (2 🐚 = 1 €)
{child.euros_balance.toFixed(2)} €
Convertis ce mois : {child.shells_converted_month}/{settings.max_shells_euro_month} 🐚
Reste : {child.remaining_euros_this_month} € dispo
{
setModalConfig({
childId: child.id,
childName: child.name,
type: 'DEPOSIT_LOCKED',
shells: 6,
activityId: activities[0]?.id || ''
});
setModalOpen(true);
}}
className="px-3 py-2.5 rounded-xl bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-400/30 text-cyan-200 text-xs font-bold transition cursor-pointer flex items-center justify-center gap-1.5"
>
Placer / Bloquer
{
setModalConfig({
childId: child.id,
childName: child.name,
type: 'CONVERT_EUROS_PHYSICAL',
shells: Math.min(2, Math.max(2, child.remaining_shells_to_convert)),
activityId: activities[0]?.id || ''
});
setModalOpen(true);
}}
className="px-3 py-2.5 rounded-xl bg-emerald-500/20 hover:bg-emerald-500/30 border border-emerald-400/30 text-emerald-200 text-xs font-bold transition cursor-pointer flex items-center justify-center gap-1.5"
>
Convertir en €
))}
{/* Illustrated Common Activities Section */}
Catalogue des Activités Illustrées (Site & Écran Alexa)
Chaque activité possède son illustration qui s'affiche aussi sur les écrans Alexa Echo Show et l'application mobile Alexa !
{activities.map((act) => {
const isUnlocked = act.current_shells >= act.cost_shells || act.status === 'UNLOCKED';
return (
{/* Activity Image Banner */}
Illustré sur Alexa
{isUnlocked && (
🎉 DÉBLOQUÉE !
)}
{
e.stopPropagation();
setEditingActivity(act);
}}
className="px-3 py-1.5 rounded-2xl bg-slate-950/85 hover:bg-slate-900 border border-white/20 hover:border-amber-400 text-slate-300 hover:text-amber-300 text-xs font-bold backdrop-blur-md transition cursor-pointer flex items-center gap-1.5 shadow-md"
title="Modifier cette activité (titre, coût, description, photo...)"
>
Modifier
Coût
{act.cost_shells} 🐚
{act.emoji}
{act.title}
{act.description}
{/* Progress Bar */}
Pot commun : {act.current_shells} / {act.cost_shells} 🐚 ({act.progress_percent}%)
{act.remaining_shells === 0
? 'Objectif atteint !'
: `Encore ${act.remaining_shells} 🐚 à réunir`}
{/* Who contributed */}
{act.contributions.length === 0 ? (
Aucun coquillage déposé sur cette activité pour l'instant.
) : (
act.contributions.map((c) => (
{c.avatar}
{c.child_name} :
{c.total_shells} 🐚
))
)}
{/* Attribution Directe des Coquillages (Parents & Enfants) */}
{
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"
>
Attribuer des 🐚
{children.map((child) => {
const isLocked = Array.isArray(act.locked_child_ids) && act.locked_child_ids.includes(child.id);
return (
{
setDirectContributeModal({
open: true,
activity: act,
childId: child.id,
shells: Math.min(5, Math.max(1, act.remaining_shells || 5)),
sourceType: 'PHYSICAL'
});
}}
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}`}
>
{isLocked ? '🔒' : child.avatar} {child.name}
);
})}
);
})}
)}
{/* ==================== TAB 2: GUICHET PARENTS (VALIDATION, IMAGES ALEXA & INTÉRÊTS) ==================== */}
{activeTab === 'parents' && (
{/* 1. Validation des remises physiques en attente */}
🧺 Guichet de Validation des Coquillages Physiques ({pending_requests.length})
Quand Hailey, Violette ou Andreas demandent à ajouter des coquillages sur une activité depuis Alexa, leur bouton se verrouille. Saisissez ici le nombre exact de coquillages physiques qu'ils vous donnent et validez !
{pending_requests.length === 0 ? (
Aucune demande ou remise de coquillages en attente pour le moment. Tout est à jour ! ✨
) : (
{pending_requests.map((req) => {
const isProposal = req.type === 'PROPOSE_ACTIVITY';
const currentVal = approvalShells[req.id] ?? (req.shells > 0 ? req.shells : (isProposal ? 25 : 5));
return (
{req.child_avatar}
{req.child_name}
Via {req.source}
{isProposal ? (
💡 Proposition d'activité
) : req.activity_title ? (
{req.activity_emoji} {req.activity_title} (Bouton Alexa verrouillé 🔒)
) : null}
{isProposal ? (
🎡 {req.proposed_title || 'Nouvelle Activité'}
{req.note}
) : (
{req.note}
)}
);
})}
)}
{/* 2. Ajouter une Activité Illustrée (avec Aperçu Écran Alexa Echo Show en direct) */}
Ajouter une Activité Illustrée pour le Skill Alexa
Choisissez une photo dans la galerie rapide, importez une image depuis votre PC/téléphone ou collez un lien d'image : elle s'affichera automatiquement sur les écrans Amazon Echo Show et dans l'application Alexa !
{/* Formulaire de création (7 colonnes) */}
{/* Aperçu en direct de l'écran Alexa Echo Show (5 colonnes) */}
Aperçu en direct sur écran Alexa Echo Show :
🐚 LA BANQUE DES COQUILLAGES
{newActivity.emoji} {newActivity.title || 'Titre de votre activité'}
0 / {newActivity.cost_shells || 30} coquillages (0%)
{newActivity.description ||
`Coût : ${newActivity.cost_shells || 30} 🐚 • Dites "Alexa, Hailey met 5 coquillages pour ${
newActivity.title || 'cette activité'
}"`}
Amazon Echo Show • Rendu APL & StandardCard
{/* Liste des activités existantes avec possibilité de les modifier */}
Catalogue actuel (cliquez sur « Modifier l'activité » pour éditer son nom, coût, emoji, description ou photo) :
{activities.map((a) => (
{a.emoji} {a.title} ({a.cost_shells} 🐚)
setEditingActivity(a)}
className="flex-1 py-1.5 px-2.5 rounded-lg bg-amber-400/15 hover:bg-amber-400/25 border border-amber-400/30 text-amber-300 text-xs font-semibold cursor-pointer transition flex items-center justify-center gap-1.5"
>
Modifier l'activité
{
if (confirm(`Supprimer l'activité "${a.title}" ?`)) {
handleDeleteActivity(a.id);
}
}}
className="p-1.5 rounded-lg bg-rose-500/20 hover:bg-rose-500/30 text-rose-300 cursor-pointer"
title="Supprimer l'activité"
>
))}
{/* 3. Moteur d'Intérêts Mensuels & Réglages Bancaires */}
Moteur d'Intérêts Mensuels (Livret Bloqué)
Dernier versement des intérêts : {settings.last_interest_run}
{children.map((c) => (
{c.avatar}
{c.name}
({c.locked_shells} 🐚 bloqués)
+{c.projected_interest_shells} 🐚 d'intérêts
))}
setUnlockAfterInterest(e.target.checked)}
className="rounded border-white/20"
/>
Débloquer automatiquement les coquillages vers le compte disponible après versement des intérêts
Générer et verser les intérêts (+{settings.interest_rate_percent}%)
handleNewMonth(false)}
className="py-3 px-4 rounded-2xl bg-white/10 hover:bg-white/15 text-slate-200 font-semibold text-xs flex items-center justify-center gap-1.5 cursor-pointer transition"
>
Nouveau mois (Reset plafond 5€)
Paramètres du Taux & de la Banque
Ajustez le pourcentage d'intérêt mensuel qui récompense l'épargne bloquée des enfants.
{/* 4. Gestion Complète & Ajustement des Comptes (Parents) */}
Gestion & Édition de la Banque des Enfants
Transférez les coquillages réels reçus ou utilisés, ou modifiez directement les soldes de Hailey, Violette et Andreas.
{children.map((c) => (
{c.avatar}
{c.name}
Total : {c.total_shells} 🐚
Dispo
{c.available_shells} 🐚
Bloqué
{c.locked_shells} 🔒
Tirelire
{c.euros_balance.toFixed(2)} €
{
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"
>
🔄 Transfert Réel/Virtuel
{
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"
>
✏️ Éditer les soldes
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 🐚
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 🐚
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 🐚
{c.locked_shells > 0 && (
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})
)}
{c.euros_balance > 0 && (
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)}€
)}
))}
Dernières opérations enregistrées
{transactions.map((t) => (
{t.description}
{new Date(t.created_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
))}
)}
{/* ==================== TAB 3: SIMULATEUR VOCAL ALEXA + ÉCRAN ECHO SHOW ==================== */}
{activeTab === 'alexa' && (
Simulateur Vocal & Écran Echo Show
Testez les phrases vocales et visualisez la carte illustrée qui s'affiche sur l'écran d'Alexa !
{/* Alexa Echo Bubble */}
Voix d'Alexa
speakText(alexaResponse)}
className="text-xs text-cyan-300 hover:text-white flex items-center gap-1 cursor-pointer"
>
Réécouter
« {alexaLoading ? 'Alexa réfléchit...' : alexaResponse} »
{/* Preset Voice Commands */}
Phrases rapides à tester en 1 clic :
triggerAlexaVoice('ProposeActivityIntent', {
ChildName: 'Hailey',
ActivityTitle: 'Parc Astérix'
})
}
className="p-3 rounded-2xl bg-amber-500/25 hover:bg-amber-500/35 border-2 border-amber-400 text-left text-xs transition cursor-pointer shadow-md"
>
💡 « Hailey propose l'activité Parc Astérix » (Envoie au guichet parents)
triggerAlexaVoice('ListActivitiesIntent', {})}
className="p-3 rounded-2xl bg-amber-500/15 hover:bg-amber-500/25 border border-amber-400/40 text-left text-xs transition cursor-pointer"
>
🖼️ « Quelles sont les activités du mois ? » (Affiche les photos des activités)
triggerAlexaVoice('ContributeActivityIntent', {
ChildName: 'Andreas',
ShellCount: 5,
ActivityName: 'Bowling'
})
}
className="p-3 rounded-2xl bg-amber-500/15 hover:bg-amber-500/25 border border-amber-400/40 text-left text-xs transition cursor-pointer"
>
🎳 « Andreas met 5 coquillages pour le Bowling » (Affiche l'activité Bowling)
triggerAlexaVoice('LaunchRequest')}
className="p-3 rounded-2xl bg-white/5 hover:bg-cyan-500/15 border border-white/10 hover:border-cyan-400/40 text-left text-xs transition cursor-pointer"
>
🗣️ « Alexa, ouvre la banque des coquillages »
triggerAlexaVoice('GetBalanceIntent', { ChildName: 'Hailey' })}
className="p-3 rounded-2xl bg-white/5 hover:bg-cyan-500/15 border border-white/10 hover:border-cyan-400/40 text-left text-xs transition cursor-pointer"
>
🧜♀️ « Combien de coquillages a Hailey ? »
triggerAlexaVoice('GetBalanceIntent', { ChildName: 'Violette' })}
className="p-3 rounded-2xl bg-white/5 hover:bg-cyan-500/15 border border-white/10 hover:border-cyan-400/40 text-left text-xs transition cursor-pointer"
>
🐬 « Quel est le solde de Violette ? »
triggerAlexaVoice('DepositShellsIntent', {
ChildName: 'Violette',
ShellCount: 8,
AccountType: 'bloqué'
})
}
className="p-3 rounded-2xl bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-400/30 text-left text-xs transition cursor-pointer"
>
🔒 « Violette veut bloquer 8 coquillages »
{/* Écran Virtuel Amazon Echo Show + Constructeur sur-mesure (5 colonnes) */}
{/* Écran Echo Show */}
Écran Amazon Echo Show
APL + StandardCard
{alexaCard && (
🐚 LA BANQUE DES COQUILLAGES
{alexaCard.title}
{alexaCard.subtitle && (
{alexaCard.subtitle}
)}
{alexaCard.text && (
{alexaCard.text}
)}
{/* Mini-carrousel si plusieurs activités sont retournées */}
{alexaCardsList.length > 1 && (
Activités affichées dans le carrousel Alexa (cliquez pour prévisualiser) :
{alexaCardsList.map((c, idx) => (
setAlexaCard(c)}
className={`relative w-24 h-14 rounded-lg overflow-hidden shrink-0 border cursor-pointer ${
alexaCard.title === c.title ? 'border-amber-400 ring-2 ring-amber-400/50' : 'border-white/15 opacity-70'
}`}
>
{c.title}
))}
)}
)}
{/* Custom Intent Builder */}
🎛️ Tester une commande vocale sur-mesure
Intention Alexa (Intent)
setCustomAlexa({ ...customAlexa, intent: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3 py-2"
>
ContributeActivityIntent (Cotiser activité + Photo)
ListActivitiesIntent (Carrousel activités illustrées)
DepositShellsIntent (Placer / Bloquer des coquillages)
ConvertEurosIntent (Convertir en Euros - Max 5€)
GetBalanceIntent (Consulter le solde)
SimulateInterestIntent (Simuler les intérêts)
{customAlexa.intent === 'ContributeActivityIntent' && (
Activité à afficher sur Alexa
setCustomAlexa({ ...customAlexa, ActivityName: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3 py-2"
>
{activities.map((a) => (
{a.title}
))}
)}
triggerAlexaVoice(customAlexa.intent, customAlexa)}
className="w-full py-2.5 rounded-xl bg-gradient-to-r from-cyan-400 to-blue-500 text-slate-950 font-extrabold text-xs cursor-pointer transition"
>
🎙️ Envoyer la commande à Alexa
)}
{/* Modal Complète pour Modifier une Activité Existante (Titre, Coût, Cagnotte, Statut, Photo) */}
{editingActivity && (
⚙️ Modification de l'activité
{editingActivity.emoji} {editingActivity.title}
setEditingActivity(null)}
className="text-slate-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
)}
{/* Modal for Child Request (Deposit / Lock / Contribute / Convert Euros) */}
{modalOpen && (
Opération pour {modalConfig.childName}
setModalOpen(false)}
className="text-slate-400 hover:text-white text-sm cursor-pointer"
>
Fermer ✕
Que souhaite faire {modalConfig.childName} ?
setModalConfig({ ...modalConfig, type: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2.5 text-sm"
>
🔒 Remettre des vrais coquillages et les BLOQUER (+{settings.interest_rate_percent}%/mois)
🐚 Remettre des vrais coquillages sur le compte disponible
📈 Bloquer des coquillages déjà sur le compte virtuel
🎡 Donner des vrais coquillages pour une activité commune
🎡 Utiliser le compte virtuel pour une activité commune
💶 Échanger des vrais coquillages contre des Euros (2 🐚 = 1 €)
💶 Convertir des coquillages virtuels en Euros (2 🐚 = 1 €)
{(modalConfig.type === 'CONTRIBUTE_PHYSICAL' || modalConfig.type === 'CONTRIBUTE_VIRTUAL') && (
Choisir l'activité commune
setModalConfig({ ...modalConfig, activityId: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2.5 text-sm"
>
{activities.map((a) => (
{a.emoji} {a.title} (Reste {a.remaining_shells} 🐚)
))}
)}
Nombre de coquillages 🐚
setModalConfig({ ...modalConfig, shells: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-4 py-2.5 text-lg font-bold text-amber-300"
/>
💡 La demande sera créée dans le Guichet Parents . Dès que les coquillages sont remis aux parents, un clic sur « Valider » créditera le compte !
Envoyer la demande aux parents
)}
{/* Modal 1 : Édition & Transfert Banque Enfant (Parents) */}
{editBankModal.open && editBankModal.child && (
{editBankModal.child.avatar}
Gérer la banque de {editBankModal.child.name}
Solde actuel : {editBankModal.child.available_shells} 🐚 dispo • {editBankModal.child.locked_shells} 🔒 bloqués • {editBankModal.child.euros_balance.toFixed(2)} €
setEditBankModal({ ...editBankModal, open: false })}
className="text-slate-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
{/* Mode Selector Tabs: Transfert Réel/Virtuel vs Édition Manuelle Directe */}
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'
}`}
>
🔄 Transferts Réel ⇄ Virtuel
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'
}`}
>
✏️ Modifier les Soldes Directement
{editBankModal.activeMode === 'transfer' ? (
<>
Type d'opération :
{[
{
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) => (
setEditBankModal({ ...editBankModal, transferType: e.target.value })}
className="mt-1 accent-amber-400"
/>
{op.icon} {op.title}
{op.desc}
))}
Motif / Raison (optionnel) :
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"
/>
>
) : (
✏️ Édition directe des soldes : saisissez ici le montant exact que vous souhaitez attribuer à {editBankModal.child.name}.
)}
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
{editBankModal.activeMode === 'manual' ? 'Enregistrer les soldes' : 'Valider l\'opération'}
)}
{/* Modal 2 : Attribution Directe de Coquillages à une Activité (Parents) */}
{directContributeModal.open && directContributeModal.activity && (
Attribution de Coquillages (Parents)
{directContributeModal.activity.emoji} {directContributeModal.activity.title}
Pot actuel : {directContributeModal.activity.current_shells} / {directContributeModal.activity.cost_shells} 🐚 (Il manque {directContributeModal.activity.remaining_shells} 🐚)
setDirectContributeModal({ ...directContributeModal, open: false })}
className="text-slate-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
{/* Étape 1 : Quel enfant participe */}
1. Quel enfant met des coquillages ?
{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 (
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'
}`}
>
{c.avatar}
{c.name}
{c.available_shells} 🐚 dispo
{alreadyPut > 0 && (
Déjà mis : {alreadyPut} 🐚
)}
);
})}
{/* Étape 2 : Combien de coquillages */}
2. Nombre de coquillages à ajouter sur cette activité :
{/* Étape 3 : Mode de prélèvement */}
3. Source des coquillages :
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
Valider l'attribution ({directContributeModal.shells} 🐚)
)}
);
}