import { prisma } from './prisma.js' import { CAMPOS_POR_DEPARTAMENTO, DEPARTAMENTOS } from '../config/campos-departamento.js' import { AppError } from '../utils/errors.js' /** Fechas emparejadas a checkboxes por departamento: tras establecerse solo las cambia un admin */ const CAMPOS_FECHA_BLOQUEADOS: Record> = {} for (const [dept, campos] of Object.entries(CAMPOS_POR_DEPARTAMENTO)) { const fechas = campos.filter((c) => c.conFecha).map((c) => c.conFecha as string) if (fechas.length > 0) CAMPOS_FECHA_BLOQUEADOS[dept] = new Set(fechas) } /** Bloquear modificación de fechas ya establecidas (solo admin) */ async function validarFechasBloqueadas( desarrollo: string, departamento: string, registros: { unidad: string; campo: string; valor: string | null }[], rol: string, ): Promise { if (rol === 'admin' || rol === 'manager') return const bloqueados = CAMPOS_FECHA_BLOQUEADOS[departamento] if (!bloqueados) return const candidatos = registros.filter((r) => bloqueados.has(r.campo)) if (candidatos.length === 0) return const existentes = await prisma.capturaDepartamento.findMany({ where: { desarrollo, departamento, unidadCodigo: { in: [...new Set(candidatos.map((r) => r.unidad))] }, campo: { in: candidatos.map((r) => r.campo) }, }, }) const previos = new Map(existentes.map((e) => [`${e.unidadCodigo}|${e.campo}`, e.valor])) for (const r of candidatos) { const previo = previos.get(`${r.unidad}|${r.campo}`) if (previo != null && previo !== r.valor) { throw new AppError(403, 'Las fechas ya establecidas solo puede modificarlas un administrador') } } } // ── Obtener unidades únicas de un desarrollo ────────────────────────── export async function obtenerUnidades(desarrollo: string): Promise { const rows = await prisma.capturaDepartamento.findMany({ where: { desarrollo }, select: { unidadCodigo: true }, distinct: ['unidadCodigo'], orderBy: { unidadCodigo: 'asc' }, }) return rows.map((r) => r.unidadCodigo) } // ── Obtener capturas de un desarrollo (agrupadas por unidad) ────────── export async function obtenerCapturas( desarrollo: string, departamento?: string, ): Promise>> { const where: Record = { desarrollo } if (departamento) where.departamento = departamento const rows = await prisma.capturaDepartamento.findMany({ where, orderBy: [{ unidadCodigo: 'asc' }, { campo: 'asc' }], }) const agrupado: Record> = {} for (const r of rows) { const u = r.unidadCodigo if (!agrupado[u]) agrupado[u] = {} agrupado[u][r.campo] = r.valor } return agrupado } // ── Obtener campos de una unidad ────────────────────────────────────── export async function obtenerCapturaUnidad( desarrollo: string, unidad: string, departamento?: string, ): Promise> { const where: Record = { desarrollo, unidadCodigo: unidad } if (departamento) where.departamento = departamento const rows = await prisma.capturaDepartamento.findMany({ where, orderBy: { campo: 'asc' }, }) const campos: Record = {} for (const r of rows) { campos[r.campo] = r.valor } return campos } // ── Guardar campo individual (upsert) ──────────────────────────────── export async function guardarCampo( desarrollo: string, unidadCodigo: string, departamento: string, campo: string, valor: string | null, usuarioId: string, rol: string, ): Promise { await validarFechasBloqueadas(desarrollo, departamento, [{ unidad: unidadCodigo, campo, valor }], rol) const tipo = CAMPOS_POR_DEPARTAMENTO[departamento]?.find((c) => c.campo === campo)?.tipo ?? 'text' await prisma.capturaDepartamento.upsert({ where: { desarrollo_unidadCodigo_departamento_campo: { desarrollo, unidadCodigo, departamento, campo, }, }, update: { valor, tipo, usuarioId }, create: { desarrollo, unidadCodigo, departamento, campo, valor, tipo, usuarioId }, }) } // ── Guardar múltiple (bulk upsert) ─────────────────────────────────── export async function guardarBulk( desarrollo: string, departamento: string, registros: { unidad: string; campo: string; valor: string | null }[], usuarioId: string, rol: string, ): Promise { await validarFechasBloqueadas(desarrollo, departamento, registros, rol) const tipoPorCampo = new Map( (CAMPOS_POR_DEPARTAMENTO[departamento] ?? []).map((c) => [c.campo, c.tipo]), ) await prisma.$transaction( registros.map((r) => prisma.capturaDepartamento.upsert({ where: { desarrollo_unidadCodigo_departamento_campo: { desarrollo, unidadCodigo: r.unidad, departamento, campo: r.campo, }, }, update: { valor: r.valor, tipo: tipoPorCampo.get(r.campo) ?? 'text', usuarioId }, create: { desarrollo, unidadCodigo: r.unidad, departamento, campo: r.campo, valor: r.valor, tipo: tipoPorCampo.get(r.campo) ?? 'text', usuarioId, }, }), ), ) } // ── Resumen de avance por departamento ──────────────────────────────── export interface ResumenDepartamento { departamento: string camposTotales: number camposLlenos: number porcentaje: number } export async function resumenAvance(desarrollo: string): Promise { const resultados: ResumenDepartamento[] = [] for (const dept of DEPARTAMENTOS) { const camposDef = CAMPOS_POR_DEPARTAMENTO[dept] ?? [] const camposTotales = camposDef.length if (camposTotales === 0) continue const camposNombres = camposDef.map((c) => c.campo) // Contar cuántos registros existen con valor no nulo const llenos = await prisma.capturaDepartamento.count({ where: { desarrollo, departamento: dept, campo: { in: camposNombres }, valor: { not: null }, }, }) // Contar unidades únicas para multiplicar const unidades = await prisma.capturaDepartamento.findMany({ where: { desarrollo, departamento: dept }, select: { unidadCodigo: true }, distinct: ['unidadCodigo'], }) const numUnidades = unidades.length || 1 // evitar div/0 const totalEsperado = camposTotales * numUnidades const porcentaje = totalEsperado > 0 ? Math.round((llenos / totalEsperado) * 100) : 0 resultados.push({ departamento: dept, camposTotales, camposLlenos: llenos, porcentaje, }) } return resultados } // ── Importar desde Excel (arreglo de arreglos) ──────────────────────── export interface ResultadoImportacion { registrosCreados: number registrosActualizados: number } export async function importarDesdeExcel( desarrollo: string, datos: string[][], usuarioId: string, ): Promise { if (datos.length < 2) return { registrosCreados: 0, registrosActualizados: 0 } // Primera fila = headers const firstRow = datos[0] if (!firstRow) return { registrosCreados: 0, registrosActualizados: 0 } const headers = firstRow.map((h) => String(h ?? '').trim()) // Mapear cada header a su departamento y campo type ColMapa = { departamento: string; campo: string; tipo: string } | null const mapaColumnas: ColMapa[] = headers.map((header) => { const norm = header.toUpperCase().replace(/\s+/g, '_').replace(/[Á]/g, 'A').replace(/[É]/g, 'E').replace(/[Í]/g, 'I').replace(/[Ó]/g, 'O').replace(/[Ú]/g, 'U').replace(/[Ñ]/g, 'N') for (const [dept, campos] of Object.entries(CAMPOS_POR_DEPARTAMENTO)) { for (const c of campos) { if (c.campo === norm || norm.includes(c.campo.toUpperCase())) { return { departamento: dept, campo: c.campo, tipo: c.tipo } } } } return null }) let creados = 0 let actualizados = 0 // Procesar cada fila de datos (skipping header row) const filas = datos.slice(1).filter((row) => row.some((cell) => cell != null && String(cell).trim() !== '')) for (const fila of filas) { // Detectar unidad: primera columna con valor const unidadCodigo = String(fila[0] ?? '').trim() if (!unidadCodigo) continue for (let col = 0; col < headers.length; col++) { const mapeo = mapaColumnas[col] if (!mapeo) continue const rawValor = fila[col] const valor = rawValor != null && String(rawValor).trim() !== '' ? String(rawValor).trim() : null try { const existente = await prisma.capturaDepartamento.findUnique({ where: { desarrollo_unidadCodigo_departamento_campo: { desarrollo, unidadCodigo, departamento: mapeo.departamento, campo: mapeo.campo, }, }, }) await prisma.capturaDepartamento.upsert({ where: { desarrollo_unidadCodigo_departamento_campo: { desarrollo, unidadCodigo, departamento: mapeo.departamento, campo: mapeo.campo, }, }, update: { valor, tipo: mapeo.tipo, usuarioId }, create: { desarrollo, unidadCodigo, departamento: mapeo.departamento, campo: mapeo.campo, valor, tipo: mapeo.tipo, usuarioId, }, }) if (existente) actualizados++ else creados++ } catch { // Skip errors silently (e.g., empty values) } } } return { registrosCreados: creados, registrosActualizados: actualizados } }