// TontiPay v2 — clavier continu + steppers corrigés import { useState, useEffect } from "react"; const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2); const addDays = (s, n) => { const d = new Date(s+"T00:00:00"); d.setDate(d.getDate()+n); return d.toISOString().split("T")[0]; }; const fmtDate = (s) => { const d=new Date(s+"T00:00:00"),J=["Dim","Lun","Mar","Mer","Jeu","Ven","Sam"],M=["Jan","Fév","Mar","Avr","Mai","Jun","Jul","Aoû","Sep","Oct","Nov","Déc"]; return `${J[d.getDay()]} ${d.getDate()} ${M[d.getMonth()]} ${d.getFullYear()}`; }; const fmtS = (s) => { const d=new Date(s+"T00:00:00"),M=["Jan","Fév","Mar","Avr","Mai","Jun","Jul","Aoû","Sep","Oct","Nov","Déc"]; return `${d.getDate()} ${M[d.getMonth()]} ${d.getFullYear()}`; }; const fmtAmt = (n) => Number(n).toLocaleString("fr-FR")+" FCFA"; const dayDiff = (s) => { const t=new Date(); t.setHours(0,0,0,0); return Math.round((new Date(s+"T00:00:00")-t)/86400000); }; const todayStr= () => new Date().toISOString().split("T")[0]; const getRounds=(t)=> Array.from({length:t.memberCount},(_,i)=>({num:i+1,isMe:i+1===t.myTurn,date:addDays(t.startDate,i*t.frequency),pot:t.memberCount*t.amount})); const KEY="tontipay_v5"; const loadDb=async()=>{ try{ const r=await window.storage.get(KEY); return r?JSON.parse(r.value):{tontines:[]}; }catch{return{tontines:[]};} }; const saveDb=async(db)=>{ try{ await window.storage.set(KEY,JSON.stringify(db)); }catch{} }; const C={bg:"#F9F6F1",card:"#FFFFFF",border:"#E5DDD0",ink:"#1C1712",muted:"#7A7060",gold:"#B8831A",goldBg:"#FDF3DC",goldBd:"#E8C97A",green:"#1A6E50",greenBg:"#E6F4EF",red:"#B83232"}; const S={ page:{background:C.bg,minHeight:"100vh",padding:"20px 16px 70px",maxWidth:480,margin:"0 auto",fontFamily:"'Segoe UI',system-ui,sans-serif"}, card:{background:C.card,border:`1px solid ${C.border}`,borderRadius:16,padding:"16px",marginBottom:10}, btn:{border:"none",borderRadius:12,padding:"13px 20px",fontSize:14,fontWeight:700,cursor:"pointer",display:"inline-flex",alignItems:"center",justifyContent:"center",gap:6,fontFamily:"inherit"}, inp:{width:"100%",border:`1px solid ${C.border}`,borderRadius:10,padding:"12px 14px",fontSize:15,background:C.card,color:C.ink,outline:"none",fontFamily:"inherit",boxSizing:"border-box"}, lbl:{display:"block",fontSize:11,fontWeight:700,letterSpacing:1,textTransform:"uppercase",color:C.muted,marginBottom:8}, row:{display:"flex",gap:10}, nav:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}, back:{background:"none",border:"none",fontSize:13,color:C.muted,cursor:"pointer",fontWeight:600,padding:0,fontFamily:"inherit"}, track:{background:C.border,borderRadius:99,height:8,overflow:"hidden"}, fill:{height:"100%",borderRadius:99,background:C.gold,transition:"width 0.5s"}, chip:{background:C.bg,border:`1px solid ${C.border}`,borderRadius:20,padding:"4px 10px",fontSize:11,color:C.muted,fontWeight:500}, empty:{textAlign:"center",padding:"40px 20px",background:C.card,border:`1px solid ${C.border}`,borderRadius:16,marginTop:8}, }; /* ═══════════════════════════════════════════════════════ STEPPER — Défini HORS du composant principal Boutons +/− ET saisie directe dans le champ ═══════════════════════════════════════════════════════ */ function Stepper({label,value,min,max,onChange,note}){ const [raw,setRaw]=useState(String(value)); useEffect(()=>{ setRaw(String(value)); },[value]); const set=(v)=>{ const c=Math.max(min,Math.min(max,v)); onChange(c); setRaw(String(c)); }; return(
{ setRaw(e.target.value); const n=parseInt(e.target.value,10); if(!isNaN(n)&&n>=min&&n<=max) onChange(n); }} onBlur={()=>{ const n=parseInt(raw,10); if(isNaN(n)||nmax) set(max); else set(n); }} style={{flex:1,border:"none",borderLeft:`1px solid ${C.goldBd}`,borderRight:`1px solid ${C.goldBd}`,textAlign:"center",fontSize:22,fontWeight:900,color:C.gold,background:C.card,outline:"none",fontFamily:"inherit",width:0,padding:"10px 0"}} />
{note&&

{note}

}
); } /* ═══════════════════════════════════════════════════════ HOME VIEW — Défini HORS du composant principal ═══════════════════════════════════════════════════════ */ function HomeView({active,archived,db,setView,archiveCurrent,startCreate}){ const [conf,setConf]=useState(false); const rounds=active?getRounds(active):[]; const done=active?Object.values(active.checks||{}).filter(Boolean).length:0; const pct=active?Math.round(done/rounds.length*100):0; const myR=active?rounds[active.myTurn-1]:null; const d=myR?dayDiff(myR.date):null; const now=todayStr(); return(
🪙 TontiPay

Mes Tontines

{active?( <>
EN COURS
{active.name}
{active.memberCount} membres · {fmtAmt(active.amount)}/tour · tous les {active.frequency}j
ACTIVE
{done}/{rounds.length} tours cochés {pct}%
{myR&&(
👑 Mon tour (#{active.myTurn})
{fmtDate(myR.date)}
{fmtAmt(myR.pot)}
{myR.date===now?"🎉 Aujourd'hui !":d>0?`⏳ Dans ${d}j`:"✅ Passé"}
)} {(()=>{const next=rounds.find(r=>!active.checks[r.num]&&r.date>=now&&!r.isMe);if(!next)return null;return(
Prochain paiementTour {next.num} · {fmtS(next.date)}
);})()}
{!conf ? :
}
):(
🪙

Aucune tontine active

Crée ta première tontine pour commencer

)} {archived.length>0&&(
{archived.length}
Archivées
{db.tontines.reduce((a,t)=>a+(t.memberCount||0)*(t.amount||0),0).toLocaleString("fr-FR")}
FCFA géré
)}
); } /* ═══════════════════════════════════════════════════════ CREATE VIEW — Défini HORS du composant principal → clavier ne disparaît plus entre chaque touche ✓ ═══════════════════════════════════════════════════════ */ function CreateView({form,setForm,onCreate,onBack,saving}){ const mc=Number(form.memberCount)||2; const mt=Number(form.myTurn)||1; const myDate=form.startDate&&form.frequency?addDays(form.startDate,(mt-1)*Number(form.frequency)):null; const cagnotte=Number(form.amount)*mc; const diff=myDate?dayDiff(myDate):null; return(
Nouvelle tontine

Configurer

Pas besoin des noms — juste les chiffres

{/* Nom */}
setForm(f=>({...f,name:e.target.value}))} />
{/* Cotisation + Fréquence */}
setForm(f=>({...f,amount:e.target.value}))} />
setForm(f=>({...f,frequency:e.target.value}))} />
{/* Date */}
setForm(f=>({...f,startDate:e.target.value}))} />
{/* Steppers — HORS composant principal donc stables */}
setForm(f=>({...f,memberCount:v,myTurn:Math.min(Number(f.myTurn)||1,v)}))} note="min 2 · max 50" />
setForm(f=>({...f,myTurn:v}))} note={`sur ${mc} tours`} />
{/* Aperçu live */} {myDate&&cagnotte>0&&(
📊 Aperçu en direct
{[ ["Je reçois", {fmtAmt(cagnotte)}], ["Mon jour", {fmtDate(myDate)}], ["Compte à rebours", 0?C.green:C.red}}>{diff>0?`⏳ ${diff}j`:diff===0?"🎉 Aujourd'hui":"Passé"}], ["Durée totale", {(mc-1)*Number(form.frequency)} jours], ["Total que je cotise", {fmtAmt(cagnotte)}], ].map(([l,v],i)=>(
{l}{v}
))}
)}
); } /* ═══════════════════════════════════════════════════════ TRACKER VIEW ═══════════════════════════════════════════════════════ */ function TrackerView({active,onToggle,setView}){ if(!active) return

Aucune tontine active.

; const rounds=getRounds(active); const done=Object.values(active.checks||{}).filter(Boolean).length; const pct=Math.round(done/rounds.length*100); const now=todayStr(); const myR=rounds[active.myTurn-1]; const d=myR?dayDiff(myR.date):null; return(
{done}/{rounds.length} cochés

{active.name}

{active.memberCount} membres · {fmtAmt(active.amount)}/tour · tous les {active.frequency}j

Progression{pct}%
{rounds.map(r=>
)}
{myR&&(
👑 Mon tour — #{active.myTurn}
{fmtDate(myR.date)}
{myR.date===now?"🎉 C'est aujourd'hui !":d>0?`⏳ Dans ${d}j`:"✅ Passé"}
{fmtAmt(myR.pot)}
je reçois
)}
☑️ Coche chaque paiement effectué
{rounds.map(r=>{ const checked=!!active.checks[r.num],isToday=r.date===now,isPast=r.dateonToggle(active.id,r.num)} style={{...S.card,display:"flex",alignItems:"center",gap:12,marginBottom:8,cursor:"pointer", opacity:checked&&!r.isMe?0.5:1,background:r.isMe?C.goldBg:C.card, border:`${r.isMe?"1.5":"1"}px solid ${isToday?C.green:r.isMe?C.goldBd:C.border}`}}>
{checked&&}
{r.num}
{r.isMe?"👑 MOI":`Tour ${r.num}`}
{fmtDate(r.date)} {isToday&&⚡ Aujourd'hui} {isPast&&!isToday&& · Passé}
{(r.pot/1000).toFixed(0)}k
FCFA
); })}
); } /* ═══════════════════════════════════════════════════════ HISTORY VIEW ═══════════════════════════════════════════════════════ */ function HistoryView({archived,setView,setDetail}){ return(
{archived.length} tontine{archived.length!==1?"s":""}

Historique

Tape sur une carte pour voir le détail

{archived.length===0?(
📋

Aucune tontine archivée

Elles apparaissent automatiquement

):archived.map(t=>{ const rounds=getRounds(t),done=Object.values(t.checks||{}).filter(Boolean).length,pct=Math.round(done/rounds.length*100); return(
{setDetail(t);setView("detail");}}>
{t.name}
{t.memberCount} membres · {fmtAmt(t.amount)}/tour · {fmtS(t.startDate)}
{t.memberCount} mbr {fmtAmt(t.memberCount*t.amount)} {done===rounds.length?"✓ Complet":`${done}/${rounds.length}`}
{pct}%
); })}
); } /* ═══════════════════════════════════════════════════════ DETAIL VIEW ═══════════════════════════════════════════════════════ */ function DetailView({detail,setView,deleteTontine}){ const [conf,setConf]=useState(false); if(!detail) return null; const rounds=getRounds(detail),done=Object.values(detail.checks||{}).filter(Boolean).length,pct=Math.round(done/rounds.length*100),myR=rounds[detail.myTurn-1]; return(
{!conf? :
}

{detail.name}

{detail.memberCount} membres {fmtAmt(detail.amount)}/tour Tous les {detail.frequency}j 📦 Archivée
Résumé
{[["Démarrée le",fmtDate(detail.startDate)],["Cagnotte / tour",fmtAmt(detail.memberCount*detail.amount)],["Mon tour",`#${detail.myTurn} · ${myR?fmtDate(myR.date):"—"}`],["Tours cochés",`${done}/${rounds.length} (${pct}%)`],["Durée totale",`${(rounds.length-1)*detail.frequency} jours`]].map(([l,v],i)=>(
{l}{v}
))}
{rounds.map(r=>(
{detail.checks[r.num]&&}
{r.num}
{r.isMe?"👑 MOI":`Tour ${r.num}`}
{fmtDate(r.date)}
{(r.pot/1000).toFixed(0)}k FCFA
))}
); } /* ═══════════════════════════════════════════════════════ APP PRINCIPAL Contient uniquement la logique d'état. Tous les views/composants sont définis EN DEHORS → les inputs ne perdent plus le focus ✓ ═══════════════════════════════════════════════════════ */ export default function TontiPay(){ const [view, setView] =useState("load"); const [db, setDb] =useState({tontines:[]}); const [active,setActive]=useState(null); const [detail,setDetail]=useState(null); const [toast, setToast] =useState(null); const [saving,setSaving]=useState(false); const [form, setForm] =useState({name:"",amount:20000,frequency:5,startDate:todayStr(),myTurn:1,memberCount:5}); useEffect(()=>{ loadDb().then(d=>{ setDb(d); const a=d.tontines.find(t=>t.status==="active"); if(a) setActive(a); setView("home"); }); },[]); const persist=async(newDb)=>{ setDb(newDb); await saveDb(newDb); }; const toast2=(msg,type="gold")=>{ setToast({msg,type}); setTimeout(()=>setToast(null),2400); }; const onToggle=async(tId,num)=>{ const was=db.tontines.find(t=>t.id===tId)?.checks?.[num]; const newDb={...db,tontines:db.tontines.map(t=>t.id!==tId?t:{...t,checks:{...t.checks,[num]:!t.checks[num]}})}; const upd=newDb.tontines.find(t=>t.id===tId); setActive(upd); if(detail?.id===tId) setDetail(upd); await persist(newDb); toast2(was?"↩ Décoché":"✓ Coché !",was?"gold":"green"); }; const startCreate=()=>{ setForm({name:"",amount:20000,frequency:5,startDate:todayStr(),myTurn:1,memberCount:5}); setView("create"); }; const onCreate=async()=>{ setSaving(true); const newT={id:uid(),name:form.name.trim()||"Ma Tontine",amount:Number(form.amount),frequency:Number(form.frequency),startDate:form.startDate,myTurn:Number(form.myTurn),memberCount:Number(form.memberCount),checks:{},status:"active",createdAt:new Date().toISOString(),archivedAt:null}; const list=db.tontines.map(t=>t.status==="active"?{...t,status:"archived",archivedAt:new Date().toISOString()}:t); list.push(newT); const newDb={...db,tontines:list}; await persist(newDb); setActive(newT); setSaving(false); toast2("🎉 Tontine créée !"); setView("tracker"); }; const archiveCurrent=async()=>{ if(!active) return; const newDb={...db,tontines:db.tontines.map(t=>t.id===active.id?{...t,status:"archived",archivedAt:new Date().toISOString()}:t)}; await persist(newDb); setActive(null); toast2("📦 Archivée"); }; const deleteTontine=async(id)=>{ const newDb={...db,tontines:db.tontines.filter(t=>t.id!==id)}; await persist(newDb); setDetail(null); toast2("🗑️ Supprimée"); setView("history"); }; const archived=db.tontines.filter(t=>t.status==="archived").sort((a,b)=>new Date(b.archivedAt||b.createdAt)-new Date(a.archivedAt||a.createdAt)); if(view==="load") return
🪙
; return(
{toast&&
{toast.msg}
} {view==="home" && } {view==="create" && setView("home")} saving={saving}/>} {view==="tracker" && } {view==="history" && } {view==="detail" && }
); }