ZenTrade

Nessuna sessione impostata
Mode ProfonditΓ  attivo: mercati, trend, promemoria e storico restano nascosti. Solo tu e il Campo.

Logout class="hero-state" aria-live="polite">Pronto quando lo sei tu

Il Campo Γ¨ la tua calma nel trading: si accende ogni volta che scegli di non controllare. Rallentare Γ¨ un progresso, non una sconfitta.

0 pt

0 / 200 pt

Capitale Mentale la tua metrica, indipendente dal P&L del broker

Inizia: scegli un impulso qui sotto per registrare il tuo primo check-in.

Serie attuale πŸ”₯
Tasso di resistenza
La tua kryptonite
Controlli esterni
🎯

Maestria e progresso

Livello
Principiante
0 impulsi resistiti totali
Badge rari
Nessun badge ancora
Sfida settimanale
Questa settimana: resisti a 3 trigger FOMO
0 / 3
Reflection dopo 30 giorni
Cosa hai imparato su di te? Scrivi una riflessione.

Hai controllato prima di aprire ZenTrade?

Registralo senza ricostruire il momento a posteriori. La streak riparte da zero, il dato entra nel tasso di resistenza e non riceve punti positivi o penalitΓ .

Un momento di onestΓ : questa mossa era prevista dal tuo piano, o Γ¨ nata da un impulso? Nessun giudizio, solo un dato vero.

Gestibile (1) Forte Perdita di controllo (10)

Il contesto non Γ¨ un giudizio: Γ¨ la mappa di dove vive il tuo impulso. Riconoscerlo Γ¨ il primo passo per disarmarlo.

Quanto ti sembra urgente?
🧭

La tua Bussola

Non ancora impostata.

πŸ“‹ I tuoi Piani Se-Allora

Decisi ora, a mente lucida, così non devi inventarli nel momento dell'impulso.

πŸ›‘οΈ Firewall Ambientale

Piccoli ostacoli volontari che rallentano il gesto automatico di aprire il broker.

