315 lines
10 KiB
TypeScript
315 lines
10 KiB
TypeScript
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<string, Set<string>> = {}
|
|
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<void> {
|
|
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<string[]> {
|
|
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<Record<string, Record<string, string | null>>> {
|
|
const where: Record<string, string> = { desarrollo }
|
|
if (departamento) where.departamento = departamento
|
|
|
|
const rows = await prisma.capturaDepartamento.findMany({
|
|
where,
|
|
orderBy: [{ unidadCodigo: 'asc' }, { campo: 'asc' }],
|
|
})
|
|
|
|
const agrupado: Record<string, Record<string, string | null>> = {}
|
|
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<Record<string, string | null>> {
|
|
const where: Record<string, string> = { desarrollo, unidadCodigo: unidad }
|
|
if (departamento) where.departamento = departamento
|
|
|
|
const rows = await prisma.capturaDepartamento.findMany({
|
|
where,
|
|
orderBy: { campo: 'asc' },
|
|
})
|
|
|
|
const campos: Record<string, string | null> = {}
|
|
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<void> {
|
|
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<void> {
|
|
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<ResumenDepartamento[]> {
|
|
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<ResultadoImportacion> {
|
|
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 }
|
|
}
|