Reworked Pop Table View
This commit is contained in:
@@ -1,278 +0,0 @@
|
||||
<?php
|
||||
$pagination_baseurl = $this->getUrl($Mod, "Index");
|
||||
$pagination_baseurl_params = ["filter" => $filter];
|
||||
$pagination_entity_name = "Pops";
|
||||
?>
|
||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
|
||||
<link href="<?= self::getResourcePath() ?>assets/css/datatables-std.css?<?= date('U') ?>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
<!-- start page title -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="page-title-box">
|
||||
<div class="page-title-right">
|
||||
<ol class="breadcrumb m-0">
|
||||
<li class="breadcrumb-item"><a href="<?= self::getUrl("Dashboard") ?>"><?= MFAPPNAME_SLUG ?></a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active">Pops</li>
|
||||
</ol>
|
||||
</div>
|
||||
<h4 class="page-title">Pops</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end page title -->
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body mb-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="float-left">
|
||||
<h4 class="header-title">Liste aller Pops</h4>
|
||||
</div>
|
||||
<div class="float-right">
|
||||
<a class="btn btn-primary mb-2" href="<?= self::getUrl("Pop", "add", ['returnto' => "pop"]) ?>"><i
|
||||
class="fas fa-plus"></i><span
|
||||
class="d-none d-lg-inline"> Neuen Pop anlegen</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table id="datatable" class="table table-striped table-hover table-sm font-13" style="width: 100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Netzgebiet</th>
|
||||
<th>Zutritt</th>
|
||||
<th>Vlan Public/Nat/IPv6</th>
|
||||
<th>Koordinaten</th>
|
||||
<th class="edit-width"></th>
|
||||
|
||||
</tr>
|
||||
<tr id="filterrow">
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pops as $pop):
|
||||
$vlans = "";
|
||||
|
||||
if (!empty(trim($pop->vlan_public)))
|
||||
$vlans .= ' <span class="order-date-pill active mb-0">Public: <span class="font-weight-500">' . $pop->vlan_public . '</span class="font-weight-500"></span>';
|
||||
if (!empty(trim($pop->vlan_nat)))
|
||||
$vlans .= ' <span class="order-date-pill active mb-0">Nat: <span class="font-weight-500">' . $pop->vlan_nat . '</span></span>';
|
||||
if (!empty(trim($pop->vlan_ipv6)))
|
||||
$vlans .= ' <span class="order-date-pill active mb-0">IPv6: <span class="font-weight-500">' . $pop->vlan_ipv6 . '</span></span>';
|
||||
?>
|
||||
|
||||
<tr>
|
||||
<td class="text-nowrap"><a
|
||||
href="<?= self::getUrl("Pop", "Detail", ["id" => $pop->id]) ?>"><?= $pop->name ?></a>
|
||||
</td>
|
||||
<td><?= $pop->networks ?></td>
|
||||
<td><?= $pop->location ?></td>
|
||||
<td class="text-center"><?= trim($vlans) ?></td>
|
||||
<td class="text-center"><a
|
||||
title="Google-Maps: <?= rtrim($pop->gps_lat, '0') ?> , <?= $pop->gps_long ?>"
|
||||
class="mapsLink"
|
||||
href="http://maps.google.com/?q=<?= $pop->gps_lat ?> , <?= $pop->gps_long ?>"
|
||||
target="_blank"><?= rtrim($pop->gps_lat, '0') ?>
|
||||
, <?= rtrim($pop->gps_long, 0) ?></a></td>
|
||||
|
||||
<td style="text-align: left; letter-spacing: 4px; font-size: 1.1em;">
|
||||
<a href="<?= self::getUrl("Pop", "edit", ["id" => $pop->id, 'returnto' => "pop"]) ?>"><i
|
||||
class="far fa-edit" title="Bearbeiten"></i></a>
|
||||
<a href="<?= self::getUrl("Pop", "index") ?>" class="text-danger" title="Löschen"><i
|
||||
class="fas fa-trash"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- --><?php //include(realpath(dirname(__FILE__) . "/../") . "/tpl/pagination-summary.php"); ?>
|
||||
<!-- --><?php //include(realpath(dirname(__FILE__) . "/../") . "/tpl/pagination.php"); ?>
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function toggleBuilding(id) {
|
||||
$('#building-detail-' + id).toggle();
|
||||
if ($('#building-detail-' + id).is(":hidden")) {
|
||||
$('#building-' + id).removeClass("table-info");
|
||||
$('#building-' + id).removeClass("text-info");
|
||||
} else {
|
||||
$('#building-' + id).addClass("text-info");
|
||||
$('#building-' + id).addClass("table-info");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTerminationControl(id, type) {
|
||||
$("#term-" + type + "-" + id + "-text").toggle();
|
||||
$("#term-" + type + "-" + id + "-input").toggle();
|
||||
$("#term-" + type + "-" + id + "-edit").toggle();
|
||||
}
|
||||
|
||||
function saveTerminationControl(id, type) {
|
||||
if (!Number.isInteger(id) || id < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var value = $("#term-" + type + "-" + id + "-input input[type=text]").val();
|
||||
|
||||
$.post("<?=self::getUrl("Termination", "Api")?>",
|
||||
{
|
||||
'do': "setValue",
|
||||
id: id,
|
||||
type: type,
|
||||
value: value
|
||||
},
|
||||
function (success) {
|
||||
if (success.status == "OK") {
|
||||
$("#term-" + type + "-" + id + "-text").text(value);
|
||||
} else {
|
||||
console.log("error saving (" + type + ", '" + value + "')");
|
||||
}
|
||||
|
||||
toggleTerminationControl(id, type);
|
||||
},
|
||||
'json');
|
||||
}
|
||||
|
||||
/*
|
||||
* Globals for map display
|
||||
*/
|
||||
|
||||
var buildingMap;
|
||||
var buildings = [];
|
||||
var markers = [];
|
||||
var markerState = true;
|
||||
var mapCenterPos = [<?=TT_PLACEHOLDER_GPS_LAT?>, <?=TT_PLACEHOLDER_GPS_LONG?>];
|
||||
|
||||
function refreshMap() {
|
||||
// get buildings and render map
|
||||
$('#map-link').hide();
|
||||
$('#map-row').show();
|
||||
getMapdata();
|
||||
}
|
||||
|
||||
function renderMap() {
|
||||
if (buildingMap) {
|
||||
markers.forEach(function (m) {
|
||||
buildingMap.removeLayer(m);
|
||||
});
|
||||
} else {
|
||||
buildingMap = L.map('building-map').setView([<?=TT_PLACEHOLDER_GPS_LAT?>, <?=TT_PLACEHOLDER_GPS_LONG?>], 12);
|
||||
}
|
||||
|
||||
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
|
||||
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
|
||||
minZoom: 4,
|
||||
maxZoom: 22,
|
||||
id: 'mapbox/streets-v11',
|
||||
accessToken: '<?=TT_MAPBOX_TILE_API_TOKEN?>'
|
||||
}).addTo(buildingMap);
|
||||
|
||||
addMarkers();
|
||||
}
|
||||
|
||||
function addMarkers() {
|
||||
if (!Array.isArray(buildings) | !buildings.length) {
|
||||
return false;
|
||||
}
|
||||
// draw markers and calculate center position
|
||||
var all_coords = [];
|
||||
buildings.forEach(function (building) {
|
||||
if (!building.gps_lat || !building.gps_long) {
|
||||
return;
|
||||
}
|
||||
var gps = [building.gps_lat, building.gps_long];
|
||||
all_coords.push(gps);
|
||||
var marker = L.marker(gps).addTo(buildingMap);
|
||||
markers[building.id] = marker;
|
||||
});
|
||||
|
||||
// calculate center position
|
||||
mapCenterPos = GetCenterFromDegrees(all_coords);
|
||||
buildingMap.setView(mapCenterPos, 12);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function centerMap() {
|
||||
buildingMap.setView(mapCenterPos, 12);
|
||||
}
|
||||
|
||||
// gets buildings and calls renderMap()
|
||||
function getMapdata() {
|
||||
filter = getFilter();
|
||||
|
||||
$.post('<?=self::getUrl("Building", "Api")?>', {
|
||||
'do': "getFilteredBuildings",
|
||||
filter: filter
|
||||
}, function (success) {
|
||||
if (success.status == "OK") {
|
||||
|
||||
if (Array.isArray(success.result.buildings)) {
|
||||
buildings = success.result.buildings;
|
||||
renderMap();
|
||||
}
|
||||
}
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function getFilter() {
|
||||
var fields = ['network_id', 'networksection_id', 'status_id', 'code', 'street'];
|
||||
var filter = {};
|
||||
fields.forEach(function (field) {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
let val = $('#filter_' + field).val();
|
||||
if (val.length) {
|
||||
filter[field] = val;
|
||||
}
|
||||
});
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
// navigation
|
||||
var building;
|
||||
var hash = window.location.hash.substr(1);
|
||||
var match = hash.match(/building=(\d+)/);
|
||||
if (match && match[1]) {
|
||||
building = match[1]
|
||||
toggleBuilding(building);
|
||||
|
||||
//$('body').scrollTop($('#building-' + building).offset() - 50);
|
||||
}
|
||||
|
||||
<?php if(is_array($filter) && count($filter)): ?>
|
||||
//refreshMap();
|
||||
<?php endif; ?>
|
||||
var hidesearch = [5];
|
||||
$(document).ready(function () {
|
||||
|
||||
});
|
||||
</script>
|
||||
<script type="text/javascript" src="<?= self::getResourcePath() ?>assets/js/datatables-std.js"></script>
|
||||
|
||||
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>
|
||||
@@ -19,11 +19,43 @@ class PopController extends mfBaseController
|
||||
|
||||
protected function indexAction()
|
||||
{
|
||||
$networks = array_map(function($network) {
|
||||
return [
|
||||
"text" => $network->name,
|
||||
"value" => $network->name
|
||||
];
|
||||
}, NetworkModel::getAll());
|
||||
|
||||
$this->layout()->setTemplate("Pop/Index");
|
||||
$pops = PopModel::getAlladv();
|
||||
$pops = array_map(function($pop) {
|
||||
return [
|
||||
"id" => $pop->id,
|
||||
"name" => $pop->name,
|
||||
"networkArea" => $pop->network->name,
|
||||
"location" => $pop->location,
|
||||
"vlan" => [
|
||||
"public" => $pop->vlan_public,
|
||||
"nat" => $pop->vlan_nat,
|
||||
"ipv6" => $pop->vlan_ipv6
|
||||
],
|
||||
"gps" => $pop->gps_lat . ", " . $pop->gps_long
|
||||
];
|
||||
}, PopModel::getAlladv());
|
||||
|
||||
$this->layout()->set("pops", $pops);
|
||||
$JSGlobals = ["BASE_URL" => self::getUrl(""),
|
||||
"DASHBOARD_URL" => self::getUrl("Dashboard"),
|
||||
"MFAPPNAME" => MFAPPNAME_SLUG,
|
||||
"PAGE_TITLE" => "Pops",
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
|
||||
["text" => "Devices", "href" => self::getUrl("Pop")]
|
||||
],
|
||||
"NETWORKS" => $networks,
|
||||
"POPS" => $pops,
|
||||
];
|
||||
|
||||
$this->layout()->set("vueViewName", "Pop");
|
||||
$this->layout()->set("JSGlobals", $JSGlobals);
|
||||
$this->layout()->setTemplate("VueViews/Vue");
|
||||
|
||||
}
|
||||
|
||||
@@ -81,7 +113,7 @@ class PopController extends mfBaseController
|
||||
$popnetwork = PopNetworkModel::getbyPopid($id);
|
||||
$this->layout()->set("popnetwork", $popnetwork['network_id']);
|
||||
$this->layout()->set("pop", $pop);
|
||||
return $this->addAction();
|
||||
$this->addAction();
|
||||
}
|
||||
|
||||
protected function saveAction()
|
||||
@@ -136,8 +168,8 @@ class PopController extends mfBaseController
|
||||
|
||||
if (!$new_id) {
|
||||
$this->layout()->setFlash("Fehler beim Speichern", "error");
|
||||
$this->layout()->set("network", $network);
|
||||
return $this->addAction();
|
||||
if (isset($network)) $this->layout()->set("network", $network);
|
||||
$this->addAction();
|
||||
}
|
||||
|
||||
if ($r->network_id) {
|
||||
@@ -188,8 +220,6 @@ class PopController extends mfBaseController
|
||||
|
||||
$this->layout()->setFlash("Pop erfolgreich gespeichert.", "success");
|
||||
$this->redirect($this->returUrl, $returnAction, $returnVariables, $returnAnker);
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected function apiAction()
|
||||
|
||||
64
public/js/pages/Pop/Pop.js
Normal file
64
public/js/pages/Pop/Pop.js
Normal file
@@ -0,0 +1,64 @@
|
||||
Vue.component('Pop', {
|
||||
//language=Vue
|
||||
template: `
|
||||
<tt-card>
|
||||
|
||||
<tt-table :data="window['TT_CONFIG']['POPS']" :config="PopTableConfig" excel-export>
|
||||
|
||||
<template v-slot:top-buttons>
|
||||
<button type="button" class="btn btn-primary" @click="window.location = window['TT_CONFIG']['BASE_URL'] + '/Pop/add'">
|
||||
<i class="fas fa-plus"></i>
|
||||
Pop hinzufügen
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-slot:name="{ row }">
|
||||
<a target="_blank" :href="window['TT_CONFIG']['BASE_URL'] +'/Pop/Detail?id=' + row.id">{{row.name}}</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:vlan="{ row }">
|
||||
<span v-if="row.vlan.public" class="order-date-pill text-nowrap active mb-1">Public: <span class="font-weight-bold">{{row.vlan.public}}</span></span>
|
||||
<span v-if="row.vlan.nat" class="order-date-pill text-nowrap active mb-1">Nat: <span class="font-weight-bold">{{row.vlan.nat}}</span></span>
|
||||
<span v-if="row.vlan.ipv6" class="order-date-pill text-nowrap active mb-0">IPv6: <span class="font-weight-bold">{{row.vlan.ipv6}}</span></span>
|
||||
</template>
|
||||
|
||||
<template v-slot:gps="{ row }">
|
||||
<a
|
||||
v-if="row.gps"
|
||||
:title="'Google Maps: ' + row.gps"
|
||||
class="mapsLink"
|
||||
:href="'http://maps.google.com/?q=' + row.gps"
|
||||
v-text="row.gps"
|
||||
target="_blank"></a>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions="{ row }">
|
||||
<a :href="window['TT_CONFIG']['BASE_URL'] +'/Pop/edit/?id=' + row.id"><i class="far fa-edit" title="Bearbeiten"></i></a>
|
||||
<a :href="window['TT_CONFIG']['BASE_URL'] +'/Pop/delete/?id=' + row.id" onclick="if(!confirm('Device wirklich löschen?')) return false;" class="text-danger" title="Löschen"><i class="fas fa-trash "></i></a>
|
||||
</template>
|
||||
|
||||
</tt-table>
|
||||
|
||||
</tt-card>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
window: window,
|
||||
PopTableConfig: {
|
||||
key: 'PopTable',
|
||||
tableHeader: 'Pops',
|
||||
defaultPageSize: 25,
|
||||
headers: [
|
||||
{text: 'Name', key: 'name', priority: 10},
|
||||
{text: 'Netzgebiet', key: 'networkArea', class: 'text-center', filter: 'autocomplete',
|
||||
filterOptions: window['TT_CONFIG']['NETWORKS'],
|
||||
priority: 8},
|
||||
{text: 'Zutritt', key: 'location', class: 'text-center', priority: 1},
|
||||
{text: 'Standort', key: 'gps', class: 'text-center', priority: 2},
|
||||
{text: 'Vlan Public/Nat/ipv6', key: 'vlan', class: 'text-center', priority: 7},
|
||||
{text: 'Aktionen', key: 'actions', class: 'text-center', sortable: false, filter: false, priority: 9},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
|
||||
.tt-table.table-sm > tbody > tr > td * {
|
||||
font-size: 13px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tt-table.table-sm > tbody > tr {
|
||||
@@ -139,4 +139,16 @@ input[type=number]::-webkit-outer-spin-button {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fa-circle-xmark, .fa-ban, .fa-trash, .fa-edit, .fa-square-check, .fa-arrows-up-down-left-right, .fa-chevron-right {
|
||||
font-size: 20px !important
|
||||
}
|
||||
|
||||
.tt-table.table-sm > tbody > tr > td * {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
@@ -109,135 +109,137 @@ Vue.component('tt-table-pagination', {
|
||||
|
||||
Vue.component('tt-table', {
|
||||
template: `
|
||||
<div class="tt-table-container" ref="tableContainer">
|
||||
<!-- Top Buttons -->
|
||||
<div class="tt-table-top-buttons-container">
|
||||
<slot name="top-buttons"></slot>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<div class="tt-table-top-pagination-container">
|
||||
<!-- if excelExport is true, show the export button fontawesome icon excel -->
|
||||
<div style="display:flex;align-items: center;">
|
||||
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
|
||||
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
|
||||
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
|
||||
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
|
||||
</div>
|
||||
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
<div class="tt-table-container" ref="tableContainer">
|
||||
<!-- Top Buttons -->
|
||||
<div class="tt-table-top-buttons-container">
|
||||
<slot name="top-buttons"></slot>
|
||||
</div>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<div class="tt-table-top-pagination-container">
|
||||
<!-- if excelExport is true, show the export button fontawesome icon excel -->
|
||||
<div style="display:flex;align-items: center;">
|
||||
<i v-if="!Object.values(columns).every(column => column.filter === false)" title="Filter zurücksetzen"
|
||||
@click="resetTable" class="fas fa-times cursor-pointer text-danger"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--orange)"></i>
|
||||
<i v-if="excelExport" title="EXCEL Export" @click="exportToExcel" class="fa fa-file-excel"
|
||||
style="font-size: 24px;margin-right: 6px;cursor: pointer; color: var(--success)"></i>
|
||||
<h4 style="margin: 0">{{ config.tableHeader }}</h4>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Table -->
|
||||
<table
|
||||
ref="table"
|
||||
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
|
||||
<thead style="border-width: 2px">
|
||||
<tr>
|
||||
<th scope="col" v-for="column in columns"
|
||||
:ref="'table_header_'+column.key"
|
||||
v-if="!hiddenColumns.includes(column.key)"
|
||||
:style="'vertical-align: top; text-align: center;' +
|
||||
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Table -->
|
||||
<table
|
||||
ref="table"
|
||||
:class="['table','tt-table','table-condensed',{ 'loading': loading },{ 'table-striped': striped },{ 'table-bordered': bordered },{ 'table-hover': hover },{ 'table-sm': small }]">
|
||||
<thead style="border-width: 2px">
|
||||
<tr>
|
||||
<th scope="col" v-for="column in columns"
|
||||
:ref="'table_header_'+column.key"
|
||||
v-if="!hiddenColumns.includes(column.key)"
|
||||
:style="'vertical-align: top; text-align: center;' +
|
||||
(column.filter === 'dateRange' ? 'min-width: 260px;' : '') +
|
||||
(originalColumnWidths[column.key] ? 'width: ' + originalColumnWidths[column.key] + 'px;' : '')"
|
||||
>
|
||||
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
|
||||
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
|
||||
@click="column.sortable ? setOrder(column.key) : undefined">
|
||||
{{ column.text }}
|
||||
<i
|
||||
v-if="column.sortable"
|
||||
:class="getSortIconClass(column.key)"></i>
|
||||
</div>
|
||||
<tt-input v-if="column.filter === 'search'" sm v-model="filters[column.key]"></tt-input>
|
||||
<tt-icon-select v-else-if="column.filter === 'iconSelect'" :options="column.filterOptions"
|
||||
v-model="filters[column.key]"></tt-icon-select>
|
||||
<tt-number-range v-else-if="column.filter === 'numberRange'" :returnText="!ssr"
|
||||
v-model="filters[column.key]"></tt-number-range>
|
||||
<tt-select v-else-if="column.filter === 'select'"
|
||||
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||
v-model="filters[column.key]"></tt-select>
|
||||
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete'"
|
||||
:items="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||
v-model="filters[column.key]"> </tt-autocomplete>
|
||||
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="pagination?.filtered_available === 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 && ssr === true) || rows === null"
|
||||
style="height: 150px">
|
||||
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
|
||||
</tr>
|
||||
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
|
||||
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
|
||||
@click="$emit('row-click', row)"
|
||||
>
|
||||
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
|
||||
<td :class="{ 'text-center': column.filter === 'iconSelect',
|
||||
[columns[key].class]: true, 'text-nowrap': !originalColumnWidths[column.key] }">
|
||||
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
|
||||
<i v-if="key === Object.keys(columns)[0] &&
|
||||
>
|
||||
<div style="text-align:center; white-space: nowrap;word-break: keep-all;"
|
||||
:style="{ 'cursor': column.sortable ? 'pointer' : 'default' }"
|
||||
@click="column.sortable ? setOrder(column.key) : undefined">
|
||||
{{ column.text }}
|
||||
<i
|
||||
v-if="column.sortable"
|
||||
:class="getSortIconClass(column.key)"></i>
|
||||
</div>
|
||||
<tt-input v-if="column.filter === 'search'" sm v-model="filters[column.key]"></tt-input>
|
||||
<tt-icon-select v-else-if="column.filter === 'iconSelect'" :options="column.filterOptions"
|
||||
v-model="filters[column.key]"></tt-icon-select>
|
||||
<tt-number-range v-else-if="column.filter === 'numberRange'" :returnText="!ssr"
|
||||
v-model="filters[column.key]"></tt-number-range>
|
||||
<tt-select v-else-if="column.filter === 'select'"
|
||||
:options="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||
v-model="filters[column.key]"></tt-select>
|
||||
|
||||
<tt-autocomplete v-else-if="column.filter === 'autocomplete'"
|
||||
:items="[{text: 'Alle', value: undefined}, ...column.filterOptions]"
|
||||
v-model="filters[column.key]"></tt-autocomplete>
|
||||
<tt-date-picker v-else-if="column.filter === 'date'" v-model="filters[column.key]"></tt-date-picker>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="pagination?.filtered_available === 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 && ssr === true) || rows === null"
|
||||
style="height: 150px">
|
||||
<td :colspan="Object.keys(columns).length" class="text-center">Laden...</td>
|
||||
</tr>
|
||||
<template v-for="(row, index) in (ssr === false ? computedRows : rows)">
|
||||
<tr :class="typeof config.customRowClass === 'function' ? config.customRowClass(row) : ''"
|
||||
@click="$emit('row-click', row)"
|
||||
>
|
||||
<template v-for="(column, key) in columns" v-if="!hiddenColumns.includes(column.key)">
|
||||
<td :class="{ 'text-center': column.filter === 'iconSelect',
|
||||
[columns[key].class]: true,
|
||||
//'text-nowrap': !originalColumnWidths[column.key]
|
||||
}">
|
||||
<!-- If td is first of row then check isExpanded and display fas.fa-chevron-right or fas.fa-chevron-down with cursor pointer -->
|
||||
<i v-if="key === Object.keys(columns)[0] &&
|
||||
($scopedSlots.expandedRow && (typeof config.expandCondition !== 'function' || config.expandCondition(row)) || hiddenColumns.length > 0)"
|
||||
|
||||
@click.stop="toggleExpand(index)"
|
||||
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
|
||||
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
|
||||
@click.stop="toggleExpand(index)"
|
||||
:class="isExpanded(index) ? 'fas fa-chevron-down' : 'fas fa-chevron-right'"
|
||||
style="cursor: pointer;font-size: 14px;padding-right: 8px;user-select: none"></i>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
<span
|
||||
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
<span v-else
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
<span v-else
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )"></span>
|
||||
|
||||
</slot>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
|
||||
<td :colspan="Object.keys(columns).length">
|
||||
|
||||
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
|
||||
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
|
||||
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
|
||||
<template v-if="hiddenColumns.includes(key)">
|
||||
<strong>{{ column.text }}:</strong>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
</slot>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
<tr v-if="isExpanded(index) && ($scopedSlots.expandedRow|| hiddenColumns.length > 0)">
|
||||
<td :colspan="Object.keys(columns).length">
|
||||
|
||||
<!-- display ul with li for each column in hiddenColumns with slot name key.toLowerCase() or span with value of row[key] -->
|
||||
<ul v-if="hiddenColumns.length > 0" style="list-style-type: none;padding: 0;margin: 0;">
|
||||
<li v-for="(column, key) in columns" :key="'hiddenColumn-'+key">
|
||||
<template v-if="hiddenColumns.includes(key)">
|
||||
<strong>{{ column.text }}:</strong>
|
||||
<slot :name="key.toLowerCase()" :value="row[key]" :row="row">
|
||||
|
||||
<span
|
||||
v-if="column.filter === 'date'">{{ row[key] ? (moment.unix(row[key]).isValid() ? moment.unix(row[key]).format('DD.MM.YYYY HH:mm') : moment(row[key]).format('DD.MM.YYYY HH:mm')) : '' }}</span>
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
<span v-else
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
|
||||
<i v-else-if="column.filter === 'iconSelect'"
|
||||
:title="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.text"
|
||||
:class="columns[key].filterOptions.find(option => option.value.toString() === row[key].toString())?.icon"></i>
|
||||
<span v-else
|
||||
v-html="(column.prefix) + (row[key] === null || typeof row[key] === 'undefined' ? '' : row[key]?.toString()?.replace('\\\\n', '<br>')) + (column.suffix )">
|
||||
</span>
|
||||
</slot>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<slot name="expandedRow" :row="row"></slot>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</nav>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<slot name="expandedRow" :row="row"></slot>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination Controls -->
|
||||
<nav aria-label="Page navigation">
|
||||
<tt-table-pagination :pagination="pagination" @fetch-rows="fetchRows"
|
||||
v-if="pagination"></tt-table-pagination>
|
||||
</nav>
|
||||
</div>
|
||||
`, props: {
|
||||
fetchUrl: String,
|
||||
data: Array,
|
||||
@@ -482,12 +484,9 @@ Vue.component('tt-table', {
|
||||
this.hiddenColumns = []
|
||||
await this.$nextTick()
|
||||
|
||||
// Initialize original column widths only if empty
|
||||
if (Object.keys(this.originalColumnWidths).length === 0) {
|
||||
for (let i = 0; i < Object.keys(this.columns).length; i++) {
|
||||
const column = Object.keys(this.columns)[i]
|
||||
this.originalColumnWidths[column] = this.$refs[`table_header_${column}`][0].offsetWidth
|
||||
}
|
||||
for (let i = 0; i < Object.keys(this.columns).length; i++) {
|
||||
const column = Object.keys(this.columns)[i]
|
||||
this.originalColumnWidths[column] = this.$refs[`table_header_${column}`][0].offsetWidth
|
||||
}
|
||||
|
||||
// Create an array of columns sorted by priority
|
||||
@@ -602,6 +601,7 @@ Vue.component('tt-table', {
|
||||
return pagesArray.length === 0 ? [1] : pagesArray;
|
||||
}, computedRows() {
|
||||
if (!this.rawRows || this.ssr === true) return null;
|
||||
|
||||
// console.time('Filtering and pagination');
|
||||
|
||||
function handleRangeFilter(filter, value) {
|
||||
@@ -647,11 +647,14 @@ Vue.component('tt-table', {
|
||||
if (header.filter === 'search') {
|
||||
|
||||
const isNegated = filterValue.startsWith('!');
|
||||
const targetValue = (row[header.key] ? row[header.key].toString().toLowerCase() : '');
|
||||
const substrings = (isNegated ? filterValue.slice(1) : filterValue).split(' ').map(s => s.toLowerCase());
|
||||
|
||||
const targetValue = !row[header.key] ? '' :
|
||||
typeof row[header.key] === 'object' ? Object.values(row[header.key]).join(' ').toLowerCase() : row[header.key].toString().toLowerCase();
|
||||
|
||||
let substringMatch = true;
|
||||
for (var k = 0, klen = substrings.length; k < klen; ++k) {
|
||||
|
||||
if (!targetValue.includes(substrings[k])) {
|
||||
substringMatch = false;
|
||||
break;
|
||||
@@ -672,7 +675,7 @@ Vue.component('tt-table', {
|
||||
}
|
||||
} else if (header.filter === 'select' || header.filter === 'iconSelect' || header.filter === 'autocomplete') {
|
||||
if (filterValue === '') continue;
|
||||
if (filterValue !== row[header.key].toString()) {
|
||||
if (filterValue !== row[header.key]?.toString()) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
@@ -708,10 +711,10 @@ Vue.component('tt-table', {
|
||||
|
||||
let valueA = isDateColumn ?
|
||||
new Date(a[this.order.key].length === 10 ? parseInt(a[this.order.key]) * 1000 : parseInt(a[this.order.key])).getTime() :
|
||||
a[this.order.key];
|
||||
b[this.order.key] || ''
|
||||
let valueB = isDateColumn ?
|
||||
new Date(b[this.order.key].length === 10 ? parseInt(b[this.order.key]) * 1000 : parseInt(b[this.order.key])).getTime() :
|
||||
b[this.order.key];
|
||||
b[this.order.key] || ''
|
||||
|
||||
if (valueA === valueB) return 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user