master
Dave S. 2 years ago
commit 4daf1630f3
  1. 5
      app.js
  2. 349
      client/3pt/css/normalize.css
  3. 231
      client/3pt/css/pikaday.css
  4. 635
      client/3pt/css/scroll.css
  5. 1
      client/3pt/js/dayjs.js
  6. 1
      client/3pt/js/dayjs_cpf.js
  7. 1298
      client/3pt/js/pikaday.js
  8. 13
      client/3pt/js/scroll.js
  9. 440
      client/css/calls.css
  10. 274
      client/css/fonts.css
  11. 616
      client/css/main.css
  12. 129
      client/css/player.css
  13. 111
      client/css/settings.css
  14. BIN
      client/favicon.ico
  15. BIN
      client/fonts/Exo2-Black.eot
  16. BIN
      client/fonts/Exo2-Black.ttf
  17. BIN
      client/fonts/Exo2-Black.woff
  18. BIN
      client/fonts/Exo2-Black.woff2
  19. BIN
      client/fonts/Exo2-BlackItalic.eot
  20. BIN
      client/fonts/Exo2-BlackItalic.ttf
  21. BIN
      client/fonts/Exo2-BlackItalic.woff
  22. BIN
      client/fonts/Exo2-BlackItalic.woff2
  23. BIN
      client/fonts/Exo2-Bold.eot
  24. BIN
      client/fonts/Exo2-Bold.ttf
  25. BIN
      client/fonts/Exo2-Bold.woff
  26. BIN
      client/fonts/Exo2-Bold.woff2
  27. BIN
      client/fonts/Exo2-BoldItalic.eot
  28. BIN
      client/fonts/Exo2-BoldItalic.ttf
  29. BIN
      client/fonts/Exo2-BoldItalic.woff
  30. BIN
      client/fonts/Exo2-BoldItalic.woff2
  31. BIN
      client/fonts/Exo2-ExtraBold.eot
  32. BIN
      client/fonts/Exo2-ExtraBold.ttf
  33. BIN
      client/fonts/Exo2-ExtraBold.woff
  34. BIN
      client/fonts/Exo2-ExtraBold.woff2
  35. BIN
      client/fonts/Exo2-ExtraBoldItalic.eot
  36. BIN
      client/fonts/Exo2-ExtraBoldItalic.ttf
  37. BIN
      client/fonts/Exo2-ExtraBoldItalic.woff
  38. BIN
      client/fonts/Exo2-ExtraBoldItalic.woff2
  39. BIN
      client/fonts/Exo2-ExtraLight.eot
  40. BIN
      client/fonts/Exo2-ExtraLight.ttf
  41. BIN
      client/fonts/Exo2-ExtraLight.woff
  42. BIN
      client/fonts/Exo2-ExtraLight.woff2
  43. BIN
      client/fonts/Exo2-ExtraLightItalic.eot
  44. BIN
      client/fonts/Exo2-ExtraLightItalic.ttf
  45. BIN
      client/fonts/Exo2-ExtraLightItalic.woff
  46. BIN
      client/fonts/Exo2-ExtraLightItalic.woff2
  47. BIN
      client/fonts/Exo2-Italic.eot
  48. BIN
      client/fonts/Exo2-Italic.ttf
  49. BIN
      client/fonts/Exo2-Italic.woff
  50. BIN
      client/fonts/Exo2-Italic.woff2
  51. BIN
      client/fonts/Exo2-Light.eot
  52. BIN
      client/fonts/Exo2-Light.ttf
  53. BIN
      client/fonts/Exo2-Light.woff
  54. BIN
      client/fonts/Exo2-Light.woff2
  55. BIN
      client/fonts/Exo2-LightItalic.eot
  56. BIN
      client/fonts/Exo2-LightItalic.ttf
  57. BIN
      client/fonts/Exo2-LightItalic.woff
  58. BIN
      client/fonts/Exo2-LightItalic.woff2
  59. BIN
      client/fonts/Exo2-Medium.eot
  60. BIN
      client/fonts/Exo2-Medium.ttf
  61. BIN
      client/fonts/Exo2-Medium.woff
  62. BIN
      client/fonts/Exo2-Medium.woff2
  63. BIN
      client/fonts/Exo2-MediumItalic.eot
  64. BIN
      client/fonts/Exo2-MediumItalic.ttf
  65. BIN
      client/fonts/Exo2-MediumItalic.woff
  66. BIN
      client/fonts/Exo2-MediumItalic.woff2
  67. BIN
      client/fonts/Exo2-Regular.eot
  68. BIN
      client/fonts/Exo2-Regular.ttf
  69. BIN
      client/fonts/Exo2-Regular.woff
  70. BIN
      client/fonts/Exo2-Regular.woff2
  71. BIN
      client/fonts/Exo2-SemiBold.eot
  72. BIN
      client/fonts/Exo2-SemiBold.ttf
  73. BIN
      client/fonts/Exo2-SemiBold.woff
  74. BIN
      client/fonts/Exo2-SemiBold.woff2
  75. BIN
      client/fonts/Exo2-SemiBoldItalic.eot
  76. BIN
      client/fonts/Exo2-SemiBoldItalic.ttf
  77. BIN
      client/fonts/Exo2-SemiBoldItalic.woff
  78. BIN
      client/fonts/Exo2-SemiBoldItalic.woff2
  79. BIN
      client/fonts/Exo2-Thin.eot
  80. BIN
      client/fonts/Exo2-Thin.ttf
  81. BIN
      client/fonts/Exo2-Thin.woff
  82. BIN
      client/fonts/Exo2-Thin.woff2
  83. BIN
      client/fonts/Exo2-ThinItalic.eot
  84. BIN
      client/fonts/Exo2-ThinItalic.ttf
  85. BIN
      client/fonts/Exo2-ThinItalic.woff
  86. BIN
      client/fonts/Exo2-ThinItalic.woff2
  87. BIN
      client/fonts/Exo2-VF.woff2
  88. BIN
      client/fonts/Exo2-VFI.woff2
  89. 634
      client/js/calls.js
  90. 112
      client/js/comcli.js
  91. 170
      client/js/player.js
  92. 1
      client/js/settings.js
  93. 75
      client/js/window.js
  94. 2038
      package-lock.json
  95. 26
      package.json
  96. 51
      server/ami.js
  97. 17
      server/comsrv.js
  98. 46
      server/config.js
  99. 77
      server/db.js
  100. 62
      server/fe.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -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;
}

Binary file not shown.

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.

@ -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));
// };
// });

2038
package-lock.json generated

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…
Cancel
Save