feat: fase 2 - detecção de crianças, tags, filtros e histórico

- GPT retorna JSON estruturado: criancas, turma, tags, relatorio
- Frontmatter agora inclui criancas[], turma, tags[] no .md
- Índice JSON estruturado com metadados completos
- Prévia do relatório mostra crianças detectadas + tags + turma
- Modal 'Minhas Observações' na sidebar
- Filtros por mês, tag e busca por criança
- Lista de observações com preview e chips
- Modal de detalhe com copiar texto
- CSS para btn-observacoes, obs-modal, obs-items, tags/chips
This commit is contained in:
2026-05-27 15:57:04 +00:00
parent ea8e33c891
commit 0b35055e23
4 changed files with 434 additions and 25 deletions
+174 -3
View File
@@ -1990,6 +1990,9 @@ const initApp = () => {
let recordingStartTime = null;
let timerInterval = null;
let generatedReport = null;
let generatedCriancas = null;
let generatedTurma = 'não informada';
let generatedTags = [];
// Abrir modal e iniciar gravação
if (fabRecord) {
@@ -2059,7 +2062,10 @@ const initApp = () => {
body: formData
});
const data = await res.json();
generatedReport = data.report;
generatedReport = data.relatorio || data.report;
generatedCriancas = data.criancas;
generatedTurma = data.turma || 'não informada';
generatedTags = data.tags || [];
showReportPreview(data);
} catch (err) {
console.error('Erro ao processar observação:', err);
@@ -2080,7 +2086,22 @@ const initApp = () => {
hour: '2-digit', minute: '2-digit'
});
reportContent.innerHTML = marked.parse(generatedReport || 'Relatório não disponível.');
// Monta HTML com metadados
const criancasHtml = data.criancas && data.criancas.length
? `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> ${data.criancas.join(', ')}</div>`
: `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> <em>não identificadas</em></div>`;
const tagsHtml = data.tags && data.tags.length
? `<div class="report-meta-item"><span class="meta-label">🏷️ Tags:</span> ${data.tags.map(t => `<span class="tag-chip">${t}</span>`).join(' ')}</div>`
: '';
const turmaHtml = data.turma && data.turma !== 'não informada'
? `<div class="report-meta-item"><span class="meta-label">🏫 Turma:</span> ${data.turma}</div>`
: '';
reportContent.innerHTML = `
<div class="report-meta">${criancasHtml}${turmaHtml}${tagsHtml}</div>
<hr style="border-color: var(--border-light); margin: 12px 0;">
${marked.parse(generatedReport || 'Relatório não disponível.')}
`;
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
};
@@ -2103,7 +2124,12 @@ const initApp = () => {
const res = await fetch('/api/observacao/salvar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ report: generatedReport })
body: JSON.stringify({
report: generatedReport,
criancas: generatedCriancas,
turma: generatedTurma,
tags: generatedTags
})
});
const data = await res.json();
if (data.success) {
@@ -2145,6 +2171,151 @@ const initApp = () => {
if (e.target === recordModal) closeRecordModal();
});
}
// ============================================================
// MODAL: MINHAS OBSERVAÇÕES
// ============================================================
const obsModal = document.getElementById('obsModal');
const btnObservacoes = document.getElementById('btnObservacoes');
const btnCloseObsModal = document.getElementById('btnCloseObsModal');
const obsList = document.getElementById('obsList');
const obsFilterMes = document.getElementById('obsFilterMes');
const obsFilterTag = document.getElementById('obsFilterTag');
const obsSearchCrianca = document.getElementById('obsSearchCrianca');
let allObservations = [];
// Abrir modal de observações
if (btnObservacoes) {
btnObservacoes.addEventListener('click', async () => {
obsModal.style.display = 'flex';
obsList.innerHTML = '<div class="obs-empty">Carregando...</div>';
try {
const res = await fetch('/api/observacoes');
const data = await res.json();
allObservations = data.observations || [];
populateMonthFilter();
renderObsList();
} catch (err) {
console.error('Erro ao carregar observações:', err);
obsList.innerHTML = '<div class="obs-empty">Erro ao carregar observações.</div>';
}
});
}
// Fechar modal
if (btnCloseObsModal) {
btnCloseObsModal.addEventListener('click', () => {
obsModal.style.display = 'none';
});
}
if (obsModal) {
obsModal.addEventListener('click', (e) => {
if (e.target === obsModal) obsModal.style.display = 'none';
});
}
// Filtros
if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList);
if (obsFilterTag) obsFilterTag.addEventListener('change', renderObsList);
if (obsSearchCrianca) obsSearchCrianca.addEventListener('input', renderObsList);
// Preenche select de meses
const populateMonthFilter = () => {
if (!obsFilterMes) return;
const months = [...new Set(allObservations.map(o => o.date?.slice(0, 7)))]
.filter(Boolean).sort().reverse();
obsFilterMes.innerHTML = '<option value="">Todos os meses</option>' +
months.map(m => `<option value="${m}">${m.split('-')[1]}/${m.split('-')[0]}</option>`).join('');
};
// Renderiza lista filtrada
const renderObsList = () => {
if (!obsList) return;
const mes = obsFilterMes?.value || '';
const tag = obsFilterTag?.value || '';
const search = obsSearchCrianca?.value.toLowerCase() || '';
const filtered = allObservations.filter(o => {
if (mes && !o.date?.startsWith(mes)) return false;
if (tag && !o.tags?.includes(tag)) return false;
if (search) {
const nomes = (o.criancas || []).join(' ').toLowerCase();
if (!nomes.includes(search) && !(o.preview || '').toLowerCase().includes(search)) return false;
}
return true;
});
if (filtered.length === 0) {
obsList.innerHTML = '<div class="obs-empty">Nenhuma observação encontrada.</div>';
return;
}
obsList.innerHTML = filtered.map(o => {
const tagsHtml = (o.tags || []).map(t => `<span class="obs-tag">${t}</span>`).join('');
const criancasHtml = (o.criancas || []).map(c => `<span class="obs-crianca">${c}</span>`).join('');
const dateFormatted = o.date ? o.date.split('-').reverse().join('/') : '';
return `
<div class="obs-item" data-filename="${o.filename}" data-yearmonth="${o.date?.slice(0, 7)}">
<div class="obs-item-header">
<strong>📋 ${dateFormatted}${o.time ? ' às ' + o.time : ''}</strong>
${o.turma && o.turma !== 'não informada' ? `<span class="obs-crianca">🏫 ${o.turma}</span>` : ''}
</div>
<div class="obs-item-preview">${o.preview || 'Sem prévia disponível.'}</div>
<div class="obs-item-footer">${tagsHtml}${criancasHtml}</div>
</div>
`;
}).join('');
// Click para abrir observação
obsList.querySelectorAll('.obs-item').forEach(item => {
item.addEventListener('click', async () => {
const ym = item.dataset.yearmonth;
const fn = item.dataset.filename;
if (!ym || !fn) return;
try {
const res = await fetch(`/api/observacoes/${ym}/${fn}`);
const text = await res.text();
showObsDetail(text);
} catch (err) {
console.error('Erro ao carregar observação:', err);
}
});
});
};
// Mostra detalhe da observação em modal
const showObsDetail = (mdText) => {
const modal = document.createElement('div');
modal.className = 'settings-modal';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="settings-modal-content obs-modal-content" style="max-width: 680px;">
<div class="settings-modal-header">
<h3>📋 Observação</h3>
<button class="btn-close-modal" onclick="this.closest('.settings-modal').remove()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<div style="padding: 20px; overflow-y: auto; max-height: 70vh;">
<div class="obs-detail-content" style="color: var(--text-primary); line-height: 1.6;">
${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
</div>
<div style="margin-top: 16px; display: flex; gap: 8px;">
<button class="btn-save-report" onclick="navigator.clipboard.writeText(this.closest('.settings-modal').querySelector('.obs-detail-content').innerText).then(()=>alert('Copiado!'))">Copiar Texto</button>
<button class="btn-discard" onclick="this.closest('.settings-modal').remove()">Fechar</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
};
};
// Player de áudio personalizado global para as mídias geradas