[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
338 lines
14 KiB
JavaScript
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">«</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;' + (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();
|
|
},
|
|
}) |