# v1.2.0 Implementation Plan — Accouplement, Sidebar, Réappro ♂/♀ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Ajouter un onglet Accouplement dédié, une sidebar de navigation overlay, intégrer les stats dans le dashboard, et ajouter les contraintes ♂/♀ dans la réappro. **Architecture:** Tout le code est dans `src/index.html` (monolithique). On modifie le CSS, le HTML, et le JS inline. Pas de framework. Rendu impératif via innerHTML. État central dans `S`, persisté via `save()`. **Tech Stack:** Vanilla JS, CSS custom properties, Electron IPC. **Fichier principal:** `/mnt/c/Users/micka/Desktop/dd-timer/src/index.html` --- ## Task 1 : Retrait des bébés des enclos + migration **But :** Supprimer le système de bébés des enclos et migrer les données existantes vers `S.archivedStats`. **Files:** - Modify: `src/index.html` **Step 1 : Migration des données au chargement** Dans la fonction `load()` ou juste après le chargement de `S`, ajouter une migration one-shot : ```javascript // Migration v1.2.0 : archiver les babyHistory des enclos if(!S._migratedBabies120){ S.enclos.forEach(enc=>{ if(enc.babyHistory&&enc.babyHistory.length){ if(!S.archivedStats)S.archivedStats={babies:[],totalMax:0}; if(!S.archivedStats.babies)S.archivedStats.babies=[]; enc.babyHistory.forEach(b=>{ S.archivedStats.babies.push({...b,enclosName:enc.name||('Enclos '+(S.enclos.indexOf(enc)+1))}); S.archivedStats.totalMax=(S.archivedStats.totalMax||0)+Math.min(5,Math.floor((b.ddCount||2)/2)); }); } }); S._migratedBabies120=true; save(); } ``` **Step 2 : Supprimer le code des bébés dans les enclos** Supprimer ou commenter les fonctions et le code suivant : - `isBabyUnlocked()` (ligne ~2095) - `updateBabyBtnState()` (ligne ~2101) - `maxBabiesForEnclos()` (ligne ~2115) - `currentBabiesCount()` (ligne ~2118) - `openBabyModal()` (ligne ~2127) - `closeBabyModal()` (ligne ~2134) - `modalBack()`, `modalNext()`, `renderModalStep()` (lignes ~2138-2250) - `selectModalGen()`, `selectModalRaceByIdx()`, `selectModalRace()`, `changeCount()` (lignes ~2250-2285) - `renderBabies()` (ligne ~2015) - `renderBabyHistory()` (ligne ~2049) - `switchSubTab()` (ligne ~2001) - `removeBaby()` (ligne ~2079) - `clearSessionBabies()` (ligne ~2087) - Le HTML du modal bébé (lignes ~352-369) **Step 3 : Nettoyer renderContent pour les enclos** Dans `renderContent()` (ligne ~1465), retirer les sous-onglets "elevage"/"history" et le bouton bébé du rendu des enclos. **Step 4 : Commit** ``` feat: retire bébés des enclos, migre données vers archivedStats ``` --- ## Task 2 : Onglet Accouplement — données et constante **But :** Créer la structure de données et le lookup inversé pour les accouplements. **Files:** - Modify: `src/index.html` **Step 1 : Ajouter 'accouplement' dans SPECIAL_TABS** Ligne ~698 : ```javascript const SPECIAL_TABS=['dashboard','appro','inventaire','accouplement','workflows']; // Note : 'stats' retiré (sera dans le dashboard) ``` **Step 2 : Créer le lookup inversé BREEDING_BY_PARENTS** Après `BREEDING_RECIPES` (ligne ~677) : ```javascript // Lookup inversé : "ParentA|ParentB" → babyRace const BREEDING_BY_PARENTS={}; Object.entries(BREEDING_RECIPES).forEach(([baby,[a,b]])=>{ BREEDING_BY_PARENTS[a+'|'+b]=baby; if(a!==b) BREEDING_BY_PARENTS[b+'|'+a]=baby; }); // Lookup : pour un parent donné, quels partenaires sont possibles ? const COMPATIBLE_PARTNERS={}; Object.entries(BREEDING_RECIPES).forEach(([baby,[a,b]])=>{ if(!COMPATIBLE_PARTNERS[a])COMPATIBLE_PARTNERS[a]=[]; COMPATIBLE_PARTNERS[a].push({partner:b,baby,gen:RACE_GEN[baby]}); if(a!==b){ if(!COMPATIBLE_PARTNERS[b])COMPATIBLE_PARTNERS[b]=[]; COMPATIBLE_PARTNERS[b].push({partner:a,baby,gen:RACE_GEN[baby]}); } }); ``` **Step 3 : Initialiser S.accouplements** Dans le chargement de S (après `load()`) : ```javascript if(!S.accouplements) S.accouplements=[]; ``` **Step 4 : Commit** ``` feat: ajoute structures données accouplement et lookup inversé ``` --- ## Task 3 : Onglet Accouplement — rendu UI **But :** Interface complète de l'onglet accouplement. **Files:** - Modify: `src/index.html` (CSS + JS) **Step 1 : CSS pour l'onglet accouplement** Ajouter dans la section ``, ligne ~313) : ```css /* Accouplement */ .accoup-grid{display:flex;flex-wrap:wrap;gap:10px;justify-content:center} .accoup-card{background:var(--bg2);border:2px solid var(--border);border-radius:var(--r);padding:10px;cursor:pointer;text-align:center;width:100px;transition:border-color .2s,transform .15s} .accoup-card:hover{border-color:var(--text);transform:translateY(-2px)} .accoup-card.selected{border-color:var(--ok);box-shadow:0 0 12px rgba(40,232,136,.25)} .accoup-result{display:flex;align-items:center;gap:20px;justify-content:center;flex-wrap:wrap;padding:20px 0} .accoup-arrow{font-size:2rem;color:var(--muted)} .accoup-inputs{display:flex;gap:16px;align-items:center;justify-content:center;flex-wrap:wrap;padding:16px 0} .accoup-input-group{display:flex;flex-direction:column;align-items:center;gap:6px} .accoup-input-group label{font-size:.82rem;color:var(--muted)} .accoup-input-group input{width:70px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:8px;font:700 1rem 'Nunito',sans-serif;text-align:center} .accoup-gen-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px;justify-content:center} .accoup-gen-tab{padding:4px 12px;border-radius:8px;font-size:.78rem;font-weight:700;cursor:pointer;border:1px solid var(--border);background:var(--bg2);color:var(--muted);transition:all .2s} .accoup-gen-tab.active{background:var(--ok);color:#fff;border-color:var(--ok)} ``` **Step 2 : État local de l'onglet** ```javascript const accoupState={parent1:null,parent2:null,filterGen:null,couples:'',babies:''}; ``` **Step 3 : Fonction renderAccouplement()** ```javascript function renderAccouplement(){ const c=document.getElementById('enclos-content'); c.removeAttribute('data-enc'); const allRaces=Object.keys(RACE_GEN).sort((a,b)=>(RACE_GEN[a]-RACE_GEN[b])||a.localeCompare(b)); const gens=[...new Set(Object.values(RACE_GEN))].sort((a,b)=>a-b); // Si parent1 sélectionné, filtrer les partenaires compatibles let availableRaces=allRaces; if(accoupState.parent1){ const partners=COMPATIBLE_PARTNERS[accoupState.parent1]||[]; availableRaces=partners.map(p=>p.partner); } // Filtrer par gen const fg=accoupState.parent1?null:accoupState.filterGen; const filtered=fg?availableRaces.filter(r=>RACE_GEN[r]===fg):availableRaces; // Déduire le bébé let baby=null,babyGen=null; if(accoupState.parent1&&accoupState.parent2){ baby=BREEDING_BY_PARENTS[accoupState.parent1+'|'+accoupState.parent2]||null; if(baby)babyGen=RACE_GEN[baby]; } // Gen tabs (uniquement si pas de parent1 sélectionné) let genTabs=''; if(!accoupState.parent1){ genTabs=`
Toutes
${gens.map(g=>`
Gen ${g}
`).join('')}
`; } let html=`
Accouplement
`; // Étape 1 ou 2 : sélection parent const stepLabel=accoupState.parent1?'Sélectionne le Parent 2':'Sélectionne le Parent 1'; const selectedParent1=accoupState.parent1; if(selectedParent1){ const gc1=GEN_COLORS[RACE_GEN[selectedParent1]]||'#888'; html+=`
Parent 1
${getDDImage(selectedParent1)}
${selectedParent1}
Gen ${RACE_GEN[selectedParent1]}
`; } // Résultat si les deux parents sont sélectionnés if(baby){ const gcb=GEN_COLORS[babyGen]||'#888'; const gc2=GEN_COLORS[RACE_GEN[accoupState.parent2]]||'#888'; html+=`
Parent 2 — ${accoupState.parent2}
${getDDImage(accoupState.parent2)}
${accoupState.parent2}
Gen ${RACE_GEN[accoupState.parent2]}
`; html+=`
Bébé à naître
${getDDImage(accoupState.parent1)} + ${getDDImage(accoupState.parent2)}
${getDDImage(baby)}
${baby}
Gen ${babyGen}
`; }else{ // Grille de sélection html+=`
${stepLabel} ${genTabs}
${filtered.map(race=>{ const gc=GEN_COLORS[RACE_GEN[race]]||'#888'; return`
${getDDImage(race)}
${race}
Gen ${RACE_GEN[race]}
`; }).join('')}
`; } c.innerHTML=html; } ``` **Step 4 : Fonctions d'interaction** ```javascript function selectAccoupParent(race){ if(!accoupState.parent1){ accoupState.parent1=race; accoupState.parent2=null; }else{ accoupState.parent2=race; } accoupState.couples=''; accoupState.babies=''; renderAccouplement(); } function enregistrerAccouplement(){ const couples=parseInt(accoupState.couples)||0; const babies=parseInt(accoupState.babies)||0; if(couples<=0){alert('Renseigne le nombre de couples.');return;} const baby=BREEDING_BY_PARENTS[accoupState.parent1+'|'+accoupState.parent2]; if(!baby)return; if(!S.accouplements)S.accouplements=[]; S.accouplements.push({ parentA:accoupState.parent1, parentB:accoupState.parent2, baby, gen:RACE_GEN[baby], couples, babiesObtained:babies, date:Date.now() }); save(); // Reset accoupState.parent1=null; accoupState.parent2=null; accoupState.couples=''; accoupState.babies=''; renderAccouplement(); } ``` **Step 5 : Router dans renderContent()** Ligne ~1465, ajouter le cas : ```javascript if(S.activeId==='accouplement'){renderAccouplement();return;} ``` **Step 6 : Ajouter l'onglet dans renderTabs()** Dans `renderTabs()` (ligne ~1341), ajouter un onglet "Accouplement" dans les special tabs, avec icône 💑. **Step 7 : Commit** ``` feat: ajoute onglet Accouplement avec sélection parents et enregistrement ``` --- ## Task 4 : Intégrer les stats dans le Dashboard + section Paramètres **But :** Déplacer les stats et les settings dans le dashboard. Supprimer l'onglet Stats. **Files:** - Modify: `src/index.html` **Step 1 : Retirer 'stats' de SPECIAL_TABS** Déjà fait dans Task 2 Step 1. **Step 2 : Modifier renderDashboard()** Après le contenu actuel du dashboard (grille des enclos), ajouter : 1. **Section Stats globales** : Reprendre le contenu de `renderStats()` (lignes ~2306-2427) mais en tant que section du dashboard plutôt qu'onglet séparé. Adapter pour inclure les données de `S.accouplements` en plus de `S.archivedStats`. 2. **Section Paramètres** : Déplacer le sélecteur de son (lignes ~1421-1433), le toggle notifs (lignes ~1435-1449) et le bouton ntfy (lignes ~1451-1457) depuis le header de `renderTabs()` vers une card "Paramètres" en bas du dashboard. **Calcul des stats depuis S.accouplements :** ```javascript // Agréger accouplements dans les stats const accoupBabies={}; let accoupTotal=0,accoupCouples=0; (S.accouplements||[]).forEach(a=>{ accoupBabies[a.baby]=(accoupBabies[a.baby]||0)+a.babiesObtained; accoupTotal+=a.babiesObtained; accoupCouples+=a.couples; }); ``` **Step 3 : Retirer les settings du header dans renderTabs()** Supprimer les lignes ~1421-1457 (sound select, notif btn, ntfy btn) de `renderTabs()`. **Step 4 : Supprimer renderStats()** Supprimer `renderStats()` (lignes ~2306-2427) et `resetStats()` (lignes ~2290-2304) — le code est intégré dans le dashboard. **Step 5 : Commit** ``` feat: intègre stats et paramètres dans le dashboard, retire onglet Stats ``` --- ## Task 5 : Sidebar navigation **But :** Ajouter un menu hamburger avec sidebar overlay. **Files:** - Modify: `src/index.html` (CSS + HTML + JS) **Step 1 : CSS de la sidebar** ```css /* Sidebar */ .sidebar-toggle{position:fixed;top:12px;left:12px;z-index:1100;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:1.3rem;padding:6px 10px;cursor:pointer;transition:background .2s;line-height:1} .sidebar-toggle:hover{background:var(--bg4)} .sidebar-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1200;opacity:0;pointer-events:none;transition:opacity .25s} .sidebar-overlay.open{opacity:1;pointer-events:all} .sidebar{position:fixed;top:0;left:-280px;width:270px;height:100%;background:var(--bg2);border-right:1px solid var(--border);z-index:1300;transition:left .25s;padding:60px 0 20px;overflow-y:auto;display:flex;flex-direction:column;gap:2px} .sidebar.open{left:0} .sidebar-item{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--muted);font-size:.88rem;font-weight:600;cursor:pointer;transition:background .15s,color .15s;border-left:3px solid transparent} .sidebar-item:hover{background:var(--bg3);color:var(--text)} .sidebar-item.active{color:var(--text);border-left-color:var(--ok);background:var(--bg3)} .sidebar-sep{height:1px;background:var(--border);margin:8px 16px} .sidebar-label{padding:8px 20px;font-size:.7rem;font-weight:800;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);opacity:.6} ``` **Step 2 : HTML — ajouter le bouton et le conteneur sidebar** Juste après `` (ou avant la section `
`) : ```html ``` **Step 3 : Ajouter un padding-left au header pour ne pas chevaucher le bouton hamburger** Ajouter `padding-left:50px` au `header` ou au conteneur principal. **Step 4 : JS — fonctions sidebar** ```javascript function toggleSidebar(){ const sb=document.getElementById('sidebar'); const ov=document.getElementById('sidebar-overlay'); const open=sb.classList.toggle('open'); ov.classList.toggle('open',open); if(open)renderSidebar(); } function closeSidebar(){ document.getElementById('sidebar').classList.remove('open'); document.getElementById('sidebar-overlay').classList.remove('open'); } function sidebarNav(id){ selectEnclos(id); closeSidebar(); } function renderSidebar(){ const sb=document.getElementById('sidebar'); let html=``; html+=``; html+=``; S.enclos.forEach((enc,i)=>{ const label=enc.name||('Enclos '+(i+1)); const isActive=S.activeId===enc.id; html+=``; }); html+=``; html+=``; html+=``; html+=``; html+=``; html+=``; sb.innerHTML=html; } ``` **Step 5 : Commit** ``` feat: ajoute sidebar navigation overlay avec menu hamburger ``` --- ## Task 6 : Réappro ♂/♀ **But :** Ajouter les contraintes de genre dans l'arbre de réapprovisionnement. **Files:** - Modify: `src/index.html` **Step 1 : Modifier calcAppro() pour assigner ♂/♀** Dans `calcAppro()` (ligne ~2507), modifier chaque étape pour indiquer le genre : ```javascript // Convention : parentA = ♂, parentB = ♀ (sauf si inversé par l'utilisateur) const inverted=approState.inverted||{}; const[rawA,rawB]=BREEDING_RECIPES[race]; const isInv=inverted[race]; const a=isInv?rawB:rawA, b=isInv?rawA:rawB; // a = ♂, b = ♀ // Pour les besoins : marquer le genre if(!needs[a])needs[a]={total:0,m:0,f:0}; if(!needs[b])needs[b]={total:0,m:0,f:0}; needs[a].m=(needs[a].m||0)+couplesReal; needs[a].total=(needs[a].total||0)+couplesReal; needs[b].f=(needs[b].f||0)+couplesReal; needs[b].total=(needs[b].total||0)+couplesReal; ``` Le `needs` actuel est un simple nombre (`needs[race]=qty`). Il faut le transformer en objet `{total, m, f}`. **Step 2 : Toggle d'inversion ♂/♀ par étape** Ajouter dans `approState` : ```javascript approState.inverted={}; // race → boolean ``` Fonction : ```javascript function toggleApproGender(race){ if(!approState.inverted)approState.inverted={}; approState.inverted[race]=!approState.inverted[race]; calcAppro(); } ``` Bouton dans le rendu de chaque étape (à côté du toggle reproducteur) : ```html ``` **Step 3 : Affichage ♂/♀ sur les mini-cards** Modifier la fonction `miniCard()` dans `calcAppro()` pour accepter un paramètre genre : ```javascript function miniCard(name,qty,gen,gender){ const gc=GEN_COLORS[gen||RACE_GEN[name]]||'#888'; const genderBadge=gender==='m'?'' :gender==='f'?'':''; return`
${getDDImage(name)} Gen ${gen||RACE_GEN[name]}
${name}${genderBadge} ×${qty}
`; } ``` Appels : `miniCard(s.parentA, s.couples, RACE_GEN[s.parentA], 'm')` et `miniCard(s.parentB, s.couples, RACE_GEN[s.parentB], 'f')`. **Step 4 : Résumé Gen 1 avec genres** Modifier le rendu des matières premières Gen 1 pour afficher `×4 ♂` et `×4 ♀` séparément : ```javascript gen1Needs.forEach(([name,data])=>{ if(data.m>0) html+=miniCard(name,data.m,1,'m'); if(data.f>0) html+=miniCard(name,data.f,1,'f'); }); ``` **Step 5 : Commit** ``` feat: ajoute contraintes ♂/♀ dans réappro avec toggle d'inversion ``` --- ## Task 7 : Mise à jour du rendu des tabs + nettoyage final **But :** S'assurer que les tabs affichent correctement les nouveaux onglets et nettoyer le code mort. **Files:** - Modify: `src/index.html` **Step 1 : Mettre à jour renderTabs()** Ajouter l'onglet Accouplement (💑) dans la barre horizontale, entre Dashboard et les enclos ou entre Réappro et Inventaire. S'assurer que l'onglet Stats n'apparaît plus. **Step 2 : Mettre à jour renderContent()** Vérifier le routing complet : ```javascript function renderContent(){ clearElCache(); const c=document.getElementById('enclos-content'); if(S.activeId==='dashboard'){renderDashboard();return;} if(S.activeId==='accouplement'){renderAccouplement();return;} if(S.activeId==='appro'){renderApprovisionnement();return;} if(S.activeId==='inventaire'){renderInventaire();return;} if(S.activeId==='workflows'){renderWorkflows();return;} // Sinon c'est un enclos const enc=activeEnclos(); if(!enc)return; renderEnclos(enc); } ``` **Step 3 : Nettoyage** - Supprimer tout code mort lié aux bébés dans les enclos - Supprimer le rendu de l'onglet Stats dans renderTabs() - Vérifier que `save()` persiste `S.accouplements` - Vérifier que la migration `_migratedBabies120` fonctionne correctement **Step 4 : Mettre à jour le README (version 1.2.0)** **Step 5 : Commit** ``` feat: nettoyage final, routing mis à jour, v1.2.0 prêt ``` --- ## Ordre d'exécution | Task | Dépendance | Description | |------|-----------|-------------| | 1 | Aucune | Retrait bébés + migration | | 2 | Aucune | Données accouplement + lookup | | 3 | Task 2 | UI onglet accouplement | | 4 | Task 1 | Stats dans dashboard + paramètres | | 5 | Aucune | Sidebar navigation | | 6 | Aucune | Réappro ♂/♀ | | 7 | Tasks 1-6 | Nettoyage + routing final | Tasks 1, 2, 5, 6 sont indépendantes et peuvent être faites en parallèle. Task 3 dépend de 2. Task 4 dépend de 1. Task 7 finalise le tout.