controllers/reporte-controller.js

/**
 * ResponseReport
 * @typedef {Object} ResponseReport
 * @property {Array<string>} keys  listado de los nombres usados en el modelo estaditico
 * @property {Array<number>} values listado de valores usados en el modelo estaditico
 * @property {Array<Object>} results listado de la consulta para generar la tabla
 * @property {string} typeChart tipo de diagrama usado para generar los reporte
 * @property {string} name_consult nombre de la consulta
 * @property {Uint8Array} [buffer] datos en bruto de la imagen, este campo se usa al dibujar en el PDF
 * @property {string} [chart] imagen base64 del diagrama
 */

const { Notification, dialog, BrowserWindow, ipcMain } = require('electron');
const CRUD = require('../database/CRUD');
const { Database } = require('../database/database');
const FILE = require('../util-functions/file');
const TIME = require('../util-functions/time');
const { ENV } = require('../env');
const { PdfController } = require('./pdf-controller');

/** clase que gestiona los reportes */
class ReporteController {

	/** @type {?Database} */
	databaseInstance = null;

	/** Propiedad get database retorna una nueva instancia de la clase Database */
	static get database() {
		return this.databaseInstance || ( this.databaseInstance = new Database() );
	}

	/**
	 * Busca la totalidad de notas por categoria
	 *
	 * @param  {?Object} periodo periodo de búsqueda
	 * @param {string} periodo.from fecha de incio de búsqueda
	 * @param {string} periodo.to fecha de final de búsqueda
	 * @returns {Promise<(ResponseReport | null)>}
	 * Retorna una promesa con un objeto que contiene los valores preparados para el modelo y la tabla.
	 * Si no hay registros devuelve null.
	 */
	static buscarNotasCategoria( periodo = null ) {

		return new Promise(( resolve, reject ) => {

			const sql = periodo ? CRUD.ObtenerTotalNotasPorCategoriaPeriodo : CRUD.ObtenerTotalNotasPorCategoriaGeneral;

			this.database.consult( sql, periodo, ( error, resultados ) => {

				if ( error ) {

					const notificacion = new Notification({
						title: 'Error !!',
						body: 'Error en la consulta'
					});

					notificacion.show();

					console.log( error );

					reject( error );

					return;
				}

				// console.log( resultados );

				if ( resultados.length > 0 ) {

					let labels = [];
					let data = [];

					for ( let obj of resultados ) {
						labels.push( obj.status );
						data.push( obj.total );
					}

					resolve({ 
						typeChart: 'pie', 
						keys: labels, 
						values: data, 
						results: resultados,
						name_consult: 'buscarNotasCategoria'
					});

					return;
				}

				// En caso de que la consulta venga vacia
				resolve( null );
			});
		});
	}

	/**
	 * Busca la totalidad de notas vendidas por vendedor
	 *
	 * @param  {?Object} periodo periodo de búsqueda
	 * @param {string} periodo.from fecha de incio de búsqueda
	 * @param {string} periodo.to fecha de final de búsqueda
	 * @returns {Promise<(ResponseReport | null)>}
	 * Retorna una promesa con un objeto que contiene los valores preparados para el modelo y la tabla.
	 * Si no hay registros devuelve null.
	*/
	static buscarNotasVendidasPorVendedor( periodo = null ) {

		return new Promise(( resolve, reject ) => {

			const sql =  periodo ? CRUD.ObtenerNotasPorVendedorPeriodo : CRUD.ObtenerNotasPorVendedorGeneral;

			this.database.consult( sql, periodo, ( error, resultados ) => {

				if ( error ) {

					const notificacion = new Notification({
						title: 'Error',
						body: 'Error de la consulta !!'
					});

					notificacion.show();

					console.log( error );

					reject( error );

					return;
				}

				// console.log( resultados );

				if ( resultados.length > 0 ) {

					let values = [];
					let data = [];

					for ( let obj of resultados ) {
						values.push( obj.cantidad_notas );
						data.push( obj.nombre_vendedor );
					}

					resolve({ 
						typeChart: 'bar', 
						keys: data, 
						values, 
						results: resultados,
						name_consult: 'buscarNotasVendidasPorVendedor'
					});

					return;
				}

				resolve( null );
			});
		});
	}


