commit
4daf1630f3
@ -0,0 +1,5 @@ |
||||
'use strict'; |
||||
|
||||
global.appRoot = require('path').resolve(__dirname); |
||||
|
||||
require('./server/server').init(); |
@ -0,0 +1,349 @@ |
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ |
||||
|
||||
/* Document |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* 1. Correct the line height in all browsers. |
||||
* 2. Prevent adjustments of font size after orientation changes in iOS. |
||||
*/ |
||||
|
||||
html { |
||||
line-height: 1.15; /* 1 */ |
||||
-webkit-text-size-adjust: 100%; /* 2 */ |
||||
} |
||||
|
||||
/* Sections |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* Remove the margin in all browsers. |
||||
*/ |
||||
|
||||
body { |
||||
margin: 0; |
||||
} |
||||
|
||||
/** |
||||
* Render the `main` element consistently in IE. |
||||
*/ |
||||
|
||||
main { |
||||
display: block; |
||||
} |
||||
|
||||
/** |
||||
* Correct the font size and margin on `h1` elements within `section` and |
||||
* `article` contexts in Chrome, Firefox, and Safari. |
||||
*/ |
||||
|
||||
h1 { |
||||
font-size: 2em; |
||||
margin: 0.67em 0; |
||||
} |
||||
|
||||
/* Grouping content |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* 1. Add the correct box sizing in Firefox. |
||||
* 2. Show the overflow in Edge and IE. |
||||
*/ |
||||
|
||||
hr { |
||||
box-sizing: content-box; /* 1 */ |
||||
height: 0; /* 1 */ |
||||
overflow: visible; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* 1. Correct the inheritance and scaling of font size in all browsers. |
||||
* 2. Correct the odd `em` font sizing in all browsers. |
||||
*/ |
||||
|
||||
pre { |
||||
font-family: monospace, monospace; /* 1 */ |
||||
font-size: 1em; /* 2 */ |
||||
} |
||||
|
||||
/* Text-level semantics |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* Remove the gray background on active links in IE 10. |
||||
*/ |
||||
|
||||
a { |
||||
background-color: transparent; |
||||
} |
||||
|
||||
/** |
||||
* 1. Remove the bottom border in Chrome 57- |
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. |
||||
*/ |
||||
|
||||
abbr[title] { |
||||
border-bottom: none; /* 1 */ |
||||
text-decoration: underline; /* 2 */ |
||||
text-decoration: underline dotted; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Add the correct font weight in Chrome, Edge, and Safari. |
||||
*/ |
||||
|
||||
b, |
||||
strong { |
||||
font-weight: bolder; |
||||
} |
||||
|
||||
/** |
||||
* 1. Correct the inheritance and scaling of font size in all browsers. |
||||
* 2. Correct the odd `em` font sizing in all browsers. |
||||
*/ |
||||
|
||||
code, |
||||
kbd, |
||||
samp { |
||||
font-family: monospace, monospace; /* 1 */ |
||||
font-size: 1em; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Add the correct font size in all browsers. |
||||
*/ |
||||
|
||||
small { |
||||
font-size: 80%; |
||||
} |
||||
|
||||
/** |
||||
* Prevent `sub` and `sup` elements from affecting the line height in |
||||
* all browsers. |
||||
*/ |
||||
|
||||
sub, |
||||
sup { |
||||
font-size: 75%; |
||||
line-height: 0; |
||||
position: relative; |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
sub { |
||||
bottom: -0.25em; |
||||
} |
||||
|
||||
sup { |
||||
top: -0.5em; |
||||
} |
||||
|
||||
/* Embedded content |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* Remove the border on images inside links in IE 10. |
||||
*/ |
||||
|
||||
img { |
||||
border-style: none; |
||||
} |
||||
|
||||
/* Forms |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* 1. Change the font styles in all browsers. |
||||
* 2. Remove the margin in Firefox and Safari. |
||||
*/ |
||||
|
||||
button, |
||||
input, |
||||
optgroup, |
||||
select, |
||||
textarea { |
||||
font-family: inherit; /* 1 */ |
||||
font-size: 100%; /* 1 */ |
||||
line-height: 1.15; /* 1 */ |
||||
margin: 0; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Show the overflow in IE. |
||||
* 1. Show the overflow in Edge. |
||||
*/ |
||||
|
||||
button, |
||||
input { /* 1 */ |
||||
overflow: visible; |
||||
} |
||||
|
||||
/** |
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE. |
||||
* 1. Remove the inheritance of text transform in Firefox. |
||||
*/ |
||||
|
||||
button, |
||||
select { /* 1 */ |
||||
text-transform: none; |
||||
} |
||||
|
||||
/** |
||||
* Correct the inability to style clickable types in iOS and Safari. |
||||
*/ |
||||
|
||||
button, |
||||
[type="button"], |
||||
[type="reset"], |
||||
[type="submit"] { |
||||
-webkit-appearance: button; |
||||
} |
||||
|
||||
/** |
||||
* Remove the inner border and padding in Firefox. |
||||
*/ |
||||
|
||||
button::-moz-focus-inner, |
||||
[type="button"]::-moz-focus-inner, |
||||
[type="reset"]::-moz-focus-inner, |
||||
[type="submit"]::-moz-focus-inner { |
||||
border-style: none; |
||||
padding: 0; |
||||
} |
||||
|
||||
/** |
||||
* Restore the focus styles unset by the previous rule. |
||||
*/ |
||||
|
||||
button:-moz-focusring, |
||||
[type="button"]:-moz-focusring, |
||||
[type="reset"]:-moz-focusring, |
||||
[type="submit"]:-moz-focusring { |
||||
outline: 1px dotted ButtonText; |
||||
} |
||||
|
||||
/** |
||||
* Correct the padding in Firefox. |
||||
*/ |
||||
|
||||
fieldset { |
||||
padding: 0.35em 0.75em 0.625em; |
||||
} |
||||
|
||||
/** |
||||
* 1. Correct the text wrapping in Edge and IE. |
||||
* 2. Correct the color inheritance from `fieldset` elements in IE. |
||||
* 3. Remove the padding so developers are not caught out when they zero out |
||||
* `fieldset` elements in all browsers. |
||||
*/ |
||||
|
||||
legend { |
||||
box-sizing: border-box; /* 1 */ |
||||
color: inherit; /* 2 */ |
||||
display: table; /* 1 */ |
||||
max-width: 100%; /* 1 */ |
||||
padding: 0; /* 3 */ |
||||
white-space: normal; /* 1 */ |
||||
} |
||||
|
||||
/** |
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera. |
||||
*/ |
||||
|
||||
progress { |
||||
vertical-align: baseline; |
||||
} |
||||
|
||||
/** |
||||
* Remove the default vertical scrollbar in IE 10+. |
||||
*/ |
||||
|
||||
textarea { |
||||
overflow: auto; |
||||
} |
||||
|
||||
/** |
||||
* 1. Add the correct box sizing in IE 10. |
||||
* 2. Remove the padding in IE 10. |
||||
*/ |
||||
|
||||
[type="checkbox"], |
||||
[type="radio"] { |
||||
box-sizing: border-box; /* 1 */ |
||||
padding: 0; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Correct the cursor style of increment and decrement buttons in Chrome. |
||||
*/ |
||||
|
||||
[type="number"]::-webkit-inner-spin-button, |
||||
[type="number"]::-webkit-outer-spin-button { |
||||
height: auto; |
||||
} |
||||
|
||||
/** |
||||
* 1. Correct the odd appearance in Chrome and Safari. |
||||
* 2. Correct the outline style in Safari. |
||||
*/ |
||||
|
||||
[type="search"] { |
||||
-webkit-appearance: textfield; /* 1 */ |
||||
outline-offset: -2px; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Remove the inner padding in Chrome and Safari on macOS. |
||||
*/ |
||||
|
||||
[type="search"]::-webkit-search-decoration { |
||||
-webkit-appearance: none; |
||||
} |
||||
|
||||
/** |
||||
* 1. Correct the inability to style clickable types in iOS and Safari. |
||||
* 2. Change font properties to `inherit` in Safari. |
||||
*/ |
||||
|
||||
::-webkit-file-upload-button { |
||||
-webkit-appearance: button; /* 1 */ |
||||
font: inherit; /* 2 */ |
||||
} |
||||
|
||||
/* Interactive |
||||
========================================================================== */ |
||||
|
||||
/* |
||||
* Add the correct display in Edge, IE 10+, and Firefox. |
||||
*/ |
||||
|
||||
details { |
||||
display: block; |
||||
} |
||||
|
||||
/* |
||||
* Add the correct display in all browsers. |
||||
*/ |
||||
|
||||
summary { |
||||
display: list-item; |
||||
} |
||||
|
||||
/* Misc |
||||
========================================================================== */ |
||||
|
||||
/** |
||||
* Add the correct display in IE 10+. |
||||
*/ |
||||
|
||||
template { |
||||
display: none; |
||||
} |
||||
|
||||
/** |
||||
* Add the correct display in IE 10. |
||||
*/ |
||||
|
||||
[hidden] { |
||||
display: none; |
||||
} |
@ -0,0 +1,231 @@ |
||||
@charset "UTF-8"; |
||||
|
||||
/*! |
||||
* Pikaday |
||||
* Copyright © 2014 David Bushell | BSD & MIT license | https://dbushell.com/ |
||||
*/ |
||||
|
||||
.pika-single { |
||||
z-index: 9999; |
||||
display: block; |
||||
position: relative; |
||||
color: #333; |
||||
background: #fff; |
||||
border: 1px solid #ccc; |
||||
border-bottom-color: #bbb; |
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; |
||||
} |
||||
|
||||
/* |
||||
clear child float (pika-lendar), using the famous micro clearfix hack |
||||
http://nicolasgallagher.com/micro-clearfix-hack/ |
||||
*/ |
||||
.pika-single:before, |
||||
.pika-single:after { |
||||
content: " "; |
||||
display: table; |
||||
} |
||||
.pika-single:after { clear: both } |
||||
|
||||
.pika-single.is-hidden { |
||||
display: none; |
||||
} |
||||
|
||||
.pika-single.is-bound { |
||||
position: absolute; |
||||
box-shadow: 0 5px 15px -5px rgba(0,0,0,.5); |
||||
} |
||||
|
||||
.pika-lendar { |
||||
float: left; |
||||
width: 240px; |
||||
margin: 8px; |
||||
} |
||||
|
||||
.pika-title { |
||||
position: relative; |
||||
text-align: center; |
||||
} |
||||
|
||||
.pika-label { |
||||
display: inline-block; |
||||
position: relative; |
||||
z-index: 9999; |
||||
overflow: hidden; |
||||
margin: 0; |
||||
padding: 5px 3px; |
||||
font-size: 14px; |
||||
line-height: 20px; |
||||
font-weight: bold; |
||||
background-color: #fff; |
||||
} |
||||
.pika-title select { |
||||
cursor: pointer; |
||||
position: absolute; |
||||
z-index: 9998; |
||||
margin: 0; |
||||
left: 0; |
||||
top: 5px; |
||||
opacity: 0; |
||||
} |
||||
|
||||
.pika-prev, |
||||
.pika-next { |
||||
display: block; |
||||
cursor: pointer; |
||||
position: relative; |
||||
outline: none; |
||||
border: 0; |
||||
padding: 0; |
||||
width: 20px; |
||||
height: 30px; |
||||
/* hide text using text-indent trick, using width value (it's enough) */ |
||||
text-indent: 20px; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
background-color: transparent; |
||||
background-position: center center; |
||||
background-repeat: no-repeat; |
||||
background-size: 75% 75%; |
||||
opacity: .5; |
||||
} |
||||
|
||||
.pika-prev:hover, |
||||
.pika-next:hover { |
||||
opacity: 1; |
||||
} |
||||
|
||||
.pika-prev, |
||||
.is-rtl .pika-next { |
||||
float: left; |
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAUklEQVR42u3VMQoAIBADQf8Pgj+OD9hG2CtONJB2ymQkKe0HbwAP0xucDiQWARITIDEBEnMgMQ8S8+AqBIl6kKgHiXqQqAeJepBo/z38J/U0uAHlaBkBl9I4GwAAAABJRU5ErkJggg=='); |
||||
} |
||||
|
||||
.pika-next, |
||||
.is-rtl .pika-prev { |
||||
float: right; |
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAeCAYAAAAsEj5rAAAAU0lEQVR42u3VOwoAMAgE0dwfAnNjU26bYkBCFGwfiL9VVWoO+BJ4Gf3gtsEKKoFBNTCoCAYVwaAiGNQGMUHMkjGbgjk2mIONuXo0nC8XnCf1JXgArVIZAQh5TKYAAAAASUVORK5CYII='); |
||||
} |
||||
|
||||
.pika-prev.is-disabled, |
||||
.pika-next.is-disabled { |
||||
cursor: default; |
||||
opacity: .2; |
||||
} |
||||
|
||||
.pika-select { |
||||
display: inline-block; |
||||
} |
||||
|
||||
.pika-table { |
||||
width: 100%; |
||||
border-collapse: collapse; |
||||
border-spacing: 0; |
||||
border: 0; |
||||
} |
||||
|
||||
.pika-table th, |
||||
.pika-table td { |
||||
width: 14.285714285714286%; |
||||
padding: 0; |
||||
} |
||||
|
||||
.pika-table th { |
||||
color: #999; |
||||
font-size: 12px; |
||||
line-height: 25px; |
||||
font-weight: bold; |
||||
text-align: center; |
||||
} |
||||
|
||||
.pika-button { |
||||
cursor: pointer; |
||||
display: block; |
||||
box-sizing: border-box; |
||||
-moz-box-sizing: border-box; |
||||
outline: none; |
||||
border: 0; |
||||
margin: 0; |
||||
width: 100%; |
||||
padding: 5px; |
||||
color: #666; |
||||
font-size: 12px; |
||||
line-height: 15px; |
||||
text-align: right; |
||||
background: #f5f5f5; |
||||
height: initial; |
||||
} |
||||
|
||||
.pika-week { |
||||
font-size: 11px; |
||||
color: #999; |
||||
} |
||||
|
||||
.is-today .pika-button { |
||||
color: #33aaff; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
.is-selected .pika-button, |
||||
.has-event .pika-button { |
||||
color: #fff; |
||||
font-weight: bold; |
||||
background: #33aaff; |
||||
box-shadow: inset 0 1px 3px #178fe5; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
.has-event .pika-button { |
||||
background: #005da9; |
||||
box-shadow: inset 0 1px 3px #0076c9; |
||||
} |
||||
|
||||
.is-disabled .pika-button, |
||||
.is-inrange .pika-button { |
||||
background: #D5E9F7; |
||||
} |
||||
|
||||
.is-startrange .pika-button { |
||||
color: #fff; |
||||
background: #6CB31D; |
||||
box-shadow: none; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
.is-endrange .pika-button { |
||||
color: #fff; |
||||
background: #33aaff; |
||||
box-shadow: none; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
.is-disabled .pika-button { |
||||
pointer-events: none; |
||||
cursor: default; |
||||
color: #999; |
||||
opacity: .3; |
||||
} |
||||
|
||||
.is-outside-current-month .pika-button { |
||||
color: #999; |
||||
opacity: .3; |
||||
} |
||||
|
||||
.is-selection-disabled { |
||||
pointer-events: none; |
||||
cursor: default; |
||||
} |
||||
|
||||
.pika-button:hover, |
||||
.pika-row.pick-whole-week:hover .pika-button { |
||||
color: #fff; |
||||
background: #ff8000; |
||||
box-shadow: none; |
||||
border-radius: 3px; |
||||
} |
||||
|
||||
/* styling for abbr */ |
||||
.pika-table abbr { |
||||
border-bottom: none; |
||||
cursor: help; |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
||||
!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.dayjs_plugin_customParseFormat=n()}(this,function(){"use strict";var t,n=/(\[[^[]*\])|([-:/.()\s]+)|(A|a|YYYY|YY?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,e=/\d\d/,r=/\d\d?/,o=/\d*[^\s\d-:/.()]+/;var s=function(t){return function(n){this[t]=+n}},i=[/[+-]\d\d:?\d\d/,function(t){var n,e;(this.zone||(this.zone={})).offset=(n=t.match(/([+-]|\d\d)/g),0===(e=60*n[1]+ +n[2])?0:"+"===n[0]?-e:e)}],a={A:[/[AP]M/,function(t){this.afternoon="PM"===t}],a:[/[ap]m/,function(t){this.afternoon="pm"===t}],S:[/\d/,function(t){this.milliseconds=100*+t}],SS:[e,function(t){this.milliseconds=10*+t}],SSS:[/\d{3}/,function(t){this.milliseconds=+t}],s:[r,s("seconds")],ss:[r,s("seconds")],m:[r,s("minutes")],mm:[r,s("minutes")],H:[r,s("hours")],h:[r,s("hours")],HH:[r,s("hours")],hh:[r,s("hours")],D:[r,s("day")],DD:[e,s("day")],Do:[o,function(n){var e=t.ordinal,r=n.match(/\d+/);if(this.day=r[0],e)for(var o=1;o<=31;o+=1)e(o).replace(/\[|\]/g,"")===n&&(this.day=o)}],M:[r,s("month")],MM:[e,s("month")],MMM:[o,function(n){var e=t,r=e.months,o=e.monthsShort,s=o?o.findIndex(function(t){return t===n}):r.findIndex(function(t){return t.substr(0,3)===n});if(s<0)throw new Error;this.month=s+1}],MMMM:[o,function(n){var e=t.months.indexOf(n);if(e<0)throw new Error;this.month=e+1}],Y:[/[+-]?\d+/,s("year")],YY:[e,function(t){t=+t,this.year=t+(t>68?1900:2e3)}],YYYY:[/\d{4}/,s("year")],Z:i,ZZ:i};var u=function(t,e,r){try{var o=function(t){for(var e=t.match(n),r=e.length,o=0;o<r;o+=1){var s=e[o],i=a[s],u=i&&i[0],f=i&&i[1];e[o]=f?{regex:u,parser:f}:s.replace(/^\[|\]$/g,"")}return function(t){for(var n={},o=0,s=0;o<r;o+=1){var i=e[o];if("string"==typeof i)s+=i.length;else{var a=i.regex,u=i.parser,f=t.substr(s),h=a.exec(f)[0];u.call(n,h),t=t.replace(h,"")}}return function(t){var n=t.afternoon;if(void 0!==n){var e=t.hours;n?e<12&&(t.hours+=12):12===e&&(t.hours=0),delete t.afternoon}}(n),n}}(e)(t),s=o.year,i=o.month,u=o.day,f=o.hours,h=o.minutes,d=o.seconds,c=o.milliseconds,m=o.zone;if(m)return new Date(Date.UTC(s,i-1,u,f||0,h||0,d||0,c||0)+60*m.offset*1e3);var l=new Date,v=u||(s||i?1:l.getDate()),p=s||l.getFullYear(),M=i>0?i-1:l.getMonth(),y=f||0,D=h||0,Y=d||0,g=c||0;return r?new Date(Date.UTC(p,M,v,y,D,Y,g)):new Date(p,M,v,y,D,Y,g)}catch(t){return new Date("")}};return function(n,e,r){var o=e.prototype,s=o.parse;o.parse=function(n){var e=n.date,o=n.format,i=n.pl,a=n.utc;this.$u=a,o?(t=i?r.Ls[i]:this.$locale(),this.$d=u(e,o,a),this.init(n),i&&(this.$L=i)):s.call(this,n)}}}); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,440 @@ |
||||
:root { |
||||
--cdr-border: #ccc; |
||||
--cdr-header-border: #bbb; |
||||
--cdr-header-color: #cfd7ff; |
||||
|
||||
|
||||
--cdr-odd-row: #eaeaea; |
||||
--cdr-even-row: #f3f3f3; |
||||
--cdr-group-row: #fffadf; |
||||
--cdr-active-row: #ffb0ee; |
||||
|
||||
|
||||
--filter-wildcard-color: #444; |
||||
--filter-wildcard-active-color: #ff18ed; |
||||
|
||||
--important-date-background: #ffa3da; |
||||
--weekend-background: #c6dfff; |
||||
|
||||
--filter-notify-active-color: #ff4df1; |
||||
--filter-notify-color: #bfb0be; |
||||
|
||||
--filter-calltype-color: #212121; |
||||
--filter-calltype-active-color: #ff18ed; |
||||
|
||||
--filter-calltype-transition1-color: #555; |
||||
--filter-calltype-transition2-color: #ff4df1; |
||||
|
||||
|
||||
} |
||||
|
||||
|
||||
.left-panel .filters { |
||||
position: relative; |
||||
padding-left: .8rem; |
||||
margin-bottom: .75rem; |
||||
} |
||||
|
||||
.left-panel .filters > * { |
||||
margin-bottom: .5rem; |
||||
} |
||||
|
||||
.left-panel .filters .filter .wrapper { |
||||
display: flex; align-items: center; |
||||
} |
||||
.left-panel .filters .filter .value { |
||||
flex: 1; |
||||
min-width: 6rem; |
||||
margin-right: .5rem; |
||||
background-color: var(--textbox-background); |
||||
} |
||||
.left-panel .filters .filter .wildcard { |
||||
width: 1.4rem; |
||||
height: 1.4rem; |
||||
margin-right: .5rem; |
||||
padding: .1rem; |
||||
cursor: pointer; |
||||
fill: var(--filter-wildcard-color); |
||||
transition: fill .15s; |
||||
} |
||||
.left-panel .filters .filter .wildcard.active { |
||||
fill: var(--filter-wildcard-active-color); |
||||
} |
||||
.left-panel .filters .filter .status { |
||||
width: .5rem; |
||||
height: .5rem; |
||||
background-color: var(--filter-notify-color); |
||||
border-radius: .25rem; |
||||
transition: background-color .2s ease-out; |
||||
} |
||||
.left-panel .filters .filter .status.active { |
||||
background-color: var(--filter-notify-active-color); |
||||
} |
||||
|
||||
|
||||
.left-panel .filters .call-types { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
.left-panel .filters .call-types .call-type { |
||||
display: flex; |
||||
flex-direction: column; |
||||
margin-left: .5rem; |
||||
margin-right: .5rem; |
||||
cursor: pointer; |
||||
} |
||||
.left-panel .filters .call-types .call-type svg { |
||||
height: 1.5rem; |
||||
fill: var(--filter-calltype-color); |
||||
transition: .15s fill; |
||||
} |
||||
.left-panel .filters .call-types .call-type span { |
||||
margin-top: .25rem; |
||||
font-size: 0.85rem; |
||||
color: var(--filter-calltype-color); |
||||
transition: .15s color; |
||||
} |
||||
.left-panel .filters .call-types .call-type.active svg { |
||||
fill: var(--filter-calltype-active-color); |
||||
} |
||||
.left-panel .filters .call-types .call-type.active span { |
||||
color: var(--filter-calltype-active-color); |
||||
} |
||||
.left-panel .filters .call-types .call-type:hover svg { |
||||
fill: var(--filter-calltype-transition1-color); |
||||
} |
||||
.left-panel .filters .call-types .call-type:hover span { |
||||
color: var(--filter-calltype-transition1-color); |
||||
} |
||||
.left-panel .filters .call-types .call-type.active:hover svg { |
||||
fill: var(--filter-calltype-transition2-color); |
||||
} |
||||
.left-panel .filters .call-types .call-type.active:hover span { |
||||
color: var(--filter-calltype-transition2-color); |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
.left-panel .filters .buttons { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
.left-panel .filters .buttons > .button { |
||||
flex: 1; |
||||
min-width: fit-content; |
||||
font-size: .9rem; |
||||
font-weight: 500; |
||||
margin-left: .5rem; margin-right: .5rem; |
||||
} |
||||
.left-panel .filters .buttons > .button:first-child { |
||||
margin-left: 0; |
||||
} |
||||
.left-panel .filters .buttons > .button:last-child { |
||||
margin-right: 0; |
||||
} |
||||
|
||||
|
||||
.left-panel .settings { |
||||
position: relative; |
||||
padding-left: .8rem; |
||||
margin-bottom: .75rem; |
||||
} |
||||
.left-panel .settings .component { |
||||
margin-bottom: .5rem; |
||||
font-size: .9rem; |
||||
text-align: left; |
||||
} |
||||
.left-panel .settings .component:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
|
||||
.left-panel .new-entries { |
||||
margin-top: .5rem; |
||||
margin-bottom: .75rem; |
||||
display: flex; flex-direction: column; align-items: center; |
||||
opacity: 0; visibility: hidden; |
||||
transition: opacity .1s, visibility .1s; |
||||
} |
||||
.left-panel .new-entries.active { |
||||
opacity: 1; visibility: visible; |
||||
} |
||||
.left-panel .new-entries .text { |
||||
display: flex; justify-content: space-around; |
||||
} |
||||
.left-panel .new-entries span { |
||||
font-size: 1.1rem; |
||||
margin-bottom: .5rem; |
||||
} |
||||
.left-panel .new-entries .num { |
||||
font-weight: 600; |
||||
color: var(--important); |
||||
} |
||||
.left-panel .new-entries .update { |
||||
min-width: 7.5rem; |
||||
font-size: .9rem; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
.right-panel .cdr { |
||||
flex-grow: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
.cdr .table { |
||||
display: flex; |
||||
flex-direction: column; |
||||
min-width: 1000px; |
||||
} |
||||
|
||||
.cdr .header { |
||||
display: flex; |
||||
background-color: var(--cdr-header-color); |
||||
border: 1px solid var(--cdr-header-border); |
||||
user-select: none; |
||||
} |
||||
|
||||
.cdr .header .cell { |
||||
border-right: 1px solid var(--cdr-header-border); |
||||
} |
||||
|
||||
.cdr .header .cell-group { |
||||
padding: 0; |
||||
flex-grow: 0; |
||||
border: none; |
||||
display: flex; |
||||
} |
||||
|
||||
.cdr .header .cell-group_col { |
||||
flex-direction: column; |
||||
align-items: stretch; |
||||
} |
||||
|
||||
.cdr .header .cell-group_col > .cell:not(:last-child) { |
||||
border-bottom: 1px solid var(--cdr-header-border); |
||||
padding-top: .25rem; |
||||
padding-bottom: .25rem; |
||||
} |
||||
|
||||
.cdr .header .cell-group_row > .cell:last-of-type { |
||||
border-right: 1px solid var(--cdr-header-border); |
||||
} |
||||
|
||||
.cdr .header .cell-clickable-area { |
||||
padding-top: .25rem; |
||||
padding-bottom: .25rem; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
flex: 1; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.cdr .header .cell.sortable .name { |
||||
color: var(--link-normal-color); |
||||
transition: .15s color; |
||||
} |
||||
|
||||
.cdr .header .cell.sortable .cell-clickable-area:hover .name { |
||||
color: var(--link-active-color); |
||||
} |
||||
|
||||
.cdr .header .cell.sortable .sort-button { |
||||
display: none; |
||||
width: .7rem; |
||||
height: .7rem; |
||||
margin-left: .4rem; |
||||
cursor: pointer; |
||||
transition: .15s fill, .3s transform; |
||||
transform-origin: center center; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
.cdr .header .cell.sortable .sort-button.sort-asc { |
||||
display: flex; |
||||
transform: rotate(180deg); |
||||
} |
||||
|
||||
.cdr .header .cell.sortable .sort-button.sort-desc { |
||||
display: flex; |
||||
} |
||||
|
||||
.cdr .header .cell.sortable .cell-clickable-area:hover .sort-button { |
||||
fill: var(--link-active-color); |
||||
} |
||||
|
||||
|
||||
.cdr .cell { |
||||
flex-grow: 1; |
||||
|
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
|
||||
border-right: 1px solid var(--border); |
||||
padding-left: .75rem; |
||||
padding-right: .75rem; |
||||
min-height: 1.35rem; |
||||
} |
||||
|
||||
.cdr .cell:last-of-type { |
||||
border-right: none; |
||||
} |
||||
|
||||
.cdr .cell.cell_id { min-width: 3.5rem; max-width: 3.5rem; } |
||||
.cdr .cell.cell_dailyct { min-width: 6rem; max-width: 6rem; } |
||||
.cdr .cell.cell_type { min-width: 4.5rem; max-width: 4.5rem; } |
||||
.cdr .cell.cell_date { min-width: 8.5%; max-width: 8.5%; } |
||||
.cdr .cell.cell_source { min-width: 12.5%; flex-basis: 100%; } |
||||
.cdr .cell.cell_destination { min-width: 12.5%; flex-basis: 100%; } |
||||
.cdr .cell.cell_total-duration { min-width: 5rem; max-width: 5rem; } |
||||
.cdr .cell.cell_call-duration { min-width: 5rem; max-width: 5rem; } |
||||
.cdr .cell.cell_result { min-width: 8rem; max-width: 8rem; } |
||||
.cdr .cell.cell_records { min-width: 7rem; max-width: 7rem; } |
||||
|
||||
|
||||
.cdr .body { |
||||
display: flex; |
||||
flex-direction: column; |
||||
border: 1px solid var(--cdr-border); |
||||
border-top: none; |
||||
max-height: calc(100vh - 12rem); |
||||
overflow-x: hidden; |
||||
} |
||||
|
||||
.cdr .body .entry:nth-child(even) { |
||||
background-color: var(--cdr-even-row); |
||||
} |
||||
|
||||
.cdr .body .entry:nth-child(odd) { |
||||
background-color: var(--cdr-odd-row); |
||||
} |
||||
|
||||
.cdr .body .entry { |
||||
display: flex; |
||||
min-height: 2.5rem; |
||||
transition: background-color .1s; |
||||
} |
||||
.cdr .body .entry.active { |
||||
background-color: var(--cdr-active-row); |
||||
} |
||||
|
||||
.cdr .body .cell { |
||||
font-size: 0.95rem; |
||||
} |
||||
|
||||
.cdr .body .cell .image { |
||||
width: 1.5rem; |
||||
height: 1.5rem; |
||||
} |
||||
|
||||
.cdr .body .cell .image.link { |
||||
width: 1.25rem; |
||||
height: 1.25rem; |
||||
cursor: pointer; |
||||
transition: fill .15s; |
||||
margin-left: .4rem; |
||||
margin-right: .4rem; |
||||
} |
||||
|
||||
.cdr .body .cell .image.link:hover { |
||||
fill: var(--control-selected); |
||||
} |
||||
|
||||
.cdr .body .entry.group { |
||||
background-color: var(--cdr-group-row); |
||||
display: flex; |
||||
align-items: center; |
||||
padding-left: 1rem; padding-right: 1rem; |
||||
} |
||||
.cdr .body .entry.group:not(:nth-child(2)) { |
||||
border-top: 1px solid var(--cdr-border); |
||||
} |
||||
.cdr .body .entry.group:not(:last-child) { |
||||
border-bottom: 1px solid var(--cdr-border); |
||||
} |
||||
.cdr .body .entry.group .group-text { |
||||
font-size: 1.2rem; |
||||
font-weight: 500; |
||||
min-width: 8rem; |
||||
text-align: left; |
||||
} |
||||
.cdr .body .entry.group .stats { |
||||
display: flex; |
||||
align-items: center; |
||||
margin-left: 1rem; |
||||
} |
||||
.cdr .body .entry.group .stats .image { |
||||
width: 1.35rem; |
||||
height: 1.35rem; |
||||
margin-right: .5rem; |
||||
margin-left: 2rem; |
||||
} |
||||
.cdr .body .entry.group .stats .num-calls { |
||||
text-align: left; |
||||
min-width: 1.5rem; |
||||
} |
||||
|
||||
|
||||
.cdr .body .no-cdrs { |
||||
display: none; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
padding-bottom: 2rem; |
||||
} |
||||
.cdr .body .no-cdrs.active { |
||||
display: flex; |
||||
} |
||||
.cdr .body .no-cdrs .text { |
||||
font-size: 2rem; |
||||
} |
||||
|
||||
.cdr .controls { |
||||
min-height: 2.5rem; |
||||
max-height: 2.5rem; |
||||
|
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
align-self: center; |
||||
border-top: 1px solid var(--border); |
||||
border-bottom: 1px solid var(--border); |
||||
padding-left: 1rem; |
||||
padding-right: 1rem; |
||||
margin-top: 1rem; |
||||
|
||||
user-select: none; |
||||
} |
||||
|
||||
.cdr .controls .button { |
||||
width: 1.5rem; |
||||
height: 1.5rem; |
||||
margin-left: .75rem; |
||||
margin-right: .75rem; |
||||
} |
||||
|
||||
.cdr .controls .button.active { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.cdr .controls .button { fill: var(--control-disabled); transition: fill .15s; } |
||||
.cdr .controls .button.active { fill: var(--control-enabled); } |
||||
.cdr .controls .button.active:hover { fill: var(--control-selected); } |
||||
|
||||
.cdr .controls .status { |
||||
margin-left: 2rem; |
||||
margin-right: 2rem; |
||||
font-size: 1.15rem; |
||||
min-width: 5rem; |
||||
} |
||||
|
||||
.cdr .controls .goto-page { |
||||
margin-left: 1rem; |
||||
width: 3.5rem; |
||||
} |
@ -0,0 +1,274 @@ |
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Regular.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Regular.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Regular.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Regular.ttf") format("truetype"); |
||||
|
||||
font-weight: normal; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Italic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Italic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Italic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Italic.ttf") format("truetype"); |
||||
|
||||
font-weight: normal; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Thin.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Thin.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Thin.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Thin.ttf") format("truetype"); |
||||
|
||||
font-weight: 100; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-ThinItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-ThinItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-ThinItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-ThinItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 100; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-ExtraLight.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-ExtraLight.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-ExtraLight.woff") format("woff"), |
||||
url("/public/fonts/Exo2-ExtraLight.ttf") format("truetype"); |
||||
|
||||
font-weight: 200; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-ExtraLightItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-ExtraLightItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-ExtraLightItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-ExtraLightItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 200; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Light.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Light.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Light.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Light.ttf") format("truetype"); |
||||
|
||||
font-weight: 300; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-LightItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-LightItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-LightItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-LightItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 300; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Medium.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Medium.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Medium.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Medium.ttf") format("truetype"); |
||||
|
||||
font-weight: 500; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-MediumItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-MediumItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-MediumItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-MediumItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 500; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-SemiBold.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-SemiBold.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-SemiBold.woff") format("woff"), |
||||
url("/public/fonts/Exo2-SemiBold.ttf") format("truetype"); |
||||
|
||||
font-weight: 600; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-SemiBoldItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-SemiBoldItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-SemiBoldItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-SemiBoldItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 600; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Bold.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Bold.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Bold.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Bold.ttf") format("truetype"); |
||||
|
||||
font-weight: bold; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-BoldItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-BoldItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-BoldItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-BoldItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: bold; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-ExtraBold.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-ExtraBold.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-ExtraBold.woff") format("woff"), |
||||
url("/public/fonts/Exo2-ExtraBold.ttf") format("truetype"); |
||||
|
||||
font-weight: 800; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-ExtraBoldItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-ExtraBoldItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-ExtraBoldItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-ExtraBoldItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 800; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-Black.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-Black.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-Black.woff") format("woff"), |
||||
url("/public/fonts/Exo2-Black.ttf") format("truetype"); |
||||
|
||||
font-weight: 900; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-BlackItalic.eot?") format("embedded-opentype"), |
||||
url("/public/fonts/Exo2-BlackItalic.woff2") format("woff2"), |
||||
url("/public/fonts/Exo2-BlackItalic.woff") format("woff"), |
||||
url("/public/fonts/Exo2-BlackItalic.ttf") format("truetype"); |
||||
|
||||
font-weight: 900; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
|
||||
@supports (font-variation-settings: normal) { |
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-VF.woff2") format("woff2 supports variations"), |
||||
url("/public/fonts/Exo2-VF.woff2") format("woff2-variations"); |
||||
|
||||
font-weight: 100 900; |
||||
font-style: normal; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
|
||||
@font-face { |
||||
font-family: "Exo 2"; |
||||
src: url("/public/fonts/Exo2-VFI.woff2") format("woff2 supports variations"), |
||||
url("/public/fonts/Exo2-VFI.woff2") format("woff2-variations"); |
||||
|
||||
font-weight: 100 900; |
||||
font-style: italic; |
||||
font-display: block; |
||||
unicode-range: U+00??, U+04??, U+20??, U+226?; |
||||
} |
||||
} |
@ -0,0 +1,616 @@ |
||||
/* // TODO: fix all transition syntax |
||||
// TODO: double-colon selectors */ |
||||
|
||||
*, ::after, ::before { |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
:root { |
||||
--root-font-size: 14px; |
||||
|
||||
--text: #212121; |
||||
--background: #fafafa; |
||||
--border: #ccc; |
||||
|
||||
--action: #ff9000; |
||||
--important: #ff4df1; |
||||
|
||||
--link-normal-color: #0d27fa; |
||||
--link-normal-sub-color: #475bff; |
||||
--link-active-color: #ff9000; |
||||
|
||||
--footer-background: #ddd; |
||||
|
||||
--left-panel-background: #c8e0cf; |
||||
--left-panel-header-background: #b8e2ba; |
||||
|
||||
--right-panel-color: #f1f1f1; |
||||
--divider-color: #a6a6a6; |
||||
|
||||
--textbox-border: #888; |
||||
--textbox-selected-border: #ff9000; |
||||
--textbox-selected-shadow: #ff810094; |
||||
--textbox-background: #fafafa; |
||||
|
||||
--checkbox-color: #ff4df1; |
||||
|
||||
--control-disabled: #aaa; |
||||
--control-enabled: #050505; |
||||
--control-selected: #ff9000; |
||||
|
||||
--scrollbar-background: #ddd; |
||||
--scrollbar-color: #aaa; |
||||
|
||||
--shadow-color: 128,128,128; |
||||
|
||||
--message-wrapper-background: rgba(190, 190, 190, 0.85); |
||||
--message-shadow-background: rgb(190, 190, 190); |
||||
--message-background: #fafafa; |
||||
|
||||
|
||||
--button-background: #ffd96f; |
||||
--button-selected-shadow: #ffd96f63; |
||||
--button-selected-color: #070707; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
html { |
||||
font-size: var(--root-font-size); |
||||
} |
||||
body { |
||||
padding: 0; |
||||
min-height: 100vh; |
||||
display: flex; |
||||
|
||||
font-size: 1rem; |
||||
font-family: 'Exo 2', 'Open Sans', 'Roboto', 'Segoe UI', Tahoma, sans-serif; |
||||
font-weight: normal; |
||||
letter-spacing: 0.02rem; |
||||
text-align: center; |
||||
|
||||
background-color: var(--background); |
||||
color: var(--text); |
||||
} |
||||
ol, ul { |
||||
list-style-type: none; |
||||
margin: 0; |
||||
padding: 0; |
||||
overflow: hidden; |
||||
} |
||||
a { |
||||
text-decoration: none; |
||||
} |
||||
svg { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
label { |
||||
} |
||||
|
||||
|
||||
|
||||
:root { |
||||
--accent-border: #ccc; |
||||
|
||||
--component-border: #999; |
||||
--component-background: #fafafa; |
||||
--component-focus: #cccccca9; |
||||
--component-shadow-action: #ff8d0099; |
||||
|
||||
--button-inset-shadow: #555555aa; |
||||
--cb-tick: #e31fe6; |
||||
|
||||
--window-wrapper-background: rgba(190, 190, 190, 0.85); |
||||
--window-shadow-background: rgb(190, 190, 190); |
||||
--window-background: #fafafa; |
||||
} |
||||
|
||||
|
||||
.component { |
||||
display: flex; justify-content: flex-start; align-items: center; |
||||
} |
||||
.component.column { |
||||
flex-direction: column; |
||||
} |
||||
.component > label { |
||||
flex-basis: auto; |
||||
width: max-content; |
||||
} |
||||
.component > input, .component > button { |
||||
position: relative; |
||||
} |
||||
.component > input[type='checkbox'], .component > input[type='checkbox'] + label, .component > button { |
||||
cursor: pointer; |
||||
} |
||||
.component > input[type='checkbox'] { |
||||
width: 1.25rem; height: 1.25rem; |
||||
min-width: 1.25rem; min-height: 1.25rem; |
||||
max-width: 1.25rem; max-height: 1.25rem; |
||||
} |
||||
.component > input + label, .component > label + input { |
||||
margin-left: .5rem; |
||||
} |
||||
.component.column > input + label, .component.column > label + input { |
||||
margin-left: 0; |
||||
} |
||||
.component > button { |
||||
min-width: 5rem; |
||||
min-height: 1.7rem; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
.component.column > label { |
||||
margin-bottom: .25rem; |
||||
} |
||||
|
||||
|
||||
@supports (-webkit-appearance: none) or (-moz-appearance: none) or (appearance: none) { |
||||
.component > input, .component > button { |
||||
-webkit-appearance: none; -moz-appearance: none; appearance: none; |
||||
outline: none; |
||||
border: 1px solid var(--component-border-action, var(--component-border)); |
||||
background: var(--component-background-action, var(--component-background)); |
||||
border-radius: 3px; |
||||
transition: border-color .15s, background-color .15s, box-shadow .15s; |
||||
} |
||||
.component > input[type='checkbox']:checked:not(:hover) { |
||||
--component-border-action: #666; |
||||
--component-background-action: #fff4ea; |
||||
} |
||||
.component > input:hover, .component > button:hover { |
||||
--component-border-action: #ff8d00; |
||||
box-shadow: 0 0 3px 1px var(--component-shadow-action); |
||||
} |
||||
.component > input:focus:not(:hover), .component > button:focus:not(:hover) { |
||||
box-shadow: 0 0 1px 2px var(--component-focus); |
||||
} |
||||
.component > input[type='checkbox']::after { |
||||
content: ''; |
||||
display: block; |
||||
left: 0; |
||||
top: 0; |
||||
position: absolute; |
||||
width: 30%; |
||||
height: 60%; |
||||
border: 2px solid var(--cb-tick); |
||||
border-top: 0; |
||||
border-left: 0; |
||||
left: 35%; |
||||
top: 15%; |
||||
opacity: 0; |
||||
transform: rotate(45deg); |
||||
transition: opacity .15s; |
||||
} |
||||
.component > input[type='checkbox']:checked::after { |
||||
opacity: 1; |
||||
} |
||||
|
||||
.component > input[type='text'] { |
||||
min-height: 1.6rem; |
||||
padding: .15rem .3rem; |
||||
min-width: 12.5rem; |
||||
width: 100%; |
||||
} |
||||
|
||||
.component > button::after { |
||||
left: -1px; |
||||
top: -1px; |
||||
content: ''; |
||||
position: absolute; |
||||
width: calc(100% + 2px); |
||||
height: calc(100% + 2px); |
||||
opacity: 0; |
||||
border-radius: 3px; |
||||
border: 1px solid transparent; |
||||
box-shadow: inset 0 0 2px 1px var(--button-inset-shadow); |
||||
transition: opacity .15s; |
||||
} |
||||
.component > button:active::after { |
||||
opacity: 1; |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
.hidden { |
||||
display: none !important; |
||||
} |
||||
|
||||
|
||||
.shadow-bottom-inset { |
||||
box-shadow: 0 10px 5px -10px rgba(var(--shadow-color), 0.75) inset; |
||||
} |
||||
|
||||
|
||||
.textbox { |
||||
padding: .25rem .5rem; |
||||
background-color: var(--background); |
||||
border: 1px solid var(--textbox-border); |
||||
border-radius: .2rem; |
||||
transition: box-shadow .2s ease-out; |
||||
} |
||||
|
||||
.textbox:focus { |
||||
border-color: var(--textbox-selected-border); |
||||
box-shadow: 0 0 0 .1rem var(--textbox-selected-shadow); |
||||
outline: .15rem solid transparent; |
||||
} |
||||
|
||||
|
||||
|
||||
/* 3rd party css styling */ |
||||
|
||||
.pd-theme td:not(.is-selected):not(.is-today) [data-pika-month="2"][data-pika-day="8"] { |
||||
background-color: var(--important-date-background); |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.pd-theme .pika-row td:nth-last-child(-n+2):not(.is-selected) .pika-button:not(:hover) { |
||||
background-color: var(--weekend-background); |
||||
} |
||||
|
||||
.simplebar-track.simplebar-vertical { |
||||
width: 1rem !important; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.body-spacer { |
||||
width: 5vw; |
||||
max-width: 5vw; |
||||
} |
||||
|
||||
.body-wrapper { |
||||
max-width: 1500px; |
||||
width: fit-content; |
||||
|
||||
display: flex; |
||||
flex-direction: column; |
||||
margin: 0 auto; |
||||
flex-grow: 1; |
||||
} |
||||
|
||||
.panel-wrapper { |
||||
flex-grow: 1; |
||||
display: flex; |
||||
} |
||||
|
||||
.left-panel { |
||||
max-width: 15.5rem; |
||||
min-width: 12.5rem; |
||||
flex-basis: 100%; |
||||
background-color: var(--left-panel-background); |
||||
border-right: 1px solid var(--border); |
||||
|
||||
display: flex; |
||||
flex-direction: column; |
||||
user-select: none; |
||||
} |
||||
|
||||
.left-panel > .header { |
||||
height: 3rem; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
border-bottom: 1px solid var(--border); |
||||
background-color: var(--left-panel-header-background); |
||||
} |
||||
|
||||
.left-panel .header .text { |
||||
font-size: 1.5rem; |
||||
font-weight: 700; |
||||
} |
||||
|
||||
.left-panel .sections { |
||||
padding: .5rem 0 .5rem 1rem; |
||||
} |
||||
|
||||
.left-panel .section { |
||||
margin: .25rem 0; |
||||
position: relative; |
||||
display: flex; align-items: center; |
||||
} |
||||
|
||||
.left-panel .section .clickable-area { |
||||
width: 80%; max-width: 100%; |
||||
padding: .5rem 1rem .5rem .5rem; |
||||
cursor: pointer; |
||||
display: flex; align-items: center; |
||||
} |
||||
.left-panel .section .image { |
||||
min-width: 1.5rem; max-width: 1.5rem; width: 1.5rem; |
||||
min-height: 1.5rem; max-height: 1.5rem; height: 1.5rem; |
||||
margin-right: 1rem; |
||||
transition: fill .15s; |
||||
} |
||||
.left-panel .section .text { |
||||
font-size: 1.15rem; font-weight: 300; |
||||
color: var(--link-normal-color); |
||||
transition: color .15s; |
||||
} |
||||
.left-panel .clickable-area:hover .text, .left-panel .clickable-area:active .text { |
||||
color: var(--link-active-color); |
||||
} |
||||
.left-panel .clickable-area:hover .image, .left-panel .clickable-area:active .image { |
||||
fill: var(--link-active-color); |
||||
} |
||||
|
||||
.left-panel .section .arrow { |
||||
display: block; |
||||
width: 1rem; |
||||
height: 1rem; |
||||
border-right: 1px solid var(--border); |
||||
border-top: 1px solid var(--border); |
||||
transform: rotate(-135deg); |
||||
position: absolute; |
||||
right: -.51rem; |
||||
background-color: var(--right-panel-color); |
||||
} |
||||
|
||||
.left-panel .section.sub .clickable-area { |
||||
width: 100%; |
||||
padding: .25rem 0; |
||||
margin-right: .5rem; |
||||
} |
||||
.left-panel .section.sub .image { |
||||
min-width: 1rem; max-width: 1rem; width: 1rem; |
||||
min-height: 1rem; max-height: 1rem; height: 1rem; |
||||
margin-right: .5rem; |
||||
} |
||||
.left-panel .section.sub .text { |
||||
font-size: 1rem; |
||||
color: var(--link-normal-sub-color); |
||||
} |
||||
.left-panel .section.sub .clickable-area:hover .image { |
||||
fill: unset; |
||||
} |
||||
.left-panel .section.sub .clickable-area:hover .image.collapser { |
||||
fill: var(--link-active-color); |
||||
} |
||||
.left-panel .section.sub .dot-filler { |
||||
flex: 1; |
||||
height: 2px; |
||||
border-top: 2px dashed rgba(0,0,0,0.25); |
||||
margin-left: .5rem; margin-right: .5rem; |
||||
} |
||||
.left-panel .section.sub .collapser { |
||||
display: none; |
||||
margin-right: 0; |
||||
} |
||||
.left-panel .section.sub .collapser.active { |
||||
display: block; |
||||
} |
||||
|
||||
.left-panel .ss-line { |
||||
position: absolute; |
||||
height: 100%; |
||||
width: 2px; |
||||
border-left: 2px dashed rgba(0,0,0,0.25); |
||||
left: 0; |
||||
top: 0; |
||||
} |
||||
|
||||
|
||||
.left-panel .divider { display: none; } |
||||
.left-panel .divider:not(:last-child) { |
||||
display: block; |
||||
width: 85%; |
||||
border: 1px solid var(--divider-color); |
||||
background-color: var(--divider-color); |
||||
border-radius: 1px; |
||||
align-self: center; |
||||
margin-bottom: .25rem; |
||||
} |
||||
|
||||
.left-panel .divider + div { |
||||
padding: .5rem 1rem 1rem 1.5rem; |
||||
display: flex; flex-direction: column; align-items: stretch; |
||||
} |
||||
|
||||
.right-panel { |
||||
flex-grow: 1; |
||||
display: flex; |
||||
background-color: var(--right-panel-color); |
||||
padding: 1rem; |
||||
} |
||||
|
||||
.right-panel .no-section { |
||||
font-size: 2rem; |
||||
font-weight: 700; |
||||
flex: 1; |
||||
} |
||||
|
||||
|
||||
|
||||
.footer { |
||||
min-height: 2rem; |
||||
max-height: 2rem; |
||||
height: 2rem; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
|
||||
background-color: var(--footer-background); |
||||
border-top: 1px solid var(--border); |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.window-wrapper { |
||||
position: fixed; |
||||
width: 100%; height: 100%; |
||||
background-color: var(--window-wrapper-background); |
||||
z-index: 100; |
||||
|
||||
overflow: hidden; |
||||
display: flex; justify-content: center; align-items: center; |
||||
opacity: 0; |
||||
visibility: hidden; |
||||
transition: opacity .35s, visibility .35s; |
||||
} |
||||
.window-wrapper.active { |
||||
opacity: 1; |
||||
visibility: visible; |
||||
} |
||||
.window { |
||||
min-width: min(400px, 80vw); max-width: 1200px; |
||||
min-height: fit-content; max-height: 800px; |
||||
position: relative; |
||||
background-color: var(--window-background); |
||||
border-radius: 1rem; |
||||
display: flex; flex-direction: column; align-items: center; |
||||
padding: 2rem; |
||||
box-shadow: 0 0 100px 40px var(--window-shadow-background); |
||||
opacity: 0; |
||||
visibility: hidden; |
||||
transition: opacity .35s, visibility .35s; |
||||
} |
||||
.window.active { |
||||
opacity: 1; |
||||
visibility: visible; |
||||
} |
||||
.window .close-button { |
||||
position: absolute; |
||||
right: 1.5rem; top: 1.5rem; |
||||
width: 2rem; height: 2rem; |
||||
opacity: .3; |
||||
transition: opacity .2s; |
||||
cursor: pointer; |
||||
} |
||||
.window .close-button:hover { |
||||
opacity: 1; |
||||
} |
||||
.window .close-button::before, .window .close-button::after { |
||||
position: absolute; |
||||
left: calc(50% - 1px); |
||||
content: ' '; |
||||
height: 100%; |
||||
width: 2px; |
||||
background-color: var(--text); |
||||
} |
||||
.window .close-button::before { transform: rotate(45deg); } |
||||
.window .close-button::after { transform: rotate(-45deg); } |
||||
|
||||
.window > *:nth-child(n+2):not(:last-child) { |
||||
margin-bottom: 1.5rem; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
/* old stuff */ |
||||
|
||||
|
||||
.message-wrapper { |
||||
position: fixed; |
||||
width: 100%; |
||||
height: 100%; |
||||
background-color: var(--message-wrapper-background); |
||||
z-index: 100; |
||||
|
||||
overflow: hidden; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
opacity: 0; |
||||
visibility: hidden; |
||||
transition: opacity .35s, visibility .35s; |
||||
} |
||||
.message-wrapper.active { |
||||
opacity: 1; |
||||
visibility: visible; |
||||
} |
||||
.message { |
||||
min-width: min(400px, 80vw); max-width: 1200px; |
||||
min-height: min(300px, 80vh); max-height: 800px; |
||||
position: relative; |
||||
background-color: var(--message-background); |
||||
border-radius: 1rem; |
||||
display: flex; flex-direction: column; align-items: center; |
||||
padding: 2rem; |
||||
box-shadow: 0 0 100px 40px var(--message-shadow-background); |
||||
} |
||||
.message .close-button { |
||||
position: absolute; |
||||
right: 1.5rem; top: 1.5rem; |
||||
width: 2rem; height: 2rem; |
||||
opacity: .3; |
||||
transition: opacity .2s; |
||||
cursor: pointer; |
||||
} |
||||
.message .close-button:hover { |
||||
opacity: 1; |
||||
} |
||||
.message .close-button:before, .message .close-button:after { |
||||
position: absolute; |
||||
left: calc(50% - 1px); |
||||
content: ' '; |
||||
height: 100%; |
||||
width: 2px; |
||||
background-color: var(--text); |
||||
} |
||||
.message .close-button:before { transform: rotate(45deg); } |
||||
.message .close-button:after { transform: rotate(-45deg); } |
||||
|
||||
.message > *:nth-child(n+2):not(:last-child) { |
||||
margin-bottom: 1.5rem; |
||||
} |
||||
.message .icon { |
||||
width: 4rem; |
||||
height: 4rem; |
||||
display: flex; justify-content: center; align-items: center; |
||||
} |
||||
.message .header { |
||||
font-size: 2.25rem; |
||||
font-weight: 600; |
||||
margin-left: 1rem; margin-right: 1rem; |
||||
} |
||||
.message .text { |
||||
font-size: 1.25rem; |
||||
flex: 1; |
||||
} |
||||
.message .button { |
||||
min-width: 10rem; |
||||
min-height: 2.5rem; |
||||
background-color: var(--button-background); |
||||
border-radius: 2rem; |
||||
display: flex; justify-content: center; align-items: center; |
||||
cursor: pointer; |
||||
user-select: none; |
||||
transition: box-shadow .15s ease-out; |
||||
color: var(--text); |
||||
} |
||||
.message .button:hover { |
||||
box-shadow: 0 0 0 .1rem var(--button-selected-shadow); |
||||
outline: .15rem solid transparent; |
||||
color: var(--button-selected-color); |
||||
} |
||||
.message .button .button-text { |
||||
font-size: 1.1rem; |
||||
font-weight: 500; |
||||
} |
@ -0,0 +1,129 @@ |
||||
:root { |
||||
--player-background: #cfd7ff; |
||||
--player-thumb: #212121; |
||||
--player-track: #ccc; |
||||
|
||||
--player-playing-shadow: #f887ff; |
||||
--player-playing-border: #ff00ff; |
||||
} |
||||
|
||||
.player { |
||||
position: fixed; |
||||
z-index: 1; |
||||
height: 2.5rem; |
||||
min-width: 400px; |
||||
max-width: 550px; |
||||
width: 50vw; |
||||
align-self: center; |
||||
user-select: none; |
||||
|
||||
bottom: -3rem; |
||||
opacity: 0; |
||||
visibility: hidden; |
||||
transition: bottom .5s ease-in-out, opacity .5s, visibility .5s; |
||||
} |
||||
.player.active { |
||||
bottom: 0; |
||||
opacity: 1; |
||||
visibility: visible; |
||||
} |
||||
.player .controls { |
||||
display: flex; |
||||
align-items: center; |
||||
background-color: var(--player-background); |
||||
border-radius: .5rem .5rem 0 0; |
||||
padding: .2rem 1rem; |
||||
|
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
.player .controls > * { |
||||
margin-left: .5rem; |
||||
margin-right: .5rem; |
||||
} |
||||
.player .controls > *:first-child { margin-left: 0; } |
||||
.player .controls > *:last-child { margin-right: 0; } |
||||
.player .time, .player.duration { |
||||
min-width: 3rem; |
||||
} |
||||
.player .button { |
||||
min-width: 1.3rem; |
||||
min-height: 1.3rem; |
||||
max-width: 1.3rem; |
||||
max-height: 1.3rem; |
||||
width: 1.3rem; |
||||
height: 1.3rem; |
||||
fill: var(--control-enabled); |
||||
transition: fill .15s; |
||||
cursor: pointer; |
||||
display: block; |
||||
} |
||||
.player .button:hover { |
||||
fill: var(--control-selected); |
||||
} |
||||
.player .button.disabled { |
||||
display: none; |
||||
} |
||||
.player .slider { |
||||
outline: none; |
||||
|
||||
flex: 1; |
||||
min-height: 2rem; |
||||
height: 2rem; |
||||
max-height: 2rem; |
||||
min-width: 4rem; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
|
||||
.player .bar { |
||||
position: absolute; |
||||
z-index: -9; |
||||
background-color: var(--player-background); |
||||
border-radius: 1rem 1rem 0 0; |
||||
margin: 0; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
.player .bar.active { |
||||
bottom: 2.5rem; |
||||
} |
||||
|
||||
.player .volume-bar { |
||||
left: 2.85rem; |
||||
width: 2rem; |
||||
height: 6rem; |
||||
padding: .7rem .1rem .3rem .1rem; |
||||
bottom: -3.5rem; |
||||
|
||||
flex-direction: column; |
||||
transition: bottom .5s ease-in-out; |
||||
} |
||||
.player .volume-bar .volume-slider { |
||||
transform: rotate(270deg); |
||||
min-width: 5rem; |
||||
width: 5rem; |
||||
max-width: 5rem; |
||||
margin-top: 1.5rem; |
||||
} |
||||
|
||||
.player .speed-bar { |
||||
right: 3.3rem; |
||||
width: 12.25rem; |
||||
height: 2rem; |
||||
padding: .5rem .8rem .2rem .8rem; |
||||
bottom: 0; |
||||
|
||||
justify-content: center; |
||||
transition: bottom .5s ease-in-out; |
||||
} |
||||
.player .speed-bar .current-speed { |
||||
text-align: right; |
||||
min-width: 3rem; width: 3rem; max-width: 3rem; |
||||
margin-right: .5rem; |
||||
} |
||||
.player .speed-bar .speed-slider { |
||||
margin-right: 1rem; |
||||
min-width: 5rem; width: 5rem; max-width: 5rem; |
||||
} |
@ -0,0 +1,111 @@ |
||||
:root { |
||||
--collection-background: #fafafa; |
||||
--collection-border: #ccc; |
||||
--collection-buttons-background: #e5e5e5; |
||||
} |
||||
|
||||
.settings { |
||||
flex-direction: column; |
||||
text-align: left; |
||||
align-self: flex-start; |
||||
padding-left: .5rem; |
||||
padding-top: 1rem; |
||||
flex: 1; |
||||
} |
||||
.settings .category { |
||||
margin-bottom: 2.5rem; |
||||
} |
||||
.settings .category .header { |
||||
font-size: 1.75rem; |
||||
font-weight: 700; |
||||
border-bottom: 1px solid var(--accent-border); |
||||
padding-bottom: .5rem; |
||||
margin: 0; |
||||
margin-bottom: 1rem; |
||||
min-width: 25rem; max-width: 35rem; |
||||
} |
||||
.settings .category > .component { |
||||
margin-bottom: .75rem; |
||||
max-width: 25rem; |
||||
} |
||||
|
||||
.settings .category .collection { |
||||
display: flex; flex-direction: column; |
||||
margin-bottom: 1.25rem; |
||||
} |
||||
.settings .collection .name { |
||||
font-size: 1.15rem; |
||||
font-weight: 500; |
||||
margin-bottom: .5rem; |
||||
} |
||||
.settings .collection .content { |
||||
background-color: var(--collection-background); |
||||
border-radius: .25rem; |
||||
border: 1px solid var(--border); |
||||
min-width: 25rem; width: 25rem; max-width: 50rem; |
||||
min-height: 10rem; height: 10rem; max-height: 30rem; |
||||
display: flex; |
||||
resize: both; |
||||
overflow: auto; |
||||
} |
||||
.settings .collection .content .items { |
||||
flex: 1; |
||||
} |
||||
.settings .collection .content .buttons { |
||||
min-width: 8rem; width: 8rem; max-width: 8rem; |
||||
display: flex; flex-direction: column; |
||||
border-left: 1px solid var(--border); |
||||
background-color: var(--collection-buttons-background); |
||||
padding: .5rem; |
||||
} |
||||
.settings .collection .content .buttons .component:not(:last-child) { |
||||
margin-bottom: .25rem; |
||||
} |
||||
|
||||
.settings .subcat { |
||||
display: flex; |
||||
} |
||||
.settings .subcat .subcat-line { |
||||
background-color: var(--border); |
||||
max-width: 1px; width: 1px; |
||||
flex: 1; |
||||
margin-bottom: .75rem; |
||||
margin-right: .75rem; |
||||
} |
||||
.settings .subcat .content { |
||||
display: flex; flex-direction: column; |
||||
} |
||||
.settings .subcat .content > .component { |
||||
margin-bottom: .75rem; |
||||
} |
||||
|
||||
.settings > .save { |
||||
margin: 1rem 0; |
||||
font-size: 1.2rem; |
||||
width: 10rem; |
||||
height: 2.5rem; |
||||
} |
||||
|
||||
|
||||
#call-category-form .header { |
||||
font-size: 1.25rem; |
||||
font-weight: 500; |
||||
width: 80%; |
||||
align-self: center; |
||||
} |
||||
#call-category-form .call-category { |
||||
align-self: flex-start; |
||||
align-items: flex-start; |
||||
flex-direction: column; |
||||
width: 100%; |
||||
} |
||||
#call-category-form .buttons { |
||||
display: flex; |
||||
align-self: stretch; |
||||
} |
||||
#call-category-form .buttons .button { |
||||
flex: 1; |
||||
} |
||||
#call-category-form .buttons .button:first-child { |
||||
margin-right: 2rem; |
||||
} |
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,634 @@ |
||||
'use strict'; |
||||
|
||||
let selectedRecord = null; |
||||
let selectedElem = null; |
||||
|
||||
const prettifyThing = (name, value, record = null, globalParms = null) => { |
||||
if (name === 'source' || name === 'destination' || name === 'any') { |
||||
return common.prettifyPhone(value); |
||||
} else if (name === 'totalDuration' || name === 'callDuration') { |
||||
return (globalParms.timeInSeconds ? value : common.convertToDuration(value)); |
||||
} else if (name === 'dateStart' || name === 'dateEnd') { |
||||
return dayjs(value).format('DD.MM.YYYY'); |
||||
} else if (name === 'callDate') { |
||||
return (globalParms.disableGrouping ? dayjs(value).format('DD.MM.YYYY HH:mm:ss') : dayjs(value).format('HH:mm:ss')); |
||||
} else if (name === 'callResult') { |
||||
return ['Отвечен', 'Не отвечен', 'Занято', 'Ошибка', 'Другое', 'Отменен'][value]; |
||||
} else if (name === 'dailyCT') { |
||||
return ['ВХ', 'ИСХ', 'ВН'][record.callType] + '-' + value; |
||||
} |
||||
return value; |
||||
}; |
||||
|
||||
const uglifyThing = (name, value) => { |
||||
if (name === 'source' || name === 'destination' || name === 'any') { |
||||
return common.uglifyPhone(value); |
||||
} else if (name === 'dateStart' || name === 'dateEnd') { |
||||
return value.replace(/(\d{1,2})\.(\d{1,2})\.(\d+)/, '$3-$2-$1'); |
||||
} else if (name === 'dailyID' || name === 'dailyCT' || name === 'id') { |
||||
return value.replace(/\D/g, ''); |
||||
} |
||||
return value; |
||||
}; |
||||
|
||||
const preferableSortDirs = { |
||||
callDate: 'desc', |
||||
source: 'asc', destination: 'asc', |
||||
totalDuration: 'desc', callDuration: 'desc' |
||||
}; |
||||
|
||||
const filterProperties = ['source', 'destination', 'any', 'dateStart', 'dateEnd', 'id', 'callTypes', 'wcSource', 'wcDestination', 'wcAny']; |
||||
const sortProperties = ['sort', 'dir']; |
||||
|
||||
const getCdrParams = () => { |
||||
let parms = {}; |
||||
|
||||
// specify page if its not the 1st one
|
||||
if (cdr.thisPage !== 1) { parms.page = cdr.thisPage; } |
||||
|
||||
// iterate over all known SORT and FILTER properties, add them if necessary
|
||||
[...sortProperties, ...filterProperties].forEach(e => { |
||||
if (cdr.filter[e]) { |
||||
parms[e] = uglifyThing(e, cdr.filter[e]); |
||||
} |
||||
}); |
||||
|
||||
// remove default sort options
|
||||
if (parms.sort === 'callDate' && parms.dir === 'desc') { |
||||
delete parms.sort; |
||||
delete parms.dir; |
||||
} |
||||
|
||||
// check if grouping is disabled
|
||||
if (document.querySelector('.left-panel .settings .checkbox.grouping > input').checked) { |
||||
parms.disableGrouping = 1; |
||||
} |
||||
|
||||
return Object.keys(parms).map(e => e + '=' + parms[e]); |
||||
}; |
||||
|
||||
const getCdr = async () => { |
||||
try { |
||||
let url = [...getCdrParams()].join('&'); |
||||
url = url ? ('?' + url) : '/'; |
||||
|
||||
// update browser url
|
||||
if (history.pushState) { |
||||
history.pushState({}, null, url); |
||||
} |
||||
|
||||
const response = await fetch('/api/query' + url); |
||||
if (!response.ok) { |
||||
throw new Error(response.status, ' ', response.statusText); |
||||
} |
||||
|
||||
return (await response.json()); |
||||
} catch (e) { |
||||
showMessage({ |
||||
icon: 'icon-error', |
||||
header: 'Ошибка загрузки данных', |
||||
text: 'Не удалось загрузить данные: ' + e |
||||
}); |
||||
console.warn('Failed to fetch data: ', e) |
||||
return null; |
||||
} |
||||
}; |
||||
|
||||
|
||||
const getCallsSinceLastUpdate = async () => { |
||||
try { |
||||
if (cdr && cdr.latest) { |
||||
const response = await fetch('/api/check-new?lastUpdateAt=' + dayjs(cdr.latest).unix()); |
||||
if (response.ok) { |
||||
return (await response.json()).numSinceLastUpdate; |
||||
} |
||||
} |
||||
} catch (e) { |
||||
console.warn('Failed to query last update API: ' + e); |
||||
} |
||||
}; |
||||
|
||||
|
||||
const performUpdate = async () => { |
||||
const count = await getCallsSinceLastUpdate(); |
||||
if (count > 0) { |
||||
if (document.querySelector('.left-panel .settings .checkbox.auto-update > input').checked) { |
||||
document.querySelector('.left-panel .new-entries').classList.remove('active'); |
||||
populate(); |
||||
} else { |
||||
document.querySelector('.left-panel .new-entries .num').innerHTML = count; |
||||
document.querySelector('.left-panel .new-entries').classList.add('active'); |
||||
} |
||||
} |
||||
|
||||
updateTimerHandle = setTimeout(performUpdate, 5000); |
||||
}; |
||||
|
||||
|
||||
let updateTimerHandle = 0; |
||||
|
||||
const handlerDoUpdate = (ev, el) => { |
||||
clearTimeout(updateTimerHandle); |
||||
updateTimerHandle = setTimeout(performUpdate, 5000); |
||||
document.querySelector('.left-panel .new-entries').classList.remove('active'); |
||||
populate(); |
||||
}; |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const columnNameToId = name => { |
||||
switch (name) { |
||||
case 'dailyid': { return 'dailyID'; } |
||||
case 'dailyct': { return 'dailyCT'; } |
||||
case 'date': { return 'callDate'; } |
||||
case 'result': { return 'callResult'; } |
||||
case 'total-duration': { return 'totalDuration'; } |
||||
case 'call-duration': { return 'callDuration'; } |
||||
case 'id': { return 'uniqueID'; } |
||||
default: { return name; } |
||||
} |
||||
}; |
||||
|
||||
const addInfoToGroup = (group, cd, gp) => { |
||||
const element = cdr.dailyStats.find( e => dayjs(e.day).isSame(cd, 'day')); |
||||
|
||||
if (element) { |
||||
const stats = document.createElement('div'); |
||||
stats.classList.add('stats'); |
||||
|
||||
const phone = createSVG(['image'], 'icon-phone'); |
||||
const numCalls = document.createElement('span'); |
||||
numCalls.classList.add('num-calls'); |
||||
numCalls.innerHTML = element.totalCalls; |
||||
|
||||
const clock = createSVG(['image'], 'icon-clock'); |
||||
const duration = document.createElement('span'); |
||||
duration.innerHTML = prettifyThing('totalDuration', element.totalDuration, null, gp) |
||||
+ ' / ' + prettifyThing('callDuration', element.callDuration, null, gp); |
||||
|
||||
stats.appendChild(phone); stats.appendChild(numCalls); |
||||
stats.appendChild(clock); stats.appendChild(duration); |
||||
group.appendChild(stats); |
||||
} |
||||
} |
||||
|
||||
const tryNewGroup = (id, cdrBody, gp) => { |
||||
if (id === 0 || !dayjs(cdr.records[id - 1].callDate).isSame(dayjs(cdr.records[id].callDate), 'day')) { |
||||
const group = document.createElement('div'); |
||||
group.classList.add('entry', 'group'); |
||||
|
||||
const groupText = document.createElement('span'); |
||||
groupText.classList.add('group-text'); |
||||
groupText.innerHTML = dayjs(cdr.records[id].callDate).format('DD.MM.YYYY'); |
||||
|
||||
if (cdr.isFirstCont && id === 0) { |
||||
groupText.innerHTML += ' (продолжение)'; |
||||
} |
||||
|
||||
group.appendChild(groupText); |
||||
|
||||
if (cdr.dailyStats) { |
||||
addInfoToGroup(group, cdr.records[id].callDate, gp); |
||||
} |
||||
|
||||
cdrBody.appendChild(group); |
||||
} |
||||
}; |
||||
|
||||
const draw = () => { |
||||
if (!cdr) { |
||||
throw new Error("Tried to draw without CDR data"); |
||||
} |
||||
|
||||
// no-cdrs box
|
||||
document.querySelector('.cdr .body .no-cdrs').classList.toggle('active', !cdr.records || cdr.records.length === 0); |
||||
|
||||
// pages
|
||||
document.querySelector('.cdr .controls .status').innerHTML = `${cdr.thisPage} из ${cdr.maxPages}`; |
||||
document.querySelector('.cdr .controls .goto-page').value = cdr.thisPage; |
||||
document.querySelector('.cdr .controls .goto-page').max = cdr.maxPages; |
||||
|
||||
// page buttons
|
||||
document.querySelector('.cdr .controls .button_begin').classList.toggle('active', cdr.thisPage > 1); |
||||
document.querySelector('.cdr .controls .button_left').classList.toggle('active', cdr.thisPage > 1); |
||||
document.querySelector('.cdr .controls .button_right').classList.toggle('active', cdr.thisPage < cdr.maxPages); |
||||
document.querySelector('.cdr .controls .button_end').classList.toggle('active', cdr.thisPage < cdr.maxPages); |
||||
document.querySelector('.cdr .controls .button_goto').classList.toggle('active', cdr.maxPages > 1); |
||||
document.querySelector('.cdr .controls .goto-page').classList.toggle('active', cdr.maxPages > 1); |
||||
|
||||
// filters
|
||||
document.querySelectorAll('.left-panel .filters .filter').forEach( e => { |
||||
let id = e.getAttribute('filter-id'); |
||||
if (id) { |
||||
e.querySelector('.value').value = cdr.filter[id] ? prettifyThing(id, cdr.filter[id]) : ""; |
||||
e.querySelector('.status').classList.toggle('active', !!cdr.filter[id]); |
||||
} |
||||
}); |
||||
|
||||
// call type filters
|
||||
document.querySelector('.left-panel .call-type.call-type_incoming').classList.toggle('active', cdr.filter.callTypes & (1 << common.ctIncoming)); |
||||
document.querySelector('.left-panel .call-type.call-type_outgoing').classList.toggle('active', cdr.filter.callTypes & (1 << common.ctOutgoing)); |
||||
document.querySelector('.left-panel .call-type.call-type_internal').classList.toggle('active', cdr.filter.callTypes & (1 << common.ctInternal)); |
||||
|
||||
// wildcards
|
||||
document.querySelector('.left-panel .filters .filter .wildcard_source').classList.toggle('active', cdr.filter.wcSource); |
||||
document.querySelector('.left-panel .filters .filter .wildcard_destination').classList.toggle('active', cdr.filter.wcDestination); |
||||
document.querySelector('.left-panel .filters .filter .wildcard_any').classList.toggle('active', cdr.filter.wcAny); |
||||
|
||||
|
||||
// sortable fields
|
||||
document.querySelectorAll('.cdr .header .cell.sortable').forEach( e => { |
||||
let id = "" + e.getAttribute('sort-id'); |
||||
if (id) { |
||||
e.querySelector('.sort-button').classList.toggle('sort-none', cdr.filter.sort !== id); |
||||
e.querySelector('.sort-button').classList.toggle('sort-asc', cdr.filter.sort === id && cdr.filter.dir === 'asc'); |
||||
e.querySelector('.sort-button').classList.toggle('sort-desc', cdr.filter.sort === id && cdr.filter.dir === 'desc'); |
||||
} |
||||
}); |
||||
|
||||
// records
|
||||
|
||||
// cache CDR body descriptor
|
||||
// (not actually cdr body due to scrollbars)
|
||||
const cdrBody = document.querySelector('.cdr .body .no-cdrs').parentElement; |
||||
|
||||
// cache some settings
|
||||
const globalParms = { |
||||
timeInSeconds: document.querySelector('.left-panel .settings .checkbox.time-in-seconds > input').checked, |
||||
disableGrouping: document.querySelector('.left-panel .settings .checkbox.grouping > input').checked |
||||
}; |
||||
|
||||
|
||||
// change date header if grouping is enabled
|
||||
document.querySelector('.cdr .header .cell.cell_date .name').innerHTML = |
||||
globalParms.disableGrouping ? 'Дата' : 'Время'; |
||||
|
||||
|
||||
|
||||
// remove all records first
|
||||
cdrBody.querySelectorAll('.entry').forEach( e => { e.remove(); }) |
||||
|
||||
// iterate over records if there are any
|
||||
let idx = (cdr.thisPage - 1) * cdr.filter.limit + 1; |
||||
let localIdx = 0; |
||||
|
||||
// clear out selectedElem
|
||||
selectedElem = null; |
||||
|
||||
cdr.records.forEach( record => { |
||||
// if grouping is enabled, attempt to place a group header
|
||||
if (!globalParms.disableGrouping) { |
||||
tryNewGroup(localIdx, cdrBody, globalParms); |
||||
} |
||||
|
||||
let entry = document.createElement('div'); |
||||
entry.classList.add('entry', 'record'); |
||||
entry.addEventListener('click', wrapListener(handlerSelectRecord, entry, record.uniqueID)); |
||||
|
||||
// check if this entry has been previously selected
|
||||
if (selectedRecord === record.uniqueID) { |
||||
entry.classList.add('active'); |
||||
selectedElem = entry; |
||||
} |
||||
|
||||
['id', 'dailyct', 'type', 'date', 'source', 'destination', 'total-duration', 'call-duration', 'result', 'records'].forEach( name => { |
||||
const cell = document.createElement('div'); |
||||
cell.classList.add('cell', 'cell_' + name); |
||||
|
||||
switch (name) { |
||||
case 'id': { |
||||
cell.innerHTML = idx; |
||||
break; |
||||
} |
||||
case 'type': { |
||||
const ct = common.callTypeToString(record.callType); |
||||
cell.appendChild(createSVG(['image', 'type_' + ct], 'icon-' + ct + '-call')) |
||||
break; |
||||
} |
||||
case 'records': { |
||||
let play = createSVG(['image', 'link', 'play'], 'icon-play'); |
||||
play.addEventListener('click', wrapListener(handlerPlay, record.uniqueID)); |
||||
cell.appendChild(play); |
||||
|
||||
let save = createSVG(['image', 'link', 'save'], 'icon-save'); |
||||
save.addEventListener('click', wrapListener(handlerSave, record.uniqueID)); |
||||
cell.appendChild(save); |
||||
break; |
||||
} |
||||
default: { |
||||
const colName = columnNameToId(name); |
||||
cell.innerHTML = prettifyThing(colName, record[colName], record, globalParms); |
||||
} |
||||
} |
||||
|
||||
entry.appendChild(cell); |
||||
}); |
||||
|
||||
cdrBody.appendChild(entry); |
||||
idx++; |
||||
localIdx++; |
||||
}); |
||||
|
||||
// TODO group view
|
||||
}; |
||||
|
||||
|
||||
const populate = async () => { |
||||
const newCdr = await getCdr(); |
||||
if (!newCdr) { |
||||
return; |
||||
} |
||||
|
||||
cdr = newCdr; |
||||
draw(); |
||||
}; |
||||
|
||||
|
||||
|
||||
function handlerSave(ev, uid) { |
||||
if (uid) { |
||||
window.location.assign('/api/recording/' + uid + '?asFile=true'); |
||||
} |
||||
|
||||
ev.stopPropagation(); |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
function handlerSelectRecord(ev, el, id) { |
||||
if (selectedRecord !== id && selectedElem) { |
||||
selectedElem.classList.remove('active'); |
||||
} |
||||
|
||||
selectedRecord = (selectedRecord === id) ? null : id; |
||||
selectedElem = (selectedRecord !== null) ? el : null; |
||||
el.classList.toggle('active'); |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function handlerFilterNotify(ev, el) { |
||||
el.parentElement.querySelector('.status').classList.toggle('active', el.value && el.value.length > 0); |
||||
} |
||||
document.querySelectorAll('.left-panel .filters .filter .value').forEach(e => e.addEventListener('change', wrapListener(handlerFilterNotify, e))); |
||||
document.querySelectorAll('.left-panel .filters .filter .value').forEach(e => e.addEventListener('input', wrapListener(handlerFilterNotify, e))); |
||||
|
||||
|
||||
|
||||
function handlerSort(ev, el) { |
||||
const id = "" + el.getAttribute('sort-id'); |
||||
|
||||
if (id) { |
||||
if (cdr.filter.sort === id) { |
||||
cdr.filter.dir = (cdr.filter.dir === 'asc') ? 'desc' : 'asc'; |
||||
} else { |
||||
cdr.thisPage = 1; |
||||
cdr.filter.sort = id; |
||||
cdr.filter.dir = preferableSortDirs[cdr.filter.sort] ? preferableSortDirs[cdr.filter.sort] : 'desc'; |
||||
} |
||||
} |
||||
|
||||
populate(); |
||||
} |
||||
|
||||
function handlerBegin() { |
||||
if (cdr.thisPage !== 1) { |
||||
cdr.thisPage = 1; |
||||
populate(); |
||||
} |
||||
} |
||||
|
||||
function handlerLeft() { |
||||
if (cdr.thisPage > 1) { |
||||
--cdr.thisPage; |
||||
populate(); |
||||
} |
||||
} |
||||
|
||||
function handlerRight() { |
||||
if (cdr.thisPage < cdr.maxPages) { |
||||
++cdr.thisPage; |
||||
populate(); |
||||
} |
||||
} |
||||
|
||||
function handlerEnd() { |
||||
if (cdr.thisPage !== cdr.maxPages) { |
||||
cdr.thisPage = cdr.maxPages; |
||||
populate(); |
||||
} |
||||
} |
||||
|
||||
function handlerGoto() { |
||||
const page = parseInt(document.querySelector('.cdr .controls .goto-page').value, 10); |
||||
|
||||
if (!isNaN(page) && page !== cdr.thisPage && page >= 1 && page <= cdr.maxPages) { |
||||
cdr.thisPage = page; |
||||
populate(); |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
// apply all filters and redraw
|
||||
function handlerApply() { |
||||
let filters = {}; |
||||
|
||||
// iterate over filters, uglify and apply
|
||||
document.querySelectorAll('.left-panel .filters .filter .value').forEach( e => { |
||||
const id = "" + e.getAttribute('filter-id'); |
||||
if (id) { |
||||
filters[id] = uglifyThing(id, e.value); |
||||
|
||||
if (!filters[id] || filters[id].length === 0) { |
||||
delete filters[id]; |
||||
} |
||||
} |
||||
}); |
||||
|
||||
// iterate over callType filters, combine into bitmask
|
||||
filters.callTypes = 0; |
||||
['internal', 'outgoing', 'incoming'].forEach( e => { |
||||
filters.callTypes = (filters.callTypes << 1) + |
||||
(document.querySelector('.left-panel .call-type.call-type_' + e).classList.contains('active') ? 1 : 0); |
||||
}); |
||||
|
||||
// do not specify callfilters if none or all are selected
|
||||
if (filters.callTypes === 0 || filters.callTypes === 7) { |
||||
delete filters.callTypes; |
||||
} |
||||
|
||||
|
||||
// iterate over wildcards
|
||||
if (document.querySelector('.left-panel .filters .filter .wildcard_source').classList.contains('active')) { filters.wcSource = true; } |
||||
if (document.querySelector('.left-panel .filters .filter .wildcard_destination').classList.contains('active')) { filters.wcDestination = true; } |
||||
if (document.querySelector('.left-panel .filters .filter .wildcard_any').classList.contains('active')) { filters.wcAny = true; } |
||||
|
||||
|
||||
// compare new filters with old filters
|
||||
let shouldPopulate = false; |
||||
for (let prop in filters) { |
||||
if (filters[prop] !== cdr.filter[prop]) { |
||||
shouldPopulate = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// compare old filters with new filters
|
||||
for (let prop in cdr.filter) { |
||||
if (typeof filters[prop] === 'undefined') { |
||||
shouldPopulate = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// should re-filter?
|
||||
if (shouldPopulate) { |
||||
cdr.thisPage = 1; |
||||
cdr.filter = filters; |
||||
populate(); |
||||
} |
||||
} |
||||
document.querySelector('.left-panel .filters .button.apply').addEventListener('click', wrapListener(handlerApply)); |
||||
|
||||
|
||||
// reset all filters and redraw
|
||||
function handlerReset() { |
||||
if (cdr.filter) { |
||||
filterProperties.forEach(e => { delete cdr.filter[e]; }); |
||||
} |
||||
|
||||
document.querySelectorAll('.left-panel .filters .filter .value').forEach( e => e.value = "" ); |
||||
document.querySelectorAll('.left-panel .filters .filter .status').forEach( e => e.classList.remove('active') ); |
||||
document.querySelectorAll('.left-panel .filters .call-type').forEach( e => e.classList.remove('active') ); |
||||
populate(); |
||||
} |
||||
document.querySelector('.left-panel .filters .button.reset').addEventListener('click', wrapListener(handlerReset)); |
||||
document.querySelector('.cdr .no-cdrs .button.reset').addEventListener('click', wrapListener(handlerReset)); |
||||
|
||||
|
||||
|
||||
// handle click on calltypes
|
||||
document.querySelectorAll('.left-panel .filters .call-types .call-type').forEach(e => e.addEventListener('click', wrapListener((ev, el) => { |
||||
el.classList.toggle('active'); |
||||
}, e))); |
||||
|
||||
// wildcard clicks
|
||||
document.querySelectorAll('.left-panel .filters .filter .wildcard').forEach(e => e.addEventListener('click', wrapListener((ev, el) => { |
||||
el.classList.toggle('active'); |
||||
}, e))); |
||||
|
||||
|
||||
|
||||
// handle click on settings and set initial values
|
||||
document.querySelectorAll('.left-panel .settings .checkbox').forEach(e => { |
||||
const state = storageGet('setting-' + e.getAttribute('component-id')); |
||||
if (state === 'on' || state === 'off') { |
||||
e.querySelector('input').checked = (state === 'on'); |
||||
} |
||||
|
||||
e.querySelector('input').addEventListener('click', wrapListener((ev, el) => { |
||||
storageSet('setting-' + el.getAttribute('component-id'), el.querySelector('input').checked ? 'on' : 'off'); |
||||
}, e)); |
||||
}); |
||||
|
||||
|
||||
|
||||
|
||||
// handle settings
|
||||
document.querySelector('.left-panel .settings .checkbox.grouping > input').addEventListener('click', populate); |
||||
document.querySelector('.left-panel .settings .checkbox.time-in-seconds > input').addEventListener('click', draw); |
||||
|
||||
// handle page buttons
|
||||
document.querySelector('.cdr .controls .button_begin').addEventListener('click', wrapListener(handlerBegin)); |
||||
document.querySelector('.cdr .controls .button_left').addEventListener('click', wrapListener(handlerLeft)); |
||||
document.querySelector('.cdr .controls .button_right').addEventListener('click', wrapListener(handlerRight)); |
||||
document.querySelector('.cdr .controls .button_end').addEventListener('click', wrapListener(handlerEnd)); |
||||
document.querySelector('.cdr .controls .button_goto').addEventListener('click', wrapListener(handlerGoto)); |
||||
|
||||
|
||||
// handle sorting
|
||||
document.querySelectorAll('.cdr .header .cell.sortable .cell-clickable-area').forEach( e => e.addEventListener('click', wrapListener(handlerSort, e))); |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
createListener(document.querySelector('.left-panel .new-entries .button.update'), 'click', handlerDoUpdate); |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
window.addEventListener('resize', ev => { |
||||
let vh = window.innerHeight; |
||||
let lph = Array.prototype.slice.call(document.querySelectorAll('.body-wrapper > .panel-wrapper > .left-panel > *')) |
||||
.map(e => e.offsetHeight) |
||||
.reduce((acc, cv) => acc + cv); |
||||
let fh = document.querySelector('.body-wrapper > .footer').offsetHeight; |
||||
|
||||
let bh = 0; |
||||
if (vh > lph + fh) { |
||||
bh = vh - 165; |
||||
} else { |
||||
bh = lph + fh - 165; |
||||
} |
||||
|
||||
document.querySelector('.cdr .table .body').style.maxHeight = bh + 'px'; |
||||
}); |
||||
|
||||
document.onreadystatechange = e => { |
||||
if (document.readyState === 'complete') { |
||||
draw(); |
||||
|
||||
updateTimerHandle = setTimeout(performUpdate, 5000); |
||||
} |
||||
} |
||||
|
||||
|
||||
// 3rd party stuff
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => { |
||||
OverlayScrollbars(document.querySelector('.cdr .body'), {}); |
||||
}); |
||||
|
||||
const commonPikadayOptions = { |
||||
format: 'dd.mm.yyyy', |
||||
i18n: { |
||||
previousMonth : 'Предыдущий месяц', |
||||
nextMonth : 'Следующий месяц', |
||||
months: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'], |
||||
weekdays: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'], |
||||
weekdaysShort: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'] |
||||
}, |
||||
toString(date, format) { |
||||
return date.toLocaleDateString('ru-ru', {}); |
||||
}, |
||||
theme: 'pd-theme', |
||||
firstDay: 1 |
||||
}; |
||||
|
||||
const startPicker = new Pikaday({ field: document.querySelector('.filters .filter_date-start .value'), ...commonPikadayOptions}); |
||||
const endPicker = new Pikaday({ field: document.querySelector('.filters .filter_date-end .value'), ...commonPikadayOptions}); |
@ -0,0 +1,112 @@ |
||||
'use strict'; |
||||
|
||||
const wrapListener = function(callback, ...args) { |
||||
return function cf(ev) { |
||||
if (ev && callback) { |
||||
callback(ev, ...args); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
const createListener = function(element, event, callback, ...args) { |
||||
element.addEventListener(event, wrapListener(callback, element, ...args)); |
||||
}; |
||||
|
||||
const createSVG = (classList, symbolId) => { |
||||
const div = document.createElement('div'); |
||||
div.classList.add(...classList); |
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); |
||||
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use'); |
||||
use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + symbolId); |
||||
|
||||
svg.appendChild(use); |
||||
div.appendChild(svg); |
||||
|
||||
return div; |
||||
}; |
||||
|
||||
const changeSVGImage = (element, newSymbolId) => { |
||||
element.querySelector('svg > use').setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', "#" + newSymbolId); |
||||
}; |
||||
|
||||
const storageGet = name => { |
||||
try { |
||||
return localStorage.getItem(name); |
||||
} catch (e) { |
||||
return null; |
||||
} |
||||
}; |
||||
|
||||
const storageSet = (name, value) => { |
||||
try { |
||||
localStorage.setItem(name, value); |
||||
} catch (e) { } |
||||
}; |
||||
|
||||
|
||||
|
||||
const handlerSubsection = function(ev, el, shouldOpen) { |
||||
const selector = el.getAttribute('ts'); |
||||
const name = el.getAttribute('cn'); |
||||
|
||||
if (selector && name) { |
||||
const target = document.querySelector(selector); |
||||
|
||||
if (target) { |
||||
const isOpen = (shouldOpen === true || shouldOpen === false) ? !shouldOpen : !target.classList.contains('hidden'); |
||||
|
||||
changeSVGImage(el.querySelector('.collapser'), isOpen ? 'icon-plus' : 'icon-minus'); |
||||
target.classList.toggle('hidden', isOpen); |
||||
storageSet('ss-' + name, isOpen ? 'closed' : 'open'); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
const initializeSubsections = function() { |
||||
document.querySelectorAll('.left-panel .section.sub').forEach( e => { |
||||
const state = storageGet('ss-' + e.getAttribute('cn')); |
||||
if (state === 'open' || state === 'closed') { |
||||
handlerSubsection(null, e, !!(state === 'open')); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
initializeSubsections(); |
||||
|
||||
document.querySelectorAll('.left-panel .section.sub .clickable-area').forEach( |
||||
e => e.addEventListener('click', wrapListener(handlerSubsection, e.parentElement)) |
||||
); |
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const showMessage = function(parms) { |
||||
if (!parms || !parms.header) { |
||||
throw new Error('Invalid message parameters'); |
||||
} |
||||
|
||||
const msg = document.querySelector('.message-wrapper'); |
||||
|
||||
if (msg.querySelector('.icon .image')) { |
||||
msg.querySelector('.icon .image').remove(); |
||||
} |
||||
if (parms.icon) { |
||||
msg.querySelector('.icon').appendChild(createSVG(['image'], parms.icon)); |
||||
} |
||||
|
||||
msg.querySelector('.header').innerHTML = parms.header; |
||||
msg.querySelector('.text').innerHTML = parms.text || ''; |
||||
msg.querySelector('.button .button-text').innerHTML = parms.button || 'Закрыть'; |
||||
|
||||
msg.classList.add('active'); |
||||
}; |
||||
|
||||
[document.querySelector('.message-wrapper .message .close-button'), document.querySelector('.message .button')].forEach( e => e.addEventListener('click', () => { |
||||
document.querySelector('.message-wrapper').classList.remove('active'); |
||||
})); |
||||
|
||||
|
||||
|
||||
document.querySelector('.message-wrapper').style.display = ""; |
@ -0,0 +1,170 @@ |
||||
'use strict'; |
||||
|
||||
const player = document.querySelector('.body-wrapper > .player'); |
||||
const audio = player.querySelector('.core'); |
||||
|
||||
const handlerPlay = function(ev, id) { |
||||
player.classList.add('active'); |
||||
|
||||
changeSVGImage(player.querySelector('.button.play-pause'), 'icon-pause'); |
||||
|
||||
player.querySelector('.time-slider').value = 0; |
||||
player.querySelector('.time-slider').max = 0; |
||||
player.querySelector('.time').innerHTML = '00:00'; |
||||
player.querySelector('.duration').innerHTML = '00:00'; |
||||
|
||||
audio.src = '/api/recording/' + id + '?asFile=false'; |
||||
audio.volume = storageGet('player-volume') || 0.25; |
||||
player.querySelector('.volume-slider').value = audio.volume; |
||||
|
||||
audio.load(); |
||||
audio.play(); |
||||
|
||||
ev.stopPropagation(); |
||||
}; |
||||
|
||||
|
||||
(() => { |
||||
let isDragging = false; |
||||
|
||||
const onLoadedMetadata = () => { |
||||
if (audio.duration && !isNaN(audio.duration)) { |
||||
player.querySelector('.duration').innerHTML = common.convertToDuration(Math.ceil(audio.duration)); |
||||
player.querySelector('.time-slider').max = audio.duration; |
||||
|
||||
audio.playbackRate = storageGet('player-speed') || 1; |
||||
player.querySelector('.speed-slider').value = audio.playbackRate; |
||||
player.querySelector('.current-speed').innerHTML = audio.playbackRate * 100 + "%"; |
||||
} |
||||
}; |
||||
|
||||
const onEnded = () => { |
||||
changeSVGImage(player.querySelector('.button.play-pause'), 'icon-play-fill'); |
||||
|
||||
player.querySelector('.time-slider').value = audio.duration; |
||||
player.querySelector('.time').innerHTML = common.convertToDuration(Math.ceil(audio.duration)); |
||||
}; |
||||
|
||||
|
||||
const onError = e => { |
||||
let errorText = ""; |
||||
switch (e.target.error.code) { |
||||
case e.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED: |
||||
errorText = "Файл записи не найден на сервере"; |
||||
break; |
||||
case e.target.error.MEDIA_ERR_NETWORK: |
||||
errorText = "Что-то произошло с сетевым подключением"; |
||||
break; |
||||
case e.target.error.MEDIA_ERR_DECODE: |
||||
errorText = "Проблема с декодированием аудиофайла"; |
||||
break; |
||||
default: |
||||
errorText = "Неизвестная ошибка: " + e.target.error.code; |
||||
} |
||||
|
||||
showMessage({ |
||||
icon: 'icon-error', |
||||
header: 'Ошибка!', |
||||
text: errorText |
||||
}); |
||||
|
||||
player.classList.remove('active'); |
||||
}; |
||||
|
||||
const handlerPlayPause = (ev, el) => { |
||||
if (!audio.paused) { |
||||
changeSVGImage(el, 'icon-play-fill'); |
||||
audio.pause(); |
||||
} else { |
||||
changeSVGImage(el, 'icon-pause'); |
||||
audio.play(); |
||||
} |
||||
}; |
||||
|
||||
const handlerVolume = () => { |
||||
player.querySelector('.volume-bar').classList.toggle('active'); |
||||
}; |
||||
|
||||
const handlerSpeed = () => { |
||||
player.querySelector('.speed-bar').classList.toggle('active'); |
||||
}; |
||||
|
||||
const handlerClose = () => { |
||||
player.classList.remove('active'); |
||||
audio.pause(); |
||||
}; |
||||
|
||||
|
||||
const handlerChangeVolume = (ev, el) => { |
||||
if (el.value >= 0 && el.value <= 1) { |
||||
audio.volume = el.value; |
||||
} |
||||
}; |
||||
const handlerSaveVolume = (ev, el) => { |
||||
if (el.value >= 0 && el.value <= 1) { |
||||
storageSet('player-volume', el.value); |
||||
} |
||||
}; |
||||
|
||||
|
||||
const handlerChangeSpeed = (ev, el) => { |
||||
audio.playbackRate = el.value; |
||||
player.querySelector('.current-speed').innerHTML = audio.playbackRate * 100 + "%"; |
||||
}; |
||||
const handlerSaveSpeed = (ev, el) => { |
||||
if (el.value >= 0) { |
||||
storageSet('player-speed', el.value); |
||||
} |
||||
}; |
||||
const handlerResetSpeed = (ev, el) => { |
||||
el.value = 1; |
||||
audio.playbackRate = 1; |
||||
player.querySelector('.speed-slider').value = 1; |
||||
player.querySelector('.current-speed').innerHTML = "100%"; |
||||
storageSet('player-speed', 1); |
||||
}; |
||||
|
||||
|
||||
|
||||
|
||||
const handlerProgress = () => { |
||||
if (!isNaN(audio.currentTime) && !isNaN(audio.duration)) { |
||||
if (!isDragging) { |
||||
player.querySelector('.time-slider').value = audio.currentTime; |
||||
} |
||||
player.querySelector('.time').innerHTML = common.convertToDuration(audio.currentTime); |
||||
} |
||||
}; |
||||
|
||||
const handlerChangeTime = e => { |
||||
if (!isNaN(audio.duration)) { |
||||
const pb = player.querySelector('.time-slider'); |
||||
audio.currentTime = pb.value; |
||||
} |
||||
}; |
||||
|
||||
|
||||
|
||||
createListener(player.querySelector('.button.play-pause'), 'click', handlerPlayPause); |
||||
|
||||
createListener(player.querySelector('.volume-slider'), 'input', handlerChangeVolume); |
||||
createListener(player.querySelector('.volume-slider'), 'change', handlerSaveVolume); |
||||
createListener(player.querySelector('.speed-slider'), 'input', handlerChangeSpeed); |
||||
createListener(player.querySelector('.speed-slider'), 'change', handlerSaveSpeed); |
||||
createListener(player.querySelector('.button.reset-speed'), 'click', handlerResetSpeed); |
||||
|
||||
|
||||
player.querySelector('.button.volume').addEventListener('click', handlerVolume); |
||||
player.querySelector('.button.speed').addEventListener('click', handlerSpeed); |
||||
player.querySelector('.button.close').addEventListener('click', handlerClose); |
||||
|
||||
player.querySelector('.time-slider').addEventListener('change', handlerChangeTime); |
||||
player.querySelector('.time-slider').addEventListener('mousedown', () => {isDragging = true}); |
||||
player.querySelector('.time-slider').addEventListener('mouseup', () => {isDragging = false}); |
||||
|
||||
|
||||
audio.addEventListener('timeupdate', handlerProgress); |
||||
audio.addEventListener('loadedmetadata', onLoadedMetadata); |
||||
audio.addEventListener('ended', onEnded); |
||||
audio.addEventListener('error', onError); |
||||
})(); |
@ -0,0 +1 @@ |
||||
'use strict'; |
@ -0,0 +1,75 @@ |
||||
'use strict'; |
||||
|
||||
// document.querySelectorAll('.body-wrapper input').forEach( e => e.disabled = true);
|
||||
// document.querySelectorAll('.body-wrapper a').forEach( e => e.tabIndex = -1);
|
||||
|
||||
// TODO: this
|
||||
(() => { |
||||
|
||||
})(); |
||||
|
||||
let stack = []; |
||||
let maxZIndex = 100; |
||||
|
||||
const wrapper = document.querySelector('.window-wrapper'); |
||||
if (!wrapper) { |
||||
throw new Error('window wrapper is missing'); |
||||
} |
||||
|
||||
const getWindowById = id => { |
||||
if (typeof id !== 'string') { |
||||
throw new Error('className is not a string'); |
||||
} |
||||
|
||||
const window = document.getElementById(id); |
||||
if (!window) { |
||||
throw new Error('Window with ID ' + className + ' not found'); |
||||
} |
||||
|
||||
return window; |
||||
}; |
||||
const openWindow = id => { |
||||
const window = getWindowById(id); |
||||
window.classList.add('active'); |
||||
wrapper.classList.add('active'); |
||||
|
||||
if (!stack.includes(window)) { |
||||
// hide all other windows
|
||||
if (stack.length) { |
||||
stack.forEach( e => e.classList.remove('active')); |
||||
} |
||||
stack.push(window); |
||||
|
||||
++maxZIndex; |
||||
window.style.zIndex = maxZIndex; |
||||
} |
||||
}; |
||||
|
||||
const closeWindow = id => { |
||||
const window = getWindowById(id); |
||||
window.classList.remove('active'); |
||||
|
||||
if (stack.length && stack[stack.length - 1] === window) { |
||||
stack.pop(); |
||||
} |
||||
|
||||
if (!stack.length) { |
||||
wrapper.classList.remove('active'); |
||||
maxZIndex = 100; |
||||
} else { |
||||
stack.forEach( e => e.classList.add('active')); |
||||
} |
||||
}; |
||||
|
||||
wrapper.style.display = ""; |
||||
|
||||
//
|
||||
// document.querySelector('.window-wrapper .window .close-button').forEach( e => {
|
||||
// createListener(e, 'click', (ev, el, wrapper) => {
|
||||
// e
|
||||
// });
|
||||
//
|
||||
// const createListener = function(element, event, callback, ...args) {
|
||||
// element.addEventListener(event, wrapListener(callback, element, ...args));
|
||||
// };
|
||||
// });
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@ |
||||
{ |
||||
"name": "ast-cdr", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"private": true, |
||||
"main": "app.js", |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1" |
||||
}, |
||||
"author": "", |
||||
"license": "ISC", |
||||
"dependencies": { |
||||
"@hapi/hapi": "latest", |
||||
"@hapi/inert": "latest", |
||||
"@hapi/vision": "latest", |
||||
"asterisk-manager": "^0.1.16", |
||||
"dayjs": "latest", |
||||
"dotenv": "latest", |
||||
"hapi-favicon": "^2.1.2", |
||||
"joi": "latest", |
||||
"knex": "latest", |
||||
"pg": "latest", |
||||
"pug": "latest", |
||||
"rfr": "^1.2.3" |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
'use strict'; |
||||
|
||||
const Assert = require('assert'); |
||||
const Joi = require('joi'); |
||||
const AsteriskManager = require('asterisk-manager'); |
||||
|
||||
let ami = null; |
||||
|
||||
const initAMI = () => { |
||||
Assert.ok(!ami, 'tried to double-initialize AMI'); |
||||
|
||||
if (!process.env.AMI_ENABLE) { |
||||
console.log('AMI is disabled, skipping AMI initialization') |
||||
} else { |
||||
if (!process.env.AMI_HOST || !process.env.AMI_USER || !process.env.AMI_PASS) { |
||||
console.warn('Some AMI parameters are missing, skipping AMI initialization') |
||||
} else { |
||||
ami = new AsteriskManager(process.env.AMI_PORT, process.env.AMI_HOST, process.env.AMI_USER, process.env.AMI_PASS, true); |
||||
ami.keepConnected(); |
||||
ami.on('error', ev => { |
||||
console.warn('AMI error: ' + ev); |
||||
}) |
||||
console.log('AMI initialized'); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
const sendAMIAction = (action, callback) => { |
||||
if (!ami) { |
||||
throw new Error('AMI is not initialized') |
||||
} |
||||
|
||||
return ami.action(action, callback); |
||||
}; |
||||
|
||||
module.exports = { |
||||
initAMI: initAMI, |
||||
action: sendAMIAction, |
||||
|
||||
config: Joi.object({ |
||||
AMI_ENABLE: Joi.boolean().optional().default(false), |
||||
AMI_HOST: Joi.alternatives().try(Joi.string().hostname(), Joi.string().ip()).when( |
||||
'AMI_ENABLE', {is: Joi.boolean().valid(true), then: Joi.required(), otherwise: Joi.optional()}), |
||||
AMI_PORT: Joi.number().port().when( |
||||
'AMI_ENABLE', {is: Joi.boolean().valid(true), then: Joi.optional().default(5038), otherwise: Joi.optional()}), |
||||
AMI_USER: Joi.string().min(1).when( |
||||
'AMI_ENABLE', {is: Joi.boolean().valid(true), then: Joi.required(), otherwise: Joi.optional()}), |
||||
AMI_PASS: Joi.string().min(1).when( |
||||
'AMI_ENABLE', {is: Joi.boolean().valid(true), then: Joi.required(), otherwise: Joi.optional()}) |
||||
}) |
||||
}; |
@ -0,0 +1,17 @@ |
||||
'use strict'; |
||||
|
||||
module.exports = { |
||||
isDebugMode: () => process.env.NODE_ENV === 'development', |
||||
|
||||
debug: (data, ...args) => { |
||||
if (process.env.NODE_ENV === 'development') { |
||||
console.log(data, ...args); |
||||
} |
||||
}, |
||||
|
||||
debugIf: (cond, data, ...args) => { |
||||
if (cond) { |
||||
this.debug(data, ...args); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,46 @@ |
||||
'use strict'; |
||||
|
||||
const Assert = require('assert'); |
||||
const Joi = require('joi'); |
||||
|
||||
|
||||
const initMainConfig = () => { |
||||
const config = require('dotenv').config(); |
||||
|
||||
if (config.error) { |
||||
console.error('Failed to read config file: ' + config.error); |
||||
} else { |
||||
console.log(`Config file loaded with ${config.parsed ? Object.keys(config.parsed).length : 0} options`); |
||||
} |
||||
}; |
||||
|
||||
const initConfigs = (configs = []) => { |
||||
Assert.ok(configs && Array.isArray(configs), '\'configs\' must be an array'); |
||||
|
||||
initMainConfig(); |
||||
|
||||
configs.forEach( c => { |
||||
try { |
||||
if (c) { |
||||
Assert.ok(Joi.isSchema(c), 'config must be a Joi schema'); |
||||
|
||||
const result = c.validate(process.env, { |
||||
allowUnknown: true, |
||||
presence: 'required' |
||||
}); |
||||
|
||||
if (result.error) { |
||||
throw result.error; |
||||
} |
||||
|
||||
if (result.value) { |
||||
process.env = {...process.env, ...result.value}; |
||||
} |
||||
} |
||||
} catch (e) { |
||||
throw new Joi.ValidationError(`Failed to validate config: ${e}`); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
module.exports = initConfigs; |
@ -0,0 +1,77 @@ |
||||
'use strict'; |
||||
|
||||
const Joi = require('joi'); |
||||
const Knex = require('knex'); |
||||
const Rfr = require('rfr'); |
||||
|
||||
const ComSrv = Rfr('/server/comsrv'); |
||||
|
||||
let db = null; |
||||
|
||||
const connectToDB = async () => { |
||||
ComSrv.debug(`Connecting to ${process.env.DB_HOST}:${process.env.DB_PORT} as ` + |
||||
`${process.env.DB_USER} (type: ${process.env.DB_TYPE}, ` + |
||||
`DB: ${process.env.DB_DATABASE})`); |
||||
|
||||
let db = Knex({ |
||||
client: process.env.DB_TYPE, |
||||
connection: { |
||||
host: process.env.DB_HOST, |
||||
port: process.env.DB_PORT, |
||||
user: process.env.DB_USER, |
||||
password: process.env.DB_PASS, |
||||
database: process.env.DB_DATABASE |
||||
}, |
||||
log: { |
||||
warn(message) { console.warn(message) }, |
||||
error(message) { console.error(message) }, |
||||
debug(message) { ComSrv.debug(message) } |
||||
} |
||||
}); |
||||
|
||||
await db.raw('select 1 as connected'); |
||||
console.log('Connected to DB'); |
||||
|
||||
return db; |
||||
} |
||||
|
||||
module.exports = { |
||||
config: Joi.object({ |
||||
DB_TYPE: Joi.string().valid('pg', 'mysql'), |
||||
DB_HOST: Joi.alternatives().try(Joi.string().hostname(), Joi.string().ip()), |
||||
DB_USER: Joi.string().min(1), |
||||
DB_PASS: Joi.string().min(1), |
||||
DB_DATABASE: Joi.string().min(1), |
||||
DB_TABLE: Joi.string().min(1), |
||||
|
||||
DB_PORT: Joi.number().port().optional().when('DB_TYPE', { |
||||
is: Joi.string().valid('pg'), |
||||
then: Joi.number().default(5432), |
||||
otherwise: Joi.number().default(3306) |
||||
}), |
||||
|
||||
DB_RETRIES: Joi.number().integer().min(0).optional().default(3) |
||||
}), |
||||
|
||||
initDB: async () => { |
||||
let retries = process.env.DB_RETRIES; |
||||
|
||||
do { |
||||
try { |
||||
db = await connectToDB(); |
||||
return; |
||||
} catch (e) { |
||||
console.error(`Failed to connect to DB: ${e}`) |
||||
if (retries >= 2 || process.env.DB_RETRIES == 0) { |
||||
console.log(`Attempting another connection after 3s`); |
||||
await new Promise(res => setTimeout(res, 3000)); |
||||
} |
||||
} |
||||
} while (process.env.DB_RETRIES == 0 || --retries > 0); |
||||
|
||||
throw new Error("Failed to connect to DB"); |
||||
}, |
||||
|
||||
query: (table = process.env.DB_TABLE) => db(table), |
||||
db: () => db |
||||
}; |
@ -0,0 +1,62 @@ |
||||
'use strict'; |
||||
|
||||
const Boom = require('@hapi/boom'); |
||||
const Joi = require('joi'); |
||||
const Rfr = require('rfr'); |
||||
|
||||
const Calls = Rfr('/server/sections/calls'); |
||||
|
||||
|
||||
const mainHandler = async (request, h) => { |
||||
let options = { |
||||
activeSection: request.params.section |
||||
}; |
||||
|
||||
switch (request.params.section) { |
||||
case 'calls': |
||||
try { |
||||
options.filter = await Calls.getFilter(request.query); |
||||
} catch (e) { |
||||
throw Boom.badRequest(`CDR query failed validation: ${e}`); |
||||
} |
||||
|
||||
options.cdr = await Calls.getCdr(options.filter); |
||||
options.title = 'Звонки'; |
||||
|
||||
break; |
||||
case 'settings': |
||||
options.title = 'Настройки'; |
||||
default: |
||||
break; |
||||
} |
||||
|
||||
if (!options.pageCard) { |
||||
options.pageCard = options.title; |
||||
} |
||||
|
||||
return h.view(request.params.section, options); |
||||
} |
||||
|
||||
module.exports = { |
||||
routes: [{ |
||||
method: 'GET', |
||||
path: '/public/{file*}', |
||||
handler: { |
||||
directory: { |
||||
path: ['./client', './shared'] |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
method: 'GET', |
||||
path: '/{section?}', |
||||
handler: mainHandler, |
||||
options: { |
||||
validate: { |
||||
params: Joi.object({ |
||||
section: Joi.string().optional().valid('calls', 'reports', 'status', 'settings').default('calls') |
||||
}) |
||||
} |
||||
} |
||||
}] |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue