Feature/rework vue schema

This commit is contained in:
Luca Haid
2024-05-10 21:03:01 +00:00
parent 1f30671cf9
commit 78c9d3ef37
34 changed files with 2290 additions and 1146 deletions

View File

@@ -1,256 +0,0 @@
<?php /** @noinspection PhpUndefinedClassInspection
* @var string $mfLayoutPackage
* @var TYPE_NAME $git_merge_ts
*/
//additional css /css/views/RaspberryDisplay.css
$JSGlobals = ["BASE_URL" => self::getUrl("Domain"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Domains",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Domain Management", "href" => self::getUrl("Domain")],
["text" => "Domains"]
],
"DOMAIN_API_URL" => self::getUrl("Domain/api"),
];
$additionalJS = [
"plugins/vue/vue.js",
"plugins/axios/axios.min.js",
"plugins/moment/moment.min.js",
"plugins/daterangepicker/daterangepicker.js",
"plugins/xlsx/xlsx.min.js",
"plugins/vue/tt-components/tt-table.js",
"plugins/vue/tt-components/tt-page-title.js",
"plugins/vue/tt-components/tt-select.js",
"plugins/vue/tt-components/tt-datepicker.js",
"plugins/vue/tt-components/tt-input.js",
"plugins/vue/tt-components/tt-autocomplete.js",
"plugins/vue/tt-components/tt-icon-select.js",
"plugins/vue/tt-components/tt-number-range.js",
];
$additionalCSS = [
"plugins/daterangepicker/daterangepicker.css",
'plugins/vue/tt-components/css/tt-table.css',
];
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<div id="app">
<!-- start page title -->
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDomains'" :config="domainsTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="reloadDomains">
<template v-if="reloadDomainsLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-sync-alt"></i>
Reload Domains
</template>
</button>
<div class="input-group">
<input type="text" class="form-control" v-model="checkDomainInput" placeholder="Neue Domain überprüfen">
<div class="input-group-append">
<button class="btn btn-primary" @click="checkDomainAvailability">
<template v-if="checkDomainLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-search"></i>
</template>
</button>
</div>
</template>
<!-- Slot to show DNS records button -->
<template v-slot:inwxroid="{ row }">
<button type="button" class="btn btn-primary" @click="showDnsRecordsModal(row.domain)"
:class="dnsRecordsModalLoading === row.domain ? 'disabled' : ''">
<template v-if="dnsRecordsModalLoading === row.domain">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<span v-else>DNS</span>
</button>
</template>
<!-- Registrant Admin Tech Billing from domainContacts -->
<template v-slot:registrant="{ row }">
{{ domainContacts[row.registrant] ? domainContacts[row.registrant]["name"] : '' }}
</template>
<template v-slot:admin="{ row }">{{ domainContacts[row.admin] ? domainContacts[row.admin]["name"] : ''}}
</template>
<template v-slot:tech="{ row }">{{ domainContacts[row.tech] ? domainContacts[row.tech]["name"] : ''}}
</template>
<template v-slot:billing="{ row }">{{ domainContacts[row.billing] ? domainContacts[row.billing]["name"] : ''}}
</template>
</tt-table>
<!-- Bootstrap Modal to query and show all DNS records for a domain -->
<div class="modal show d-block" tabindex="-1" role="dialog" style="background: rgba(0, 0, 0, 0.5);" ref="dnsRecordsModal" @click="dnsRecordsModal.domain = null" @keydown.esc="dnsRecordsModal.domain = null"
v-if="dnsRecordsModal.domain">
<div class="modal-dialog" role="document" style="max-width: fit-content" @click.stop>
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">DNS Records for {{ dnsRecordsModal.domain ?? '' }}</h5>
<button type="button" class="close" @click="dnsRecordsModal.domain = null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table class="tt-table table-striped table-bordered table-hover table-sm table-condensed">
<thead>
<tr>
<th>Record Class</th>
<th>Record Type</th>
<th>Record Host</th>
<th>Record Value</th>
<th>Record TTL</th>
</tr>
</thead>
<tbody>
<tr v-for="record in dnsRecordsModal.records">
<td>{{ record.class }}</td>
<td>{{ record.type }} {{ record.pri ? '(' + record.pri + ')' : '' }}</td>
<td>{{ record.host }}</td>
<td>{{ record.value }}</td>
<td>{{ record.ttl }}</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="dnsRecordsModal.domain = null">Close</button>
</div>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
window: window,
domainContacts: {},
reloadDomainsLoading: false,
dnsRecordsModalLoading: null,
dnsRecordsModal: {
domain: null,
records: []
},
checkDomainInput: '',
checkDomainResult: null,
checkDomainLoading: false
},
mounted() {
this.fetchDomainContacts()
},
computed: {
domainsTableConfig() {
const base = {
headers: [
{text: "DNS", key: "inwxRoId", filter: false, sortable: false},
{text: "Domain", key: "domain"},
{text: "Plesk", key: "pleskId", filter: 'iconSelect', filterOptions: [
{value: 1, text: 'Yes', icon: 'fas fa-check text-success'},
{value: 0, text: 'No', icon: 'fas fa-times text-danger'}
], sortable: false},
{text: "Created Date", key: "crDate", filter: "date"},
{text: "Expiration Date", key: "exDate", filter: "date"},
{text: "Renewal Date", key: "reDate", filter: "date"},
{text: "Updated Date", key: "upDate", filter: "date"},
{text: "Transfer Lock", key: "transferLock", filter: 'iconSelect', filterOptions: [
{value: 1, text: 'Locked', icon: 'fas fa-lock text-danger'},
{value: 0, text: 'Unlocked', icon: 'fas fa-unlock text-success'}
]},
{text: "Authorization Code", key: "authCode", sortable: false},
{text: "Registrant ID", key: "registrant", sortable: false},
{text: "Admin ID", key: "admin", sortable: false},
{text: "Tech ID", key: "tech", sortable: false},
{text: "Billing ID", key: "billing", sortable: false},
{text: "Name Servers", key: "ns"}
],
tableHeader: 'Bestellungen',
}
const domainContactsSorted = Object.entries(this.domainContacts).sort(([, a], [, b]) => a.name.localeCompare(b.name))
const domainContactsFilterOptions = domainContactsSorted.map(([, contact]) => {
return {text: contact.name, value: contact.inwxRoId}
})
// for registrant admin tech billing set filter to select with domainContacts if domainContacts is not empty
if (Object.keys(this.domainContacts).length > 0) {
base.headers = base.headers.map(header => {
if (['registrant', 'admin', 'tech', 'billing'].includes(header.key)) {
header.filter = 'select'
header.filterOptions = domainContactsFilterOptions
}
return header
})
}
return base
}
},
methods: {
async showDnsRecordsModal(domain) {
this.dnsRecordsModalLoading = domain
this.dnsRecordsModal = {
domain: null,
records: []
}
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDnsRecords&domain=' + domain)
this.dnsRecordsModal.domain = domain
this.dnsRecordsModal.records = response.data.map(record => {
if (typeof record.entries === 'object') {
record.value = record.entries[0]
} else {
record.value = record.target || record.txt || record.ip
}
if (record.type === 'SOA') {
record.value = record.mname + ' ' + record.rname + ' ' + record.serial + ' ' + record.refresh + ' ' + record.retry + ' ' + record.expire
}
return record
})
this.dnsRecordsModalLoading = null
this.$nextTick(() => {
this.$refs.dnsRecordsModal.focus()
})
},
async fetchDomainContacts() {
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=getDomainContacts')
this.domainContacts = response.data
},
async reloadDomains() {
this.reloadDomainsLoading = true
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=importAllDomains')
window.notify('success', response.data["importMessages"].join('<br>'))
await Promise.all([
this.fetchDomainContacts(),
this.$refs.table.fetchData(this.$refs.table.pagination.page)
])
this.reloadDomainsLoading = false
},
//TODO: make this cleaner
async checkDomainAvailability() {
this.checkDomainLoading = true
const response = await axios.get(window['TT_CONFIG']['DOMAIN_API_URL'] + '?do=checkDomain&domain=' + this.checkDomainInput)
const priceInformation = response.data.price.domain[this.checkDomainInput]
window.notify(response.data.status === 'free' ? 'success' : 'error',
`Domain ist ${response.data.status === 'free' ? 'verfügbar. Registrieren um' : 'nicht frei. Transfer um'} ${priceInformation.price}${priceInformation.currency}/${priceInformation.period === '1Y' ? 'Jahr' : priceInformation.period}`)
this.checkDomainLoading = false
}
}
})
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -1,222 +0,0 @@
<?php /** @noinspection PhpUndefinedClassInspection
* @var string $mfLayoutPackage
* @var TYPE_NAME $git_merge_ts
*/
//additional css /css/views/RaspberryDisplay.css
$JSGlobals = ["BASE_URL" => self::getUrl("Domain"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Historische Tickets",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Historische Tickets", "href" => self::getUrl("HistoricTicket")]
],
"HISTORIC_TICKET_API_URL" => self::getUrl("HistoricTicket/api"),
];
$additionalJS = [
"plugins/vue/vue.js",
"plugins/axios/axios.min.js",
"plugins/moment/moment.min.js",
"plugins/daterangepicker/daterangepicker.js",
"plugins/xlsx/xlsx.min.js",
"plugins/vue/tt-components/tt-table.js",
"plugins/vue/tt-components/tt-page-title.js",
"plugins/vue/tt-components/tt-select.js",
"plugins/vue/tt-components/tt-datepicker.js",
"plugins/vue/tt-components/tt-input.js",
"plugins/vue/tt-components/tt-autocomplete.js",
"plugins/vue/tt-components/tt-icon-select.js",
"plugins/vue/tt-components/tt-number-range.js",
];
$additionalCSS = [
"plugins/daterangepicker/daterangepicker.css",
'plugins/vue/tt-components/css/tt-table.css',
];
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<div id="app">
<!-- start page title -->
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['HISTORIC_TICKET_API_URL'] + '?do=getHistoricTickets'"
:config="historicTicketTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<!-- add input for global search with label and bootstrap class-->
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="Globale Suche" v-model="globalSearch" @keydown.enter="doGlobalSearch">
<div class="input-group-append">
<button class="btn btn-primary" type="button" @click="doGlobalSearch">Submit</button>
</div>
</div>
</template>
<template v-slot:first_name="{ row }">
{{ row.first_name }} {{ row.last_name }}
</template>
<template v-slot:ctime="{ row }">
{{ new Date(row.ctime * 1000).toLocaleString() }}
</template>
<template v-slot:ticket_number="{ row }">
<a href="#" @click="clickTicketNumber(row.ticket_number)">{{ row.ticket_number }}</a>
</template>
</tt-table>
<!-- Bootstrap Modal to show global search results -->
<div class="modal show d-block" tabindex="0" role="dialog" style="background: rgba(0, 0, 0, 0.5);" @click="globalSearchModal = false" @keydown.esc="globalSearchModal = false" ref="globalSearchModal"
v-if="globalSearchModal">
<div class="modal-dialog" role="document" @click.stop
style="width:fit-content;max-width: 80vw ; max-height: 80vh; overflow-y: auto;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Suchergebnisse</h5>
<button type="button" class="close" @click="globalSearchModal = false">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<tt-table
:fetch-url="`${window['TT_CONFIG']['HISTORIC_TICKET_API_URL']}?do=findHistoricTicket&query=${globalSearch}`"
:config="globalSearchModalTableConfig"
small ref="table">
<template v-slot:ctime="{ row }">
{{ window.moment(row.ctime * 1000).format('DD.MM.YYYY HH:mm') }}
</template>
<template v-slot:ticket_number="{ row }">
<a href="#" @click="clickTicketNumber(row.ticket_number)">{{ row.ticket_number }}</a>
</template>
</tt-table>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="globalSearchModal = false">Close</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap Modal to show ticket messages -->
<div class="modal show d-block" tabindex="0" role="dialog" style="background: rgba(0, 0, 0, 0.5);" @click="selectedTicketNumber = null" @keydown.esc="selectedTicketNumber = null" ref="selectedTicketModal"
v-if="selectedTicketNumber">
<div class="modal-dialog" role="document" @click.stop
style="width:fit-content;max-width: 80vw ; max-height: 80vh; overflow-y: auto;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ticket {{ selectedTicketNumber }}</h5>
<button type="button" class="close" @click="selectedTicketNumber = null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div v-if="selectedTicketData">
<h5>{{ selectedTicketData.ticket.subject }}</h5>
<p>{{ selectedTicketData.ticket.message }}</p>
<div v-for="message in selectedTicketData.messages">
<hr>
<h6>{{ new Date(message.ctime * 1000).toLocaleString()}}</h6>
<p style="word-break: break-all;" v-html="message.content?.replaceAll('\n', '<br>')"></p>
</div>
</div>
<div v-else class="spinner-border text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
window: window,
selectedTicketNumber: null,
selectedTicketData: null,
globalSearch: '',
globalSearchModal: false,
globalSearchModalTableConfig: {
headers: [
{text: 'Ticket Number', key: 'ticket_number', filter: false, sortable: false},
{text: 'Erstellt', key: 'ctime', filter: false, sortable: false},
{text: 'Subject', key: 'ticket_subject', filter: false, sortable: false},
{text: 'Message', key: 'ticket_message', filter: false, sortable: false},
],
tableHeader: 'Suchergebnisse',
key: 'HistoricTicketGlobalSearch',
},
historicTicketTableConfig: {
headers: [
{text: 'Ticket Number', key: 'ticket_number', filter: 'search'},
{text: 'Erstellt', key: 'ctime', filter: false},
{text: 'Subject', key: 'subject', filter: 'search', sortable: false},
{text: 'Type', key: 'type', filter: 'select', filterOptions: [
{value: 'BACKOFFICE', text: 'BACKOFFICE'},
{value: 'KUNDENANFRAGEN', text: 'KUNDENANFRAGEN'},
{value: 'STÖRUNGEN', text: 'STÖRUNGEN'},
{value: 'ALLGEMEINES', text: 'ALLGEMEINES'},
{value: 'TERMIN VEREINBART', text: 'TERMIN VEREINBART'},
{value: 'VERRECHNEN AB DATUM', text: 'VERRECHNEN AB DATUM'},
{value: 'ONLINE-TICKETS', text: 'ONLINE-TICKETS'},
{value: 'KÜNDIGUNG', text: 'KÜNDIGUNG'},
{value: 'BESTELLUNGEN', text: 'BESTELLUNGEN'},
{value: 'PORTIERUNG', text: 'PORTIERUNG'},
{value: 'KABEL-TV', text: 'KABEL-TV'},
{value: 'TIEFBAU', text: 'TIEFBAU'},
{value: 'ENERGIE STEIERMARK', text: 'ENERGIE STEIERMARK'},
{value: 'INTERN', text: 'INTERN'},
{value: 'FELIX', text: 'FELIX'},
{value: '0', text: '0'},
{value: 'JETTEN', text: 'JETTEN'},
]},
{text: 'Status', key: 'status', filter: 'select', filterOptions: [
{value: 'Geschlossen', text: 'Geschlossen'},
{value: 'In Evidenz', text: 'In Evidenz'},
{value: 'In Bearbeitung', text: 'In Bearbeitung'},
{value: 'Business In Bearbeitung', text: 'Business In Bearbeitung'},
{value: 'Business Angebot gelegt', text: 'Business Angebot gelegt'},
]},
{text: 'Name', key: 'first_name', filter: 'search', sortable: false},
{text: 'Email', key: 'email', filter: 'search', sortable: false},
{text: 'Phone', key: 'phone', filter: 'search', sortable: false},
],
defaultPageSize: 25,
tableHeader: 'Historische Tickets',
key: 'HistoricTicket',
}
},
methods: {
async doGlobalSearch() {
if (this.globalSearch.length > 0) {
this.globalSearchModal = true;
this.$nextTick(() => {
this.$refs.globalSearchModal.focus();
});
}
},
async clickTicketNumber(ticketNumber) {
this.globalSearchModal = false;
this.selectedTicketData = null;
this.selectedTicketNumber = ticketNumber;
const response = await axios.post(`${window['TT_CONFIG']['HISTORIC_TICKET_API_URL']}?do=getHistoricTicketMessages`, {ticketNumber});
this.selectedTicketData = response.data;
this.$nextTick(() => {
this.$refs.selectedTicketModal.focus();
});
}
}
})
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -1,99 +0,0 @@
<?php /** @noinspection PhpUndefinedClassInspection
* @var string $mfLayoutPackage
* @var TYPE_NAME $git_merge_ts
*/
//additional css /css/views/RaspberryDisplay.css
$JSGlobals = ["BASE_URL" => self::getUrl("RaspberryDisplay"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Raspberry Displays",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Raspberry Displays", "href" => self::getUrl("RaspberryDisplay")]
]
];
$additionalJS = ["plugins/vue/vue.min.js",
"plugins/axios/axios.min.js",
"plugins/vue/tt-components/tt-page-title.js",
"plugins/vue/tt-components/tt-loader.js"];
$additionalCSS = ["css/views/RaspberryDisplay.css", "plugins/vue/tt-components/css/tt-loader.css"];
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<div id="app">
<!-- start page title -->
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<div class="card">
<tt-loader v-if="loading"></tt-loader>
<div class="p-2">
<h3>8322 Studenzen NOC Displays</h3>
<div class="display-grid">
<div v-for="display in displays" :key="display.id"
:class="['display', display['display_label'].includes('-B-') ? 'big-42-inch' : 'small-27-inch']"
:style="display['custom_style']" style="">
<div style="display: grid; grid-template-columns: max-content auto max-content; justify-items: center;width:100%; padding: 0 2px">
<div>
<!-- FONT AWESOME ONLINE GREEN CIRCLE -->
<i class="fas fa-circle" data-toggle="tooltip" title="ONLINE" style="color: green"></i>
</div>
<div>
<div @click.prevent="enableDisplayURLEditMode(display.id)" style="cursor: pointer">
<span v-if="displaysURLEditMode !== display.id">{{ display['display_url'] | cleanupURL }}</span>
<input v-else-if="displaysURLEditMode === display.id"
v-model="display['display_url']"
@keyup.enter="disableDisplayURLEditMode(display.id, display['display_url'])"
@blur="disableDisplayURLEditMode(display.id, display['display_url'])"
ref="displayURLEditInput"
class="form-control"
type="text">
</div>
</div>
<div style="cursor: pointer">
<!-- FONT AWESOME REBOOT ICON -->
<i class="fas fa-red fa-sync-alt" data-toggle="tooltip" title="Reboot this Raspberry"
@click="rebootRaspberry(display.id)"
style="color: green"></i>
</div>
</div>
<div>
<!-- Checkbox for Auto Refresh Enabled -->
<div style="display: inline-block" data-toggle="tooltip"
:title="`Auto refresh is ${display['auto_refresh_enabled'] ? 'enabled' : 'disabled'}.`">
<input type="checkbox" :id="'auto_refresh_enabled_checkbox_' + display.id"
v-model="display['auto_refresh_enabled']"
@change="submitChanges(display.id, 'auto_refresh_enabled', display['auto_refresh_enabled'])">
<label :for="'auto_refresh_enabled_checkbox_' + display.id">ARF</label>
</div>
<!-- This will only display if both are true, consider adjusting logic as needed -->
<span style="margin: 0 4px"> | </span>
<!-- Checkbox for Margin Hotfix Enabled -->
<div style="display: inline-block" data-toggle="tooltip"
:title="`Margin Hotfix is ${display['margin_hot_fix_enabled'] ? 'enabled' : 'disabled'}.`">
<input type="checkbox" :id="'margin_hot_fix_enabled_checkbox_' + display.id"
v-model="display['margin_hot_fix_enabled']"
@change="submitChanges(display.id, 'margin_hot_fix_enabled', display['margin_hot_fix_enabled'])">
<label :for="'margin_hot_fix_enabled_checkbox_' + display.id">MHF</label>
</div>
</div>
<div v-text="display['display_label']"></div>
</div>
</div>
</div>
</div>
</div>
<script src="<?=self::getResourcePath()?>js/pages/raspberryDisplay.js?<?=$git_merge_ts?>"></script>