	/**
	 * Busca la totalidad de productos por categoria
	 *
	 * @returns {Promise<(ResponseReport | null)>}
	 * Retorna una promesa con un objeto que contiene los valores preparados para el modelo y la tabla.
	 * Si no hay registros devuelve null.
	 */
	static buscarTotalProductosPorCategoria() {

		return new Promise(( resolve, reject ) => {

			this.database.consult( CRUD.ObtenerTotalProductosPorCategoria, null, ( error, resultados ) => {

				if ( error ) {

					const notificacion = new Notification({
						title: 'Error',
						body: 'Error de la consulta !!'
					});

					notificacion.show();

					console.log( error );

					reject( error );

					return;
				}

				if ( resultados.length > 0 ) {

					let values = [];
					let data = [];

					for ( let obj of resultados ) {
						data.push( obj.categoria );
						values.push( obj.cantidad_productos );
					}

					resolve({ 
						typeChart: 'pie', 
						keys: data, 
						values,
						results: resultados,
						name_consult: 'buscarTotalProductosPorCategoria'
					});

					return;
				}

				resolve( null );
			});
		});
	}

	/**
	 * Busca la totalidad maxima vendida de cada producto
	 *
	 * @param  {?Object} periodo periodo de búsqueda
	 * @param {string} periodo.from fecha de incio de búsqueda
	 * @param {string} periodo.to fecha de final de búsqueda
	 * @returns {Promise<(ResponseReport | null)>}
	 * Retorna una promesa con un objeto que contiene los valores preparados para el modelo y la tabla.
	 * Si no hay registros devuelve null.
	*/
	static buscarCantidadMaximaVendida( periodo = null ) {
			
		return new Promise(( resolve, reject ) => {
			
			const sql = periodo ? CRUD.ObtenerCantidadMaximaVendidaPeriodo : CRUD.ObtenerCantidadMaximaVendidaGeneral;

			this.database.consult( sql, periodo, ( error, resultados ) => {
				
				if ( error ) {

					const notificacion = new Notification({
						title: 'Error',
						body: 'Error de la consulta !!'
					});

					notificacion.show();

					console.log( error );

					reject( error );

					return;
				}

				if ( resultados.length > 0 ) {

					let values = [];
					let data = [];

					for ( let obj of resultados ) {
						data.push( obj.nombre );
						values.push( obj.cantidad_max_vendida );
					}

					resolve({ 
						typeChart: 'bar', 
						keys: data, 
						values, 
						results: resultados, 
						name_consult: 'buscarCantidadMaximaVendida'
					});

					return;
				}

				resolve( null );
			});
		});
	}

	/**
	 * Busca la totalidad de productos vendidos en el anio actual
	 *
	 * @returns {Promise<(ResponseReport | null)>}
	 * Retorna una promesa con un objeto que contiene los valores preparados para el modelo y la tabla.
	 * Si no hay registros devuelve null.
	 */
	static buscarCantidadProductosVendidosAnual() {
		return new Promise(( resolve, reject ) => {
			
			this.database.consult( CRUD.ObtenerCantidadVendidoAnual, null, ( error, queries ) => {

				if ( error ) {
					
					const notificacion = new Notification({
						title: 'Error',
						body: 'Error en el consulta en la BD!!'
					});

					notificacion.show();

					console.log( error );

					reject( error );

					return;
				}

				const resultados = ReporteController.ordenarMeses( queries[1] );

				if ( resultados.length > 0 ) {
					
					let values = [];
					let data = [];

					for ( let obj of resultados ) {
						data.push( obj.mes );
						values.push( obj.total );
					}

					resolve({ 
						typeChart: 'line', 
						keys: data, 
						values, 
						results: resultados, 
						name_consult: 'buscarCantidadProductosVendidosAnual'
					});

					return;
				}

				resolve( null );
			});
		});
	}

	/**
	 * Permite ordenar los meses del año, según el calendario
	 *
	 * @param  {Array<Object>} resultados array de los resultaods de la base de datos
	 * @returns {Array<{ id: number, mes: string, total: number }>} devuelve el listado de los meses y el total ordenados
	 */
	static ordenarMeses( resultados ) {

		const arrayMeses = [
			{ id: 1, name: 'enero' },
			{ id: 2, name: 'febrero' },
			{ id: 3, name: 'marzo' },
			{ id: 4, name: 'abril' },
			{ id: 5, name: 'mayo' },
			{ id: 6, name: 'junio' },
			{ id: 7, name: 'julio' },
			{ id: 8, name: 'agosto' },
			{ id: 9, name: 'septiembre' },
			{ id: 10, name: 'octubre' },
			{ id: 11, name: 'noviembre' },
			{ id: 12, name: 'diciembre' }
		];

		// transforma a instancias de arrayMeses
		let order = resultados.map(( mounth ) => {

			let find = arrayMeses.find(( mon ) => mon.name === mounth.mes );

			return {
				id:  find.id,
				mes: find.name,
				total: mounth.total
			};
		});

		// console.log( order );

		// ordena con los valores
		order = order.sort(( value, nextValue ) => {

			if ( value.id > nextValue.id ) {
				return 1;

			} else if ( value.id < nextValue.id ) {
				return -1;

			} else {
				return 0;
			}
		});

		// console.log( order );
		return order;
	}

