Files
thetool/public/plugins/vue/tt-components/tt-table.js
Luca Haid f39978e7a9 Added Voice Functionality
[KolmisoftMore] implemented getActiveCalls function
[menu.php] added menu point for active voice calls
[config.sample.php] added KOLMISOFT configuration constants
[VoiceCallActive] implemented active voice calls view
[VoiceCallHistoryController] fixed importCallsFromToday Time
[tt-table] fixed pagination displays
2024-04-11 10:00:27 +02:00

338 lines
14 KiB
JavaScript

//TODO: tt-autocomplete , tt-select aswell as tt-input should be used for filtering
//TODO: Add sorting functionality
//TODO: Add export to excel and pdf functionality
//TODO: Add Date-Range filter
//TODO: Add Exact Date filter
//TODO: Add new prop serverSide to disable pagination and filtering on the client side
//TODO: Add filtering function if serverSide is disabled
//TODO: Add JSDoc for various functions and props
/**
* @typedef {Object} ttTableColumnConfig
* @property {string} text - The display text of the column.
* @property {string} key - The unique key of the column.
* @property {string} filter - Indicates if filtering is enabled for the column.
* @property {boolean} sortEnabled - Indicates if sorting is enabled for the column.
* @property {string} class - The CSS class(es) applied to the column.
*/
//TODO: export this to its own file
Vue.component('tt-date-range', {
template: `
<input type="text" class="form-control form-control-sm" ref="input" @click="initialize" style="cursor: pointer;background-color: #ffffff">
`, props: ['value'],
data() {
return {
inputValue: '',
isInitialized: false,
locale: {
"format": "DD.MM.YYYY HH:mm",
"separator": " - ",
"applyLabel": "Übernehmen",
"cancelLabel": "Abbrechen",
"fromLabel": "Von",
"toLabel": "Bis",
"customRangeLabel": "Benutzerdefiniert",
"weekLabel": "W",
"daysOfWeek": [
"So",
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa"
],
"monthNames": [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
],
"firstDay": 1
},
}
},
methods: {
initialize() {
if (!this.isInitialized) {
this.isInitialized = true;
$(this.$refs.input).daterangepicker({
autoUpdateInput: true,
timePicker: true,
timePicker24Hour: true,
locale: this.locale,
});
this.$refs.input.click();
const _this = this;
$(this.$refs.input).on('apply.daterangepicker', function(ev, picker) {
console.log('now emitting chang', picker.startDate.unix(), picker.endDate.unix());
_this.$emit('change', {
target: {
value: {
from: picker.startDate.unix() + 7200,
to: picker.endDate.unix() + 7200
}
}
});
});
}
}
},
beforeDestroy() {
$(this.$refs.input).off('apply.daterangepicker');
},
})
Vue.component('tt-table', {
template: `
<div class="card tt-table-card">
<div class="card-body">
<!-- Top Buttons -->
<div
style="display:grid; grid-template-columns: auto auto auto auto auto; grid-gap: 8px; padding-bottom: 8px">
<button v-if="excelExport" class="btn btn-success" @click="exportToExcel">
<i class="fa fa-file
"></i>
Excel
</button>
<button v-if="pdfExport" class="btn btn-danger" @click="exportToPdf">
<i class="fa fa-file
"></i>
PDF
</button>
<slot name="top-buttons"></slot>
</div>
<!-- Pagination Controls -->
<nav aria-label="Page navigation">
<div
style="display:grid; grid-template-columns: 1fr 1fr;padding-bottom: 8px;align-items:center; justify-content: space-between">
<h4 style="margin: 0">{{ tableConfig.tableHeader }}</h4>
<div v-if="pagination && pagination.total_rows > 0"
style="display:grid; grid-template-rows: auto auto; grid-template-columns: auto auto; grid-auto-flow: column; grid-gap: 4px; justify-content: end">
<ul class="pagination" style="margin: 0">
<li class="page-item" v-bind:class="{ disabled: pagination.page === 1 }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(1)" aria-label="First">
<span aria-hidden="true">&laquo;</span>
<span class="sr-only">First</span>
</a>
</li>
<li class="page-item" v-for="pageNumber in pagesToDisplay"
v-bind:class="{ 'active disabled': pageNumber === pagination.page }">
<a class="page-link disabled" href="#"
v-on:click.prevent="fetchRows(pageNumber)">{{ pageNumber }}</a>
</li>
<li class="page-item" v-bind:class="{ disabled: pagination.page === pagination.total_pages }">
<a class="page-link" href="#" v-on:click.prevent="fetchRows(pagination.total_pages)"
aria-label="Last">
<span aria-hidden="true">&raquo;</span>
<span class="sr-only">Last</span>
</a>
</li>
</ul>
<span class="text-center"
v-text="Math.min(pagination.page * pagination.per_page - pagination.per_page + 1, pagination.total_rows)
+ ' bis ' + Math.min(pagination.page * pagination.per_page, pagination.total_rows) + ' von ' + pagination.total_rows"></span>
<select v-model="pagination.per_page" v-on:change="fetchRows(1)" class="form-control form-control-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
<span class="text-center">Einträge pro Seite</span>
</div>
</div>
</nav>
<!-- Table -->
<table
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
<thead>
<tr>
<th scope="col" v-for="column in columns" :style="'vertical-align: top; text-align: center;' + (column.filter === 'dateRange' ? 'min-width: 260px;' : '')">
<div style="text-align:center">{{ column.text }}</div>
<input v-if="column.filter === 'search'" type=text v-on:input="applyFilter($event, column.key)"
class="form-control form-control-sm">
<select v-if="column.filter === 'select'" v-on:change="applyFilter($event, column.key)"
class="form-control form-control-sm">
<option value="all">Alle</option>
<option v-for="filterOption in column.filterOptions" :value="filterOption.value">
{{ filterOption.text }}
</option>
</select>
<tt-date-range v-if="column.filter === 'dateRange'" v-on:change="applyFilter($event, column.key)"></tt-date-range>
</th>
</tr>
</thead>
<tbody>
<tr v-if="pagination?.total_rows === 0" style="height: 150px">
<td :colspan="Object.keys(columns).length" :rowspan="5" class="text-center">Keine Ergebnisse!</td>
</tr>
<tr v-else-if="pagination === null || rows === null" style="height: 150px">
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
</tr>
<tr v-for="row in rows">
<!--suppress JSUnusedLocalSymbols -->
<template v-for="(value, key) in columns">
<td :class="columns[key].class">
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
<span
v-html="row[key] === null || typeof row[key] === 'undefined' ? null : row[key]?.toString()?.replace('\\n', '<br>')"></span>
</slot>
</td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
`, props: {
fetchUrl: String, striped: {
type: Boolean, default: true
}, bordered: {
type: Boolean, default: true
}, hover: {
type: Boolean, default: true
}, small: Boolean, excelExport: Boolean, pdfExport: Boolean, tableConfig: {
type: Object, default: () => ({})
}
}, data() {
return {
loading: false, rows: null, pagination: null, filters: {}, debounceTimeout: null, latestFetchTimestamp: null
};
},
methods: {
/**
* Creates a debounced function that delays invoking `fn` until after `wait` milliseconds
* have elapsed since the last time the debounced function was invoked.
*
* @param {Function} fn The function to debounce.
* @param {number} wait The number of milliseconds to delay.
* @return {Function} The debounced function.
*/
debounce(fn, wait) {
return function (...args) {
const context = this;
if (this.debounceTimeout) clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => fn.apply(context, args), wait);
}
}, /**
* Fetches and updates data for a specified page.
*
* @param {number} page The page number to fetch data for.
* @async
*/
async fetchData(page= 0) {
try {
const fetchTimestamp = Date.now();
this.latestFetchTimestamp = fetchTimestamp;
const response = await axios.post(this.fetchUrl, {
pagination: {
page: Math.max(page, 1), per_page: this.pagination?.per_page ? this.pagination.per_page : 10,
}, filters: this.filters,
});
if (fetchTimestamp !== this.latestFetchTimestamp) return;
if (typeof response.data !== 'object') { // if the response is not an object
this.rows = [];
this.pagination = {page: 1, per_page: 10, total_rows: 0, total_pages: 1};
} else {
this.rows = response.data.rows;
this.pagination = response.data.pagination;
}
this.loading = false;
} catch (error) {
console.error('Error fetching data:', error);
}
}, /**
* Fetches rows for a given page, with an option to debounce the fetch operation.
*
* @param {number} page The page number to fetch. Defaults to 1.
* @param {boolean} debounce Whether to debounce the fetch operation. Defaults to false.
*/
async fetchRows(page = 1, debounce = false) {
this.loading = true
if (debounce) {
this.debounce(this.fetchData.bind(this), 300)(page);
} else {
await this.fetchData(page); // Directly call fetchData without debounce
}
},
applyFilter(event, key) {
this.$set(this.filters, key, event.target.value); // Ensure reactivity
}
}, watch: {
filters: {
handler: function () {
this.fetchRows(this.pagination.page, true).then();
}, deep: true
}
}, computed: {
/**
* Returns an object containing the columns' configuration.
* @return {ttTableColumnConfig} The columns configuration.
*/
columns() {
return this.tableConfig.headers.reduce((columns, column) => {
if (!column.key) {
console.warn('WARN: tt-table: Column text or key is not defined:', column);
return columns; // Continue to the next iteration without modifying the accumulator
}
columns[column.key] = {
text: column.text,
key: column.key,
filter: column.filter !== undefined ? column.filter : 'search',
filterOptions: column.filterOptions || undefined,
sortEnabled: column.sortEnabled !== undefined ? column.sortEnabled : true,
class: column.class !== undefined ? column.class : ''
};
return columns;
}, {});
}, pagesToDisplay() {
let range = 2; // Number of pages before and after the current page
let start = (this.pagination.page < 4 ? 1 : this.pagination.page - range) ;
let end = (this.pagination.page + range > this.pagination.total_pages ? this.pagination.total_pages : this.pagination.page + range);
if (end < 5) end = 5;
// Adjust start and end if they are out of bounds
end = end > this.pagination.total_pages ? this.pagination.total_pages : end;
// Adjust the start and end if we are at the end of the page range
if (this.pagination.page > this.pagination.total_pages - 2) {
start = this.pagination.total_pages - 4 < 1 ? 1 : this.pagination.total_pages - 4;
}
// Create an array of page numbers to display
let pagesArray = [];
for (let i = start; i <= end; i++) {
pagesArray.push(i);
}
return pagesArray;
}
}, mounted() {
if(this.tableConfig.defaultPageSize) {
this.pagination = {page: 1, per_page: this.tableConfig.defaultPageSize, total_rows: null, total_pages: 1};
}
this.fetchRows().then();
},
})