View File

@@ -1,163 +0,0 @@
<?php /** @noinspection PhpUndefinedClassInspection
* @var string $mfLayoutPackage
* @var TYPE_NAME $git_merge_ts
*/
//additional css /css/views/RaspberryDisplay.css
$JSGlobals = ["BASE_URL" => self::getUrl("VoiceCallActive"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Active Voice Calls",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Active Voice Calls", "href" => self::getUrl("VoiceCallActive")]
],
"VOICE_CALL_ACTIVE_API_URL" => self::getUrl("VoiceCallActive/api"),
];
$additionalJS = [
"plugins/vue/vue.js",
"plugins/axios/axios.min.js",
"plugins/moment/moment.min.js",
"plugins/daterangepicker/daterangepicker.js",
"plugins/xlsx/xlsx.min.js",
"plugins/vue/tt-components/tt-table.js",
"plugins/vue/tt-components/tt-page-title.js",
"plugins/vue/tt-components/tt-select.js",
"plugins/vue/tt-components/tt-datepicker.js",
"plugins/vue/tt-components/tt-input.js",
"plugins/vue/tt-components/tt-autocomplete.js",
"plugins/vue/tt-components/tt-icon-select.js",
"plugins/vue/tt-components/tt-number-range.js",
];
$additionalCSS = [
"plugins/daterangepicker/daterangepicker.css",
'plugins/vue/tt-components/css/tt-table.css',
];
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<div id="app">
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['VOICE_CALL_ACTIVE_API_URL'] + '?do=getActiveCalls'" :config="VoiceCallActiveTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="refresh" data-toggle="tooltip" data-placement="bottom" title="Refreshing too often will run into API-Rate limits and will cause errors.">
<template v-if="refreshLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-sync-alt"></i>
Refresh
</template>
</button>
<div class="d-flex">
<label style="margin-bottom: 0 !important;">
<input type="checkbox" id="autoRefresh" data-toggle="toggle" data-size="lg" @change="clickedCheckBox(this.checked)">
<span class="ml-2">Auto Refresh (5sec)</span>
</label>
<span style="width: 50px"></span>
<div class="voice-yellow p-2">Ringing</div>
<div class="voice-red p-2">Outgoing</div>
<div class="voice-green p-2">Ingoing</div>
</div>
</template>
<template v-slot:answer_time="{ row }">
{{ !isNaN(new Date(row.answer_time)) ? window.moment(row.answer_time, 'YYYY-MM-DD HH:mm:ss Z').format('DD.MM.YYYY HH:mm:ss') : 'Call is not running' }}
</template>
<template v-slot:status="{ row }">
<i v-if="!row.dst_device_extension && row.status !== 'Ringing'" class="fas fa-phone-arrow-up-right"></i>
<i v-else-if="!row.dst_device_extension && row.status === 'Ringing'" class="fas fa-phone-arrow-up-right fa-shake"></i>
<i v-else-if="row.status === 'Ringing'" class="fas fa-phone-arrow-down-left fa-shake"></i>
<i v-else class="fas fa-phone-arrow-down-left"></i>
{{ row.status }}
</template>
</tt-table>
</div>
<style>
.voice-green {
background-color: #6CAE75 !important
}
.voice-yellow {
background-color: #E8E288 !important
}
.voice-red {
background-color: #FF8360 !important
}
.table-sm td {
padding: 4px !important;
}
</style>
<script>
new Vue({
el: '#app',
data: {
window: window,
VoiceCallActiveTableConfig: {
customRowClass: function (row) {
if (row.status.toLowerCase() === 'ringing') {
return 'voice-yellow';
}
if (!row.dst_device_extension) {
return 'voice-red';
}
if (row.status.toLowerCase() === 'answered') {
return 'voice-green';
}
},
headers: [
{text: 'Call ID', key: 'id', filter: false, sortable: false},
{text: 'Status', key: 'status', filter: false, sortable: false},
{text: 'Answer Time', key: 'answer_time', filter: false, sortable: false},
{text: 'Duration', key: 'duration', filter: false, sortable: false},
{text: 'Source', key: 'src', filter: false, sortable: false},
{text: 'Device Type', key: 'device_type', filter: false, sortable: false},
{text: 'Destination', key: 'localized_dst', filter: false, sortable: false},
{text: 'Destination User', key: 'dst_user', filter: false, sortable: false},
{text: 'Destination Device Extension', key: 'dst_device_extension', filter: false, sortable: false},
],
tableHeader: 'Active Voice Calls',
},
refreshLoading: false,
autoRefresh: null,
},
mounted() {
//TODO: create vue tooltip component
$('[data-toggle="tooltip"]').tooltip();
const _this = this;
$('#autoRefresh').change(function () {
console.log(this.checked);
if (this.checked) {
_this.autoRefresh = setInterval(function () {
_this.refresh();
}, 5000);
} else {
clearInterval(_this.autoRefresh);
}
})
},
methods: {
async refresh() {
this.refreshLoading = true;
this.$refs.table.loading = true;
await this.$refs.table.fetchData();
$('.tooltip').tooltip('hide');
this.$refs.table.loading = false;
this.refreshLoading = false;
},
}
})
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -1,97 +0,0 @@
<?php /** @noinspection PhpUndefinedClassInspection
* @var string $mfLayoutPackage
* @var TYPE_NAME $git_merge_ts
*/
//additional css /css/views/RaspberryDisplay.css
$JSGlobals = ["BASE_URL" => self::getUrl("Domain"),
"DASHBOARD_URL" => self::getUrl("Dashboard"),
"MFAPPNAME" => MFAPPNAME_SLUG,
"PAGE_TITLE" => "Domains",
"PATH" => [
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
["text" => "Voice Calls History", "href" => self::getUrl("VoiceCallHistory")]
],
"VOICE_CALL_HISTORY_API_URL" => self::getUrl("VoiceCallHistory/api"),
];
$additionalJS = [
"plugins/vue/vue.js",
"plugins/axios/axios.min.js",
"plugins/moment/moment.min.js",
"plugins/daterangepicker/daterangepicker.js",
"plugins/xlsx/xlsx.min.js",
"plugins/vue/tt-components/tt-table.js",
"plugins/vue/tt-components/tt-page-title.js",
"plugins/vue/tt-components/tt-select.js",
"plugins/vue/tt-components/tt-datepicker.js",
"plugins/vue/tt-components/tt-input.js",
"plugins/vue/tt-components/tt-autocomplete.js",
"plugins/vue/tt-components/tt-icon-select.js",
"plugins/vue/tt-components/tt-number-range.js",
];
$additionalCSS = [
"plugins/daterangepicker/daterangepicker.css",
'plugins/vue/tt-components/css/tt-table.css',
];
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/header.php"); ?>
<div id="app">
<tt-page-title :title="window['TT_CONFIG']['PAGE_TITLE']" :path="window['TT_CONFIG']['PATH']"></tt-page-title>
<tt-table :fetch-url="window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=getCalls'" :config="VoiceCallHistoryTableConfig"
small ssr ref="table">
<template v-slot:top-buttons>
<button type="button" class="btn btn-primary" @click="importCallsFromToday">
<template v-if="importCallsFromTodayLoading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</template>
<template v-else>
<i class="fas fa-sync-alt"></i>
Re-Import Calls from Today
</template>
</button>
</template>
</tt-table>
</div>
<script>
new Vue({
el: '#app',
data: {
window: window,
VoiceCallHistoryTableConfig: {
headers: [
{text: "Call-ID", key: "uid"},
{text: "Voice Account", key: "voice_account"},
{text: "Time Range", key: "start", filter: "date"},
{text: "Source", key: "source"},
{text: "Destination", key: "destination"},
{text: "Billable", key: "billable", filter: "iconSelect", filterOptions: [
{value: 1, text: 'Yes', icon: 'fas fa-check text-success'},
{value: 0, text: 'No', icon: 'fas fa-times text-danger'}
]},
{text: "Duration", key: "duration"},
],
tableHeader: 'Voice Call History',
key: 'VoiceCallHistory',
},
importCallsFromTodayLoading: false,
},
mounted() {},
methods: {
async importCallsFromToday() {
this.importCallsFromTodayLoading = true;
const response = await axios.get(window['TT_CONFIG']['VOICE_CALL_HISTORY_API_URL'] + '?do=importCallsFromToday');
window.notify(response.data.status === 'success' ? 'success' : 'error', response.data.message);
await this.$refs.table.fetchData();
this.importCallsFromTodayLoading = false;
},
}
})
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -0,0 +1,49 @@
<?php
if (!isset($vueViewName)) {
die("vueViewName is not set");
}
if (!isset($mfLayoutPackage)) {
die("mfLayoutPackage is not set");
}
$additionalCSS = $additionalCSS ?? [];
$additionalJS = $additionalJS ?? [];
$additionalJS = [
...$additionalJS,
"bundler.php",
"js/pages/" . $vueViewName . "/" . $vueViewName . ".js",
];
$additionalCSS = [
...$additionalCSS,
'plugins/daterangepicker/daterangepicker.css',
'plugins/vue/tt-components/css/tt-table.css',
'plugins/vue/tt-components/css/tt-loader.css',
];
/**
* Convert PascalCase to snake_case (e.g. PascalCase to pascal-case)
* @param $str string PascalCase string
* @return string snake-case string
*/
function pascalToSnakeCase(string $str): string {
$snakeCase = preg_replace('/(?<!^)([A-Z])/', '-$1', $str);
return strtolower($snakeCase);
}
$vueTagName = pascalToSnakeCase($vueViewName);
include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/vueHeader.php"); ?>
<div id="app">
<<?php echo $vueTagName; ?>></<?php echo $vueTagName; ?>>
</div>
<script>
const view = new Vue({el: '#app'});
</script>
<?php include(realpath(dirname(__FILE__) . "/../../$mfLayoutPackage") . "/footer.php"); ?>

View File

@@ -16,14 +16,14 @@
<?php else: ?>
<?php if(!$me->is("Admin")): ?>
<li class="has-submenu">
<a href="<?=self::getUrl("Dashboard")?>"><i class="fe-airplay"></i> Dashboard</a>
<a href="<?=self::getUrl("Dashboard")?>"><i class="fa-solid fa-airplay"></i> Dashboard</a>
</li>
<?php endif; ?>
<?php if($me->is("Admin")): ?>
<li class="has-submenu">
<a href="<?=self::getUrl("Dashboard")?>"><i class="fe-airplay"></i> Dashboard <div class="arrow-down"></div></a>
<a href="<?=self::getUrl("Dashboard")?>"><i class="fa-solid fa-airplay"></i> Dashboard <div class="arrow-down"></div></a>
<ul class="submenu">
<li><a href="<?=self::getUrl("News")?>"><i class="far fa-fw fa-th-list text-info"></i> News</a></li>
<?php if($me->is("employee")): ?>
@@ -114,6 +114,7 @@
<?php if($me->isAdmin() || $me->can("Cpeprovisioning")): ?><li><a href="<?=self::getUrl("Cpeprovisioning")?>"><i class="fad fa-fw fa-hdd text-info"></i> CPE Provisioning</a></li><?php endif; ?>
<?php if($me->isAdmin() || $me->can("Cpeshipping")): ?><li><a href="<?=self::getUrl("Cpeshipping")?>"><i class="fad fa-fw fa-shipping-fast text-info"></i> CPE Versand</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("Domain")?>"><i class="fad fa-fw fa-globe text-info"></i> Domains</a></li><?php endif; ?>
<?php if($me->isAdmin()) : ?><li><a href="<?=self::getUrl("IpNetwork")?>"><i class="fa-solid fa-network-wired text-info"></i> IPAM</a></li><?php endif; ?>
</ul>
</li>
<?php endif; ?>