	/**
	 * Funcion que genera el reporte estadistico
	 * @param {Array<ResponseReport>} consults consultas generadas
	 * @returns {Promise<void>} retorna una promesa que se resolvera cuando finalice el reporte
	 */
	static generarReporte( consults ) {

		return new Promise(( resolve, reject ) => {

			const pdfController = new PdfController(); // creas un objeto PDF controller
			
			// genera la fecha del reporte
			let date = TIME.dateSpanish();

			const options = {
				title : 'Guardar Archivo',
				buttonLabel : 'Guardar' ,
				filters : [{ name: 'pdf', extensions: ['pdf'] }],
				defaultPath: FILE.getHomePath('reporte_' + date )
			};

			const notificacion = new Notification({
				title: 'Éxito',
				body: 'Documento generado con éxito'
			});

			dialog.showSaveDialog( null, options )
				.then( async ( response ) => {
					
					/*	Documentación
					*  response == { canceled: boolean,  filePath: string }
					*  extensions = 'extensiones permitidas'
					*/

					const extensions = ['.pdf'];

					if ( response['canceled'] ) {

						reject({ error: 'cancelled' });

						return;
					}

					// verifica la extension del archivo que sea pdf
					if ( !response['filePath'].includes( extensions[0] ) ) {

						notificacion['title'] = 'Error !!';
						notificacion['body'] = 'Extension del archivo no valida';

						notificacion.show();

						// console.log( response['filePath'] );

						reject({ error: 'extension not valid' });

						return;
					}
					
					consults = await ReporteController.getImageBuffer( consults );
					const data = await pdfController.crearReporte( consults );

					if ( FILE.checkAsset( response['filePath'], false ) ){
						FILE.deleteFileSync( response['filePath'] );
					}

					FILE.appendFile( response['filePath'], data, ( error ) => {

						if ( error ) {

							console.log( error );

							notificacion['title'] = 'Error!!';
							notificacion['body'] = 'Error al guardar archivo';

							notificacion.show();

							// throw error;

							reject({ error });

							return;
						}

						notificacion.show();

						resolve();
					});
				})
				.catch(( error ) => {
					console.log( error );

					reject( error );
				});
		});
	}

	/**
	 * Obtiene los datos de las imagenes antes de ser generados en el PDF
	 * @param {Array<ResponseReport>} consults  listado de consultas estadisticas
	 * @returns {Promise<Array<ResponseReport>>} Devuelve el mismo listado pero con los datos en buffer de las imagenes
	 * cargados para el diseno PDF
	 */
	static getImageBuffer( consults ) {

		return new Promise(( resolve ) => {

			/**
			 * Para obtener los datos en buffer se necesita que todas las estadisticas
			 * esten conectadas al DOM para ello se crea una ventana modal nueva para poder generar los elementos 
			 * y registrar las imagenes en el buffer.
			 */
	
			let window = new BrowserWindow({
				width: 400,
				height: 300,
				show: false,
				webPreferences: {
					nodeIntegration: true
				}
			});
	
			window.loadFile( FILE.formatUrl( ENV.PATH_VIEWS, 'reports/reports-pdf/reports-pdf.html' ) );
	
			const contents = window.webContents;
			
			// entrada al dom oculto
			contents.once('did-finish-load', () => {
				contents.send( 'consults', consults );
			});
	
			// recibe los datos en el buffer
			// crea el listener lo escucha una vez y se desmonta
			ipcMain.once('receiveBuffer', ( $event, consultsWindow ) => {
				
				// se valida la instancia de window
				if ( window ) {

					window.close();

					// se limpia la variable de la ventana una vez cerrada para evitar sobrecarga de memoria
					window = null;
				}

				// console.log( consultsWindow );

				resolve( consultsWindow );
			});	
		});
	}
}



module.exports = {
	ReporteController
};