Add HistoricTickets to thetool
This commit is contained in:
259
public/plugins/vue/tt-components/tt-table.js
Normal file
259
public/plugins/vue/tt-components/tt-table.js
Normal file
@@ -0,0 +1,259 @@
|
||||
|
||||
//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.
|
||||
*/
|
||||
|
||||
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">«</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">»</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">
|
||||
<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>
|
||||
</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) {
|
||||
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();
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user