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:
+174
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user