dedup sesiones + calendario desde BD + scroll por fecha

- analyzeAsSession: detecta duplicados por UTC timestamp antes de guardar
- TranscriptionsView: sessionDatesList reemplaza sessionOffsets
- session dates se leen directo de tabla sessions (no desde markdown)
- scrollToSession busca por fecha en markdown con regex escapada
- calendario usa BD como fuente de verdad
This commit is contained in:
2026-05-28 14:02:35 -05:00
parent 837a264e81
commit 8cec93b90a
+49 -39
View File
@@ -14,7 +14,7 @@ import {
import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer'
import { generateMasterDoc } from '@/services/project-doc'
import { parseFile } from '@/services/parse-transcription'
import { saveSession, saveSessionSummary, saveProjectState, getSessionCount } from '@/services/transcriptions-db'
import { saveSession, saveSessionSummary, saveProjectState, getSessionCount, getSessionsByProject } from '@/services/transcriptions-db'
import { parseTeamsUTC, toColombiaTime } from '@/services/timezone'
import {
Card,
@@ -77,7 +77,7 @@ const docGenerated = ref(false)
const docMarkdown = ref('')
const docSessionCount = ref(0)
const docUpdatedAt = ref('')
const sessionOffsets = ref<{ date: string; title: string; offset: number }[]>([])
const sessionDatesList = ref<{ date: string; title: string; fileName: string }[]>([])
const viewerRef = ref<HTMLTextAreaElement | null>(null)
// ─── Calendar state ──────────────────────────────────────
@@ -96,45 +96,43 @@ const selectedProject = computed(() =>
watch(selectedProjectId, async (id) => {
if (!id) return
clearAll()
const count = await getSessionCount(id)
await loadSessionDates(id)
const count = sessionDatesList.value.length
docSessionCount.value = count
if (count > 0) {
const md = await generateMasterDoc(id, selectedProject.value?.name || '')
docMarkdown.value = md
parseSessionOffsets(md)
}
}, { immediate: false })
function parseSessionOffsets(md: string) {
const offsets: { date: string; title: string; offset: number }[] = []
const re = /## 📍 Sesión \d+:\s*(.+?)\n\*\*Archivo fuente:/g
let match
while ((match = re.exec(md)) !== null) {
const title = match[1].trim()
const dateMatch = title.match(/(\d{4}-\d{2}-\d{2})/)
offsets.push({
date: dateMatch?.[1] || '',
title,
offset: match.index,
})
}
sessionOffsets.value = offsets
async function loadSessionDates(projectId: number) {
const sessions = await getSessionsByProject(projectId)
sessionDatesList.value = sessions.map(s => ({
date: s.date,
title: s.title,
fileName: s.fileName,
}))
}
function scrollToSession(offset: number) {
if (viewerRef.value) {
const textarea = viewerRef.value
const md = docMarkdown.value
// Calculate line number from offset
const lineNum = md.slice(0, offset).split('\n').length
const lineHeight = 20 // approximate
textarea.scrollTop = (lineNum - 5) * lineHeight
textarea.focus()
}
function scrollToSession(date: string) {
if (!viewerRef.value || !docMarkdown.value) return
const md = docMarkdown.value
// Buscar la sesión por fecha en el markdown: ## 📍 Sesión: YYYY-MM-DD
const re = new RegExp(`## 📍 Sesión: ${escapeRegex(date)}`)
const match = re.exec(md)
if (!match) return
const lineNum = md.slice(0, match.index).split('\n').length
const lineHeight = 20
viewerRef.value.scrollTop = (lineNum - 5) * lineHeight
viewerRef.value.focus()
}
function escapeRegex(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function sessionDates(): string[] {
return sessionOffsets.value.map(s => s.date).filter(Boolean)
return sessionDatesList.value.map(s => s.date).filter(Boolean)
}
// ─── Calendar helpers ────────────────────────────────────
@@ -149,14 +147,13 @@ function firstDayOfMonth(m: number, y: number) {
}
function isSessionDate(d: number) {
const ds = sessionDates()
const dateStr = `${calYear.value}-${String(calMonth.value + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
return ds.includes(dateStr)
return sessionDatesList.value.some(s => s.date === dateStr)
}
function getSessionForDate(d: number) {
const dateStr = `${calYear.value}-${String(calMonth.value + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
return sessionOffsets.value.find(s => s.date === dateStr)
return sessionDatesList.value.find(s => s.date === dateStr) || null
}
function prevMonth() {
@@ -283,7 +280,20 @@ async function analyzeAsSession() {
sessionTeamsInfo.value = teamsDate ? { utc: teamsDate.utc, timeStr: teamsDate.timeStr } : null
const sessionDate = teamsDate?.dateStr || result.sessionDate || now.slice(0, 10)
// 1b. Guardar transcripción en BD
// 1b. Deduplicación: verificar si ya existe una sesión con el mismo UTC timestamp
const existing = await getSessionsByProject(selectedProjectId.value)
const isDuplicate = existing.some(s => {
// Comparar por UTC timestamp en filename o por fecha + título similar
const existingUTC = parseTeamsUTC(s.fileName)
return teamsDate !== null && existingUTC !== null && teamsDate.utc === existingUTC.utc
})
if (isDuplicate) {
sessionError.value = `Esta sesión ya fue registrada (UTC ${teamsDate?.utc}). Se descarta.`
sessionLoading.value = false
return
}
// 1c. Guardar transcripción en BD
const sessionId = await saveSession({
projectId: selectedProjectId.value,
date: sessionDate,
@@ -313,9 +323,9 @@ async function analyzeAsSession() {
// 4. Regenerar documento MD
const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
docMarkdown.value = md
docSessionCount.value = await getSessionCount(selectedProjectId.value)
await loadSessionDates(selectedProjectId.value)
docSessionCount.value = sessionDatesList.value.length
docUpdatedAt.value = now.slice(0, 16).replace('T', ' ')
parseSessionOffsets(md)
docGenerated.value = true
// 5. Mostrar resultado en UI
@@ -391,9 +401,9 @@ async function generateDoc() {
try {
const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
docMarkdown.value = md
docSessionCount.value = await getSessionCount(selectedProjectId.value)
await loadSessionDates(selectedProjectId.value)
docSessionCount.value = sessionDatesList.value.length
docUpdatedAt.value = new Date().toISOString().slice(0, 16).replace('T', ' ')
parseSessionOffsets(md)
docGenerated.value = true
} catch (e: any) {
sessionError.value = e.message
@@ -667,7 +677,7 @@ function clearAll() {
</div>
</CardHeader>
<CardContent class="pt-0">
<div v-if="sessionOffsets.length === 0" class="text-xs text-muted-foreground text-center py-2">
<div v-if="sessionDatesList.length === 0" class="text-xs text-muted-foreground text-center py-2">
{{ t('transcriptions.noSessions') }}
</div>
<template v-else>
@@ -681,7 +691,7 @@ function clearAll() {
:key="d"
class="py-0.5 rounded hover:bg-muted transition-colors relative"
:class="isSessionDate(d) ? 'bg-primary/10 text-primary font-bold' : 'text-muted-foreground'"
@click="() => { const s = getSessionForDate(d); if (s) scrollToSession(s.offset) }"
@click="() => { const s = getSessionForDate(d); if (s) scrollToSession(s.date) }"
:title="getSessionForDate(d)?.title || ''"
>
{{ d }}