1import {dataStorage} from './dataStorage.js?v=1.0.10'; 2import {exportFunctions} from './exportFunctions.js?v=1.0.10'; 3import axios from '../vendor/axios.min.js'; 4 5export default { 6 data() { 7 return { 8 tableId: 'px-multiples-dashboard--table-datatable', 9 tableHeaderOffset: 0, 10 dataStorage 11 } 12 }, 13 // inject: ['EventBus'], 14 mounted() { 15 }, 16 // created() { 17 // this.EventBus.on('applyTableFilters', this.applyTableFilters); 18 // }, 19 // beforeUnmount() { 20 // this.EventBus.off('applyTableFilters', this.applyTableFilters); 21 // }, 22 computed: { 23 // computed getters are cached based on their reactive dependencies 24 hasTableFilters() { 25 const tableFiltersDiffer = !this.dataStorage.compareCleanedObjects( 26 this.dataStorage.activeTableFilters, 27 this.dataStorage.getEmptyTableFilters(), 28 ['tableIsLoading'] 29 ); 30 return this.dataStorage.activeTableFilters && Object.keys(this.dataStorage.activeTableFilters).length > 0 && tableFiltersDiffer; 31 }, 32 hasCompareFilter() { 33 if (this.dataStorage.activeTableFilters && Object.keys(this.dataStorage.activeTableFilters).length > 0) { 34 if (this.dataStorage.activeTableFilters.datum[0] === 'compare' || this.dataStorage.activeTableFilters.datum[0] === 'custom-period-compare') { 35 return true; 36 } 37 } 38 return false; 39 }, 40 getCompareFilterType() { 41 if (this.dataStorage.activeTableFilters && Object.keys(this.dataStorage.activeTableFilters).length > 0) { 42 if (this.dataStorage.activeTableFilters.datum[0] === 'compare' || this.dataStorage.activeTableFilters.datum[0] === 'custom-period-compare') { 43 if (this.dataStorage.activeTableFilters?.compareType === 'total') { 44 return 'Total'; 45 } else { 46 return '%'; 47 } 48 } 49 } 50 return ''; 51 } 52 }, 53 methods: { 54 buildTable(data) { 55 if (!data || !data.length) { 56 console.error('MainTableView: No data provided'); 57 dataStorage.resetTableFilters(); 58 59 toastr.error('Es gab einen Fehler beim Laden der Daten. Filter wurden zurückgesetzt.'); 60 return; 61 } 62 63 const $tableSupportLinks = jQuery('#px-multiples-dashboard--support-links-table'); 64 65 if (jQuery?.fn?.dataTable?.isDataTable('#' + this.tableId)) { 66 // update existing table 67 68 this.dataStorage.tableObject = jQuery('#' + this.tableId).DataTable(); 69 70 this.dataStorage.tableObject.columns(['ebitdamin:name', 'ebitdamax:name']).visible( 71 !dataStorage.activeTableFilters?.kennzahlen?.length || dataStorage.activeTableFilters.kennzahlen.includes('ebitda') 72 ); 73 this.dataStorage.tableObject.columns(['ebitminimum:name', 'ebitmaximum:name']).visible( 74 !dataStorage.activeTableFilters?.kennzahlen?.length || dataStorage.activeTableFilters.kennzahlen.includes('ebit') 75 ); 76 this.dataStorage.tableObject.columns(['umsatzminimum:name', 'umsatzmaximum:name']).visible( 77 !dataStorage.activeTableFilters?.kennzahlen?.length || dataStorage.activeTableFilters.kennzahlen.includes('umsatz') 78 ); 79 80 // clear existing and add new data 81 this.dataStorage.tableObject.clear(); 82 this.dataStorage.tableObject.rows.add(data); 83 84 // redraw the table 85 this.dataStorage.tableObject.draw(); 86 87 } else { 88 // create new table 89 90 let columnConfig = [ 91 { 92 name: 'id', 93 data: 'wdt_ID' 94 }, 95 { 96 name: 'branche', 97 data: 'branche' 98 }, 99 { 100 name: 'sortdate', 101 data: null, 102 render: { 103 // default, for display and export 104 '_': function(data, type, row) { 105 return row.monat + ' ' + row.jahr; 106 }, 107 // for sorting 108 'sort': 'sortdate', 109 } 110 }, 111 { 112 name: 'unternehmensgrsse', 113 data: null, 114 className: 'unternehmensgrsse-cell', 115 render: { 116 // default, for display and export 117 '_': function(data, type, row) { 118 if (!row || !row.unternehmensgrsse) { 119 return '<span class="empty-unternehmensgrsse" data-tooltip-content="Keine Daten verfügbar">-</span>'; 120 } 121 if (row.unternehmensgrsse === 'Micro-Cap') { 122 return `<span class="${row.unternehmensgrsse.toLowerCase()}" data-tooltip-content="Micro-Cap seit September 2022 verfügbar">${row.unternehmensgrsse}</span>`;
123 } 124 return `<span class="${row.unternehmensgrsse.toLowerCase()}">${row.unternehmensgrsse}</span>`; 125 }, 126 // for sorting 127 'sort': 'unternehmensgrssesort', 128 } 129 }, 130 ]; 131 let numberColumnNames = ['ebitminimum', 'ebitmaximum', 'ebitdamin', 'ebitdamax', 'umsatzminimum', 'umsatzmaximum']; 132 133 numberColumnNames.forEach(name => { 134 let baseName = name.replace(/minimum|maximum|min|max/g, ''); 135 136 columnConfig.push({ 137 name: name, 138 data: name, 139 className: 'dt-type-numeric', 140 render: { 141 // default, for display 142 '_': function(data, type, row) { 143 let output = row[name]; 144 if (row.comparisonData?.[name]) { 145 let comparisonData = dataStorage.formatComparisonData(row, name); 146 if (comparisonData) { 147 output = comparisonData; 148 } 149 } 150 output = dataStorage.formatTableNumberData(output); 151 if (output === '-') { 152 output = '<span class="empty-number" data-tooltip-content="Keine Daten verfügbar">-</span>'; 153 } 154 return output; 155 }, 156 // special export, without comparison data 157 exportCsv: function(data, type, row) { 158 return dataStorage.formatTableNumberData(row[name]); 159 }, 160 // special export, without comparison data and with "." 161 exportExcel: function(data, type, row) { 162 return dataStorage.formatTableExcelData(row[name]); 163 }, 164 // for sorting 165 'sort': name, 166 }, 167 visible: !dataStorage.activeTableFilters?.kennzahlen.length || dataStorage.activeTableFilters?.kennzahlen?.includes(baseName) 168 }); 169 }); 170 171 let $tableElement = jQuery('#' + this.tableId); 172 if ($tableElement.length === 1) { 173 174 this.calculateHeaderOffset(); 175 176 const $headerWrap = jQuery('.td-header-template-wrap'); 177 178 if ($headerWrap.length) { 179 // if the header is resized, recalculate the offset 180 const observer = new ResizeObserver(this.calculateHeaderOffset); 181 observer.observe($headerWrap.get(0)); 182 } 183 184 // recalc on scroll, because parts of header get fixed 185 jQuery(window).on('scroll', window['throttle'](this.calculateHeaderOffset.bind(this), 100)); 186 187 this.dataStorage.tableObject = $tableElement.DataTable({ 188 processing: true, 189 data: data, 190 columns: columnConfig, 191 order: [ 192 [2, 'desc'], // first sort by date 193 [1, 'asc'], // then by branche 194 [3, 'asc'] // then by unternehmensgrsse 195 ], 196 fixedHeader: { 197 headerOffset: (this.tableHeaderOffset) 198 }, 199 layout: { 200 topStart: null, 201 topEnd: null, 202 bottomStart: $tableSupportLinks.length ? $tableSupportLinks : null, 203 bottomEnd: ['pageLength', 'info', 'paging'], 204 },
205 stripeClasses: ['odd', 'even'], 206 scrollX: (window.innerWidth < 1200), // allow horizontal scrolling on smaller screens 207 buttons: [ 208 { 209 extend: 'csv', 210 name: 'csvExport', 211 filename: dataStorage.formatDownloadFilename, 212 title: 'FINANCE Multiples', 213 exportOptions: { 214 columns: ':visible', 215 orthogonal: 'exportCsv', 216 format: { 217 header: dataStorage.formatExportHeader 218 } 219 } 220 }, 221 { 222 extend: 'excel', 223 name: 'excelExport', 224 filename: dataStorage.formatDownloadFilename, 225 sheetName: 'Multiples', 226 title: 'FINANCE Multiples', 227 exportOptions: { 228 columns: ':visible', 229 orthogonal: 'exportExcel', 230 format: { 231 header: dataStorage.formatExportHeader 232 } 233 }, 234 autoFilter: true, 235 236 // customize the excel file 237 customize: exportFunctions.customizeExcel, 238 } 239 ], 240 language: { 241 decimal: ',', 242 lengthMenu: "Zeilen pro Seite: _MENU_", 243 paginate: { 244 previous: '<i class="fas fa-chevron-left"></i>', 245 next: '<i class="fas fa-chevron-right"></i>' 246 }, 247 aria: { 248 paginate: { 249 previous: 'Zurück', 250 next: 'Weiter' 251 } 252 }, 253 url: $tableElement.data('languagejson') ?? '' 254 }, 255 lengthMenu: [10, 50, 100, 200], 256 pageLength: 100, 257 pagingType: 'simple_numbers', 258 searching: false, 259 drawCallback: function(settings) { 260 // get table container width and save to dataStorage 261 const container = this.api()?.table()?.container(); 262 if (container) { 263 const tableWidth = container.offsetWidth; 264 if (tableWidth > 0) { 265 dataStorage.tableWidth = tableWidth; 266 } 267 268 // show pagination only if more records than current pageLength 269 const pageLength = this.api().page.len(); 270 const totalRecords = this.api().data().length; 271 if (totalRecords <= pageLength) { 272 jQuery(container).find('.dt-paging').hide(); 273 } else { 274 jQuery(container).find('.dt-paging').show(); 275 276 // add tooltips after 500ms to finish rendering 277 setTimeout(function() { 278 const $nextButtons = jQuery(container).find('.dt-paging-button.next'); 279 if ($nextButtons.length > 0) { 280 tippy( 281 $nextButtons.toArray(), 282 { 283 content: 'Weiter', 284 touch: 'hold', 285 } 286 ); 287 } 288 const $prevButtons = jQuery(container).find('.dt-paging-button.previous'); 289 if ($prevButtons.length > 0) { 290 tippy( 291 $prevButtons.toArray(), 292 { 293 content: 'Zurück', 294 touch: 'hold', 295 } 296 ); 297 } 298 }, 500); 299 } 300 301 setTimeout(function() { 302 const $length = jQuery(container).find('.dt-length'); 303 if ($length.length > 0) { 304 tippy( 305 $length.toArray(), 306 { 307 content: 'Anzahl angezeigter Zeilen', 308 touch: 'hold', 309 } 310 ); 311 } 312 313 // init tooltips in table 314 const $tooltipElements = jQuery(container).find('[data-tooltip-content]'); 315 if ($tooltipElements.length > 0) { 316 tippy( 317 $tooltipElements.toArray(), 318 { 319 allowHTML: true, 320 content: (reference) => reference.getAttribute('data-tooltip-content'), 321 touch: 'hold', 322 } 323 ); 324 } 325 326 }, 500); 327 328 const api = this.api(); 329 const order = api.order(); 330 const rows = api.rows({page: 'current'}).nodes(); 331 332 // get columns to group by from current order 333 let groupCols = []; 334 const orderCols = jQuery.map(order, function(o) { 335 return o[0]; 336 }); 337 // default is ordered by 3 columns: do not group by column-3 in this case 338 if (orderCols.length > 2) { 339 // remove column-3 from grouping if itâs in there 340 groupCols = jQuery.grep(orderCols, function(idx) { 341 return idx !== 3; 342 }); 343 } else { 344 // if only one column is sorted, use it for grouping 345 groupCols = orderCols; 346 } 347 348 // clear old dividers 349 jQuery(rows).removeClass('group-divider'); 350 351 // keep track of last seen values per grouping column 352 let lastVals = {}; 353 354 // walk each rendered row in display order 355 jQuery.each(rows, function(i, row) { 356 let isBreak = false;
357 358 // check each grouping column for a change 359 jQuery.each(groupCols, function(_, colIdx) { 360 const val = api.cell(row, colIdx, {page: 'current'}).render('sort'); 361 if (lastVals[colIdx] !== undefined && val !== lastVals[colIdx]) { 362 isBreak = true; 363 return false; 364 } 365 }); 366 367 // update lastVals 368 jQuery.each(groupCols, function(_, colIdx) { 369 lastVals[colIdx] = api.cell(row, colIdx, {page: 'current'}).render('sort'); 370 }); 371 372 if (isBreak) { 373 jQuery(row).addClass('group-divider'); 374 } 375 }); 376 } 377 }, 378 379 // force odd/even classes for rows 380 rowCallback: function(row, data, displayIndex) { 381 if (displayIndex % 2 === 0) { 382 // even index â âoddânumberedâ row in human terms 383 jQuery(row) 384 .removeClass('even') // clean up in case it was there from a previous draw 385 .addClass('odd'); 386 } else { 387 // odd index â âevenânumberedâ row 388 jQuery(row) 389 .removeClass('odd') 390 .addClass('even'); 391 } 392 } 393 }); 394 } 395 } 396 }, 397 calculateHeaderOffset() { 398 const $headerWrap = jQuery('.td-header-template-wrap'); 399 const $wpadminbar = jQuery('#wpadminbar'); 400 401 this.tableHeaderOffset = 0; 402 if ($headerWrap.length === 1) { 403 404 // find all visible fixed descendants 405 const $fixedChildren = $headerWrap 406 .find('*') 407 .filter((_, el) => 408 jQuery(el).css('display') !== 'none' && 409 jQuery(el).css('position') === 'fixed' 410 ); 411 412 if ($fixedChildren.length) { 413 // sum the heights of the fixed elements 414 $fixedChildren.each((_, el) => { 415 this.tableHeaderOffset += el.getBoundingClientRect().height; 416 }); 417 } else { 418 // no fixed kids â take the wrapper's full rendered height 419 this.tableHeaderOffset += parseInt($headerWrap[0].getBoundingClientRect().height); 420 // this.tableHeaderOffset += parseInt($headerWrap.height()); 421 } 422 423 } 424 if ($wpadminbar.length === 1 && $wpadminbar.css('position') === 'fixed') { 425 this.tableHeaderOffset += parseInt($wpadminbar.height()); 426 } 427 428 if (this.dataStorage.tableObject !== null && this.dataStorage.tableObject.fixedHeader) { 429 this.dataStorage.tableObject.fixedHeader.headerOffset(this.tableHeaderOffset); 430 } 431 } 432 }, 433 watch: { 434 // TODO: move to dataStorage? 435 'dataStorage.activeTableFilters': function(activeTableFilters, oldTableFilters) { 436 // run only on changes 437 if (dataStorage.compareCleanedObjects(activeTableFilters, oldTableFilters, ['tableIsLoading'])) { 438 dataStorage.markTableExportComplete(); 439 440 // hide loading indicator, if active 441 if (dataStorage.activeTableFilters.tableIsLoading) { 442 dataStorage.activeTableFilters.tableIsLoading = false;
443 } 444 return; 445 } 446 447 // check if filter_id is in the URL and is numeric 448 const urlParams = new URLSearchParams(window.location.search); 449 const filterId = urlParams.get('filter_id'); 450 if (filterId && !isNaN(filterId)) { 451 const numericFilterId = parseInt(filterId); 452 if ( 453 // check only NULL, if empty string it means it was manually resetted 454 dataStorage.activeCustomFilter === null || 455 ( 456 dataStorage.activeCustomFilter?.filter_id > 0 && 457 dataStorage.activeCustomFilter?.filter_id !== numericFilterId 458 ) 459 ) { 460 // if filter_id is in URL but customFilter not yet loaded, skip 461 dataStorage.exportConfig.exportRequested = false; 462 return; 463 } 464 } 465 466 // show loading indicator 467 if (!dataStorage.activeTableFilters.tableIsLoading) { 468 dataStorage.activeTableFilters.tableIsLoading = true; 469 } 470 471 axios.post('/wp-json/px-multiples-dashboard/v1/multiples', { 472 filter: activeTableFilters, 473 singleReportConfig: dataStorage.singleReportConfig 474 }).then((response) => { 475 if (response && response.status === 200 && response.data) { 476 this.buildTable(response.data); 477 dataStorage.markTableExportComplete(); 478 } 479 }).catch((error) => { 480 console.error('MainTableView: Multiples API error', error); 481 dataStorage.markTableExportComplete(); 482 }).finally(() => { 483 484 // hide loading indicator 485 if (dataStorage.activeTableFilters.tableIsLoading) { 486 dataStorage.activeTableFilters.tableIsLoading = false; 487 } 488 }); 489 } 490 }, 491}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.