feat: importar csv y auditar ediciones
This commit is contained in:
14
packages/core/src/entities/Auditoria.ts
Normal file
14
packages/core/src/entities/Auditoria.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type EntidadAuditada = 'PRODUCTOR' | 'PRODUCTO' | 'VENTA';
|
||||
|
||||
export interface Auditoria {
|
||||
id: string;
|
||||
entidad: EntidadAuditada;
|
||||
registroId: string;
|
||||
operacion: 'CREAR' | 'EDITAR';
|
||||
modificadoPor: string;
|
||||
motivo: string;
|
||||
valoresAnteriores?: string;
|
||||
valoresNuevos: string;
|
||||
fecha: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -6,3 +6,4 @@ export * from './Gasto';
|
||||
export * from './Ajuste';
|
||||
export * from './CuadreCaja';
|
||||
export * from './JornadaToldo';
|
||||
export * from './Auditoria';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import type { Productor, Producto, MovimientoInventario, Venta, VentaLinea, Gasto, Ajuste, CuadreCaja, JornadaToldo } from '@toldo/core';
|
||||
import type { Productor, Producto, MovimientoInventario, Venta, VentaLinea, Gasto, Ajuste, CuadreCaja, JornadaToldo, Auditoria } from '@toldo/core';
|
||||
|
||||
export class ToldoDB extends Dexie {
|
||||
productores!: Table<Productor, string>;
|
||||
@@ -11,6 +11,7 @@ export class ToldoDB extends Dexie {
|
||||
ajustes!: Table<Ajuste, string>;
|
||||
cuadres!: Table<CuadreCaja, string>;
|
||||
jornadas!: Table<JornadaToldo, string>;
|
||||
auditorias!: Table<Auditoria, string>;
|
||||
|
||||
constructor() {
|
||||
super('ToldoConjuntoDB');
|
||||
@@ -35,6 +36,18 @@ export class ToldoDB extends Dexie {
|
||||
cuadres: 'id, fecha',
|
||||
jornadas: 'id, fecha, estado'
|
||||
});
|
||||
this.version(3).stores({
|
||||
productores: 'id, nombre, activo',
|
||||
productos: 'id, productorId, nombrePresentacion, [productorId+nombrePresentacion]',
|
||||
movimientosInventario: 'id, fecha, tipo, productoId',
|
||||
ventas: 'id, fecha',
|
||||
ventaLineas: 'id, ventaId, productoId, formaPago',
|
||||
gastos: 'id, fecha',
|
||||
ajustes: 'id, fecha, productoId',
|
||||
cuadres: 'id, fecha',
|
||||
jornadas: 'id, fecha, estado',
|
||||
auditorias: 'id, entidad, registroId, fecha'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
packages/services/src/auditoria/AuditoriaService.ts
Normal file
29
packages/services/src/auditoria/AuditoriaService.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { db } from '@toldo/data';
|
||||
import type { Auditoria, EntidadAuditada } from '@toldo/core';
|
||||
|
||||
export interface DatosEdicion {
|
||||
modificadoPor: string;
|
||||
motivo: string;
|
||||
}
|
||||
|
||||
export function validarEdicion(datos: DatosEdicion) {
|
||||
if (!datos.modificadoPor.trim()) throw new Error('Indica quién realiza el cambio.');
|
||||
if (!datos.motivo.trim()) throw new Error('Indica el motivo del cambio.');
|
||||
}
|
||||
|
||||
export class AuditoriaService {
|
||||
async registrar(entidad: EntidadAuditada, registroId: string, anteriores: unknown | undefined, nuevos: unknown, datos: DatosEdicion) {
|
||||
validarEdicion(datos);
|
||||
const auditoria: Auditoria = {
|
||||
id: crypto.randomUUID(), entidad, registroId, operacion: anteriores ? 'EDITAR' : 'CREAR',
|
||||
modificadoPor: datos.modificadoPor.trim(), motivo: datos.motivo.trim(),
|
||||
valoresAnteriores: anteriores ? JSON.stringify(anteriores) : undefined,
|
||||
valoresNuevos: JSON.stringify(nuevos), fecha: new Date(), createdAt: new Date(),
|
||||
};
|
||||
return db.auditorias.add(auditoria);
|
||||
}
|
||||
|
||||
async listar(entidad?: EntidadAuditada) {
|
||||
return entidad ? db.auditorias.where('entidad').equals(entidad).reverse().sortBy('fecha') : db.auditorias.orderBy('fecha').reverse().toArray();
|
||||
}
|
||||
}
|
||||
37
packages/services/src/auditoria/EdicionService.ts
Normal file
37
packages/services/src/auditoria/EdicionService.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { db } from '@toldo/data';
|
||||
import type { Producto, Productor, Venta, VentaLinea } from '@toldo/core';
|
||||
import { validarEdicion, type DatosEdicion } from './AuditoriaService';
|
||||
|
||||
export class EdicionService {
|
||||
async editarProductor(productor: Productor, anterior: Productor, datos: DatosEdicion) {
|
||||
validarEdicion(datos);
|
||||
return db.transaction('rw', db.productores, db.auditorias, async () => {
|
||||
await db.productores.put(productor);
|
||||
await db.auditorias.add({ id: crypto.randomUUID(), entidad: 'PRODUCTOR', registroId: productor.id, operacion: 'EDITAR', ...datos, modificadoPor: datos.modificadoPor.trim(), motivo: datos.motivo.trim(), valoresAnteriores: JSON.stringify(anterior), valoresNuevos: JSON.stringify(productor), fecha: new Date(), createdAt: new Date() } as any);
|
||||
});
|
||||
}
|
||||
|
||||
async editarProducto(producto: Producto, anterior: Producto, datos: DatosEdicion) {
|
||||
validarEdicion(datos);
|
||||
return db.transaction('rw', db.productos, db.auditorias, async () => {
|
||||
await db.productos.put(producto);
|
||||
await db.auditorias.add({ id: crypto.randomUUID(), entidad: 'PRODUCTO', registroId: producto.id, operacion: 'EDITAR', ...datos, modificadoPor: datos.modificadoPor.trim(), motivo: datos.motivo.trim(), valoresAnteriores: JSON.stringify(anterior), valoresNuevos: JSON.stringify(producto), fecha: new Date(), createdAt: new Date() } as any);
|
||||
});
|
||||
}
|
||||
|
||||
async editarVenta(venta: Venta, anterior: Venta, lineasAnteriores: VentaLinea[], datos: DatosEdicion) {
|
||||
validarEdicion(datos);
|
||||
return db.transaction('rw', db.ventas, db.ventaLineas, db.movimientosInventario, db.auditorias, async () => {
|
||||
const lineasNuevas = venta.lineas;
|
||||
for (const anteriorLinea of lineasAnteriores) {
|
||||
const nueva = lineasNuevas.find((linea) => linea.id === anteriorLinea.id);
|
||||
if (!nueva) continue;
|
||||
const diferencia = nueva.cantidad - anteriorLinea.cantidad;
|
||||
if (diferencia) await db.movimientosInventario.add({ id: crypto.randomUUID(), fecha: new Date(), tipo: 'VENTA', productoId: nueva.productoId, cantidad: -diferencia, referenciaId: venta.id, motivo: `Corrección de venta: ${datos.motivo}`, createdAt: new Date() } as any);
|
||||
await db.ventaLineas.put(nueva);
|
||||
}
|
||||
await db.ventas.put(venta);
|
||||
await db.auditorias.add({ id: crypto.randomUUID(), entidad: 'VENTA', registroId: venta.id, operacion: 'EDITAR', ...datos, modificadoPor: datos.modificadoPor.trim(), motivo: datos.motivo.trim(), valoresAnteriores: JSON.stringify({ venta: anterior, lineas: lineasAnteriores }), valoresNuevos: JSON.stringify({ venta, lineas: lineasNuevas }), fecha: new Date(), createdAt: new Date() } as any);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Productor, Producto, MovimientoInventario, Venta, VentaLinea, Gasto, Ajuste, CuadreCaja, JornadaToldo } from '@toldo/core';
|
||||
import type { Productor, Producto, MovimientoInventario, Venta, VentaLinea, Gasto, Ajuste, CuadreCaja, JornadaToldo, Auditoria } from '@toldo/core';
|
||||
|
||||
export interface ExportData {
|
||||
schemaVersion: number;
|
||||
@@ -14,6 +14,7 @@ export interface ExportData {
|
||||
ajustes: Ajuste[];
|
||||
cuadres: CuadreCaja[];
|
||||
jornadas?: JornadaToldo[];
|
||||
auditorias?: Auditoria[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +53,6 @@ export class ExportImportService {
|
||||
throw new Error('Versión de esquema incompatible. Actualice la aplicación.');
|
||||
}
|
||||
// Simple pass-through for v1; migrations would go here
|
||||
return { ...(data as ExportData), data: { ...(data as ExportData).data, jornadas: (data as ExportData).data.jornadas ?? [] } };
|
||||
return { ...(data as ExportData), data: { ...(data as ExportData).data, jornadas: (data as ExportData).data.jornadas ?? [], auditorias: (data as ExportData).data.auditorias ?? [] } };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,6 @@ export * from './cuadre/CuadreService';
|
||||
export * from './reportes/ReportesService';
|
||||
export * from './export-import/ExportImportService';
|
||||
export * from './JornadaService';
|
||||
export * from './auditoria/AuditoriaService';
|
||||
export * from './auditoria/EdicionService';
|
||||
export * from './inventario/ImportInventarioCsvService';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { db } from '@toldo/data';
|
||||
import type { Producto, Productor } from '@toldo/core';
|
||||
|
||||
export interface ResultadoImportacionCsv {
|
||||
creados: number;
|
||||
reutilizados: number;
|
||||
movimientos: number;
|
||||
omitidos: number;
|
||||
errores: Array<{ linea: number; mensaje: string }>;
|
||||
}
|
||||
|
||||
export function normalizarNombre(valor: string) {
|
||||
return valor.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/^\uFEFF/, '').trim().replace(/\s+/g, ' ').toLocaleLowerCase();
|
||||
}
|
||||
|
||||
function separar(linea: string) {
|
||||
const separador = (linea.match(/;/g)?.length ?? 0) > (linea.match(/,/g)?.length ?? 0) ? ';' : ',';
|
||||
return linea.split(separador).map((valor) => valor.trim().replace(/^"|"$/g, ''));
|
||||
}
|
||||
|
||||
export class ImportInventarioCsvService {
|
||||
async importar(texto: string, fecha = new Date()): Promise<ResultadoImportacionCsv> {
|
||||
const filas = texto.replace(/^\uFEFF/, '').split(/\r?\n/).filter((fila) => fila.trim());
|
||||
const resultado: ResultadoImportacionCsv = { creados: 0, reutilizados: 0, movimientos: 0, omitidos: 0, errores: [] };
|
||||
if (!filas.length) throw new Error('El CSV está vacío.');
|
||||
const encabezados = separar(filas[0]).map(normalizarNombre);
|
||||
const requeridos = ['nombre de producto', 'nombre productor', 'precio unitario', 'cantidad'];
|
||||
const indices = requeridos.map((nombre) => encabezados.indexOf(normalizarNombre(nombre)));
|
||||
if (indices.slice(0, 3).some((indice) => indice < 0)) throw new Error('El CSV debe incluir: Nombre de producto, Nombre Productor y precio unitario.');
|
||||
|
||||
await db.transaction('rw', db.productores, db.productos, db.movimientosInventario, async () => {
|
||||
const productores = await db.productores.toArray();
|
||||
const productos = await db.productos.toArray();
|
||||
for (let i = 1; i < filas.length; i++) {
|
||||
const linea = i + 1;
|
||||
const columnas = separar(filas[i]);
|
||||
const nombreProducto = columnas[indices[0]]?.trim();
|
||||
const nombreProductor = columnas[indices[1]]?.trim();
|
||||
const precio = Number((columnas[indices[2]] ?? '').replace(',', '.'));
|
||||
const cantidadTexto = indices[3] >= 0 ? columnas[indices[3]]?.trim() : '';
|
||||
const cantidad = cantidadTexto ? Number(cantidadTexto.replace(',', '.')) : 0;
|
||||
if (!nombreProducto || !nombreProductor) { resultado.errores.push({ linea, mensaje: 'Falta nombre de producto o productor.' }); continue; }
|
||||
if (!Number.isFinite(precio) || precio < 0) { resultado.errores.push({ linea, mensaje: 'El precio unitario debe ser numérico y no negativo.' }); continue; }
|
||||
if (!Number.isFinite(cantidad) || cantidad < 0) { resultado.errores.push({ linea, mensaje: 'La cantidad debe ser cero, vacía o positiva.' }); continue; }
|
||||
let productor = productores.find((item) => normalizarNombre(item.nombre) === normalizarNombre(nombreProductor));
|
||||
if (!productor) {
|
||||
productor = { id: crypto.randomUUID(), nombre: nombreProductor, activo: true, createdAt: new Date() } as Productor;
|
||||
productores.push(productor); await db.productores.add(productor); resultado.creados++;
|
||||
} else resultado.reutilizados++;
|
||||
let producto = productos.find((item) => item.productorId === productor!.id && normalizarNombre(item.nombrePresentacion) === normalizarNombre(nombreProducto));
|
||||
if (!producto) {
|
||||
producto = { id: crypto.randomUUID(), productorId: productor.id, nombrePresentacion: nombreProducto, precioUnitario: precio, activo: true, createdAt: new Date() } as Producto;
|
||||
productos.push(producto); await db.productos.add(producto); resultado.creados++;
|
||||
} else { resultado.reutilizados++; }
|
||||
if (cantidad > 0) {
|
||||
await db.movimientosInventario.add({ id: crypto.randomUUID(), fecha, tipo: 'INGRESO_INICIAL', productoId: producto.id, cantidad, createdAt: new Date() } as any);
|
||||
resultado.movimientos++;
|
||||
} else resultado.omitidos++;
|
||||
}
|
||||
});
|
||||
return resultado;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user