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