2508 lines
123 KiB
JavaScript
2508 lines
123 KiB
JavaScript
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>
|
||
);
|
||
}
|