' + row.replace('
', '').replace(/^
/, '') + '
'
+ '
';
}
function initSwipeOnTasks() {
if (window.innerWidth > 768) return;
document.querySelectorAll('[id^="inner-"]').forEach(function(el) {
var taskId = el.id.replace('inner-', '');
if (!el._swipe) {
el._swipe = true;
makeSwipeable(el, taskId);
}
});
}
// ═══════════════════════════════════════════
// PULL TO REFRESH
// ═══════════════════════════════════════════
var _pullStartY = 0;
var _pullDist = 0;
var _pullActive = false;
var PULL_THRESHOLD = 70;
function initPullToRefresh() {
if (window.innerWidth > 768) return;
var el = document.getElementById('page-today');
if (!el || el._pull) return;
el._pull = true;
el.addEventListener('touchstart', function(e) {
// Only activate if scrolled to top
var scroller = el.querySelector('.today-cols') || el.querySelector('.today-main');
if (scroller && scroller.scrollTop > 5) return;
_pullStartY = e.touches[0].clientY;
_pullDist = 0;
_pullActive = true;
}, {passive: true});
el.addEventListener('touchmove', function(e) {
if (!_pullActive) return;
var dy = e.touches[0].clientY - _pullStartY;
if (dy < 0) { _pullActive = false; return; }
_pullDist = Math.min(dy, PULL_THRESHOLD * 1.5);
var indicator = document.getElementById('pull-indicator');
var text = document.getElementById('pull-text');
if (indicator) {
var h = Math.min(52, _pullDist * 0.6);
indicator.style.height = h + 'px';
if (text) text.textContent = _pullDist > PULL_THRESHOLD ? 'Soltar para atualizar' : 'Puxar para atualizar';
}
}, {passive: true});
el.addEventListener('touchend', function() {
if (!_pullActive) return;
_pullActive = false;
var indicator = document.getElementById('pull-indicator');
if (_pullDist > PULL_THRESHOLD) {
// Trigger refresh
if (indicator) { indicator.classList.add('show', 'loading'); }
var text = document.getElementById('pull-text');
if (text) text.textContent = 'Atualizando...';
// Refresh calendar and data
if (localStorage.getItem('ffos_gcal_connected') === '1') {
gcalFetch();
}
pullCloud(window._fbUid);
setTimeout(function() {
if (indicator) { indicator.classList.remove('show', 'loading'); indicator.style.height = '0'; }
toast('Atualizado!');
}, 1500);
} else {
if (indicator) { indicator.style.height = '0'; }
}
_pullDist = 0;
}, {passive: true});
}
// ═══════════════════════════════════════════
// OFFLINE MODE
// ═══════════════════════════════════════════
function initOfflineDetection() {
function onOnline() {
toast('🌐 Conexão restaurada');
// Re-sync when back online
if (window._fbUid) pullCloud(window._fbUid);
if (localStorage.getItem('ffos_gcal_connected') === '1') gcalFetch();
}
function onOffline() {
toast('📵 Sem conexão — modo offline');
}
window.addEventListener('online', onOnline);
window.addEventListener('offline', onOffline);
}
// ═══════════════════════════════════════════
// PUSH NOTIFICATIONS (Web Push API)
// ═══════════════════════════════════════════
function initWebPush() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
// Already handled by requestPushPermission
}
// ═══════════════════════════════════════════
// CAPTURE SHEET
// ═══════════════════════════════════════════
function openCaptureSheet() {
document.getElementById('capture-sheet').classList.add('open');
document.getElementById('sheet-overlay').classList.add('show');
}
function closeCaptureSheet() {
document.getElementById('capture-sheet').classList.remove('open');
document.getElementById('sheet-overlay').classList.remove('show');
}
// ═══════════════════════════════════════════
// GEOLOCATION
// ═══════════════════════════════════════════
function getCity() {
var cityEl = document.getElementById('mob-city');
if (!cityEl) return;
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(function(pos) {
var lat = pos.coords.latitude;
var lon = pos.coords.longitude;
fetch('https://nominatim.openstreetmap.org/reverse?lat='+lat+'&lon='+lon+'&format=json')
.then(function(r){ return r.json(); })
.then(function(d) {
var city = d.address && (d.address.city || d.address.town || d.address.municipality || d.address.state);
if (city && cityEl) cityEl.textContent = city;
})
.catch(function(){});
}, function(){
// Permission denied - keep Rio de Janeiro default
});
}
// Update mobile date header
function updateMobDateHeader() {
var txData = TX[lang] || TX['pt'];
var days = txData.days;
var months = txData.months;
var mobBig = document.getElementById('mob-date-big');
var mobBtn = document.getElementById('mob-today-btn');
var todayStr = dk(today0());
var isToday = dk(curDate) === todayStr;
// Format: "Segunda-feira, Junho 15"
if (mobBig) mobBig.textContent = days[curDate.getDay()] + ', ' + months[curDate.getMonth()] + ' ' + curDate.getDate();
if (mobBtn) {
mobBtn.textContent = lang === 'en' ? 'Today' : 'Hoje';
mobBtn.className = 'mob-today-btn' + (isToday ? ' current' : '');
}
}
// ═══════════════════════════════════════════
// NOTIFICATIONS
// ═══════════════════════════════════════════
var _notifications = [];
function addNotification(icon, title, body) {
_notifications.unshift({
id: uid(),
icon: icon,
title: title,
body: body,
time: new Date().toLocaleTimeString('pt-BR', {hour:'2-digit',minute:'2-digit'}),
date: dk(today0()),
read: false
});
if (_notifications.length > 20) _notifications = _notifications.slice(0,20);
localStorage.setItem('ffos_notifications', JSON.stringify(_notifications));
updateBellBadge();
}
function updateBellBadge() {
var badge = document.getElementById('bell-badge');
var unread = _notifications.filter(function(n){return !n.read;}).length;
if (badge) badge.style.display = unread > 0 ? 'block' : 'none';
}
function openNotifications() {
// Mark all as read
_notifications.forEach(function(n){n.read=true;});
localStorage.setItem('ffos_notifications', JSON.stringify(_notifications));
updateBellBadge();
var el = document.getElementById('notif-list');
if (!el) return;
if (!_notifications.length) {
el.innerHTML = '
Nenhuma notificação.
';
} else {
el.innerHTML = _notifications.map(function(n) {
return '
'
+ '
'+n.icon+'
'
+ '
'
+ '
'+n.title+'
'
+ '
'+n.body+'
'
+ '
'+n.date+' '+n.time+'
'
+ '
';
}).join('');
}
openMo('mo-notif');
}
function clearNotifications() {
_notifications = [];
localStorage.removeItem('ffos_notifications');
updateBellBadge();
closeMo('mo-notif');
toast('Notificações limpas.');
}
// Init notifications from storage
function initNotifications() {
_notifications = JSON.parse(localStorage.getItem('ffos_notifications') || '[]');
updateBellBadge();
}
// Override showPushNotification to also add to notification center
var _origShowPush = null;
function initNotifOverride() {
_origShowPush = showPushNotification;
showPushNotification = function(title, body) {
addNotification('🔔', title, body);
if (Notification.permission === 'granted') {
try { new Notification(title, {body:body, icon:'/icon-192.png'}); } catch(e) {
showPushBanner(title + ' — ' + body);
}
} else {
showPushBanner(title + ' — ' + body);
}
};
}
// ═══════════════════════════════════════════
// CREATE GOOGLE CALENDAR EVENT
// ═══════════════════════════════════════════
function openCreateEvent() {
var el = document.getElementById('ev-date');
if (el) el.value = dk(today0());
var s = document.getElementById('ev-start');
if (s) s.value = new Date().toTimeString().substring(0,5);
var e2 = document.getElementById('ev-end');
if (e2) {
var d = new Date(); d.setHours(d.getHours()+1);
e2.value = d.toTimeString().substring(0,5);
}
openMo('mo-create-event');
}
function createCalendarEvent() {
var token = localStorage.getItem('ffos_gcal_token');
if (!token) { toast('Conecte o Google Calendar primeiro.'); return; }
var title = document.getElementById('ev-title').value.trim();
if (!title) { toast('Digite um título para o evento.'); return; }
var date = document.getElementById('ev-date').value;
var start = document.getElementById('ev-start').value;
var end = document.getElementById('ev-end').value;
var desc = document.getElementById('ev-desc').value;
if (!date || !start || !end) { toast('Preencha data e horários.'); return; }
var body = {
summary: title,
description: desc,
start: { dateTime: date+'T'+start+':00', timeZone: 'America/Sao_Paulo' },
end: { dateTime: date+'T'+end+':00', timeZone: 'America/Sao_Paulo' }
};
fetch('https://www.googleapis.com/calendar/v3/calendars/primary/events', {
method: 'POST',
headers: {
'Authorization': 'Bearer '+token,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.error) {
toast('Erro: '+d.error.message);
} else {
closeMo('mo-create-event');
document.getElementById('ev-title').value = '';
document.getElementById('ev-desc').value = '';
toast('✓ Evento criado no Calendar!');
gcalFetch();
}
})
.catch(function(){ toast('Erro ao criar evento.'); });
}
// ═══════════════════════════════════════════
// CALENDAR EVENT COLORS BY TYPE
// ═══════════════════════════════════════════
function getEventColor(ev) {
var title = (ev.summary || '').toLowerCase();
if (/aula|aluno|turma|inglês|espanhol|lesson|class/.test(title)) return '#7c3aed';
if (/reunião|meeting|call|zoom|teams/.test(title)) return '#1d4ed8';
if (/almoço|jantar|café|lunch|dinner/.test(title)) return '#0f9d58';
if (/médico|saúde|academia|health|doctor/.test(title)) return '#db4437';
if (/pagamento|cobrança|financeiro|boleto/.test(title)) return '#d97706';
return '#4285f4';
}
// ═══════════════════════════════════════════
// NOTIFICATION SETTINGS
// ═══════════════════════════════════════════
var _notifSettings = {};
function loadNotifSettings() {
_notifSettings = JSON.parse(localStorage.getItem('ffos_notif_settings') || '{}');
var defaults = { morning:'07:00', afternoon:'13:00', evening:'20:00',
morning_on:true, afternoon_on:true, evening_on:true, calendar_on:true };
Object.keys(defaults).forEach(function(k){ if(_notifSettings[k]===undefined) _notifSettings[k]=defaults[k]; });
}
function saveNotifTimes() {
['morning','afternoon','evening'].forEach(function(p){
var el = document.getElementById('notif-'+p);
if (el) _notifSettings[p] = el.value;
});
localStorage.setItem('ffos_notif_settings', JSON.stringify(_notifSettings));
toast('Horários salvos!');
}
function toggleNotif(key) {
loadNotifSettings();
_notifSettings[key+'_on'] = !_notifSettings[key+'_on'];
localStorage.setItem('ffos_notif_settings', JSON.stringify(_notifSettings));
updateNotifToggles();
}
function updateNotifToggles() {
loadNotifSettings();
['morning','afternoon','evening','calendar'].forEach(function(k){
var el = document.getElementById('toggle-'+k);
if (el) el.classList.toggle('on', !!_notifSettings[k+'_on']);
var inp = document.getElementById('notif-'+k);
if (inp && _notifSettings[k]) inp.value = _notifSettings[k];
});
}
// Check for calendar event reminders (15 min before)
function checkCalendarReminders() {
if (!_notifSettings.calendar_on) return;
if (!_gcalEvents || !_gcalEvents.length) return;
var now = new Date();
var soon = new Date(now.getTime() + 15*60*1000);
_gcalEvents.forEach(function(ev){
if (!ev.start || !ev.start.dateTime) return;
var evTime = new Date(ev.start.dateTime);
if (evTime > now && evTime <= soon) {
var key = 'reminded_'+ev.id;
if (!sessionStorage.getItem(key)) {
sessionStorage.setItem(key, '1');
showPushNotification('📅 Em 15 min: '+ev.summary, new Date(ev.start.dateTime).toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'}));
}
}
});
}
// ═══════════════════════════════════════════
// COMPACT MODE
// ═══════════════════════════════════════════
function toggleCompact() {
var isCompact = document.body.classList.toggle('compact');
localStorage.setItem('ffos_compact', isCompact ? '1' : '0');
var el = document.getElementById('toggle-compact');
if (el) el.classList.toggle('on', isCompact);
}
function initCompactMode() {
var isCompact = localStorage.getItem('ffos_compact') === '1';
if (isCompact) document.body.classList.add('compact');
var el = document.getElementById('toggle-compact');
if (el) el.classList.toggle('on', isCompact);
}
// ═══════════════════════════════════════════
// CUSTOM ACCENT COLOR
// ═══════════════════════════════════════════
function setAccentColor(color) {
document.documentElement.style.setProperty('--accent', color);
// Update sidebar-bg contrast
var isDark = isColorDark(color);
document.documentElement.style.setProperty('--sidebar-bg-contrast', isDark ? '#fff' : '#000');
localStorage.setItem('ffos_accent', color);
updateAccentSwatches(color);
}
function isColorDark(hex) {
var r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16);
return (r*299+g*587+b*114)/1000 < 128;
}
function updateAccentSwatches(color) {
document.querySelectorAll('.accent-swatch').forEach(function(s){
s.style.border = '2px solid '+(s.getAttribute('onclick').includes(color) ? color : 'transparent');
});
var ci = document.getElementById('custom-accent');
if (ci) ci.value = color;
}
function initAccentColor() {
var saved = localStorage.getItem('ffos_accent');
if (saved) { setAccentColor(saved); }
}
// ═══════════════════════════════════════════
// AI SUGGESTIONS
// ═══════════════════════════════════════════
function suggestTop3WithAI() {
var groqKey = localStorage.getItem('ffos_groq_key');
if (!groqKey) return;
var tasks = getTasks().filter(function(t){ return !t.done; }).slice(0, 20);
var calToday = (_gcalEvents||[]).map(function(ev){
return (ev.start&&ev.start.dateTime ? new Date(ev.start.dateTime).toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'}) : '') + ' ' + ev.summary;
}).join('; ');
var prompt = 'Você é um assistente de produtividade. Com base nas tarefas abertas e agenda de hoje, sugira as 3 tarefas mais importantes para o dia. '
+ 'Tarefas abertas: ' + tasks.map(function(t){return t.name+(t.date?' ('+t.date+')':'');}).join(', ') + '. '
+ 'Agenda de hoje: ' + (calToday||'nenhum evento') + '. '
+ 'Responda SOMENTE em JSON: {"top3":["tarefa1","tarefa2","tarefa3"],"motivo":"explicação breve"}';
fetch('https://api.groq.com/openai/v1/chat/completions', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+groqKey},
body:JSON.stringify({model:'llama-3.3-70b-versatile',max_tokens:200,temperature:0.3,
messages:[{role:'user',content:prompt}]})
}).then(function(r){return r.json();})
.then(function(d){
var text = d.choices&&d.choices[0]?d.choices[0].message.content:'';
try {
var clean = text.replace(/```json|```/g,'').trim();
var parsed = JSON.parse(clean);
showTop3Suggestions(parsed);
} catch(e){}
}).catch(function(){});
}
function showTop3Suggestions(data) {
var el = document.getElementById('ai-top3-suggestions');
if (!el || !data.top3) return;
el.innerHTML = '
💡 IA sugere para hoje:
'
+ data.top3.map(function(s,i){
return '
'
+ '
'+(i===0?'1️⃣':i===1?'2️⃣':'3️⃣')+''
+ '
';
}).join('')
+ (data.motivo?'
'+escHtml(data.motivo)+'
':'');
el.style.display = 'block';
}
function useAISuggestion(name) {
var tasks = getTasks();
var match = tasks.find(function(t){ return t.name.toLowerCase().includes(name.toLowerCase()) && !t.done; });
if (match) {
match.starred = true;
setTasks(tasks);
render();
toast('⭐ Adicionada ao Top 3!');
} else {
openAddTask();
setTimeout(function(){
var el = document.getElementById('t-name');
if (el) el.value = name;
}, 100);
}
var sugEl = document.getElementById('ai-top3-suggestions');
if (sugEl) sugEl.style.display = 'none';
}
// End of day summary
function requestDaySummary() {
var groqKey = localStorage.getItem('ffos_groq_key');
if (!groqKey) { toast('Configure a Groq API Key para usar a IA.'); return; }
var tasks = getTasks();
var todayStr = dk(today0());
var done = tasks.filter(function(t){ return t.done && t.updated===todayStr; });
var pending = tasks.filter(function(t){ return !t.done && t.date===todayStr; });
var overdue = tasks.filter(function(t){ return !t.done && t.date && t.date < todayStr; });
var prompt = 'Faça um resumo do dia de forma encorajadora e concisa (máximo 3 linhas). '
+ 'Concluídas hoje: ' + done.map(function(t){return t.name;}).join(', ') + '. '
+ 'Pendentes do dia: ' + pending.map(function(t){return t.name;}).join(', ') + '. '
+ 'Atrasadas: ' + overdue.map(function(t){return t.name;}).join(', ') + '. '
+ 'Responda em português.';
fetch('https://api.groq.com/openai/v1/chat/completions', {
method:'POST',
headers:{'Content-Type':'application/json','Authorization':'Bearer '+groqKey},
body:JSON.stringify({model:'llama-3.3-70b-versatile',max_tokens:150,
messages:[{role:'user',content:prompt}]})
}).then(function(r){return r.json();})
.then(function(d){
var text = d.choices&&d.choices[0]?d.choices[0].message.content:'';
if (text) showDaySummary(text);
}).catch(function(){});
}
function showDaySummary(text) {
var el = document.getElementById('day-summary-content');
if (el) {
el.textContent = text;
document.getElementById('day-summary-box').style.display = 'block';
} else {
toast(text.substring(0,80)+'...');
}
}
// Suggest recurrence for repeating tasks
function detectRecurringPatterns() {
var tasks = getTasks();
var nameCount = {};
tasks.filter(function(t){ return t.done; }).forEach(function(t){
var key = t.name.toLowerCase().trim();
if (!nameCount[key]) nameCount[key] = [];
nameCount[key].push(t.date);
});
var suggestions = [];
Object.keys(nameCount).forEach(function(name){
if (nameCount[name].length >= 3 && !tasks.find(function(t){ return t.name.toLowerCase()===name && t.recur; })) {
suggestions.push(name);
}
});
if (suggestions.length > 0) {
var existing = getTasks().filter(function(t){ return !t.done && !t.recur; });
var toSuggest = existing.filter(function(t){ return suggestions.includes(t.name.toLowerCase().trim()); });
toSuggest.slice(0,3).forEach(function(t){
addNotification('↻', 'Tarefa repetida detectada', '"'+t.name+'" — deseja adicionar recorrência?');
});
}
}
// ═══════════════════════════════════════════
// GLOBAL SEARCH (⌘K)
// ═══════════════════════════════════════════
var _gsSelected = -1;
var _gsResults = [];
function openGlobalSearch() {
var overlay = document.getElementById('global-search-overlay');
if (overlay) overlay.classList.add('open');
setTimeout(function() {
var inp = document.getElementById('global-search-input');
if (inp) { inp.value = ''; inp.focus(); }
}, 50);
runGlobalSearch('');
}
function closeGlobalSearch() {
var overlay = document.getElementById('global-search-overlay');
if (overlay) overlay.classList.remove('open');
}
function runGlobalSearch(q) {
q = (q||'').toLowerCase().trim();
var el = document.getElementById('global-search-results');
if (!el) return;
_gsResults = [];
_gsSelected = -1;
var tasks = getTasks().filter(function(t) {
return !q || (t.name||'').toLowerCase().includes(q)
|| (t.notes||'').toLowerCase().includes(q)
|| (t.tags||[]).some(function(tg){return tg.toLowerCase().includes(q);});
});
var projects = getProjects().filter(function(p) {
return !q || (p.name||'').toLowerCase().includes(q) || (p.desc||'').toLowerCase().includes(q);
});
var library = getLib().filter(function(l) {
return !q || (l.title||'').toLowerCase().includes(q) || (l.body||'').toLowerCase().includes(q);
});
if (!tasks.length && !projects.length && !library.length) {
el.innerHTML = '
'+(q?'Nenhum resultado para "'+escHtml(q)+'"':'Digite para pesquisar em tudo')+'
';
return;
}
var html2 = '';
if (tasks.length) {
html2 += '
Tarefas
';
tasks.slice(0,6).forEach(function(t) {
var idx = _gsResults.length;
_gsResults.push({type:'task', id:t.id});
var catColor = getCatColor(t.cat);
html2 += '
'
+ '
✓
'
+ '
'
+ '
'+escHtml(t.name)+'
'
+ '
'+(t.date||'Sem data')+(t.cat?' · '+t.cat:'')+'
'
+ '
'
+ (t.done?'
Concluída':t.starred?'
⭐':'')
+ '
';
});
html2 += '
';
}
if (projects.length) {
html2 += '
Projetos
';
projects.slice(0,4).forEach(function(p) {
var idx = _gsResults.length;
_gsResults.push({type:'project', id:p.id});
html2 += '
'
+ '
▣
'
+ '
'
+ '
'+escHtml(p.name)+'
'
+ '
'+escHtml((p.desc||'').substring(0,50))+'
'
+ '
'
+ '
'+p.status+''
+ '
';
});
html2 += '
';
}
if (library.length) {
html2 += '
Biblioteca
';
library.slice(0,4).forEach(function(l) {
var idx = _gsResults.length;
_gsResults.push({type:'lib', id:l.id});
html2 += '
'
+ '
📝
'
+ '
'
+ '
'+escHtml(l.title||'Sem título')+'
'
+ '
'+escHtml((l.body||'').substring(0,50))+'
'
+ '
'
+ '
';
});
html2 += '
';
}
el.innerHTML = html2;
}
function gsOpen(idx) {
var item = _gsResults[idx];
if (!item) return;
closeGlobalSearch();
if (item.type === 'task') { goTo('tasks'); setTimeout(function(){ openEditTask(item.id); }, 100); }
else if (item.type === 'project') { goTo('projects'); setTimeout(function(){ openEditProject(item.id); }, 100); }
else if (item.type === 'lib') { goTo('library'); setTimeout(function(){ openEditLib(item.id); }, 100); }
}
function handleSearchKey(e) {
var items = document.querySelectorAll('#global-search-results .gs-item');
if (e.key === 'ArrowDown') {
_gsSelected = Math.min(_gsSelected+1, items.length-1);
} else if (e.key === 'ArrowUp') {
_gsSelected = Math.max(_gsSelected-1, 0);
} else if (e.key === 'Enter') {
if (_gsSelected >= 0 && _gsResults[_gsSelected]) gsOpen(_gsSelected);
return;
} else if (e.key === 'Escape') {
closeGlobalSearch(); return;
}
items.forEach(function(el,i){ el.classList.toggle('selected', i===_gsSelected); });
if (items[_gsSelected]) items[_gsSelected].scrollIntoView({block:'nearest'});
}
// ⌘K / Ctrl+K shortcut
document.addEventListener('keydown', function(e) {
if ((e.metaKey||e.ctrlKey) && e.key==='k') {
e.preventDefault();
var overlay = document.getElementById('global-search-overlay');
if (overlay && overlay.classList.contains('open')) closeGlobalSearch();
else openGlobalSearch();
}
if (e.key==='Escape') closeGlobalSearch();
});
// ═══════════════════════════════════════════
// KANBAN VIEW
// ═══════════════════════════════════════════
var _kanbanMode = false;
function toggleKanban() {
_kanbanMode = !_kanbanMode;
var btn = document.getElementById('btn-kanban');
var list = document.getElementById('tasks-list');
var kanban = document.getElementById('kanban-view');
var sortBar = document.querySelector('.sort-bar');
if (btn) btn.classList.toggle('active', _kanbanMode);
if (list) list.style.display = _kanbanMode ? 'none' : '';
if (kanban) kanban.style.display = _kanbanMode ? 'block' : 'none';
if (sortBar) sortBar.style.display = _kanbanMode ? 'none' : '';
if (_kanbanMode) renderKanban();
}
function renderKanban() {
var tasks = getTasks();
var todo = tasks.filter(function(t){ return !t.done && t.status !== 'progress'; });
var progress = tasks.filter(function(t){ return t.status === 'progress'; });
var done = tasks.filter(function(t){ return t.done; }).slice(0,20);
function cardHtml(t) {
var catColor = getCatColor(t.cat);
var card = '
'
+ '
'+escHtml(t.name)+'
'
+ '
'
+ (t.date?''+t.date+'':'')
+ (t.cat?''+t.cat+'':'')
+ (t.starred?'⭐':'')
+ '
';
if (!t.done && t.status!=='progress') {
card += '';
}
if (t.status==='progress') {
card += '';
}
if (!t.done) {
card += '';
}
card += '
';
return card;
}
var kTodo = document.getElementById('k-todo');
var kProg = document.getElementById('k-progress');
var kDone = document.getElementById('k-done');
if (kTodo) { kTodo.innerHTML = todo.map(cardHtml).join(''); document.getElementById('k-todo-count').textContent = todo.length; }
if (kProg) { kProg.innerHTML = progress.map(cardHtml).join(''); document.getElementById('k-progress-count').textContent = progress.length; }
if (kDone) { kDone.innerHTML = done.map(cardHtml).join(''); document.getElementById('k-done-count').textContent = done.length; }
}
function moveKanban(taskId, dest) {
var tasks = getTasks();
var t = tasks.find(function(x){return x.id===taskId;});
if (!t) return;
if (dest==='done') { t.done=true; t.updated=dk(today0()); delete t.status; }
else if (dest==='progress') { t.done=false; t.status='progress'; }
else { t.done=false; delete t.status; }
setTasks(tasks);
renderKanban();
render();
}
// ═══════════════════════════════════════════
// SORT
// ═══════════════════════════════════════════
var _taskSort = 'date';
function setSort(s, btn) {
_taskSort = s;
document.querySelectorAll('.sort-btn').forEach(function(b){ b.classList.remove('active'); });
if (btn) btn.classList.add('active');
renderTasks();
}
function applySort(tasks) {
var sorted = tasks.slice();
if (_taskSort === 'date') {
sorted.sort(function(a,b){
if(a.date&&b.date) return a.date>b.date?1:-1;
if(a.date) return -1; if(b.date) return 1;
return a.name>b.name?1:-1;
});
} else if (_taskSort === 'priority') {
sorted.sort(function(a,b){ return (b.starred?1:0)-(a.starred?1:0); });
} else if (_taskSort === 'cat') {
sorted.sort(function(a,b){ return (a.cat||'zzz')>(b.cat||'zzz')?1:-1; });
} else if (_taskSort === 'name') {
sorted.sort(function(a,b){ return a.name>b.name?1:-1; });
}
return sorted;
}
// ═══════════════════════════════════════════
// DASHBOARD
// ═══════════════════════════════════════════
function renderDashboard() {
var tasks = getTasks();
var todayStr = dk(today0());
// Done today
var doneToday = tasks.filter(function(t){ return t.done && t.updated===todayStr; }).length;
var el = document.getElementById('dash-done-today');
if (el) el.textContent = doneToday;
// Open
var open = tasks.filter(function(t){ return !t.done; }).length;
var el2 = document.getElementById('dash-open');
if (el2) el2.textContent = open;
// Weekly goal
var goal = parseInt(localStorage.getItem('ffos_dash_goal')||'20');
var inp = document.getElementById('dash-goal-input');
if (inp) inp.value = goal;
var week = getWeekRange ? getWeekRange() : {start:todayStr,end:todayStr};
var doneWeek = tasks.filter(function(t){ return t.done && t.updated>=week.start && t.updated<=week.end; }).length;
var pct = Math.min(100, Math.round(doneWeek/goal*100));
var bar = document.getElementById('dash-goal-bar');
var lbl = document.getElementById('dash-goal-label');
var pctEl = document.getElementById('dash-goal-pct');
if (bar) bar.style.width = pct+'%';
if (lbl) lbl.textContent = doneWeek+' de '+goal;
if (pctEl) pctEl.textContent = pct+'%';
// Weekly chart
renderWeeklyChart(tasks);
// By category
renderCatChart(tasks);
// Streaks
renderDashStreaks();
}
function saveDashGoal(val) {
localStorage.setItem('ffos_dash_goal', val);
renderDashboard();
}
function renderWeeklyChart(tasks) {
var el = document.getElementById('dash-chart');
if (!el) return;
var shortDays = ['Dom','Seg','Ter','Qua','Qui','Sex','Sab'];
var days = [];
for (var i=6;i>=0;i--) {
var d=new Date(today0()); d.setDate(d.getDate()-i);
days.push({str:dk(d),label:shortDays[d.getDay()],isToday:i===0});
}
var max=1;
var counts=days.map(function(day){
var c=tasks.filter(function(t){return t.done&&t.updated===day.str;}).length;
if(c>max)max=c; return c;
});
el.innerHTML=days.map(function(day,i){
var c=counts[i];
var pct=Math.max(4,Math.round(c/max*100));
var color=day.isToday?'var(--blue)':'var(--accent)';
return '
'
+'
'+(c||'')+'
'
+'
'
+'
'+day.label+'
'
+'
';
}).join('');
}
function renderCatChart(tasks) {
var el = document.getElementById('dash-by-cat');
if (!el) return;
var cats = ['aula','admin','cobranca','pessoal'];
var catNames = {aula:'Aulas',admin:'Admin',cobranca:'Cobranças',pessoal:'Pessoal'};
var total = tasks.filter(function(t){return t.done;}).length || 1;
var rows = cats.map(function(cat){
var count = tasks.filter(function(t){return t.done && t.cat===cat;}).length;
var pct = Math.round(count/total*100);
var color = getCatColor(cat);
return {name:catNames[cat],count:count,pct:pct,color:color};
}).filter(function(r){return r.count>0;});
if (!rows.length) { el.innerHTML='
Nenhuma tarefa concluída ainda.
'; return; }
el.innerHTML = rows.map(function(r){
return '
'
+'
'+r.name+'
'
+'
'
+'
'+r.count+'
'
+'
';
}).join('');
}
function renderDashStreaks() {
var el = document.getElementById('dash-streaks');
if (!el) return;
var routines = getRoutines();
if (!routines.length) { el.innerHTML='
Nenhuma rotina cadastrada.
'; return; }
el.innerHTML = routines.map(function(r){
var streak = getStreak ? getStreak(r) : 0;
var fire = streak>=7?'🔥':streak>=3?'⚡':'·';
return '
'
+''+fire+''
+''+escHtml(r.name)+''
+''+streak+' dias'
+'
';
}).join('');
}
// ═══════════════════════════════════════════
// PDF REPORT
// ═══════════════════════════════════════════
function exportPDFReport() {
var tasks = getTasks();
var todayStr = dk(today0());
var week = getWeekRange ? getWeekRange() : {start:todayStr,end:todayStr};
var doneWeek = tasks.filter(function(t){ return t.done && t.updated>=week.start && t.updated<=week.end; });
var openTasks = tasks.filter(function(t){ return !t.done; });
var content = '
'
+'
Fast Fluent OS v2.0617.1631'
+''
+'
Fast Fluent OS — Relatório Semanal
'
+'
'+week.start+' a '+week.end+'
'
+'
✓ Concluídas esta semana ('+doneWeek.length+')
'
+ doneWeek.map(function(t){
return '
'+escHtml(t.name)+''+t.updated+'
';
}).join('')
+'
⏳ Em aberto ('+openTasks.length+')
'
+ openTasks.slice(0,20).map(function(t){
return '
'+escHtml(t.name)+''+(t.date||'Sem data')+'
';
}).join('')
+'';
var w = window.open('','_blank');
if (w) { w.document.write(content); w.document.close(); setTimeout(function(){ w.print(); }, 500); }
else toast('Permita popups para exportar o PDF.');
}
// ═══════════════════════════════════════════
// CLOUD SYNC (Firestore)
// ═══════════════════════════════════════════
function pushCloud(key, value) {
if (!window._db || !window._fbUid) return;
showSyncStatus('syncing');
window._db.collection('users').doc(window._fbUid).set({ [key]: value }, { merge: true })
.then(function() { showSyncStatus('done'); })
.catch(function(e) { console.warn('Firestore write error:', e); showSyncStatus('error'); });
}
function pullCloud(uid) {
if (!window._db) return;
window._db.collection('users').doc(uid).get().then(function(snap) {
if (snap.exists) {
var data = snap.data();
['tasks','routines','projects','library','profile'].forEach(function(k) {
if (data[k] !== undefined) localStorage.setItem('ffos_'+k, JSON.stringify(data[k]));
});
}
render();
// Real-time listener
window._db.collection('users').doc(uid).onSnapshot(function(s) {
if (!s.exists) return;
var d = s.data();
['tasks','routines','projects','library'].forEach(function(k) {
if (d[k] !== undefined) localStorage.setItem('ffos_'+k, JSON.stringify(d[k]));
});
render();
});
}).catch(function() { render(); });
}
// ═══════════════════════════════════════════
// LOGIN / LOGOUT
// ═══════════════════════════════════════════
function doGoogleLogin() {
var btn = document.getElementById('login-btn');
var err = document.getElementById('login-err');
if (err) err.textContent = '';
if (!window._auth) { if(err) err.textContent = 'Carregando... tente em 3s.'; return; }
if (btn) { btn.disabled = true; btn.textContent = 'Entrando...'; }
var keep = document.getElementById('login-keep');
var persistence = (keep && keep.checked)
? firebase.auth.Auth.Persistence.LOCAL
: firebase.auth.Auth.Persistence.SESSION;
window._auth.setPersistence(persistence).then(function() {
var p = new firebase.auth.GoogleAuthProvider();
p.addScope('https://www.googleapis.com/auth/calendar.readonly');
return window._auth.signInWithPopup(p);
}).then(function(result) {
if (btn) { btn.disabled = false; btn.innerHTML = 'Continuar com Google'; }
if (result.credential && result.credential.accessToken) {
localStorage.setItem('ffos_gcal_token', result.credential.accessToken);
localStorage.setItem('ffos_gcal_connected', '1');
}
}).catch(function(e) {
if (btn) { btn.disabled = false; btn.innerHTML = 'Continuar com Google'; }
var msgs = {
'auth/popup-blocked': 'Permita popups para este site.',
'auth/popup-closed-by-user': 'Login cancelado.',
'auth/unauthorized-domain': 'Domínio não autorizado no Firebase.',
'auth/operation-not-allowed': 'Ative o login com Google no Firebase Console.'
};
if (err) err.textContent = msgs[e.code] || e.message;
});
}
function doLogout() {
if (window._auth) window._auth.signOut();
localStorage.removeItem('ffos_gcal_token');
localStorage.setItem('ffos_gcal_connected', '0');
}
// ═══════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════
document.addEventListener('DOMContentLoaded', function() {
// Force clear old SW caches if version mismatch
if ('caches' in window) {
caches.keys().then(function(keys) {
keys.filter(function(k){ return k !== 'ffos-v8'; })
.forEach(function(k){ caches.delete(k); });
});
}
lang = localStorage.getItem('ffos_lang') || 'pt';
applyTranslations();
initNotifications();
// Set date immediately on load
curDate = today0();
updateMobDateHeader();
updateDateHeader();
// Init offline detection
initOfflineDetection();
// Update date display immediately (render() called after Firebase ready)
setTimeout(function() {
updateMobDateHeader();
updateDateHeader();
}, 50);
// Theme
var theme = localStorage.getItem('ffos_theme') || 'light';
document.documentElement.setAttribute('data-theme', theme === 'dark' ? 'dark' : '');
var themeBtn = document.getElementById('theme-btn');
if (themeBtn) themeBtn.textContent = theme === 'dark' ? '☀' : '◐';
// Cat colors
applyCatColors();
// Lang
lang = localStorage.getItem('ffos_lang') || 'pt';
});
function checkForUpdates() {
var el = document.getElementById('app-version-label');
if (el) el.textContent = 'v' + APP_VERSION + ' · Verificando...';
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('skipWaiting');
}
setTimeout(function() {
if (el) el.textContent = 'v' + APP_VERSION + ' · ' + APP_BUILD + ' ✓';
toast('Versão ' + APP_VERSION + ' — atualizado!');
}, 1500);
}
let _neId=null,_neLinkTimer=null,_neBody=null,_graphOpen=false,_graphSim=null,_activityOpen=false,_fabOpen=false;
function openNoteEditor(id,type){_neId=id||null;_neBody=document.getElementById('note-editor-content');const ov=document.getElementById('note-editor-overlay');const titleEl=document.getElementById('editor-title');const metaEl=document.getElementById('editor-meta');const badge=document.getElementById('editor-date-badge');if(!ov||!_neBody)return;const today=dk(today0());if(badge)badge.textContent='📅 '+today;if(id){const item=getLib().find(l=>l.id===id);if(!item)return;if(titleEl)titleEl.value=item.title||'';_neBody.innerHTML=neMarkdownToHtml(item.body||'');if(metaEl)metaEl.textContent=item.date||today;neRenderTags(item.tags?item.tags.split(',').filter(Boolean):[]);}else{if(titleEl)titleEl.value='';_neBody.innerHTML='';if(metaEl)metaEl.textContent=today;neRenderTags([]);}ov.setAttribute('data-type',type||'nota');ov.classList.add('open');setTimeout(()=>{if(!id&&titleEl)titleEl.focus();else _neBody.focus();},100);}
function neRenderTags(tags){const row=document.getElementById('editor-tags-row');if(!row)return;row.innerHTML=tags.map(t=>'
'+escHtml(t.trim())+'').join('')+'
+ tag';}
function neAddTag(){const tag=prompt('Nova tag:');if(!tag)return;const row=document.getElementById('editor-tags-row');const tags=Array.from(row.querySelectorAll('.etag')).map(c=>c.dataset.tag);tags.push(tag.trim());neRenderTags(tags);}
function neGetTags(){const row=document.getElementById('editor-tags-row');return Array.from(row?row.querySelectorAll('.etag'):[]).map(c=>c.dataset.tag).join(',');}
function closeNoteEditor(){const ov=document.getElementById('note-editor-overlay');if(ov)ov.classList.remove('open');neHideAC();_neId=null;}
function saveNoteEditor(){let title=document.getElementById('editor-title').value.trim();const body=neHtmlToMarkdown(_neBody?_neBody.innerHTML:'');const type=document.getElementById('note-editor-overlay').getAttribute('data-type')||'nota';const tags=neGetTags();const today=dk(today0());if(!title&&!body.trim()){toast('Escreva algo antes de salvar.');return;}if(!title)title=body.split('\n')[0].replace(/^#+\s*/,'').substring(0,50)||'Sem título';const items=getLib();if(_neId){const idx=items.findIndex(l=>l.id===_neId);if(idx>=0){items[idx].title=title;items[idx].body=body;items[idx].tags=tags;items[idx].updated=today;}}else{items.unshift({id:uid(),title,body,type,tags,date:today,updated:today});}setLib(items);closeNoteEditor();render();toast('✓ Nota salva!');if(_graphOpen)setTimeout(renderGraph,200);}
function neFormat(cmd){document.execCommand(cmd,false,null);_neBody&&_neBody.focus();neUpdateToolbar();}
function neBlock(tag){document.execCommand('formatBlock',false,tag);_neBody&&_neBody.focus();}
function neInsertList(){document.execCommand('insertUnorderedList',false,null);_neBody&&_neBody.focus();}
function neInsertCode(){const sel=window.getSelection();document.execCommand('insertHTML',false,'
'+(sel&&sel.toString()?escHtml(sel.toString()):'código')+'');_neBody&&_neBody.focus();}
function neInsertDate(){document.execCommand('insertHTML',false,'
'+dk(today0())+' ');_neBody&&_neBody.focus();}
function neInsertWikiPrompt(){const t=prompt('Nome da nota:');if(!t)return;neInsertWikiLink(t);}
function neInsertWikiLink(title){const f=getLib().find(l=>(l.title||'').toLowerCase()===title.toLowerCase());document.execCommand('insertHTML',false,'
'+escHtml(title)+' ');_neBody&&_neBody.focus();}
function neUpdateToolbar(){['bold','italic','underline'].forEach(cmd=>{const btn=document.querySelector('.tb-btn[onclick*="\''+cmd+'\'"]');if(btn)btn.classList.toggle('active',document.queryCommandState(cmd));});}
function neOnInput(){clearTimeout(_neLinkTimer);_neLinkTimer=setTimeout(neCheckWikiLink,150);}
function neCheckWikiLink(){const sel=window.getSelection();if(!sel||!sel.anchorNode)return;const text=sel.anchorNode.textContent||'';const match=text.slice(0,sel.anchorOffset).match(/\[\[([^\]\[]*)$/);if(!match){neHideAC();return;}neShowAC(match[1]);}
function neShowAC(q){const ac=document.getElementById('link-autocomplete');if(!ac)return;const sel=window.getSelection();if(!sel||!sel.getRangeAt){neHideAC();return;}const rect=sel.getRangeAt(0).getBoundingClientRect();ac.style.left=rect.left+'px';ac.style.top=(rect.bottom+6)+'px';ac.style.display='block';const libs=getLib().filter(l=>!q||(l.title||'').toLowerCase().includes(q.toLowerCase())).slice(0,6);const icons={nota:'📝',citacao:'💬',livro:'📚'};let h=libs.map(l=>'
'+(icons[l.type]||'📝')+''+escHtml(l.title||'Sem título')+'
').join('');if(q)h+='
+ Criar "'+escHtml(q)+'"
';if(!h){neHideAC();return;}ac.innerHTML=h;const first=ac.querySelector('.lac-item');if(first)first.classList.add('sel');}
function neACPick(title){neHideAC();const sel=window.getSelection();if(!sel||!sel.anchorNode)return;const node=sel.anchorNode,pos=sel.anchorOffset,text=node.textContent;const match=text.slice(0,pos).match(/\[\[([^\]\[]*)$/);if(match){const range=document.createRange();range.setStart(node,pos-match[0].length);range.setEnd(node,pos);range.deleteContents();sel.removeAllRanges();sel.addRange(range);}neInsertWikiLink(title);}
function neHideAC(){const ac=document.getElementById('link-autocomplete');if(ac)ac.style.display='none';}
function neOnKeydown(e){const ac=document.getElementById('link-autocomplete');if(ac&&ac.style.display!=='none'){const items=ac.querySelectorAll('.lac-item'),sel=ac.querySelector('.lac-item.sel'),idx=Array.from(items).indexOf(sel);if(e.key==='ArrowDown'){e.preventDefault();sel&&sel.classList.remove('sel');const n=items[Math.min(idx+1,items.length-1)];n&&n.classList.add('sel');return;}if(e.key==='ArrowUp'){e.preventDefault();sel&&sel.classList.remove('sel');const p=items[Math.max(idx-1,0)];p&&p.classList.add('sel');return;}if(e.key==='Enter'&&sel){e.preventDefault();sel.click();return;}if(e.key==='Escape'){neHideAC();return;}}if(e.key==='Tab'){e.preventDefault();document.execCommand('insertHTML',false,' ');}if((e.ctrlKey||e.metaKey)&&e.key==='f'){e.preventDefault();noteSearchOpen();}}
function neWikiOpen(id){if(id){closeNoteEditor();setTimeout(()=>openNoteEditor(id),100);}}
function neMarkdownToHtml(md){if(!md)return '';let h=md.replace(/&/g,'&').replace(//g,'>').replace(/\[\[([^\]]+)\]\]/g,(m,t)=>{const f=getLib().find(l=>(l.title||'').toLowerCase()===t.toLowerCase());return '
'+escHtml(t)+'';}).replace(/^### (.+)$/gm,'
$1
').replace(/^## (.+)$/gm,'
$1
').replace(/^# (.+)$/gm,'
$1
').replace(/\*\*([^*]+)\*\*/g,'
$1').replace(/\*([^*]+)\*/g,'
$1').replace(/^> (.+)$/gm,'
$1
').replace(/`([^`]+)`/g,'
$1').replace(/^- (.+)$/gm,'
$1').replace(/\n\n/g,'
').replace(/\n/g,'
');return '
'+h+'
';}
function neHtmlToMarkdown(h){return h.replace(/