@@ -8,6 +8,7 @@ import Group from '../admin/groups/model/group.model'
88import formatDate from '../../services/date-format/date-format.service'
99import { CachedQueryService } from '../../services/cache-service/cached-query.service'
1010import { ArimaService } from '../../services/prediction/arima.service'
11+ import { TensorflowService } from '../../services/prediction/tensorflow.service'
1112import { TimeFormatService } from '../../services/time-format/time-format.service'
1213import { QueryOptions } from 'mongoose'
1314import ServerLogService from '../../services/server-log/server-log.service'
@@ -483,7 +484,6 @@ export class DashboardController {
483484 // Obtener el dashboard
484485 const dashboard = await Dashboard . findById ( req . params . id ) ;
485486 if ( ! dashboard ) {
486- console . log ( 'Dashboard not found with this id:' + req . params . id ) ;
487487 return next ( new HttpException ( 500 , 'Dashboard not found with this id' ) ) ;
488488 }
489489
@@ -499,7 +499,6 @@ export class DashboardController {
499499 dashboard . user . toString ( ) !== user ;
500500
501501 if ( visibilityCheck && roleCheck ) {
502- console . log ( "You don't have permission " + user + ' for dashboard ' + req . params . id ) ;
503502 return next ( new HttpException ( 500 , "You don't have permission" ) ) ;
504503 }
505504
@@ -1797,8 +1796,10 @@ export class DashboardController {
17971796 // Suma acumulativa
17981797 DashboardController . cumulativeSum ( output , req . body . query ) ;
17991798
1800- // ARIMA (si aplica)
1801- DashboardController . applyArimaIfNeeded ( output , req . body , myQuery ) ;
1799+ // Revisar si hay predicción y el tipo
1800+ if ( req . body . query ?. prediction && req . body . query ?. prediction !== 'None' && req . body . output ?. config ?. chartType === 'line' ) {
1801+ await DashboardController . setupPrediction ( output , req . body , myQuery , connection , dataModelObject , req . user )
1802+ }
18021803
18031804 console . log (
18041805 '\x1b[32m%s\x1b[0m' ,
@@ -1814,55 +1815,180 @@ export class DashboardController {
18141815 }
18151816 }
18161817
1817- // Formula de arima
1818- static applyArimaIfNeeded ( output : any , body : any , myQuery : any ) {
1819- // Si no tenemos arima o no estamos en linea, salimos
1820- if ( body . query ?. prediction !== 'Arima' || body . output . config . chartType !== 'line' ) {
1821- return ;
1822- }
18231818
1824- // Número de fechas a predecir
1825- const steps = 3 ;
1819+ static async setupPrediction ( output : any , body : any , myQuery : any , connection ?: any , dataModelObject ?: any , user ?: any ) {
1820+ // Leer configuración de predicción del body
1821+ const predictionConfig = body . query ?. predictionConfig || { } ;
1822+ const steps = predictionConfig . steps || 3 ;
18261823
1827- // Buscamos campo de la fecha, su formato y su último valor
1824+ // Buscamos campo de la fecha — sin él no podemos generar fechas futuras
18281825 const dateField = myQuery . fields . find ( field => field . column_type === 'date' ) ;
18291826 if ( ! dateField ) {
18301827 return ;
1831- }
1832-
1828+ }
1829+
18331830 const timeFormat = dateField . format ;
18341831 const lastDate = output [ 1 ] [ output [ 1 ] . length - 1 ] [ 0 ] ;
18351832
1836- // A partir de los datos anteriores generamos las proximas fechas
1833+ // Generamos las próximas fechas en el mismo formato que las existentes (mes, semana, día...)
18371834 const nextLabels = TimeFormatService . nextInSequenceGeneric ( timeFormat , lastDate , steps ) ;
18381835
18391836 const rows = output [ 1 ] ;
18401837 const lastIndex = rows . length - 1 ;
1838+ // Dataset numérico: solo la columna de valores (índice 1), descartando nulos/infinitos
18411839 const originalDataset = rows . map ( row => row [ 1 ] ) . filter ( val => Number . isFinite ( val ) ) ;
18421840
18431841 let predictions : number [ ] = [ ] ;
1842+ const setup = { steps : steps , rows : rows , lastIndex : lastIndex , nextLabels : nextLabels }
1843+
1844+ switch ( body . query ?. prediction ) {
1845+ case 'Arima' :
1846+ // arimaParams puede ser undefined (usará configs automáticas) o {p,d,q} manual
1847+ await DashboardController . applyArimaPredicction (
1848+ predictions , originalDataset , setup , predictionConfig . arimaParams ) ;
1849+ break ;
1850+ case 'Tensorflow' :
1851+ // Construir datasets de referencia para predicción multivariante
1852+ // referenceColumns ahora es [{table_name, column_name, display_name}]
1853+ const referenceColumns : { table_name : string , column_name : string , display_name : string } [ ] =
1854+ predictionConfig . tensorflowParams ?. referenceColumns || [ ] ;
1855+ let referenceDatasets : number [ ] [ ] = [ ] ;
1856+
1857+ if ( referenceColumns . length > 0 && connection && dataModelObject ) {
1858+ try {
1859+ // Para cada columna de referencia lanzamos una query auxiliar
1860+ // alineada con el mismo campo de fecha
1861+ referenceDatasets = await DashboardController . fetchReferenceDatasets (
1862+ referenceColumns , dateField , myQuery , connection , dataModelObject , user , body . query ?. queryLimit
1863+ ) ;
1864+ } catch ( err ) {
1865+ console . warn ( '[Prediction] Error fetching reference datasets, falling back to univariate:' , err . message ) ;
1866+ referenceDatasets = [ ] ;
1867+ }
1868+ }
1869+
1870+ // tfParams puede ser undefined o {epochs, lookback, learningRate}
1871+ await DashboardController . applyTensorflowPredicction (
1872+ predictions , originalDataset , setup , predictionConfig . tensorflowParams , referenceDatasets ) ;
1873+ break ;
1874+ }
1875+ }
1876+
1877+ // encontrar los datasets de las columnas de referencia para la predicción multivariante con TensorFlow. Cada dataset es un array de números alineado con el campo de fecha.
1878+ static async fetchReferenceDatasets ( referenceColumns : { table_name : string , column_name : string } [ ] ,
1879+ dateField : any , myQuery : any , connection : any , dataModelObject : any , user : any , queryLimit ?: number ) : Promise < number [ ] [ ] > {
1880+ const referenceDatasets : number [ ] [ ] = [ ] ;
1881+
1882+ for ( const refCol of referenceColumns ) {
1883+ // Encontrar tablas
1884+ const table = dataModelObject . ds ?. model ?. tables ?. find ( t => t . table_name === refCol . table_name ) ;
1885+ if ( ! table ) {
1886+ console . warn ( `[Prediction] Reference table not found: ${ refCol . table_name } ` ) ;
1887+ continue ;
1888+ }
1889+ // Encontrar columnas
1890+ const colDef = table . columns ?. find ( c => c . column_name === refCol . column_name ) ;
1891+ if ( ! colDef ) {
1892+ console . warn ( `[Prediction] Reference column not found: ${ refCol . column_name } in ${ refCol . table_name } ` ) ;
1893+ continue ;
1894+ }
1895+
1896+ // Build a query with the same date field + this reference column
1897+ const refQuery = {
1898+ fields : [
1899+ { ...dateField } , // Same date field as main query for alignment
1900+ {
1901+ column_name : colDef . column_name ,
1902+ column_type : colDef . column_type || 'numeric' ,
1903+ table_id : refCol . table_name ,
1904+ display_name : colDef . display_name ?. default || colDef . column_name ,
1905+ order : 0 ,
1906+ aggregation_type : 'sum' ,
1907+ }
1908+ ] ,
1909+ filters : myQuery . filters || [ ] ,
1910+ queryMode : myQuery . queryMode || 'EDA' ,
1911+ simple : myQuery . simple ,
1912+ joinType : myQuery . joinType || 'inner' ,
1913+ } ;
1914+
1915+ try {
1916+ const refQueryBuilt = await connection . getQueryBuilded ( refQuery , dataModelObject , user , queryLimit ) ;
1917+ connection . client = await connection . getclient ( ) ;
1918+ const refResults = await connection . execQuery ( refQueryBuilt ) ;
1919+
1920+ // Extract numeric values from results (column index 1 = the reference column)
1921+ let dataset : number [ ] ;
1922+ if ( Array . isArray ( refResults ) && refResults . length > 0 ) {
1923+ if ( Array . isArray ( refResults [ 0 ] ) ) {
1924+ // MongoDB format: already arrays
1925+ dataset = refResults . map ( row => row [ 1 ] ) . filter ( val => Number . isFinite ( val ) ) ;
1926+ } else {
1927+ // SQL format: objects
1928+ const keys = Object . keys ( refResults [ 0 ] ) ;
1929+ const valueKey = keys [ 1 ] ; // Second column is the numeric value
1930+ dataset = refResults . map ( row => {
1931+ const val = parseFloat ( row [ valueKey ] ) ;
1932+ return isNaN ( val ) ? null : val ;
1933+ } ) . filter ( val => val !== null && Number . isFinite ( val ) ) ;
1934+ }
1935+ }
18441936
1937+ if ( dataset && dataset . length > 0 ) {
1938+ referenceDatasets . push ( dataset ) ;
1939+ } else {
1940+ console . warn ( `[Prediction] No data for reference column ${ refCol . table_name } .${ refCol . column_name } ` ) ;
1941+ }
1942+ } catch ( err ) {
1943+ console . warn ( `[Prediction] Failed to fetch reference column ${ refCol . table_name } .${ refCol . column_name } :` , err . message ) ;
1944+ }
1945+ }
1946+
1947+ return referenceDatasets ;
1948+ }
1949+
1950+ // Formula de arima
1951+ static applyArimaPredicction ( predictions : number [ ] , originalDataset : any , setup : { steps , rows , lastIndex , nextLabels } , arimaParams ?: { p : number , d : number , q : number } ) {
18451952 try {
1846- // Calculamos las predicciones a través del servicio ARIMA
1847- predictions = ArimaService . forecast ( originalDataset , steps ) ;
1953+ predictions = ArimaService . forecast ( originalDataset , setup . steps , arimaParams ) ;
18481954 } catch ( err ) {
18491955 console . error ( 'Error ARIMA:' , err ) ;
18501956 return ;
18511957 }
18521958
1853- // Añadir columna predictionet
1854- rows . forEach ( ( row , index ) => {
1855- row . push ( index === lastIndex ? row [ 1 ] : null ) ;
1959+ // Añadir columna predicción a las filas existentes
1960+ // Solo el último punto real tiene valor para "unir" visualmente ambas líneas
1961+ setup . rows . forEach ( ( row , index ) => {
1962+ row . push ( index === setup . lastIndex ? row [ 1 ] : null ) ;
18561963 } ) ;
18571964
1858- // Añadir fila de fechas
1859- nextLabels . forEach ( ( label , index ) => {
1860- rows . push ( [ label , null , predictions [ index ] ?? null ] ) ;
1965+ // Añadir filas de fechas futuras con los valores predichos
1966+ setup . nextLabels . forEach ( ( label , index ) => {
1967+ setup . rows . push ( [ label , null , predictions [ index ] ?? null ] ) ;
18611968 } ) ;
18621969
1863- console . log ( 'Predicción ARIMA aplicada' ) ;
18641970 }
18651971
1972+ // Predicción con TensorFlow
1973+ static async applyTensorflowPredicction ( predictions : number [ ] , originalDataset : any , setup : { steps , rows , lastIndex , nextLabels } , tfParams ?: any , referenceDatasets ?: number [ ] [ ] ) {
1974+ try {
1975+ predictions = await TensorflowService . forecast ( originalDataset , setup . steps , tfParams , referenceDatasets ) ;
1976+ } catch ( err ) {
1977+ console . error ( 'Error TensorFlow:' , err ) ;
1978+ return ;
1979+ }
1980+
1981+ // Añadir columna predicción a las filas existentes
1982+ setup . rows . forEach ( ( row , index ) => {
1983+ row . push ( index === setup . lastIndex ? row [ 1 ] : null ) ;
1984+ } ) ;
1985+
1986+ // Añadir filas de fechas futuras con los valores predichos
1987+ setup . nextLabels . forEach ( ( label , index ) => {
1988+ setup . rows . push ( [ label , null , predictions [ index ] ?? null ] ) ;
1989+ } ) ;
1990+
1991+ }
18661992
18671993 /**
18681994 * Executa una consulta SQL per un dashboard
0 commit comments