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
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
<button class="tag-filter" data-tag="estudos">📚 Estudos</button>
|
||||
</div>
|
||||
|
||||
<!-- Botão: Minhas Observações -->
|
||||
<button class="btn-observacoes" id="btnObservacoes">
|
||||
<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="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
<span>Minhas Observações</span>
|
||||
</button>
|
||||
|
||||
<!-- Histórico de Conversas -->
|
||||
<div class="chat-history" id="chatHistory">
|
||||
<!-- Histórico filtrado/busca -->
|
||||
@@ -386,6 +394,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Minhas Observações -->
|
||||
<div id="obsModal" class="settings-modal" style="display: none;">
|
||||
<div class="settings-modal-content obs-modal-content">
|
||||
<div class="settings-modal-header">
|
||||
<h3>📒 Minhas Observações</h3>
|
||||
<button id="btnCloseObsModal" class="btn-close-modal">
|
||||
<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 class="obs-modal-body">
|
||||
<!-- Filtros -->
|
||||
<div class="obs-filters" id="obsFilters">
|
||||
<select id="obsFilterMes" class="obs-select">
|
||||
<option value="">Todos os meses</option>
|
||||
</select>
|
||||
<select id="obsFilterTag" class="obs-select">
|
||||
<option value="">Todas as tags</option>
|
||||
<option value="social">Social</option>
|
||||
<option value="cognitivo">Cognitivo</option>
|
||||
<option value="motor">Motor</option>
|
||||
<option value="linguagem">Linguagem</option>
|
||||
<option value="emocional">Emocional</option>
|
||||
<option value="colaborativo">Colaborativo</option>
|
||||
<option value="autónomo">Autónomo</option>
|
||||
</select>
|
||||
<input type="text" id="obsSearchCrianca" class="obs-input" placeholder="Buscar por criança...">
|
||||
</div>
|
||||
<!-- Lista de observações -->
|
||||
<div id="obsList" class="obs-list">
|
||||
<div class="obs-empty">Carregando observações...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bibliotecas externas para renderizar markdown e blocos de código -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
+171
-1
@@ -2804,7 +2804,177 @@ code {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Animação fadeIn global */
|
||||
/* Meta info do relatório na prévia */
|
||||
.report-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.report-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--text-muted, #666);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
display: inline-block;
|
||||
background: var(--brand-green, #10a37f);
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Botão Minhas Observações na sidebar */
|
||||
.btn-observacoes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: calc(100% - 40px);
|
||||
margin: 0 auto 8px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-secondary, #1a1a1a);
|
||||
border: 1px solid var(--border-light, #262626);
|
||||
border-radius: 12px;
|
||||
color: var(--text-secondary, #a3a3a3);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-observacoes:hover {
|
||||
background: var(--brand-green, #10a37f);
|
||||
color: white;
|
||||
border-color: var(--brand-green, #10a37f);
|
||||
}
|
||||
|
||||
.btn-observacoes svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Modal de Observações */
|
||||
.obs-modal-content {
|
||||
max-width: 640px !important;
|
||||
max-height: 85vh;
|
||||
}
|
||||
|
||||
.obs-modal-body {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.obs-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-light, #262626);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.obs-select, .obs-input {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-light, #262626);
|
||||
background: var(--bg-input, #121212);
|
||||
color: var(--text-primary, #ffffff);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.obs-input {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.obs-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.obs-empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted, #666);
|
||||
padding: 40px 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.obs-item {
|
||||
background: var(--bg-secondary, #1a1a1a);
|
||||
border: 1px solid var(--border-light, #262626);
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.obs-item:hover {
|
||||
border-color: var(--brand-green, #10a37f);
|
||||
background: var(--bg-input, #121212);
|
||||
}
|
||||
|
||||
.obs-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.obs-item-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted, #666);
|
||||
}
|
||||
|
||||
.obs-item-preview {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary, #a3a3a3);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.obs-item-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.obs-tag {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(16, 163, 127, 0.2);
|
||||
color: var(--brand-green, #10a37f);
|
||||
}
|
||||
|
||||
.obs-crianca {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-secondary, #a3a3a3);
|
||||
}
|
||||
|
||||
/* FAB: Floating Action Button para gravação */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
|
||||
@@ -252,24 +252,32 @@ app.post('/api/observacao/transcrever', requireAuth, multerUpload.single('audio'
|
||||
}
|
||||
|
||||
// Gera relatório estruturado via GPT
|
||||
// Envia transcrição + pedido de extrair: crianças, turma, tags, relatório
|
||||
const reportPrompt = `Você é a Kemily, assistente pedagógica da professora Camila Martella Gasparini Reifonas.
|
||||
|
||||
Ela acabou de gravar uma observação em voz sobre seus alunos da educação infantil. Sua tarefa é analisar a transcrição e gerar um RELATÓRIO ESTRUTURADO de observação pedagógica.
|
||||
Ela acabou de gravar uma observação em voz sobre seus alunos da educação infantil. Sua tarefa é ANALISAR a transcrição e retornar UM OBJETO JSON (sem markdown, sem código, apenas o JSON puro) com:
|
||||
|
||||
REGRAS DO RELATÓRIO:
|
||||
- Use linguagem formal-pedagógica com calor humano
|
||||
- Estruture em seções claras com_MARKDOWN
|
||||
- Identifique crianças mencionadas, comportamentos, interações, conquistas, dificuldades
|
||||
- Inclua sugestões de seguimento para o planejamento
|
||||
- Foque em evidências observáveis, não interpretções subjetivas
|
||||
- Seja objetiva mas detalhista
|
||||
{
|
||||
"criancas": ["lista de nomes de crianças mencionadas ou 'null' se não identificar nenhuma"],
|
||||
"turma": "turma mencionada ou 'não informada'",
|
||||
"tags": ["tag1", "tag2"] (social, cognitivo, motor, linguagem, emocional,轮的哪个),
|
||||
"relatorio": "...markdown estruturado do relatório pedagógico..."
|
||||
}
|
||||
|
||||
TRANSCRIÇÃO DA OBSERVAÇÃO:
|
||||
REGRAS:
|
||||
- Identifique crianças pelo nome próprio mencionado na fala
|
||||
- Se não conseguir identificar nomes, use "criancas": null
|
||||
- Tags: escolha entre social, cognitivo, motor, linguagem, emocional, colaborativo, autónomo
|
||||
- Relatório em português, linguagem formal-pedagógica, seções claras em **markdown**
|
||||
- Foque em evidências observáveis
|
||||
- Máximo 120 palavras no relatório
|
||||
|
||||
TRANSCRIÇÃO:
|
||||
"""
|
||||
${transcribedText}
|
||||
"""
|
||||
|
||||
Gerando o relatório...`;
|
||||
Retorne APENAS o JSON válido:`;
|
||||
|
||||
const reportRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
@@ -290,11 +298,29 @@ Gerando o relatório...`;
|
||||
}
|
||||
|
||||
const reportData = await reportRes.json();
|
||||
const reportText = reportData.choices?.[0]?.message?.content || 'Relatório não pôde ser gerado.';
|
||||
const rawContent = reportData.choices?.[0]?.message?.content || '';
|
||||
|
||||
// Parse JSON da resposta
|
||||
let parsedData = { criancas: null, turma: 'não informada', tags: [], relatorio: rawContent };
|
||||
try {
|
||||
// Limpa markdown code blocks se houver
|
||||
const cleanJson = rawContent.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
||||
const parsed = JSON.parse(cleanJson);
|
||||
parsedData = {
|
||||
criancas: parsed.criancas || null,
|
||||
turma: parsed.turma || 'não informada',
|
||||
tags: parsed.tags || [],
|
||||
relatorio: parsed.relatorio || rawContent
|
||||
};
|
||||
} catch (parseErr) {
|
||||
// Se não conseguir parsear JSON, retorna o texto como relatório
|
||||
console.warn('Não foi possível parsear JSON do GPT, usando texto bruto');
|
||||
parsedData.relatorio = rawContent;
|
||||
}
|
||||
|
||||
res.json({
|
||||
transcribedText,
|
||||
report: reportText
|
||||
...parsedData
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
@@ -306,7 +332,7 @@ Gerando o relatório...`;
|
||||
|
||||
// Rota: POST /api/observacao/salvar — salva relatório em .md
|
||||
app.post('/api/observacao/salvar', requireAuth, (req, res) => {
|
||||
const { report } = req.body;
|
||||
const { report, criancas, turma, tags } = req.body;
|
||||
if (!report) return res.status(400).json({ error: 'Relatório vazio' });
|
||||
|
||||
const now = new Date();
|
||||
@@ -316,13 +342,7 @@ app.post('/api/observacao/salvar', requireAuth, (req, res) => {
|
||||
const timestamp = now.toISOString();
|
||||
|
||||
// Monta markdown com frontmatter
|
||||
const mdContent = `---
|
||||
data: "${dateStr}"
|
||||
hora: "${timeStr}"
|
||||
timestamp: "${timestamp}"
|
||||
---
|
||||
|
||||
${report}
|
||||
const mdContent = `---\ndata: "${dateStr}"\nhora: "${timeStr}"\ntimestamp: "${timestamp}"\ncriancas: ${JSON.stringify(criancas)}\nturma: "${turma || 'não informada'}"\ntags: ${JSON.stringify(tags)}\n---\n\n${report}
|
||||
`;
|
||||
|
||||
const filename = `${dateStr}_${timeStr}.md`;
|
||||
@@ -332,7 +352,7 @@ ${report}
|
||||
const filePath = path.join(monthDir, filename);
|
||||
fs.writeFileSync(filePath, mdContent, 'utf8');
|
||||
|
||||
// Atualiza índice
|
||||
// Atualiza índice com metadados estruturados
|
||||
const idx = readObsIndex();
|
||||
idx.observations.unshift({
|
||||
id: Date.now(),
|
||||
@@ -340,6 +360,9 @@ ${report}
|
||||
date: dateStr,
|
||||
time: timeStr,
|
||||
timestamp,
|
||||
criancas: criancas || null,
|
||||
turma: turma || 'não informada',
|
||||
tags: tags || [],
|
||||
preview: report.slice(0, 120) + '...'
|
||||
});
|
||||
writeObsIndex(idx);
|
||||
|
||||
Reference in New Issue
Block a user