View File

@@ -0,0 +1,77 @@
<?php /** @var TYPE_NAME $git_merge_ts */ ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title><?=MFAPPNAME_FULL?></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- App favicon -->
<link rel="shortcut icon" href="<?=self::getResourcePath()?>assets/images/favicon.ico">
<link href="<?=self::getResourcePath()?>fontawesome/css/all.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-solid.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-regular.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-light.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>fontawesome/css/sharp-thin.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>assets/css/bootstrap.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>assets/css/app.min.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>plugins/notification/notify.min.css" rel="stylesheet" type="text/css" />
<link href="<?=self::getResourcePath()?>assets/css/thetool.css?<?=$git_merge_ts?>" rel="stylesheet" type="text/css" />
<?php if(isset($additionalCSS) && is_array($additionalCSS) && count($additionalCSS)): ?>
<?php foreach($additionalCSS as $css): ?>
<link rel="stylesheet" href="<?=self::getResourcePath()?><?=$css?>?<?=$git_merge_ts?>" />
<?php endforeach; ?>
<?php endif; ?>
<script type="text/javascript">
window.mfNotify = <?=isset($mfNotify) ? json_encode($mfNotify) : "null"; ?>;
window.TT_CONFIG = {};
<?php
if(isset($JSGlobals) && is_array($JSGlobals) && count($JSGlobals)):
foreach($JSGlobals as $key => $value): ?>
window.TT_CONFIG.<?=$key?> = <?=is_array($value) ? json_encode($value) : "'$value'"; ?>;
<?php endforeach; endif;?>
</script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/jquery.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/popper.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>js/bootstrap.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>assets/js/bootstrap-select.min.js"></script>
<script type="text/javascript" src="<?=self::getResourcePath()?>plugins/notification/notify.js"></script>
<?php if(isset($additionalJS) && is_array($additionalJS) && count($additionalJS)): ?>
<?php foreach($additionalJS as $js): ?>
<script src="<?=self::getResourcePath()?><?=$js?>?<?=$git_merge_ts?>"></script>
<?php endforeach; ?>
<?php endif; ?>
<?php if(MFAPPNAME == "devthetool"): ?>
<style type="text/css">
body {
border-left: 8px dashed #f672a7;
}
</style>
<?php endif; ?>
</head>
<body>
<script type="text/javascript">
baseurl = '<?=self::getResourcePath()?>';
</script>
<!-- Navigation Bar-->
<header id="topnav">
<?php include(realpath(dirname(__FILE__)."/")."/topbar.php"); ?>
<?php include(realpath(dirname(__FILE__)."/")."/menu.php"); ?>
</header>
<!-- End Navigation Bar-->
<div class="wrapper pl-0 pl-lg-1 pr-0 pr-lg-1 ">
<div class="container-fluid">