Files
arrweek/front/src/App.jsx

2508 lines
123 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div className="min-h-screen flex items-center justify-center bg-slate-950 text-amber-300">
<div className="flex flex-col items-center gap-3">
<div className="text-5xl animate-bounce">🐚</div>
<p className="text-lg font-medium">Chargement de la Banque des Coquillages...</p>
</div>
</div>
);
}
const { settings, summary, children, activities, pending_requests, transactions } = data;
return (
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-cyan-950/30 to-slate-950 text-slate-100 pb-20">
{/* Toast Notification */}
{toast && (
<div
className={`fixed bottom-6 right-6 z-50 px-5 py-3.5 rounded-2xl shadow-2xl border flex items-center gap-3 transition-all ${
toast.isError
? 'bg-rose-950/95 border-rose-500 text-rose-100'
: 'bg-emerald-950/95 border-emerald-500 text-emerald-100'
}`}
>
{toast.isError ? <XCircle className="w-5 h-5 text-rose-400" /> : <CheckCircle2 className="w-5 h-5 text-emerald-400" />}
<span className="font-medium text-sm">{toast.msg}</span>
</div>
)}
{/* Header */}
<header className="border-b border-white/10 bg-slate-900/70 backdrop-blur-md sticky top-0 z-30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-4 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-2xl shadow-lg shadow-amber-500/20">
🐚
</div>
<div>
<h1 className="text-xl sm:text-2xl font-extrabold tracking-tight bg-gradient-to-r from-amber-200 via-yellow-300 to-cyan-300 bg-clip-text text-transparent">
La Banque des Coquillages
</h1>
<p className="text-xs sm:text-sm text-slate-400">
Hailey, Violette & Andreas • Épargne bloquée à{' '}
<span className="text-emerald-400 font-semibold">+{settings.interest_rate_percent}% / mois</span> • Max{' '}
<span className="text-amber-300 font-semibold">{settings.max_euros_per_month}€ ({settings.max_shells_euro_month} 🐚)</span>
</p>
</div>
</div>
{/* Navigation Tabs */}
<div className="flex items-center gap-2 bg-slate-950/80 p-1.5 rounded-2xl border border-white/10">
<button
onClick={() => setActiveTab('kids')}
className={`px-4 py-2 rounded-xl text-sm font-semibold transition flex items-center gap-2 cursor-pointer ${
activeTab === 'kids'
? 'bg-gradient-to-r from-amber-400 to-orange-500 text-slate-950 shadow-md'
: 'text-slate-300 hover:text-white hover:bg-white/5'
}`}
>
<span>🏖️</span> Espace Enfants & Activités
</button>
<button
onClick={() => setActiveTab('parents')}
className={`px-4 py-2 rounded-xl text-sm font-semibold transition flex items-center gap-2 cursor-pointer relative ${
activeTab === 'parents'
? 'bg-gradient-to-r from-emerald-400 to-teal-500 text-slate-950 shadow-md'
: 'text-slate-300 hover:text-white hover:bg-white/5'
}`}
>
<ShieldCheck className="w-4 h-4" />
Guichet Parents
{pending_requests.length > 0 && (
<span className="ml-1 px-2 py-0.5 text-xs font-bold rounded-full bg-rose-500 text-white animate-pulse">
{pending_requests.length}
</span>
)}
</button>
<button
onClick={() => setActiveTab('alexa')}
className={`px-4 py-2 rounded-xl text-sm font-semibold transition flex items-center gap-2 cursor-pointer ${
activeTab === 'alexa'
? 'bg-gradient-to-r from-cyan-400 to-blue-500 text-slate-950 shadow-md'
: 'text-slate-300 hover:text-white hover:bg-white/5'
}`}
>
<Mic className="w-4 h-4" />
Simulateur Alexa & Écran
</button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 pt-8 space-y-10">
{/* Top Banner if pending requests exist */}
{pending_requests.length > 0 && activeTab !== 'parents' && (
<div className="bg-amber-500/15 border border-amber-400/40 rounded-2xl p-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<span className="text-2xl">🧺</span>
<div>
<p className="font-semibold text-amber-200">
{pending_requests.length} remise(s) de coquillages physiques en attente de validation parentale !
</p>
<p className="text-xs text-amber-300/80">{pending_requests[0].note}</p>
</div>
</div>
<button
onClick={() => 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 →
</button>
</div>
)}
{/* ==================== TAB 1: ESPACE ENFANTS & ACTIVITÉS ==================== */}
{activeTab === 'kids' && (
<div className="space-y-10">
{/* Children Cards: Hailey, Violette, Andreas */}
<section>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-5">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<span>🧜‍♀️🐬🦈</span> Les Comptes de Hailey, Violette & Andreas
</h2>
<p className="text-sm text-slate-400">
Dépose tes vrais coquillages auprès des parents pour les placer, les bloquer à +{settings.interest_rate_percent}% ou choisir une activité !
</p>
</div>
<div className="flex items-center gap-3 bg-slate-900/90 border border-white/10 px-4 py-2.5 rounded-2xl text-xs">
<span>
Dispo famille : <strong className="text-amber-300">{summary.total_available_shells} 🐚</strong>
</span>
<span>•</span>
<span>
Bloqués : <strong className="text-cyan-300">{summary.total_locked_shells} 🔒</strong>
</span>
<span>•</span>
<span>
Intérêts fin de mois : <strong className="text-emerald-400">+{summary.total_projected_interest} 🐚</strong>
</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{children.map((child) => (
<div
key={child.id}
className="rounded-3xl bg-slate-900/80 border border-white/10 p-6 flex flex-col justify-between relative overflow-hidden shadow-xl hover:border-amber-400/40 transition"
>
<div>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-14 h-14 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center text-3xl">
{child.avatar}
</div>
<div>
<h3 className="text-2xl font-extrabold text-white">{child.name}</h3>
<span className="text-xs text-slate-400">
Total : {child.total_shells} coquillages en banque
</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">
<div className="bg-amber-500/10 border border-amber-400/25 rounded-2xl p-3.5 flex items-center justify-between">
<div>
<span className="text-xs font-medium text-amber-200/80 block">
Coquillages Disponibles
</span>
<span className="text-xs text-slate-400">Prêts pour activités ou euros</span>
</div>
<div className="text-2xl font-black text-amber-300 flex items-center gap-1.5">
<span>{child.available_shells}</span>
<span className="text-xl">🐚</span>
</div>
</div>
<div className="bg-cyan-500/10 border border-cyan-400/25 rounded-2xl p-3.5">
<div className="flex items-center justify-between">
<div>
<span className="text-xs font-medium text-cyan-200/90 flex items-center gap-1">
<Lock className="w-3.5 h-3.5 text-cyan-400" /> Livret Bloqué (+{settings.interest_rate_percent}%/mois)
</span>
<span className="text-xs text-emerald-400 font-semibold mt-0.5 block">
✨ Rapporte +{child.projected_interest_shells} 🐚 gratuits ce mois-ci !
</span>
</div>
<div className="text-2xl font-black text-cyan-300 flex items-center gap-1.5">
<span>{child.locked_shells}</span>
<span className="text-lg">🔒</span>
</div>
</div>
</div>
<div className="bg-emerald-500/10 border border-emerald-400/25 rounded-2xl p-3.5">
<div className="flex items-center justify-between mb-1.5">
<span className="text-xs font-medium text-emerald-200/90 flex items-center gap-1">
<PiggyBank className="w-3.5 h-3.5 text-emerald-400" /> Tirelire Euros (2 🐚 = 1 €)
</span>
<span className="text-xl font-black text-emerald-300">
{child.euros_balance.toFixed(2)} €
</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden">
<div
className="bg-emerald-400 h-full rounded-full transition-all"
style={{
width: `${Math.min(100, (child.shells_converted_month / settings.max_shells_euro_month) * 100)}%`
}}
/>
</div>
<div className="flex justify-between text-[11px] text-slate-400 mt-1">
<span>Convertis ce mois : {child.shells_converted_month}/{settings.max_shells_euro_month} 🐚</span>
<span>Reste : {child.remaining_euros_this_month} € dispo</span>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-white/10">
<button
onClick={() => {
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"
>
<Lock className="w-3.5 h-3.5" /> Placer / Bloquer
</button>
<button
onClick={() => {
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"
>
<Coins className="w-3.5 h-3.5" /> Convertir en €
</button>
</div>
</div>
))}
</div>
</section>
{/* Illustrated Common Activities Section */}
<section>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-5">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<Gift className="w-5 h-5 text-amber-400" /> Catalogue des Activités Illustrées (Site & Écran Alexa)
</h2>
<p className="text-sm text-slate-400">
Chaque activité possède son illustration qui s'affiche aussi sur les écrans Alexa Echo Show et l'application mobile Alexa !
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{activities.map((act) => {
const isUnlocked = act.current_shells >= act.cost_shells || act.status === 'UNLOCKED';
return (
<div
key={act.id}
className={`rounded-3xl overflow-hidden border transition flex flex-col justify-between ${
isUnlocked
? 'bg-gradient-to-br from-emerald-950/60 to-slate-900 border-emerald-400/50 shadow-lg shadow-emerald-500/10'
: 'bg-slate-900/80 border-white/10 hover:border-amber-400/30'
}`}
>
<div>
{/* Activity Image Banner */}
<div className="relative h-48 w-full overflow-hidden bg-slate-950">
<img
src={act.image_url}
alt={act.title}
className="w-full h-full object-cover transition-transform duration-500 hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/40 to-transparent" />
<div className="absolute top-3 left-3 flex items-center gap-2">
<span className="px-3 py-1 rounded-full text-xs font-bold bg-slate-950/80 backdrop-blur-md border border-white/15 text-cyan-300 flex items-center gap-1">
<Tv className="w-3.5 h-3.5" /> Illustré sur Alexa
</span>
{isUnlocked && (
<span className="px-3 py-1 text-xs font-extrabold rounded-full bg-emerald-400 text-slate-950 shadow-lg">
🎉 DÉBLOQUÉE !
</span>
)}
</div>
<div className="absolute top-3 right-3 flex items-center gap-2">
<button
type="button"
onClick={(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...)"
>
<Settings className="w-3.5 h-3.5" /> Modifier
</button>
<div className="px-3.5 py-1.5 rounded-2xl bg-slate-950/85 backdrop-blur-md border border-amber-400/40 text-right">
<span className="text-[10px] text-slate-300 block leading-none">Coût</span>
<span className="text-lg font-black text-amber-300">{act.cost_shells} 🐚</span>
</div>
</div>
<div className="absolute bottom-3 left-4 right-4 flex items-end justify-between">
<div className="flex items-center gap-2.5">
<span className="text-3xl drop-shadow">{act.emoji}</span>
<div>
<h3 className="text-xl font-extrabold text-white drop-shadow">{act.title}</h3>
<p className="text-xs text-slate-200 line-clamp-1">{act.description}</p>
</div>
</div>
</div>
</div>
<div className="p-5 pb-3">
{/* Progress Bar */}
<div className="mb-3">
<div className="flex justify-between text-xs font-semibold mb-1.5">
<span className="text-amber-200">
Pot commun : {act.current_shells} / {act.cost_shells} 🐚 ({act.progress_percent}%)
</span>
<span className="text-slate-400">
{act.remaining_shells === 0
? 'Objectif atteint !'
: `Encore ${act.remaining_shells} 🐚 à réunir`}
</span>
</div>
<div className="w-full h-3.5 bg-slate-950 rounded-full overflow-hidden border border-white/10">
<div
className={`h-full rounded-full transition-all duration-500 ${
isUnlocked
? 'bg-gradient-to-r from-emerald-400 to-teal-300'
: 'bg-gradient-to-r from-amber-400 to-orange-500'
}`}
style={{ width: `${act.progress_percent}%` }}
/>
</div>
</div>
{/* Who contributed */}
<div className="flex flex-wrap items-center gap-2 my-3">
{act.contributions.length === 0 ? (
<span className="text-xs text-slate-500 italic">
Aucun coquillage déposé sur cette activité pour l'instant.
</span>
) : (
act.contributions.map((c) => (
<span
key={c.child_id}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold bg-white/5 border border-white/10 text-slate-200"
>
<span>{c.avatar}</span>
<span>{c.child_name} :</span>
<strong className="text-amber-300">{c.total_shells} 🐚</strong>
</span>
))
)}
</div>
</div>
</div>
{/* 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);
return (
<button
key={child.id}
onClick={() => {
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}`}
>
<span>{isLocked ? '🔒' : child.avatar}</span> {child.name}
</button>
);
})}
</div>
</div>
</div>
);
})}
</div>
</section>
</div>
)}
{/* ==================== TAB 2: GUICHET PARENTS (VALIDATION, IMAGES ALEXA & INTÉRÊTS) ==================== */}
{activeTab === 'parents' && (
<div className="space-y-10">
{/* 1. Validation des remises physiques en attente */}
<section className="bg-slate-900/90 border border-white/10 rounded-3xl p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-bold flex items-center gap-2 text-amber-300">
<span>🧺</span> Guichet de Validation des Coquillages Physiques ({pending_requests.length})
</h2>
<p className="text-sm text-slate-400">
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 !
</p>
</div>
</div>
{pending_requests.length === 0 ? (
<div className="bg-slate-950/60 border border-white/5 rounded-2xl p-8 text-center text-slate-400">
Aucune demande ou remise de coquillages en attente pour le moment. Tout est à jour ! ✨
</div>
) : (
<div className="space-y-3">
{pending_requests.map((req) => {
const isProposal = req.type === 'PROPOSE_ACTIVITY';
const currentVal = approvalShells[req.id] ?? (req.shells > 0 ? req.shells : (isProposal ? 25 : 5));
return (
<div
key={req.id}
className={`bg-slate-950/90 border rounded-2xl p-4 flex flex-col md:flex-row md:items-center justify-between gap-4 ${
isProposal ? 'border-amber-400/60 shadow-lg shadow-amber-500/10' : 'border-amber-400/30'
}`}
>
<div className="flex items-start gap-3">
<div className="w-12 h-12 rounded-2xl bg-white/5 flex items-center justify-center text-2xl shrink-0">
{req.child_avatar}
</div>
<div>
<div className="flex items-center flex-wrap gap-2">
<span className="font-bold text-white">{req.child_name}</span>
<span className="px-2 py-0.5 text-[11px] font-bold rounded-full bg-cyan-500/20 text-cyan-300 border border-cyan-400/30">
Via {req.source}
</span>
{isProposal ? (
<span className="px-2.5 py-0.5 text-xs font-extrabold rounded-full bg-amber-400/20 text-amber-300 border border-amber-400/40">
💡 Proposition d'activité
</span>
) : req.activity_title ? (
<span className="px-2.5 py-0.5 text-xs font-bold rounded-full bg-amber-400/20 text-amber-300 border border-amber-400/30">
{req.activity_emoji} {req.activity_title} (Bouton Alexa verrouillé 🔒)
</span>
) : null}
</div>
{isProposal ? (
<div className="mt-1">
<span className="text-base font-extrabold text-amber-300">
🎡 {req.proposed_title || 'Nouvelle Activité'}
</span>
<p className="text-xs text-slate-300 mt-0.5">{req.note}</p>
</div>
) : (
<p className="text-sm text-slate-200 mt-1">{req.note}</p>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2.5 shrink-0">
<div className="flex items-center gap-1.5 bg-slate-900 border border-amber-400/40 rounded-xl px-3 py-1.5">
<span className="text-xs text-slate-300">
{isProposal ? 'Coût en 🐚 fixé :' : 'Nombre de 🐚 reçus :'}
</span>
<input
type="number"
min="1"
max="500"
value={currentVal}
onChange={(e) =>
setApprovalShells({ ...approvalShells, [req.id]: e.target.value })
}
className="w-16 bg-slate-950 border border-white/15 rounded-lg px-2 py-1 text-center font-extrabold text-amber-300 text-sm"
/>
{isProposal && <span className="text-xs font-bold text-amber-300">🐚</span>}
</div>
<button
onClick={() =>
handleResolveRequest(
req.id,
'approve',
req.shells,
isProposal
? {
title: req.proposed_title,
cost_shells: Number(currentVal),
emoji: '💡'
}
: {}
)
}
className="px-4 py-2.5 rounded-xl bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold text-xs flex items-center gap-1.5 cursor-pointer transition shadow-lg shadow-emerald-500/20"
>
<CheckCircle2 className="w-4 h-4" />
{isProposal
? `Valider & Créer (${currentVal} 🐚)`
: `Valider ${currentVal} 🐚`}
</button>
<button
onClick={() => handleResolveRequest(req.id, 'reject', req.shells)}
className="px-3 py-2.5 rounded-xl bg-rose-500/20 hover:bg-rose-500/30 border border-rose-500/40 text-rose-300 font-bold text-xs flex items-center gap-1 cursor-pointer transition"
>
<XCircle className="w-4 h-4" /> Refuser
</button>
</div>
</div>
);
})}
</div>
)}
</section>
{/* 2. Ajouter une Activité Illustrée (avec Aperçu Écran Alexa Echo Show en direct) */}
<section className="bg-slate-900/90 border border-amber-400/30 rounded-3xl p-6">
<div className="mb-6">
<h2 className="text-xl font-bold flex items-center gap-2 text-white">
<ImageIcon className="w-5 h-5 text-amber-400" /> Ajouter une Activité Illustrée pour le Skill Alexa
</h2>
<p className="text-xs text-slate-400 mt-1">
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 <strong>Amazon Echo Show</strong> et dans l'application <strong>Alexa</strong> !
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
{/* Formulaire de création (7 colonnes) */}
<form onSubmit={handleCreateActivity} className="lg:col-span-7 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="sm:col-span-2">
<label className="block text-xs font-semibold text-slate-300 mb-1">
Nom de l'activité (reconnu par Alexa)
</label>
<input
type="text"
required
placeholder="Ex: Parc Astérix, Soirée Crêpes, Laser Game..."
value={newActivity.title}
onChange={(e) => setNewActivity({ ...newActivity, title: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2.5 text-sm"
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Coût (en 🐚)</label>
<input
type="number"
min="1"
required
value={newActivity.cost_shells}
onChange={(e) => setNewActivity({ ...newActivity, cost_shells: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2.5 text-sm font-bold text-amber-300"
/>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">
Description (affichée sous le titre sur l'écran Alexa)
</label>
<input
type="text"
placeholder="Ex: Grande journée en famille avec manèges à sensations !"
value={newActivity.description}
onChange={(e) => setNewActivity({ ...newActivity, description: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-sm"
/>
</div>
{/* Choix de l'illustration : Galerie 1-clic + Upload Fichier + URL */}
<div className="bg-slate-950/80 border border-white/10 rounded-2xl p-4 space-y-3">
<label className="block text-xs font-bold text-amber-300 uppercase tracking-wider">
🖼️ Illustration de l'activité pour Alexa :
</label>
{/* 1. Galerie rapide d'images HD */}
<div>
<span className="text-[11px] text-slate-400 block mb-1.5">
Option 1 : Choisir une illustration prête à l'emploi en 1 clic
</span>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{PRESET_GALLERY.map((preset) => (
<button
type="button"
key={preset.label}
onClick={() =>
setNewActivity({
...newActivity,
image_url: preset.url,
emoji: preset.emoji,
title: newActivity.title || preset.label
})
}
className={`group relative h-16 rounded-xl overflow-hidden border text-left p-2 flex items-end cursor-pointer transition ${
newActivity.image_url === preset.url
? 'border-amber-400 ring-2 ring-amber-400/50'
: 'border-white/10 opacity-75 hover:opacity-100'
}`}
>
<img
src={preset.url}
alt={preset.label}
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/85 via-black/30 to-transparent" />
<span className="relative text-[11px] font-bold text-white leading-tight">
{preset.emoji} {preset.label}
</span>
</button>
))}
</div>
</div>
{/* 2. Upload depuis l'ordinateur ou URL personnalisée */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2 border-t border-white/10">
<div>
<span className="text-[11px] text-slate-400 block mb-1">
Option 2 : Envoyer une photo depuis votre PC/téléphone
</span>
<label className="w-full py-2 px-3 rounded-xl bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-400/40 text-cyan-200 text-xs font-bold flex items-center justify-center gap-2 cursor-pointer transition">
<Upload className="w-4 h-4" />
{uploadingImage ? 'Envoi en cours...' : 'Choisir un fichier image...'}
<input
type="file"
accept="image/*"
onChange={(e) => handleFileUpload(e, false)}
className="hidden"
/>
</label>
</div>
<div>
<span className="text-[11px] text-slate-400 block mb-1">
Option 3 : Ou coller l'URL d'une image (HTTPS)
</span>
<input
type="url"
placeholder="https://exemple.com/photo.jpg"
value={newActivity.image_url}
onChange={(e) => setNewActivity({ ...newActivity, image_url: e.target.value })}
className="w-full bg-slate-900 border border-white/15 rounded-xl px-3 py-2 text-xs text-slate-200"
/>
</div>
</div>
</div>
{/* Emoji selector */}
<div>
<label className="block text-xs text-slate-400 mb-1">Emoji associé</label>
<div className="flex flex-wrap gap-1.5">
{EMOJI_OPTIONS.map((em) => (
<button
type="button"
key={em}
onClick={() => setNewActivity({ ...newActivity, emoji: em })}
className={`w-9 h-9 rounded-xl text-lg flex items-center justify-center cursor-pointer border ${
newActivity.emoji === em
? 'bg-amber-400/20 border-amber-400'
: 'bg-slate-950 border-white/10'
}`}
>
{em}
</button>
))}
</div>
</div>
<button
type="submit"
className="w-full py-3 rounded-2xl bg-gradient-to-r from-amber-400 to-orange-500 hover:opacity-95 text-slate-950 font-extrabold text-sm cursor-pointer transition shadow-lg shadow-amber-500/20"
>
➕ Enregistrer l'activité illustrée
</button>
</form>
{/* Aperçu en direct de l'écran Alexa Echo Show (5 colonnes) */}
<div className="lg:col-span-5">
<div className="text-xs font-bold uppercase tracking-wider text-cyan-300 mb-2 flex items-center gap-1.5">
<Tv className="w-4 h-4" /> Aperçu en direct sur écran Alexa Echo Show :
</div>
<div className="rounded-3xl p-3 bg-slate-950 border-4 border-slate-800 shadow-2xl">
<div className="relative h-72 rounded-2xl overflow-hidden bg-slate-900 flex flex-col justify-end p-6">
<img
src={newActivity.image_url || PRESET_GALLERY[0].url}
alt="Aperçu Alexa"
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/65 to-slate-950/20" />
<div className="relative z-10 space-y-1.5">
<span className="text-[11px] font-extrabold tracking-widest text-amber-400 uppercase block">
🐚 LA BANQUE DES COQUILLAGES
</span>
<h3 className="text-2xl font-black text-white drop-shadow">
{newActivity.emoji} {newActivity.title || 'Titre de votre activité'}
</h3>
<p className="text-sm font-bold text-cyan-300">
0 / {newActivity.cost_shells || 30} coquillages (0%)
</p>
<p className="text-xs text-slate-200 line-clamp-2">
{newActivity.description ||
`Coût : ${newActivity.cost_shells || 30} 🐚 • Dites "Alexa, Hailey met 5 coquillages pour ${
newActivity.title || 'cette activité'
}"`}
</p>
</div>
</div>
<div className="text-center text-[11px] text-slate-500 mt-2">
Amazon Echo Show • Rendu APL & StandardCard
</div>
</div>
</div>
</div>
{/* Liste des activités existantes avec possibilité de les modifier */}
<div className="mt-8 pt-6 border-t border-white/10">
<h3 className="text-sm font-bold text-slate-300 mb-3">
Catalogue actuel (cliquez sur « Modifier l'activité » pour éditer son nom, coût, emoji, description ou photo) :
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
{activities.map((a) => (
<div
key={a.id}
className="bg-slate-950/80 border border-white/10 rounded-2xl overflow-hidden flex flex-col justify-between"
>
<div className="relative h-28">
<img src={a.image_url} alt={a.title} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent" />
<span className="absolute bottom-2 left-2.5 text-xs font-bold text-white drop-shadow">
{a.emoji} {a.title} ({a.cost_shells} 🐚)
</span>
</div>
<div className="p-2.5 flex items-center justify-between gap-2">
<button
onClick={() => 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"
>
<Settings className="w-3.5 h-3.5" /> Modifier l'activité
</button>
<button
onClick={() => {
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é"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
</div>
</section>
{/* 3. Moteur d'Intérêts Mensuels & Réglages Bancaires */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<section className="bg-slate-900/90 border border-cyan-500/30 rounded-3xl p-6 flex flex-col justify-between">
<div>
<h2 className="text-lg font-bold flex items-center gap-2 text-cyan-300 mb-1">
<TrendingUp className="w-5 h-5" /> Moteur d'Intérêts Mensuels (Livret Bloqué)
</h2>
<p className="text-xs text-slate-400 mb-5">
Dernier versement des intérêts : <strong>{settings.last_interest_run}</strong>
</p>
<div className="space-y-2.5 mb-5">
{children.map((c) => (
<div
key={c.id}
className="bg-slate-950/80 border border-white/10 rounded-2xl p-3 flex items-center justify-between text-sm"
>
<div className="flex items-center gap-2">
<span>{c.avatar}</span>
<span className="font-semibold">{c.name}</span>
<span className="text-xs text-slate-400">({c.locked_shells} 🐚 bloqués)</span>
</div>
<span className="font-bold text-emerald-400">
+{c.projected_interest_shells} 🐚 d'intérêts
</span>
</div>
))}
</div>
<label className="flex items-center gap-2 text-xs text-slate-300 mb-4 cursor-pointer">
<input
type="checkbox"
checked={unlockAfterInterest}
onChange={(e) => 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
</label>
</div>
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={handleApplyMonthlyInterest}
className="flex-1 py-3 px-4 rounded-2xl bg-gradient-to-r from-cyan-400 to-emerald-400 hover:opacity-95 text-slate-950 font-extrabold text-sm flex items-center justify-center gap-2 cursor-pointer transition"
>
<Sparkles className="w-4 h-4" /> Générer et verser les intérêts (+{settings.interest_rate_percent}%)
</button>
<button
onClick={() => 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"
>
<RefreshCw className="w-3.5 h-3.5" /> Nouveau mois (Reset plafond 5€)
</button>
</div>
</section>
<section className="bg-slate-900/90 border border-white/10 rounded-3xl p-6 flex flex-col justify-between">
<div>
<h2 className="text-lg font-bold flex items-center gap-2 text-white mb-1">
<Settings className="w-5 h-5 text-amber-400" /> Paramètres du Taux & de la Banque
</h2>
<p className="text-xs text-slate-400 mb-5">
Ajustez le pourcentage d'intérêt mensuel qui récompense l'épargne bloquée des enfants.
</p>
<form onSubmit={handleSaveRate} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5">
Taux d'intérêt mensuel sur les coquillages bloqués (%)
</label>
<div className="flex items-center gap-3">
<input
type="number"
min="0"
max="200"
value={interestInput}
onChange={(e) => setInterestInput(e.target.value)}
className="w-32 bg-slate-950 border border-white/15 rounded-xl px-4 py-2.5 text-lg font-bold text-cyan-300"
/>
<span className="text-sm text-slate-400">
% par mois (Ex: 10 🐚 bloqués → +{Math.ceil((10 * Number(interestInput || 0)) / 100)} 🐚 gratuits)
</span>
</div>
</div>
<div className="bg-slate-950/60 border border-white/5 rounded-2xl p-3.5 text-xs text-slate-400 space-y-1">
<p>
• Règle Tirelire Euros : <strong>{settings.shells_per_euro} coquillages = 1,00 €</strong>
</p>
<p>
• Plafond mensuel par enfant :{' '}
<strong>
{settings.max_euros_per_month} € maximum ({settings.max_shells_euro_month} coquillages)
</strong>
</p>
</div>
<button
type="submit"
className="px-5 py-2.5 rounded-xl bg-amber-400 hover:bg-amber-300 text-slate-950 font-bold text-xs cursor-pointer transition"
>
Enregistrer le nouveau taux
</button>
</form>
</div>
</section>
</div>
{/* 4. Gestion Complète & Ajustement des Comptes (Parents) */}
<section className="bg-slate-900/90 border border-white/10 rounded-3xl p-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/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
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>
))}
</div>
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400 mb-2 flex items-center gap-1.5">
<History className="w-4 h-4 text-cyan-400" /> Dernières opérations enregistrées
</h3>
<div className="space-y-1.5 max-h-44 overflow-y-auto pr-1">
{transactions.map((t) => (
<div
key={t.id}
className="text-xs bg-slate-950/50 border border-white/5 rounded-lg px-3 py-2 flex items-center justify-between"
>
<span className="text-slate-300">{t.description}</span>
<span className="text-slate-500 text-[10px] shrink-0 ml-2">
{new Date(t.created_at).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
))}
</div>
</section>
</div>
)}
{/* ==================== TAB 3: SIMULATEUR VOCAL ALEXA + ÉCRAN ECHO SHOW ==================== */}
{activeTab === 'alexa' && (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
<div className="lg:col-span-7 bg-slate-900/90 border border-cyan-500/30 rounded-3xl p-6 space-y-6">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-cyan-500/20 border border-cyan-400/40 flex items-center justify-center text-cyan-300">
<Mic className="w-6 h-6" />
</div>
<div>
<h2 className="text-xl font-bold text-white">Simulateur Vocal & Écran Echo Show</h2>
<p className="text-xs text-slate-400">
Testez les phrases vocales et visualisez la carte illustrée qui s'affiche sur l'écran d'Alexa !
</p>
</div>
</div>
{/* Alexa Echo Bubble */}
<div className="bg-slate-950 border-2 border-cyan-400/50 rounded-3xl p-5 shadow-inner relative">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-bold uppercase tracking-wider text-cyan-400 flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-full bg-cyan-400 animate-ping" />
Voix d'Alexa
</span>
<button
onClick={() => speakText(alexaResponse)}
className="text-xs text-cyan-300 hover:text-white flex items-center gap-1 cursor-pointer"
>
<Volume2 className="w-4 h-4" /> Réécouter
</button>
</div>
<p className="text-base text-slate-100 leading-relaxed font-medium">
« {alexaLoading ? 'Alexa réfléchit...' : alexaResponse} »
</p>
</div>
{/* Preset Voice Commands */}
<div className="space-y-2.5">
<h3 className="text-xs font-bold uppercase tracking-wider text-slate-400">
Phrases rapides à tester en 1 clic :
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<button
onClick={() =>
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"
>
💡 <strong>« Hailey propose l'activité Parc Astérix »</strong> (Envoie au guichet parents)
</button>
<button
onClick={() => 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"
>
🖼️ <strong>« Quelles sont les activités du mois ? »</strong> (Affiche les photos des activités)
</button>
<button
onClick={() =>
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"
>
🎳 <strong>« Andreas met 5 coquillages pour le Bowling »</strong> (Affiche l'activité Bowling)
</button>
<button
onClick={() => 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"
>
🗣️ <strong>« Alexa, ouvre la banque des coquillages »</strong>
</button>
<button
onClick={() => 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"
>
🧜‍♀️ <strong>« Combien de coquillages a Hailey ? »</strong>
</button>
<button
onClick={() => 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"
>
🐬 <strong>« Quel est le solde de Violette ? »</strong>
</button>
<button
onClick={() =>
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"
>
🔒 <strong>« Violette veut bloquer 8 coquillages »</strong>
</button>
</div>
</div>
</div>
{/* Écran Virtuel Amazon Echo Show + Constructeur sur-mesure (5 colonnes) */}
<div className="lg:col-span-5 space-y-6">
{/* Écran Echo Show */}
<div className="bg-slate-900/90 border border-white/15 rounded-3xl p-5 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-bold uppercase tracking-wider text-cyan-300 flex items-center gap-1.5">
<Tv className="w-4 h-4" /> Écran Amazon Echo Show
</span>
<span className="text-[11px] text-slate-400">APL + StandardCard</span>
</div>
{alexaCard && (
<div className="rounded-2xl p-2.5 bg-slate-950 border-4 border-slate-800 shadow-2xl">
<div className="relative h-64 rounded-xl overflow-hidden bg-slate-900 flex flex-col justify-end p-5">
<img
src={alexaCard.imageUrl}
alt={alexaCard.title}
className="absolute inset-0 w-full h-full object-cover transition-all duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/65 to-transparent" />
<div className="relative z-10 space-y-1">
<span className="text-[10px] font-extrabold tracking-widest text-amber-400 uppercase block">
🐚 LA BANQUE DES COQUILLAGES
</span>
<h3 className="text-xl font-black text-white drop-shadow">{alexaCard.title}</h3>
{alexaCard.subtitle && (
<p className="text-xs font-bold text-cyan-300">{alexaCard.subtitle}</p>
)}
{alexaCard.text && (
<p className="text-xs text-slate-200 line-clamp-2 whitespace-pre-line">{alexaCard.text}</p>
)}
</div>
</div>
{/* Mini-carrousel si plusieurs activités sont retournées */}
{alexaCardsList.length > 1 && (
<div className="pt-2.5 mt-2 border-t border-white/10">
<span className="text-[10px] text-slate-400 block mb-1.5">
Activités affichées dans le carrousel Alexa (cliquez pour prévisualiser) :
</span>
<div className="flex gap-2 overflow-x-auto pb-1">
{alexaCardsList.map((c, idx) => (
<button
key={idx}
onClick={() => 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'
}`}
>
<img src={c.imageUrl} alt={c.title} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/50 flex items-end p-1">
<span className="text-[9px] font-bold text-white truncate">{c.title}</span>
</div>
</button>
))}
</div>
</div>
)}
</div>
)}
</div>
{/* Custom Intent Builder */}
<div className="bg-slate-900/90 border border-white/10 rounded-3xl p-5 space-y-4">
<h3 className="text-sm font-bold">🎛️ Tester une commande vocale sur-mesure</h3>
<div className="space-y-3 text-xs">
<div>
<label className="block text-slate-400 mb-1">Intention Alexa (Intent)</label>
<select
value={customAlexa.intent}
onChange={(e) => setCustomAlexa({ ...customAlexa, intent: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3 py-2"
>
<option value="ContributeActivityIntent">ContributeActivityIntent (Cotiser activité + Photo)</option>
<option value="ListActivitiesIntent">ListActivitiesIntent (Carrousel activités illustrées)</option>
<option value="DepositShellsIntent">DepositShellsIntent (Placer / Bloquer des coquillages)</option>
<option value="ConvertEurosIntent">ConvertEurosIntent (Convertir en Euros - Max 5€)</option>
<option value="GetBalanceIntent">GetBalanceIntent (Consulter le solde)</option>
<option value="SimulateInterestIntent">SimulateInterestIntent (Simuler les intérêts)</option>
</select>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-slate-400 mb-1">Enfant</label>
<select
value={customAlexa.ChildName}
onChange={(e) => setCustomAlexa({ ...customAlexa, ChildName: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3 py-2"
>
<option value="Hailey">Hailey</option>
<option value="Violette">Violette</option>
<option value="Andreas">Andreas</option>
</select>
</div>
<div>
<label className="block text-slate-400 mb-1">Nombre de 🐚</label>
<input
type="number"
min="1"
value={customAlexa.ShellCount}
onChange={(e) => setCustomAlexa({ ...customAlexa, ShellCount: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3 py-2"
/>
</div>
</div>
{customAlexa.intent === 'ContributeActivityIntent' && (
<div>
<label className="block text-slate-400 mb-1">Activité à afficher sur Alexa</label>
<select
value={customAlexa.ActivityName}
onChange={(e) => 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) => (
<option key={a.id} value={a.title}>
{a.title}
</option>
))}
</select>
</div>
)}
<button
onClick={() => 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
</button>
</div>
</div>
</div>
</div>
)}
</main>
{/* Modal Complète pour Modifier une Activité Existante (Titre, Coût, Cagnotte, Statut, Photo) */}
{editingActivity && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 overflow-y-auto">
<div className="bg-slate-900 border border-white/15 rounded-3xl max-w-xl w-full p-6 space-y-5 shadow-2xl my-8">
<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">
⚙️ Modification de l'activité
</span>
<h3 className="text-xl font-extrabold text-white flex items-center gap-2 mt-0.5">
<span>{editingActivity.emoji}</span> {editingActivity.title}
</h3>
</div>
<button
onClick={() => setEditingActivity(null)}
className="text-slate-400 hover:text-white text-sm cursor-pointer p-1"
>
✕
</button>
</div>
<form onSubmit={handleSaveEditedActivity} className="space-y-4">
{/* 1. Titre & Emoji & Coût */}
<div className="grid grid-cols-1 sm:grid-cols-12 gap-3">
<div className="sm:col-span-2">
<label className="block text-xs font-bold text-slate-300 mb-1">Emoji</label>
<input
type="text"
value={editingActivity.emoji}
onChange={(e) => setEditingActivity({ ...editingActivity, emoji: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-2.5 py-2 text-center text-xl font-bold"
/>
</div>
<div className="sm:col-span-6">
<label className="block text-xs font-bold text-slate-300 mb-1">
Nom de l'activité (reconnu par Alexa)
</label>
<input
type="text"
required
value={editingActivity.title}
onChange={(e) => setEditingActivity({ ...editingActivity, title: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-sm text-white font-semibold"
/>
</div>
<div className="sm:col-span-4">
<label className="block text-xs font-bold text-slate-300 mb-1">Coût (en 🐚)</label>
<input
type="number"
min="1"
required
value={editingActivity.cost_shells}
onChange={(e) => setEditingActivity({ ...editingActivity, cost_shells: Number(e.target.value) })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-sm font-extrabold text-amber-300 text-center"
/>
</div>
</div>
{/* 2. Cagnotte actuelle & Statut */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">
Cagnotte réunie (coquillages déjà mis)
</label>
<input
type="number"
min="0"
value={editingActivity.current_shells}
onChange={(e) => setEditingActivity({ ...editingActivity, current_shells: Number(e.target.value) })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-sm font-extrabold text-emerald-400"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">Statut de l'activité</label>
<select
value={editingActivity.status}
onChange={(e) => setEditingActivity({ ...editingActivity, status: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-xs font-bold text-slate-200"
>
<option value="ACTIVE">⏳ En cours (ACTIVE)</option>
<option value="UNLOCKED">🎉 Débloquée ! (UNLOCKED)</option>
<option value="COMPLETED">✅ Terminée / Faite (COMPLETED)</option>
</select>
</div>
</div>
{/* 3. Description */}
<div>
<label className="block text-xs font-bold text-slate-300 mb-1">
Description (lue par Alexa et affichée sur l'écran)
</label>
<textarea
rows="2"
value={editingActivity.description || ''}
onChange={(e) => setEditingActivity({ ...editingActivity, description: e.target.value })}
className="w-full bg-slate-950 border border-white/15 rounded-xl px-3.5 py-2 text-xs text-slate-200 resize-none"
/>
</div>
{/* 4. Illustration Alexa & Écran Echo Show */}
<div className="bg-slate-950/70 border border-white/10 rounded-2xl p-3.5 space-y-3">
<div className="flex items-center justify-between">
<label className="text-xs font-bold text-amber-300 uppercase tracking-wider block">
🖼️ Illustration Alexa & Écran Echo Show
</label>
</div>
{/* Aperçu de l'image sélectionnée */}
<div className="relative h-32 rounded-xl overflow-hidden border border-white/15 bg-slate-950">
<img
src={editingActivity.image_url}
alt={editingActivity.title}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-transparent to-transparent flex items-end p-3">
<span className="font-extrabold text-xs text-white drop-shadow">
{editingActivity.emoji} {editingActivity.title} ({editingActivity.cost_shells} 🐚)
</span>
</div>
</div>
{/* Galerie 1-clic */}
<div>
<span className="text-[11px] text-slate-400 block mb-1">Choisir dans la galerie rapide :</span>
<div className="grid grid-cols-4 sm:grid-cols-8 gap-1.5">
{PRESET_GALLERY.map((p) => (
<button
type="button"
key={p.label}
onClick={() => setEditingActivity({ ...editingActivity, image_url: p.url })}
className={`h-11 rounded-lg overflow-hidden border cursor-pointer ${
editingActivity.image_url === p.url ? 'border-amber-400 ring-2 ring-amber-400' : 'border-white/10 opacity-70 hover:opacity-100'
}`}
title={p.label}
>
<img src={p.url} alt={p.label} className="w-full h-full object-cover" />
</button>
))}
</div>
</div>
{/* Upload et URL */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label className="py-2 px-3 rounded-xl bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-400/40 text-cyan-200 text-xs font-bold flex items-center justify-center gap-1.5 cursor-pointer">
<Upload className="w-4 h-4" /> Envoyer une nouvelle photo...
<input
type="file"
accept="image/*"
onChange={(e) => handleFileUpload(e, true)}
className="hidden"
/>
</label>
<input
type="url"
value={editingActivity.image_url}
onChange={(e) => setEditingActivity({ ...editingActivity, image_url: e.target.value })}
placeholder="Ou coller une URL d'image HTTPS..."
className="bg-slate-900 border border-white/15 rounded-xl px-3 py-2 text-xs"
/>
</div>
</div>
{/* Boutons d'action */}
<div className="pt-2 flex items-center justify-between gap-3">
<button
type="button"
onClick={() => {
if (confirm(`Êtes-vous sûr de vouloir supprimer l'activité "${editingActivity.title}" ?`)) {
handleDeleteActivity(editingActivity.id);
setEditingActivity(null);
}
}}
className="px-3.5 py-2.5 rounded-xl bg-rose-500/20 hover:bg-rose-500/30 border border-rose-500/40 text-rose-300 text-xs font-bold flex items-center gap-1.5 cursor-pointer transition"
>
<Trash2 className="w-4 h-4" /> Supprimer l'activité
</button>
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setEditingActivity(null)}
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"
>
Enregistrer les modifications
</button>
</div>
</div>
</form>
</div>
</div>
)}
{/* Modal for Child Request (Deposit / Lock / Contribute / Convert Euros) */}
{modalOpen && (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-slate-900 border border-white/15 rounded-3xl max-w-md w-full p-6 space-y-5 shadow-2xl">
<div className="flex items-center justify-between">
<h3 className="text-lg font-extrabold text-white">Opération pour {modalConfig.childName}</h3>
<button
onClick={() => setModalOpen(false)}
className="text-slate-400 hover:text-white text-sm cursor-pointer"
>
Fermer ✕
</button>
</div>
<form onSubmit={handleSubmitRequest} className="space-y-4">
<div>
<label className="block text-xs text-slate-400 mb-1">Que souhaite faire {modalConfig.childName} ?</label>
<select
value={modalConfig.type}
onChange={(e) => 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"
>
<option value="DEPOSIT_LOCKED">
🔒 Remettre des vrais coquillages et les BLOQUER (+{settings.interest_rate_percent}%/mois)
</option>
<option value="DEPOSIT_AVAILABLE">
🐚 Remettre des vrais coquillages sur le compte disponible
</option>
<option value="LOCK_VIRTUAL">
📈 Bloquer des coquillages déjà sur le compte virtuel
</option>
<option value="CONTRIBUTE_PHYSICAL">
🎡 Donner des vrais coquillages pour une activité commune
</option>
<option value="CONTRIBUTE_VIRTUAL">
🎡 Utiliser le compte virtuel pour une activité commune
</option>
<option value="CONVERT_EUROS_PHYSICAL">
💶 Échanger des vrais coquillages contre des Euros (2 🐚 = 1 €)
</option>
<option value="CONVERT_EUROS_VIRTUAL">
💶 Convertir des coquillages virtuels en Euros (2 🐚 = 1 €)
</option>
</select>
</div>
{(modalConfig.type === 'CONTRIBUTE_PHYSICAL' || modalConfig.type === 'CONTRIBUTE_VIRTUAL') && (
<div>
<label className="block text-xs text-slate-400 mb-1">Choisir l'activité commune</label>
<select
value={modalConfig.activityId}
onChange={(e) => 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) => (
<option key={a.id} value={a.id}>
{a.emoji} {a.title} (Reste {a.remaining_shells} 🐚)
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-xs text-slate-400 mb-1">Nombre de coquillages 🐚</label>
<input
type="number"
min="1"
max="100"
required
value={modalConfig.shells}
onChange={(e) => 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"
/>
</div>
<p className="text-xs text-slate-400 bg-slate-950/70 p-3 rounded-xl border border-white/5">
💡 La demande sera créée dans le <strong>Guichet Parents</strong>. Dès que les coquillages sont remis aux parents, un clic sur « Valider » créditera le compte !
</p>
<button
type="submit"
className="w-full py-3 rounded-2xl bg-gradient-to-r from-amber-400 to-orange-500 text-slate-950 font-extrabold text-sm cursor-pointer"
>
Envoyer la demande aux parents
</button>
</form>
</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>
);
}