diff --git a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx index 3d22627..ba19041 100644 --- a/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx +++ b/src/components/asignaturas/detalle/AsignaturaDetailPage.tsx @@ -15,6 +15,7 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects' +import { columnParsers } from '@/lib/asignaturaColumnParsers' export interface BibliografiaEntry { id: string @@ -38,6 +39,10 @@ export interface AsignaturaResponse { datos: AsignaturaDatos } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + type CriterioEvaluacionRow = { criterio: string porcentaje: number @@ -791,80 +796,3 @@ function EvaluationView({ items }: { items: Array }) { ) } - -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value) -} - -function parseContenidoTematicoToPlainText(value: unknown): string { - if (!Array.isArray(value)) return '' - - const blocks: Array = [] - - for (const item of value) { - if (!isRecord(item)) continue - - const unidad = - typeof item.unidad === 'number' && Number.isFinite(item.unidad) - ? item.unidad - : undefined - const titulo = typeof item.titulo === 'string' ? item.titulo : '' - - const header = `${unidad ?? ''}${unidad ? '.' : ''} ${titulo}`.trim() - if (!header) continue - - const lines: Array = [header] - - const temas = Array.isArray(item.temas) ? item.temas : [] - temas.forEach((tema, idx) => { - const temaNombre = - typeof tema === 'string' - ? tema - : isRecord(tema) && typeof tema.nombre === 'string' - ? tema.nombre - : '' - if (!temaNombre) return - - if (unidad != null) { - lines.push(`${unidad}.${idx + 1} ${temaNombre}`.trim()) - } else { - lines.push(`${idx + 1}. ${temaNombre}`) - } - }) - - blocks.push(lines.join('\n')) - } - - return blocks.join('\n\n').trimEnd() -} - -function parseCriteriosEvaluacionToPlainText(value: unknown): string { - if (!Array.isArray(value)) return '' - - const lines: Array = [] - for (const item of value) { - if (!isRecord(item)) continue - const label = typeof item.criterio === 'string' ? item.criterio.trim() : '' - const valueNum = - typeof item.porcentaje === 'number' - ? item.porcentaje - : typeof item.porcentaje === 'string' - ? Number(item.porcentaje) - : NaN - - if (!label) continue - if (!Number.isFinite(valueNum)) continue - - const v = Math.trunc(valueNum) - if (v < 1 || v > 100) continue - - lines.push(`${label}: ${v}%`) - } - - return lines.join('\n') -} - -const columnParsers: Partial string>> = { - contenido_tematico: parseContenidoTematicoToPlainText, - criterios_de_evaluacion: parseCriteriosEvaluacionToPlainText, -} diff --git a/src/data/api/document.api.ts b/src/data/api/document.api.ts index cd6ead1..0e8b322 100644 --- a/src/data/api/document.api.ts +++ b/src/data/api/document.api.ts @@ -1,14 +1,18 @@ // document.api.ts +import { supabaseBrowser } from '../supabase/client' import { invokeEdge } from '../supabase/invokeEdge' +import { requireData, throwIfError } from './_helpers' + +import type { Tables } from '@/types/supabase' + +import { columnParsers } from '@/lib/asignaturaColumnParsers' + const EDGE = { carbone_io_wrapper: 'carbone-io-wrapper', } as const -const DOCUMENT_PDF_ASIGNATURA_URL = - 'https://n8n.app.lci.ulsa.mx/webhook/041a68be-7568-46d0-bc08-09ded12d017d' - interface GeneratePdfParams { plan_estudio_id: string } @@ -16,6 +20,52 @@ interface GeneratePdfParamsAsignatura { asignatura_id: string } +function isPlainRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function toStringValue(value: unknown): string { + if (typeof value === 'string') return value + if (typeof value === 'number' || typeof value === 'boolean') + return String(value) + if (value === null || value === undefined) return '' + try { + return JSON.stringify(value) + } catch { + return String(value) + } +} + +function buildAsignaturaReportData( + row: Pick< + Tables<'asignaturas'>, + 'datos' | 'contenido_tematico' | 'criterios_de_evaluacion' + >, +): Record { + const out: Record = {} + + const datosRaw = row.datos + if (isPlainRecord(datosRaw)) { + for (const [k, v] of Object.entries(datosRaw)) { + if (v === null || v === undefined) continue + out[k] = toStringValue(v) + } + } + + for (const [key, parser] of Object.entries(columnParsers)) { + if (!parser) continue + + const current = out[key] + if (typeof current === 'string' && current.trim()) continue + + const rawValue = (row as any)?.[key] + const parsed = parser(rawValue) + if (parsed.trim()) out[key] = parsed + } + + return out +} + export async function fetchPlanPdf({ plan_estudio_id, }: GeneratePdfParams): Promise { @@ -38,18 +88,41 @@ export async function fetchPlanPdf({ export async function fetchAsignaturaPdf({ asignatura_id, }: GeneratePdfParamsAsignatura): Promise { - const response = await fetch(DOCUMENT_PDF_ASIGNATURA_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', + const supabase = supabaseBrowser() + + const { data, error } = await supabase + .from('asignaturas') + .select('*') + .eq('id', asignatura_id) + .single() + + throwIfError(error) + + const row = requireData( + data as Pick< + Tables<'asignaturas'>, + 'datos' | 'contenido_tematico' | 'criterios_de_evaluacion' + >, + 'Asignatura no encontrada', + ) + + // const reportData = buildAsignaturaReportData(row) + + return await invokeEdge( + EDGE.carbone_io_wrapper, + { + action: 'downloadReport', + asignatura_id, + body: { + data: row, + convertTo: 'pdf', + }, }, - body: JSON.stringify({ asignatura_id }), - }) - - if (!response.ok) { - throw new Error('Error al generar el PDF') - } - - // n8n devuelve el archivo → lo tratamos como blob - return await response.blob() + { + headers: { + 'Content-Type': 'application/json', + }, + responseType: 'blob', + }, + ) } diff --git a/src/lib/asignaturaColumnParsers.ts b/src/lib/asignaturaColumnParsers.ts new file mode 100644 index 0000000..1b285ee --- /dev/null +++ b/src/lib/asignaturaColumnParsers.ts @@ -0,0 +1,78 @@ +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +export function parseContenidoTematicoToPlainText(value: unknown): string { + if (!Array.isArray(value)) return '' + + const blocks: Array = [] + + for (const item of value) { + if (!isRecord(item)) continue + + const unidad = + typeof item.unidad === 'number' && Number.isFinite(item.unidad) + ? item.unidad + : undefined + const titulo = typeof item.titulo === 'string' ? item.titulo : '' + + const header = `${unidad ?? ''}${unidad ? '.' : ''} ${titulo}`.trim() + if (!header) continue + + const lines: Array = [header] + + const temas = Array.isArray(item.temas) ? item.temas : [] + temas.forEach((tema, idx) => { + const temaNombre = + typeof tema === 'string' + ? tema + : isRecord(tema) && typeof tema.nombre === 'string' + ? tema.nombre + : '' + if (!temaNombre) return + + if (unidad != null) { + lines.push(`${unidad}.${idx + 1} ${temaNombre}`.trim()) + } else { + lines.push(`${idx + 1}. ${temaNombre}`) + } + }) + + blocks.push(lines.join('\n')) + } + + return blocks.join('\n\n').trimEnd() +} + +export function parseCriteriosEvaluacionToPlainText(value: unknown): string { + if (!Array.isArray(value)) return '' + + const lines: Array = [] + for (const item of value) { + if (!isRecord(item)) continue + const label = typeof item.criterio === 'string' ? item.criterio.trim() : '' + const valueNum = + typeof item.porcentaje === 'number' + ? item.porcentaje + : typeof item.porcentaje === 'string' + ? Number(item.porcentaje) + : NaN + + if (!label) continue + if (!Number.isFinite(valueNum)) continue + + const v = Math.trunc(valueNum) + if (v < 1 || v > 100) continue + + lines.push(`${label}: ${v}%`) + } + + return lines.join('\n') +} + +export const columnParsers: Partial< + Record string> +> = { + contenido_tematico: parseContenidoTematicoToPlainText, + criterios_de_evaluacion: parseCriteriosEvaluacionToPlainText, +}