I tuoi Log

); printWindow.document.close(); printWindow.focus(); setTimeout(() => printWindow.print(), 250); } reportMonthInput.addEventListener('change', () => { showToast('Mese del report impostato su ' + getMonthLabel(reportMonthInput.value) + '.'); }); exportReportBtn.addEventListener('click', exportMonthlyReport); printReportBtn.addEventListener('click', printMonthlyReport); // ---------- EXPORT / IMPORT ---------- exportCsvBtn.addEventListener('click', () => { const logs = getLogs(); if (logs.length === 0) { alert('Nessun dato da esportare.'); return; } let csv = 'Data,Ora,Tipo_Evento,Trigger,Intenzionalita,Mismatch_Trigger_Piano,Intensita_Iniziale,Intensita_Finale,Esito,Protocollo_Pausa,Punti_Capitale,Canale_Impulso,Fase_Ciclo,Stato_Posizione,Urgenza_Temporale,Audit_Stato,Audit_Sollievo,Audit_Stati,Brain_Dump,Ristrutturazione_CBT\n'; logs.forEach(log => { const d = new Date(log.date); const esito = log.outcome === 'resisted' ? 'Resistito' : (log.outcome === 'checked' ? 'Controllato' : ''); const finalInt = log.finalIntensity !== null && log.finalIntensity !== undefined ? log.finalIntensity : ''; const intentType = log.intent && typeof log.intent === 'object' ? log.intent.type : log.intent; const triggerPlanMismatch = log.intent && ( log.intent.triggerPlanMismatch === true || log.intent.conflict === true ) ? 'Si' : 'No'; const auditStatus = log.audit ? log.audit.status : 'none'; const auditRelief = log.audit && log.audit.relief ? log.audit.relief : ''; const auditStates = log.audit && log.audit.states ? log.audit.states.join(' | ') : ''; const eventType = log.entryType === 'external_lapse' ? 'Controllo esterno' : 'Check-in'; const points = computeLogCapitalDelta(log); const ch = (log.context && log.context.channel) ? log.context.channel : ''; const ph = (log.context && log.context.phase) ? log.context.phase : ''; const ps = (log.context && log.context.positionState) ? log.context.positionState : ''; const row = [ d.toLocaleDateString('it-IT'), d.toLocaleTimeString('it-IT').slice(0, 5), eventType, log.emotion, intentType || '', triggerPlanMismatch, log.intensity === null ? '' : log.intensity, finalInt, esito, log.protocolStatus || 'not_required', points, ch, ph, ps, log.timeUrgency || '', auditStatus, auditRelief, auditStates, log.notes || '', log.cbtNotes || '' ].map(f => `"${String(f).replace(/"/g, '""')}"`).join(','); csv += row + '\n'; }); const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'ZenTrade_Export_' + new Date().toISOString().split('T')[0] + '.csv'; link.click(); URL.revokeObjectURL(link.href); }); exportJsonBtn.addEventListener('click', () => { const logs = getLogs(); if (logs.length === 0) { alert('Nessun dato da esportare.'); return; } const payload = { app: 'ZenTrade', exportedAt: new Date().toISOString(), logs, values: getValues(), plans: getPlans() }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'ZenTrade_Backup_' + new Date().toISOString().split('T')[0] + '.json'; link.click(); URL.revokeObjectURL(link.href); showToast('Backup JSON salvato. πŸ”’'); }); importBtn.addEventListener('click', () => importFileInput.click()); importFileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (evt) => { try { const parsed = JSON.parse(evt.target.result); const importedLogs = Array.isArray(parsed) ? parsed : parsed.logs; if (!Array.isArray(importedLogs)) throw new Error('formato non valido'); const valid = importedLogs.every(l => { const isExternalLapse = l && l.entryType === 'external_lapse'; return l && (isExternalLapse || typeof l.emotion === 'string') && (isExternalLapse || typeof l.intensity === 'number'); }); if (!valid) throw new Error('voci non valide'); const existing = getLogs(); const existingIds = new Set(existing.map(l => l.id)); const merged = existing.slice(); let added = 0; importedLogs.forEach(l => { const isExternalLapse = l.entryType === 'external_lapse'; const safeLog = { id: typeof l.id === 'number' ? l.id : Date.now() + Math.floor(Math.random() * 1000), date: l.date || new Date().toISOString(), entryType: isExternalLapse ? 'external_lapse' : 'check_in', source: isExternalLapse ? 'manual_external' : (l.source || 'check_in'), emotion: isExternalLapse ? 'Controllo fuori ZenTrade' : l.emotion, intent: normalizeIntent(l.intent, l.date || new Date().toISOString()), intensity: isExternalLapse ? null : l.intensity, finalIntensity: !isExternalLapse && typeof l.finalIntensity === 'number' ? l.finalIntensity : null, completedUrgeSurfing: typeof l.completedUrgeSurfing === 'boolean' ? l.completedUrgeSurfing : false, protocolStatus: typeof l.protocolStatus === 'string' ? l.protocolStatus : 'not_required', completedAt: l.completedAt || null, bypassedAt: l.bypassedAt || null, interruptedAt: l.interruptedAt || null, context: normalizeContext(l.context), timeUrgency: typeof l.timeUrgency === 'string' && l.timeUrgency ? l.timeUrgency : null, notes: l.notes || '', cbtNotes: l.cbtNotes || '', outcome: isExternalLapse ? 'checked' : ((l.outcome === 'resisted' || l.outcome === 'checked') ? l.outcome : null), audit: normalizeAudit(l.audit) }; if (!existingIds.has(safeLog.id)) { merged.push(safeLog); existingIds.add(safeLog.id); added++; } }); merged.sort((a, b) => b.id - a.id); if (parsed.values && parsed.values.statement) saveValues(parsed.values.statement); if (parsed.plans && typeof parsed.plans === 'object') savePlans(parsed.plans); if (saveLogs(merged)) { renderCompassWidget(); renderPlansList(); renderHistory(); updateStats(); alert('Importazione completata: ' + added + ' nuovi check-in aggiunti.'); } } catch (err) { console.error(err); alert("Errore durante l'importazione: file non valido o corrotto."); } finally { importFileInput.value = ''; } }; reader.readAsText(file); }); clearBtn.addEventListener('click', () => { if (confirm('Ricominciare da zero? Eliminerai tutto lo storico.\n\nConsiglio: esporta prima un backup JSON dal pulsante qui sopra.')) { try { localStorage.removeItem(storageKey); } catch (err) { console.error(err); } initApp(); } }); // ══════════ MODULE: SESSION TIMER ══════════ const sessionSettingsKey = 'zenTradeSessionSettings'; const sessionStateKey = 'zenTradeSessionState'; let sessionCountdownTimeout = null; const sessionDot = document.getElementById('session-dot'); const sessionTimerLabel = document.getElementById('session-timer-label'); const sessionTimerClock = document.getElementById('session-timer-clock'); const sessionSettingsToggle = document.getElementById('session-settings-toggle'); const sessionSettingsPanel = document.getElementById('session-settings-panel'); const sessionStartBtn = document.getElementById('session-start-btn'); const sessionEndBtn = document.getElementById('session-end-btn'); const sessionSaveSettingsBtn = document.getElementById('session-save-settings'); const sessionProgressWrap = document.getElementById('session-progress-wrap'); const sessionProgressFill = document.getElementById('session-progress-fill'); const sessionStart1Input = document.getElementById('session-start-1'); const sessionEnd1Input = document.getElementById('session-end-1'); const sessionStart2Input = document.getElementById('session-start-2'); const sessionEnd2Input = document.getElementById('session-end-2'); function getSessionSettings() { try { const raw = localStorage.getItem(sessionSettingsKey); return raw ? JSON.parse(raw) : { windows: [{ start: '09:00', end: '12:00' }, { start: '', end: '' }] }; } catch (e) { return { windows: [{ start: '09:00', end: '12:00' }, { start: '', end: '' }] }; } } function saveSessionSettings(settings) { try { localStorage.setItem(sessionSettingsKey, JSON.stringify(settings)); } catch (e) {} } function getSessionState() { try { const raw = localStorage.getItem(sessionStateKey); return raw ? JSON.parse(raw) : null; } catch (e) { return null; } } function saveSessionState(state) { try { state ? localStorage.setItem(sessionStateKey, JSON.stringify(state)) : localStorage.removeItem(sessionStateKey); } catch (e) {} } function loadSessionSettingsUI() { const s = getSessionSettings(); if (s.windows[0]) { sessionStart1Input.value = s.windows[0].start || ''; sessionEnd1Input.value = s.windows[0].end || ''; } if (s.windows[1]) { sessionStart2Input.value = s.windows[1].start || ''; sessionEnd2Input.value = s.windows[1].end || ''; } } function timeToMinutes(t) { if (!t) return null; const [h, m] = t.split(':').map(Number); return h * 60 + m; } function getCurrentSessionWindow() { const settings = getSessionSettings(); const now = new Date(); const cur = now.getHours() * 60 + now.getMinutes(); for (const w of settings.windows) { const s = timeToMinutes(w.start), e = timeToMinutes(w.end); if (s !== null && e !== null && cur >= s && cur < e) return { start: w.start, end: w.end, startMins: s, endMins: e }; } return null; } function getNextSessionWindow() { const settings = getSessionSettings(); const cur = new Date().getHours() * 60 + new Date().getMinutes(); let nearest = null, nearestDiff = Infinity; for (const w of settings.windows) { const s = timeToMinutes(w.start); if (s !== null) { let diff = s - cur; if (diff < 0) diff += 1440; if (diff < nearestDiff) { nearestDiff = diff; nearest = { start: w.start, end: w.end, diffMinutes: diff }; } } } return nearest; } function formatSessionCountdown(sec) { const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60; if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); } function updateSessionTimerUI() { const state = getSessionState(); const now = new Date(); const cur = now.getHours() * 60 + now.getMinutes(); if (state && state.active) { const endMins = timeToMinutes(state.end); if (endMins !== null && cur >= endMins) { endSession('auto'); return; } const remaining = Math.max(0, (endMins - cur) * 60); const total = (endMins - timeToMinutes(state.start)) * 60; const elapsed = total - remaining; const pct = total > 0 ? Math.min(100, (elapsed / total) * 100) : 0; const warn = remaining <= 600; sessionDot.className = 'session-dot' + (warn ? ' warning' : ' active'); sessionTimerLabel.textContent = 'Sessione attiva '; sessionTimerClock.textContent = ' ' + formatSessionCountdown(remaining); sessionSettingsToggle.style.display = 'none'; sessionStartBtn.style.display = 'none'; sessionEndBtn.style.display = ''; sessionProgressWrap.style.display = ''; sessionProgressFill.style.width = pct + '%'; sessionProgressFill.style.background = warn ? 'var(--warning)' : 'var(--accent)'; } else { sessionDot.className = 'session-dot'; sessionProgressWrap.style.display = 'none'; sessionStartBtn.style.display = 'none'; sessionEndBtn.style.display = 'none'; sessionSettingsToggle.style.display = ''; sessionTimerClock.textContent = ''; const inWindow = getCurrentSessionWindow(); if (inWindow) { sessionTimerLabel.textContent = 'Finestra attiva pronta per iniziare'; sessionTimerClock.textContent = ' ' + inWindow.start + '' + inWindow.end; sessionStartBtn.style.display = ''; } else { const next = getNextSessionWindow(); const settings = getSessionSettings(); const hasAny = settings.windows.some(w => w.start && w.end); if (next) { sessionTimerLabel.textContent = 'Prossima sessione tra ' + next.diffMinutes + ' min'; sessionTimerClock.textContent = ' ' + next.start; } else if (hasAny) { sessionTimerLabel.textContent = 'Sessione conclusa per oggi'; } else { sessionTimerLabel.textContent = 'Nessuna sessione impostata'; } } } } function startSession() { const inWindow = getCurrentSessionWindow(); if (!inWindow) return; saveSessionState({ active: true, start: inWindow.start, end: inWindow.end, startedAt: new Date().toISOString() }); updateSessionTimerUI(); startSessionCountdown(); showToast('Sessione avviata. Opera con calma, ZenTrade ti accompagna.'); } function endSession(reason) { saveSessionState(null); clearTimeout(sessionCountdownTimeout); updateSessionTimerUI(); showToast(reason === 'auto' ? 'Sessione terminata. Registra eventuali impulsi residui.' : 'Sessione terminata. Buon lavoro.'); if (reason === 'auto') openJournalModal(); } function startSessionCountdown() { clearTimeout(sessionCountdownTimeout); function tick() { updateSessionTimerUI(); const state = getSessionState(); if (!state || !state.active) return; const endMins = timeToMinutes(state.end); const cur = new Date().getHours() * 60 + new Date().getMinutes(); const remaining = Math.max(0, (endMins - cur) * 60); sessionCountdownTimeout = setTimeout(tick, remaining <= 300 ? 1000 : 30000); } tick(); } sessionSettingsToggle.addEventListener('click', () => { sessionSettingsPanel.classList.toggle('hidden'); if (!sessionSettingsPanel.classList.contains('hidden')) loadSessionSettingsUI(); }); sessionSaveSettingsBtn.addEventListener('click', () => { saveSessionSettings({ windows: [ { start: sessionStart1Input.value || '', end: sessionEnd1Input.value || '' }, { start: sessionStart2Input.value || '', end: sessionEnd2Input.value || '' } ] }); sessionSettingsPanel.classList.add('hidden'); updateSessionTimerUI(); showToast('Sessioni salvate.'); }); sessionStartBtn.addEventListener('click', startSession); sessionEndBtn.addEventListener('click', () => { if (confirm('Terminare la sessione attuale?')) endSession('manual'); }); (function restoreSessionTimer() { const s = getSessionState(); if (s && s.active) startSessionCountdown(); updateSessionTimerUI(); })(); // ══════════ MODULE: GOAL SYSTEM ══════════ const goalKey = 'zenTradeGoal'; const milestoneKey = 'zenTradeLastMilestone'; const goalSection = document.getElementById('goal-section'); const goalBarFill = document.getElementById('goal-bar-fill'); const goalLabel = document.getElementById('goal-label'); const goalEditTrigger = document.getElementById('goal-edit-trigger'); const goalEditPanel = document.getElementById('goal-edit-panel'); const goalTargetInput = document.getElementById('goal-target-input'); const goalDeadlineInput = document.getElementById('goal-deadline-input'); const goalSaveBtn = document.getElementById('goal-save-btn'); function getGoal() { try { const raw = localStorage.getItem(goalKey); return raw ? JSON.parse(raw) : { target: 200, deadline: '' }; } catch (e) { return { target: 200, deadline: '' }; } } function saveGoal(goal) { try { localStorage.setItem(goalKey, JSON.stringify(goal)); } catch (e) {} } function updateGoalUI() { if (!goalSection) return; const goal = getGoal(); const logs = getLogs(); const capital = computeMentalCapital(logs); const target = goal.target || 200; const pct = Math.min(100, Math.round((capital / target) * 100)); goalBarFill.style.width = pct + '%'; let html = '' + capital + ' / ' + target + ' pt'; if (goal.deadline) { const dl = new Date(goal.deadline + 'T23:59:59'); const now = new Date(); const daysLeft = Math.ceil((dl - now) / 86400000); if (daysLeft > 0) html += ' ' + daysLeft + ' giorni rimanenti'; else if (daysLeft === 0) html += ' oggi!'; else html += ' scaduto'; } html += ' Β· '; goalLabel.innerHTML = html; const innerBtn = document.getElementById('goal-edit-inner'); if (innerBtn) innerBtn.addEventListener('click', toggleGoalEdit); checkMilestone(capital, target); } function checkMilestone(capital, target) { if (target <= 0) return; const pct = Math.round((capital / target) * 100); const milestones = [25, 50, 75, 100]; let lastSeen = 0; try { lastSeen = parseInt(localStorage.getItem(milestoneKey) || '0', 10); } catch (e) {} for (const m of milestones) { if (pct >= m && lastSeen < m) { try { localStorage.setItem(milestoneKey, String(m)); } catch (e) {} if (m === 100) showToast('Obiettivo raggiunto! Hai completato il 100% del tuo target. 🎯'); else showToast('Milestone ' + m + '% del target raggiunto! Continua cosΓ¬. πŸ’ͺ'); break; } } } function toggleGoalEdit() { goalEditPanel.classList.toggle('hidden'); if (!goalEditPanel.classList.contains('hidden')) { const goal = getGoal(); goalTargetInput.value = goal.target || 200; goalDeadlineInput.value = goal.deadline || ''; goalTargetInput.focus(); } } goalEditTrigger.addEventListener('click', toggleGoalEdit); window.toggleGoalEdit = toggleGoalEdit; goalSaveBtn.addEventListener('click', () => { const target = parseInt(goalTargetInput.value, 10); if (!target || target < 10) { alert('Il target deve essere almeno 10 pt.'); return; } const oldGoal = getGoal(); saveGoal({ target, deadline: goalDeadlineInput.value || '' }); if (oldGoal.target !== target) { try { localStorage.setItem(milestoneKey, '0'); } catch (e) {} } goalEditPanel.classList.add('hidden'); updateGoalUI(); showToast('Obiettivo aggiornato.'); }); // ══════════ MODULE: QUICK CHECK-IN ══════════ const gamFab = document.getElementById('gamification-fab'); if (gamFab) gamFab.onclick = function () { const m = document.getElementById('gamification-modal'); const level = document.getElementById('mastery-level'); const badges = document.getElementById('badges-list'); const chLabel = document.getElementById('challenge-label'); const chCur = document.getElementById('challenge-current'); if (level) document.getElementById('modal-mastery').textContent = level.textContent; if (badges) document.getElementById('modal-badges').innerHTML = badges.innerHTML || 'Nessun badge'; if (chLabel) document.getElementById('modal-challenge').textContent = chLabel.textContent || ''; if (chCur) document.getElementById('modal-challenge-progress').textContent = (chCur.textContent || '0') + ' / ' + (document.getElementById('challenge-target')?.textContent || '3'); m.classList.remove('hidden','opacity-0'); }; const quickFab = document.getElementById('quick-fab'); const quickModal = document.getElementById('quick-modal'); const quickModalClose = document.getElementById('quick-modal-close'); const quickTriggerGrid = document.getElementById('quick-trigger-grid'); const quickLogBtn = document.getElementById('quick-log-btn'); const quickFullBtn = document.getElementById('quick-full-btn'); let quickSelectedTrigger = null; function openQuickModal() { quickSelectedTrigger = null; quickLogBtn.disabled = true; quickTriggerGrid.querySelectorAll('.quick-trigger-btn').forEach(b => b.classList.remove('active')); quickModal.classList.remove('hidden'); setTimeout(() => quickModal.classList.remove('opacity-0'), 50); document.addEventListener('keydown', handleQuickKeydown); } function closeQuickModal() { quickModal.classList.add('opacity-0'); setTimeout(() => quickModal.classList.add('hidden'), 350); document.removeEventListener('keydown', handleQuickKeydown); } function handleQuickKeydown(e) { if (e.key === 'Escape') closeQuickModal(); else trapFocus(quickModal, e); } quickFab.addEventListener('click', openQuickModal); quickModalClose.addEventListener('click', closeQuickModal); quickModal.addEventListener('click', (e) => { if (e.target === quickModal) closeQuickModal(); }); quickTriggerGrid.addEventListener('click', (e) => { const btn = e.target.closest('.quick-trigger-btn'); if (!btn) return; quickTriggerGrid.querySelectorAll('.quick-trigger-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); quickSelectedTrigger = btn.getAttribute('data-value'); quickLogBtn.disabled = false; }); quickLogBtn.addEventListener('click', () => { if (!quickSelectedTrigger) return; const now = new Date().toISOString(); const quickLog = { id: Date.now() + Math.floor(Math.random() * 1000), date: now, entryType: 'check_in', source: 'quick_check_in', emotion: quickSelectedTrigger, intent: { type: null, declaredAt: now, triggerPlanMismatch: false }, intensity: 5, finalIntensity: null, completedUrgeSurfing: false, protocolStatus: 'not_required', context: { channel: null, phase: null, positionState: null }, timeUrgency: null, notes: '[Log rapido]', cbtNotes: '', outcome: null, audit: { status: 'none', dueAt: null, openedAt: null, completedAt: null, notificationSentAt: null, relief: null, states: [] } }; const logs = getLogs(); logs.unshift(quickLog); if (saveLogs(logs)) { closeQuickModal(); renderHistory(); updateStats(); showToast('Log rapido registrato. Ora lascia andare. 🌬️'); setTimeout(() => { const card = historyList.querySelector('[data-log-id="' + quickLog.id + '"]'); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 400); } }); quickFullBtn.addEventListener('click', () => { const trigger = quickSelectedTrigger; closeQuickModal(); if (trigger) { emotionBtns.forEach(btn => { if (btn.getAttribute('data-value') === trigger) btn.click(); }); } setTimeout(() => { const formCard = document.querySelector('.form-card'); if (formCard) formCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 400); }); // ══════════ MODULE: QUICK FAB BADGE ══════════ function updateQuickFabBadge() { if (!quickFab) return; const logs = getLogs(); const pending = logs.filter(l => l.outcome === null && l.entryType !== 'external_lapse').length; let badge = quickFab.querySelector('.quick-fab-badge'); if (pending > 0) { if (!badge) { badge = document.createElement('span'); badge.className = 'quick-fab-badge'; quickFab.appendChild(badge); } badge.textContent = pending > 9 ? '9+' : pending; } else if (badge) { badge.remove(); } } // ══════════ MODULE: STREAK CALENDAR ══════════ const streakCalendarGrid = document.getElementById('streak-calendar-grid'); const DAYS_IT = ['Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa', 'Do']; function renderStreakCalendar(logs) { if (!streakCalendarGrid) return; streakCalendarGrid.innerHTML = ''; DAYS_IT.forEach(d => { const hdr = document.createElement('div'); hdr.className = 'streak-cal-header'; hdr.textContent = d; streakCalendarGrid.appendChild(hdr); }); const today = new Date(); today.setHours(0, 0, 0, 0); const dayMs = 86400000; const logMap = {}; logs.forEach(l => { const d = new Date(l.date); d.setHours(0, 0, 0, 0); const key = d.getTime(); if (!logMap[key]) logMap[key] = { resisted: 0, checked: 0 }; if (l.outcome === 'resisted') logMap[key].resisted++; else if (l.outcome === 'checked') logMap[key].checked++; }); const startDay = new Date(today); startDay.setDate(startDay.getDate() - 27); const firstDow = startDay.getDay(); const offset = firstDow === 0 ? 6 : firstDow - 1; for (let i = 0; i < offset; i++) { const empty = document.createElement('div'); empty.className = 'streak-cal-day future'; streakCalendarGrid.appendChild(empty); } for (let i = 0; i < 28; i++) { const date = new Date(startDay.getTime() + i * dayMs); const key = date.getTime(); const cell = document.createElement('div'); cell.className = 'streak-cal-day'; cell.textContent = date.getDate(); const isToday = key === today.getTime(); const isFuture = key > today.getTime(); const data = logMap[key]; if (isFuture) { cell.classList.add('future'); } else if (data) { const hasR = data.resisted > 0; const hasC = data.checked > 0; if (hasR && hasC) cell.classList.add('mixed'); else if (hasR) cell.classList.add('resisted'); else if (hasC) cell.classList.add('checked'); const total = data.resisted + data.checked; cell.title = total + ' event' + (total > 1 ? 'i' : 'o') + ': ' + data.resisted + ' resistiti, ' + data.checked + ' controllati'; } if (isToday) cell.classList.add('today'); streakCalendarGrid.appendChild(cell); } } // ══════════ MODULE: PRE-SESSION RITUAL ══════════ const ritualKey = 'zenTradeRitualItems'; const ritualCheckedKey = 'zenTradeRitualChecked'; const ritualCard = document.getElementById('ritual-card'); const ritualItemsContainer = document.getElementById('ritual-items'); const ritualToggleEdit = document.getElementById('ritual-toggle-edit'); const ritualEditArea = document.getElementById('ritual-edit-area'); const ritualAddInput = document.getElementById('ritual-add-input'); const ritualAddBtn = document.getElementById('ritual-add-btn'); const ritualCompleteBtn = document.getElementById('ritual-complete-btn'); function getRitualItems() { try { const raw = localStorage.getItem(ritualKey); return raw ? JSON.parse(raw) : []; } catch (e) { return []; } } function saveRitualItems(items) { try { localStorage.setItem(ritualKey, JSON.stringify(items)); } catch (e) {} } function getRitualChecked() { try { const raw = localStorage.getItem(ritualCheckedKey); return raw ? JSON.parse(raw) : {}; } catch (e) { return {}; } } function saveRitualChecked(checked) { try { localStorage.setItem(ritualCheckedKey, JSON.stringify(checked)); } catch (e) {} } function renderRitualChecklist() { if (!ritualItemsContainer) return; const items = getRitualItems(); const checked = getRitualChecked(); ritualItemsContainer.innerHTML = ''; if (items.length === 0) { ritualCompleteBtn.classList.add('hidden'); return; } ritualCompleteBtn.classList.remove('hidden'); let allChecked = true; items.forEach((item, i) => { const row = document.createElement('label'); row.className = 'ritual-item' + (checked[i] ? ' done' : ''); if (!checked[i]) allChecked = false; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !!checked[i]; cb.setAttribute('aria-label', item); const text = document.createElement('span'); text.className = 'ritual-item-text'; text.textContent = item; const del = document.createElement('button'); del.className = 'ritual-item-delete'; del.type = 'button'; del.textContent = 'Γ—'; del.setAttribute('aria-label', 'Elimina'); cb.addEventListener('change', () => { const c = getRitualChecked(); c[i] = cb.checked; saveRitualChecked(c); renderRitualChecklist(); }); del.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const items = getRitualItems(); items.splice(i, 1); saveRitualItems(items); const c = getRitualChecked(); const newC = {}; let idx = 0; for (let k = 0; k < items.length; k++) { while (c[idx] === undefined && idx < Object.keys(c).length + items.length) idx++; if (c[idx] !== undefined) { newC[k] = c[idx]; idx++; } else idx++; } saveRitualChecked(newC); renderRitualChecklist(); }); row.appendChild(cb); row.appendChild(text); row.appendChild(del); ritualItemsContainer.appendChild(row); }); ritualCompleteBtn.disabled = !allChecked; } function updateRitualVisibility() { if (!ritualCard) return; const items = getRitualItems(); const state = getSessionState(); if (state && state.active) { ritualCard.classList.add('hidden'); } else { ritualCard.classList.toggle('hidden', items.length === 0); } } ritualToggleEdit.addEventListener('click', () => { ritualEditArea.classList.toggle('hidden'); if (!ritualEditArea.classList.contains('hidden')) ritualAddInput.focus(); }); function addRitualItem() { const text = ritualAddInput.value.trim(); if (!text) return; const items = getRitualItems(); items.push(text); saveRitualItems(items); ritualAddInput.value = ''; renderRitualChecklist(); updateRitualVisibility(); } ritualAddBtn.addEventListener('click', addRitualItem); ritualAddInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); addRitualItem(); } }); ritualCompleteBtn.addEventListener('click', () => { const items = getRitualItems(); const checked = getRitualChecked(); const allDone = items.length > 0 && items.every((_, i) => checked[i]); if (!allDone) { showToast('Completa tutti i passi prima di iniziare.'); return; } saveRitualChecked({}); startSession(); }); // ══════════ MODULE: POST-SESSION JOURNAL ══════════ const journalKey = 'zenTradeJournals'; const journalModal = document.getElementById('journal-modal'); const journalTextarea = document.getElementById('journal-textarea'); const journalSaveBtn = document.getElementById('journal-save-btn'); const journalSkipBtn = document.getElementById('journal-skip-btn'); let lastJournalSessionEnd = null; function getJournals() { try { const raw = localStorage.getItem(journalKey); return raw ? JSON.parse(raw) : []; } catch (e) { return []; } } function saveJournalEntry(entry) { try { const journals = getJournals(); journals.unshift(entry); localStorage.setItem(journalKey, JSON.stringify(journals)); } catch (e) {} } function openJournalModal() { lastJournalSessionEnd = new Date().toISOString(); journalTextarea.value = ''; journalModal.classList.remove('hidden'); setTimeout(() => journalModal.classList.remove('opacity-0'), 50); setTimeout(() => journalTextarea.focus(), 100); document.addEventListener('keydown', handleJournalKeydown); } function closeJournalModal() { journalModal.classList.add('opacity-0'); setTimeout(() => journalModal.classList.add('hidden'), 350); document.removeEventListener('keydown', handleJournalKeydown); } function handleJournalKeydown(e) { if (e.key === 'Escape') { closeJournalModal(); return; } trapFocus(journalModal, e); } journalSaveBtn.addEventListener('click', () => { const text = journalTextarea.value.trim(); saveJournalEntry({ date: lastJournalSessionEnd || new Date().toISOString(), note: text || '', type: text ? 'reflection' : 'skipped' }); closeJournalModal(); showToast(text ? 'Riflessione salvata. Buon riposo. πŸŒ™' : 'Giornale saltato.'); }); journalSkipBtn.addEventListener('click', () => { saveJournalEntry({ date: lastJournalSessionEnd || new Date().toISOString(), note: '', type: 'skipped' }); closeJournalModal(); showToast('Giornale saltato.'); }); journalModal.addEventListener('click', (e) => { if (e.target === journalModal) closeJournalModal(); }); });