github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/templates/search_spends.html (about)

     1  <!DOCTYPE html>
     2  <html lang="en">
     3  
     4  <head>
     5  	<meta charset="UTF-8">
     6  	<meta name="viewport" content="width=device-width, initial-scale=1.0">
     7  	<title>Search & Statistics | Budget Manager</title>
     8  
     9  	<!-- Theme Switcher -->
    10  	<script src="{{ asStaticURL `/static/js/theme-switcher.js` }}"></script>
    11  
    12  	<link rel="stylesheet" href="{{ asStaticURL `/static/css/common.css` }}">
    13  
    14  	<style>
    15  		input[type=date] {
    16  			min-width: 120px;
    17  		}
    18  
    19  		.tooltip {
    20  			position: relative;
    21  		}
    22  
    23  		.tooltip .tooltip__text {
    24  			border: 2px solid var(--border-color--accent);
    25  			border-radius: 5px;
    26  			font-size: 14px;
    27  			padding: 3px 10px;
    28  			position: absolute;
    29  			top: 0;
    30  			visibility: hidden;
    31  			white-space: nowrap;
    32  		}
    33  
    34  		.tooltip:hover .tooltip__text {
    35  			visibility: visible;
    36  		}
    37  
    38  		/* Inspired by https://stackoverflow.com/a/23429132/7752659 */
    39  
    40  		.tooltip .tooltip__text::after,
    41  		.tooltip .tooltip__text::before {
    42  			border: 7px solid;
    43  			border-color: transparent var(--border-color--accent) transparent transparent;
    44  			content: "";
    45  			position: absolute;
    46  			right: 100%;
    47  			top: 1px;
    48  			z-index: 1;
    49  		}
    50  
    51  		.tooltip .tooltip__text::before {
    52  			border-width: 5px;
    53  			border-color: transparent var(--border-color--accent) transparent transparent;
    54  			top: 3px;
    55  			z-index: 2;
    56  		}
    57  
    58  		/* | Header */
    59  
    60  		#header {
    61  			position: relative;
    62  		}
    63  
    64  		#header__current-month-link {
    65  			position: absolute;
    66  			right: 0px;
    67  			top: 0;
    68  		}
    69  
    70  		#header__current-month-link svg {
    71  			height: 30px;
    72  			width: 30px;
    73  		}
    74  
    75  		/* | Search */
    76  
    77  		#content {
    78  			column-gap: 20px;
    79  			display: grid;
    80  			grid-template-columns: 1fr 5fr;
    81  			height: 100%;
    82  		}
    83  
    84  		/* || Filters */
    85  
    86  		#filters {
    87  			height: 100%;
    88  		}
    89  
    90  		#filters .card__body {
    91  			padding-left: 10px;
    92  			padding-right: 10px;
    93  		}
    94  
    95  		.filter {
    96  			margin-bottom: 15px;
    97  		}
    98  
    99  		.filter:last-child {
   100  			margin-bottom: 0;
   101  		}
   102  
   103  		#filters__cost,
   104  		#filters__time {
   105  			column-gap: 5px;
   106  			display: grid;
   107  			grid-template-columns: 1fr auto 1fr;
   108  		}
   109  
   110  		.filters__separator::before {
   111  			content: "–";
   112  		}
   113  
   114  		#filters__types {
   115  			margin-left: auto;
   116  			margin-right: auto;
   117  			min-width: 60%;
   118  			width: min-content;
   119  		}
   120  
   121  		#filters__types__header {
   122  			border-bottom: 1px solid var(--border-color);
   123  			margin: 0 auto 7px;
   124  			padding: 0 10px;
   125  			text-align: center;
   126  			width: min-content;
   127  			white-space: nowrap;
   128  		}
   129  
   130  		.filters__types__type {
   131  			white-space: nowrap;
   132  		}
   133  
   134  		#filters__buttons {
   135  			column-gap: 20px;
   136  			display: grid;
   137  			grid-template-columns: repeat(2, min-content);
   138  			justify-content: center;
   139  		}
   140  
   141  		.filters__button.feather-icon>svg {
   142  			height: 25px;
   143  			width: 25px;
   144  		}
   145  
   146  		/* || Result */
   147  
   148  		#result {
   149  			display: grid;
   150  			grid-template-rows: min-content auto;
   151  			position: relative;
   152  			row-gap: 10px;
   153  			overflow-y: auto;
   154  		}
   155  
   156  		#result__no-spends {
   157  			font-size: 25px;
   158  			left: 50%;
   159  			position: absolute;
   160  			text-align: center;
   161  			top: 30%;
   162  			transform: translateX(-50%);
   163  		}
   164  
   165  		#result__tab-titles {
   166  			border-bottom: 1px solid var(--border-color);
   167  			column-gap: 20px;
   168  			display: grid;
   169  			grid-template-columns: repeat(2, min-content);
   170  			justify-content: center;
   171  		}
   172  
   173  		.result__tab-title {
   174  			cursor: pointer;
   175  			font-size: 19px;
   176  			margin-bottom: 2px;
   177  			padding: 2px 5px;
   178  		}
   179  
   180  		.result__tab-title:focus,
   181  		.result__tab-title:hover,
   182  		.result__tab-title--chosen {
   183  			border-bottom: 2px solid var(--border-color--accent);
   184  			margin-bottom: 0;
   185  		}
   186  
   187  		#result__tabs {
   188  			background-color: var(--base-color);
   189  			overflow-y: auto;
   190  			z-index: 0;
   191  		}
   192  
   193  		.result__tab {
   194  			display: none;
   195  			height: 100%;
   196  		}
   197  
   198  		.result__tab--chosen {
   199  			display: block;
   200  		}
   201  
   202  		/* ||| Spends */
   203  
   204  		#spends__table {
   205  			/*
   206  				We have to overwrite property 'border-collapse' because it doesn't work properly with 'position: sticky':
   207  				https://stackoverflow.com/a/53559396/7752659
   208  			*/
   209  			border-collapse: separate;
   210  			border-spacing: 0;
   211  		}
   212  
   213  		#spends__table th {
   214  			background-color: var(--background-color);
   215  			border-bottom: 1px solid var(--border-color--accent);
   216  			font-size: 18px;
   217  			position: sticky;
   218  			top: 0;
   219  		}
   220  
   221  		#spends__table th,
   222  		#spends__table td {
   223  			padding-right: 15px;
   224  			padding-left: 15px;
   225  		}
   226  
   227  		#spends__table th:first-child,
   228  		#spends__table td:first-child {
   229  			padding-left: 5px;
   230  		}
   231  
   232  		#spends__table th:last-child,
   233  		#spends__table td:last-child {
   234  			padding-right: 5px;
   235  		}
   236  
   237  		#spends__table th * {
   238  			color: inherit
   239  		}
   240  
   241  		.spends__table__date,
   242  		.spends__table__type,
   243  		.spends__table__cost,
   244  		.spends__table__link {
   245  			width: 1px;
   246  			white-space: nowrap;
   247  		}
   248  
   249  		#spends__table .spends__table__cost {
   250  			padding-right: 20px;
   251  		}
   252  
   253  		.spends__table__cost {
   254  			text-align: right;
   255  		}
   256  
   257  		.spends__table__link {
   258  			text-align: center;
   259  			z-index: 1;
   260  		}
   261  
   262  		.spends__table__link .feather-icon>svg {
   263  			height: 20px;
   264  			width: 20px;
   265  		}
   266  
   267  		.spends__table__sort-icon__wrapper.feather-icon {
   268  			display: inline-grid;
   269  			grid-template-rows: 1fr 1fr;
   270  			vertical-align: middle;
   271  		}
   272  
   273  		.spends__table__sort-icon__wrapper svg {
   274  			height: 12px;
   275  			stroke-width: 4;
   276  			width: 12px;
   277  		}
   278  
   279  		.spends__table__sort-icon__wrapper svg.chosen {
   280  			opacity: 1;
   281  			stroke: var(--icon-color--hover);
   282  		}
   283  
   284  		/* ||| Statistics */
   285  
   286  		#statistics__charts {
   287  			column-gap: 50px;
   288  			display: grid;
   289  			grid-template-columns: 1fr 2fr;
   290  			grid-template-rows: 1fr 1fr;
   291  			grid-template-areas:
   292  				"spent-by-spend-type cost-distribution"
   293  				"spent-by-day spent-by-day";
   294  			row-gap: 30px;
   295  			overflow-y: auto;
   296  			width: 100%;
   297  		}
   298  
   299  		#spent-by-spend-type {
   300  			grid-area: spent-by-spend-type;
   301  		}
   302  
   303  		#spent-by-spend-type__total-cost {
   304  			font-size: 18px;
   305  			left: 50%;
   306  			position: absolute;
   307  			text-align: center;
   308  			top: 50%;
   309  			transform: translate(-50%, -50%);
   310  			z-index: -1;
   311  		}
   312  
   313  		#cost-distribution {
   314  			grid-area: cost-distribution;
   315  		}
   316  
   317  		#spent-by-day {
   318  			grid-area: spent-by-day;
   319  		}
   320  
   321  		.statistics__chart-title {
   322  			font-size: 18px;
   323  			margin-bottom: 15px;
   324  			text-align: center;
   325  		}
   326  
   327  		.statistics__chart {
   328  			height: 80%;
   329  			position: relative;
   330  		}
   331  
   332  		/* | Layouts */
   333  
   334  		/* For medium screens (<= 1350px) */
   335  		@media (max-width: 1350px) {
   336  
   337  			#filters__cost,
   338  			#filters__time {
   339  				grid-template-columns: 1fr;
   340  				grid-template-rows: 1fr 1fr;
   341  				row-gap: 10px;
   342  			}
   343  
   344  			.filters__separator {
   345  				display: none;
   346  			}
   347  
   348  			/* Hide notes */
   349  			.spends__table__notes {
   350  				display: none;
   351  			}
   352  
   353  			#statistics__charts {
   354  				grid-template-columns: 1fr;
   355  				grid-template-rows: 1fr 1fr 1fr;
   356  				grid-template-areas:
   357  					"spent-by-spend-type"
   358  					"cost-distribution"
   359  					"spent-by-day";
   360  				height: unset;
   361  			}
   362  
   363  			#spent-by-spend-type .statistics__chart {
   364  				max-width: 500px;
   365  			}
   366  
   367  			.statistics__chart {
   368  				margin: auto;
   369  				width: 90%;
   370  			}
   371  		}
   372  	</style>
   373  </head>
   374  
   375  <body>
   376  	<div id="app">
   377  		<div id="header">
   378  			<div>
   379  				<span class="header__path__element">Search</span>
   380  				<span class="header__path__element">Spends</span>
   381  			</div>
   382  
   383  			<div id="header__current-month-link">
   384  				<a href="/" class="feather-icon" title="Go to the Current Month">
   385  					{{ template "components/icon" "home" }}
   386  				</a>
   387  			</div>
   388  		</div>
   389  
   390  		<div id="content">
   391  			<!-- Filters -->
   392  			<div id="filters" class="card">
   393  				<div class="card__title noselect">Filters</div>
   394  
   395  				<form action="/search/spends" class="card__body">
   396  					<!-- Title -->
   397  					<div id="filters__title" class="filter">
   398  						<input type="text" name="title" placeholder="Title" title="Title">
   399  					</div>
   400  
   401  					<!-- Notes -->
   402  					<div id="filters__notes" class="filter">
   403  						<input type="text" name="notes" placeholder="Notes" title="Notes">
   404  					</div>
   405  
   406  					<!-- Cost -->
   407  					<div id="filters__cost" class="filter">
   408  						<div id="filters__cost__min">
   409  							<input type="text" name="min_cost" placeholder="Min Cost" title="Minimal Cost">
   410  						</div>
   411  
   412  						<div class="filters__separator"></div>
   413  
   414  						<div id="filters__cost__max">
   415  							<input type="text" name="max_cost" placeholder="Max Cost" title="Maximal Cost">
   416  						</div>
   417  					</div>
   418  
   419  					<!-- Time -->
   420  					<div id="filters__time" class="filter">
   421  						<div id="filters__time__after">
   422  							<input type="date" name="after" title="After">
   423  						</div>
   424  
   425  						<div class="filters__separator"></div>
   426  
   427  						<div id="filters__time__before">
   428  							<input type="date" name="before" title="Before">
   429  						</div>
   430  					</div>
   431  
   432  					<!-- Spend Types -->
   433  					<div id="filters__types" class="filter">
   434  						<div id="filters__types__header" class="noselect">Spend Types</div>
   435  						<!-- Without Type option -->
   436  						<div class="filters__types__type">
   437  							<input id="filters__types__type-0" type="checkbox" name="type_id" value="0">
   438  							<label for="filters__types__type-0" title="Use this option to search for Spends without type">Without Type</label>
   439  						</div>
   440  						{{ range .SpendTypes }}
   441  						<div class="filters__types__type">
   442  							<input id="filters__types__type-{{ .ID }}" type="checkbox" name="type_id" value="{{ .ID }}">
   443  							<label for="filters__types__type-{{ .ID }}">{{ .FullName }}</label>
   444  						</div>
   445  						{{ end }}
   446  					</div>
   447  
   448  					<!-- Hidden fields -->
   449  					<div style="display: none;">
   450  						<input id="filters__types__hidden__sort" type="text" name="sort" value="">
   451  						<input id="filters__types__hidden__order" type="text" name="order" value="">
   452  					</div>
   453  
   454  					<!-- Buttons -->
   455  					<div id="filters__buttons">
   456  						<button type="button" class="filters__button feather-icon" title="Reset" onclick="resetForm(this.form)">
   457  							{{ template "components/icon" "rotate-ccw" }}
   458  						</button>
   459  
   460  						<button type="submit" class="filters__button feather-icon" title="Search" onclick="updateFormActionHash(this.form)">
   461  							{{ template "components/icon" "search" }}
   462  						</button>
   463  					</div>
   464  				</form>
   465  			</div>
   466  
   467  			<!-- Result -->
   468  			<div id="result">
   469  				{{ if not .Spends }}
   470  				<div id="result__no-spends" class="noselect">No Spends found</div>
   471  				{{ else }}
   472  				<div id="result__tab-titles" class="noselect">
   473  					<button class="result__tab-title" data-title-for-tab="spends" onclick="switchTab('spends')">
   474  						Spends
   475  					</button>
   476  					<button class="result__tab-title" data-title-for-tab="statistics" onclick="switchTab('statistics')">
   477  						Statistics
   478  					</button>
   479  				</div>
   480  
   481  				<div id="result__tabs">
   482  					<!-- Spends -->
   483  					<div id="spends" class="result__tab">
   484  						<table id="spends__table">
   485  							<thead>
   486  								<tr class="noselect">
   487  									<th class="spends__table__date">
   488  										<span>Date</span>
   489  										<button class="feather-icon spends__table__sort-icon__wrapper" title="Sort" onclick="searchWithSort('date')">
   490  											{{ template "components/icon" "chevron-up" }}
   491  											{{ template "components/icon" "chevron-down" }}
   492  										</button>
   493  									</th>
   494  									<th class="spends__table__title">
   495  										<span>Title</span>
   496  										<button class="feather-icon spends__table__sort-icon__wrapper" title="Sort" onclick="searchWithSort('title')">
   497  											{{ template "components/icon" "chevron-up" }}
   498  											{{ template "components/icon" "chevron-down" }}
   499  										</button>
   500  									</th>
   501  									<th class="spends__table__notes">Notes</th>
   502  									<th class="spends__table__type">Type</th>
   503  									<th class="spends__table__cost">
   504  										<span>Cost</span>
   505  										<button class="feather-icon spends__table__sort-icon__wrapper" title="Sort" onclick="searchWithSort('cost')">
   506  											{{ template "components/icon" "chevron-up" }}
   507  											{{ template "components/icon" "chevron-down" }}
   508  										</button>
   509  									</th>
   510  									<th class="spends__table__link">Link</th>
   511  								</tr>
   512  							</thead>
   513  
   514  							<tbody>
   515  								{{ range .Spends }}
   516  								<tr>
   517  									<td class="spends__table__date">{{ .Year }}-{{ printf "%02d" .Month }}-{{ printf "%02d" .Day }}</td>
   518  									<td class="spends__table__title">{{ .Title }}</td>
   519  									<td class="spends__table__notes">{{ .Notes }}</td>
   520  									<td class="spends__table__type">
   521  										{{ if .Type }}
   522  										<span>{{ .Type.Name }}</span>
   523  										{{ else }}
   524  										<span>-</span>
   525  										{{ end }}
   526  									</td>
   527  									<td class="spends__table__cost">{{ .Cost }}</td>
   528  									<td class="spends__table__link">
   529  										<a href="/months/month?year={{ .Year }}&month={{ printf `%d` .Month }}#{{ .Day }}" class="feather-icon" title="View Day">
   530  											{{ template "components/icon" "external-link" }}
   531  										</a>
   532  									</td>
   533  								</tr>
   534  								{{ end }}
   535  							</tbody>
   536  						</table>
   537  					</div>
   538  
   539  					<!-- Statistics -->
   540  					<div id="statistics" class="result__tab">
   541  						<div id="statistics__charts">
   542  							<!-- Total cost is not centered vertically without 'height: min-content' for some reason -->
   543  							<div id="spent-by-spend-type" style="height: min-content;">
   544  								<div class="statistics__chart-title noselect">Spent by Type</div>
   545  								<div class="statistics__chart">
   546  									<div id="spent-by-spend-type__total-cost">
   547  										<span class="money--lose">{{ .TotalCost }}</span>
   548  										<br>
   549  										<span>({{ len .Spends }})</span>
   550  									</div>
   551  
   552  									<canvas id="spent-by-spend-type__chart"></canvas>
   553  								</div>
   554  							</div>
   555  
   556  							<div id="cost-distribution">
   557  								<div class="statistics__chart-title">
   558  									<span class="noselect">Cost Distribution</span>
   559  									<span class="feather-icon tooltip" style="vertical-align: middle;">
   560  										{{ template "components/icon" "help-circle" }}
   561  
   562  										<span class="tooltip__text">Without 5% of the smallest and largest Spends</span>
   563  									</span>
   564  								</div>
   565  								<div class="statistics__chart">
   566  									<canvas id="cost-distribution__chart"></canvas>
   567  								</div>
   568  							</div>
   569  
   570  							<div id="spent-by-day">
   571  								<div class="statistics__chart-title noselect">Spent by Day</div>
   572  								<div class="statistics__chart">
   573  									<canvas id="spent-by-day__chart"></canvas>
   574  								</div>
   575  							</div>
   576  						</div>
   577  					</div>
   578  				</div>
   579  				{{ end }}
   580  			</div>
   581  		</div>
   582  
   583  		{{ template "components/footer.html" .Footer }}
   584  	</div>
   585  
   586  	<script>
   587  		// Sort and order must be set in the first 'load' event listener
   588  		let CurrentSort = "";
   589  		let CurrentOrder = "";
   590  
   591  		// Update Search Options with query params
   592  		window.addEventListener("load", () => {
   593  			const query = new URLSearchParams(location.search);
   594  
   595  			// Title
   596  			const title = query.get("title");
   597  			setOptionValue("filters__title", title);
   598  
   599  			// Notes
   600  			const notes = query.get("notes");
   601  			setOptionValue("filters__notes", notes);
   602  
   603  			// Min Cost
   604  			const minCost = query.get("min_cost");
   605  			setOptionValue("filters__cost__min", minCost);
   606  
   607  			// Max cost
   608  			const maxCost = query.get("max_cost");
   609  			setOptionValue("filters__cost__max", maxCost);
   610  
   611  			// After
   612  			const after = query.get("after");
   613  			setOptionValue("filters__time__after", after);
   614  
   615  			// Before
   616  			const before = query.get("before");
   617  			setOptionValue("filters__time__before", before);
   618  
   619  			// Sort and Order
   620  
   621  			CurrentSort = query.get("sort");
   622  			let sortSelector = ""
   623  			if (CurrentSort === "title") {
   624  				sortSelector = ".spends__table__title"
   625  			} else if (CurrentSort === "cost") {
   626  				sortSelector = ".spends__table__cost"
   627  			} else {
   628  				// Default sort is by date
   629  				CurrentSort = "date";
   630  				sortSelector = ".spends__table__date"
   631  			}
   632  
   633  			CurrentOrder = query.get("order");
   634  			let orderSelector = "";
   635  			if (CurrentOrder === "desc") {
   636  				orderSelector = "svg:last-child";
   637  			} else {
   638  				// Default order is asc
   639  				CurrentOrder = "asc";
   640  				orderSelector = "svg:first-child";
   641  			}
   642  
   643  			const selector = sortSelector + " " + orderSelector;
   644  			const icon = document.querySelector(selector);
   645  			if (icon !== null) {
   646  				icon.classList.add("chosen");
   647  			}
   648  
   649  			// Don't set sort and order in form. It allows to 'reset' sort/order by clicking 'Search' button
   650  
   651  			// Spend Types
   652  			const typeIDs = query.getAll("type_id");
   653  			for (let i = 0; i < typeIDs.length; i++) {
   654  				const elemID = "filters__types__type-" + typeIDs[i];
   655  				const checkbox = document.getElementById(elemID);
   656  				if (!checkbox) {
   657  					console.error(`element '${elemID}' doesn't exist`);
   658  					continue;
   659  				}
   660  				checkbox.checked = true;
   661  			}
   662  		})
   663  
   664  		/**
   665  		* @param {string} parentID - id of an input parent
   666  		* @param {string} value - new value
   667  		*/
   668  		function setOptionValue(parentID, value) {
   669  			if (value === "") {
   670  				return;
   671  			}
   672  
   673  			const parent = document.getElementById(parentID);
   674  			if (parent === undefined) {
   675  				console.error(`element '${parentID}' doesn't exist`);
   676  				return;
   677  			}
   678  			if (parent.childElementCount < 1) {
   679  				console.error(`element '${parentID}' doesn't have any children`);
   680  				return;
   681  			}
   682  			if (parent.children[0].tagName !== "INPUT") {
   683  				console.error(`the first child of element '${parentID}' must be <input>`);
   684  				return;
   685  			}
   686  
   687  			const elem = parent.children[0];
   688  			elem.value = value;
   689  		}
   690  
   691  		// Set date titles
   692  		window.addEventListener("load", () => {
   693  			const dates = document.querySelectorAll("td.spends__table__date");
   694  			for (let i = 0; i < dates.length; i++) {
   695  				const date = dates[i].textContent;
   696  				dates[i].title = convertDateToHumanReadableFormat(date);
   697  			}
   698  		})
   699  
   700  		let humanReadableDateCache = {};
   701  		/**
   702  		 * Format date '2019-11-01' as 'Friday, November 1, 2019'
   703  		 */
   704  		function convertDateToHumanReadableFormat(dateStr) {
   705  			const dateFormatOptions = { weekday: "long", year: "numeric", month: "long", day: "numeric" };
   706  
   707  			let date = humanReadableDateCache[dateStr];
   708  			if (!date) {
   709  				date = new Date(dateStr).toLocaleDateString("en-US", dateFormatOptions);
   710  				humanReadableDateCache[dateStr] = date;
   711  			}
   712  			return date;
   713  		}
   714  
   715  		// Group Spends by date with accent color
   716  		window.addEventListener("load", () => {
   717  			if (CurrentSort !== "date") {
   718  				// Group Spends only when they are sorted by date
   719  				return;
   720  			}
   721  
   722  			let lastDate = "";
   723  			const rows = document.querySelectorAll("#spends__table tbody tr");
   724  			for (const row of rows) {
   725  				const dateColumn = row.querySelector("td.spends__table__date");
   726  				if (!dateColumn) continue;
   727  
   728  				const date = dateColumn.innerText;
   729  				if (lastDate === "") {
   730  					lastDate = date;
   731  					continue;
   732  				}
   733  				if (lastDate === date) {
   734  					continue;
   735  				}
   736  
   737  				lastDate = date;
   738  
   739  				const columns = row.querySelectorAll("td");
   740  				for (const column of columns) {
   741  					column.style.borderTop = "1px solid var(--border-color--accent)";
   742  				}
   743  			}
   744  		})
   745  
   746  		/**
   747  		 * Change Sort and Order and submit form
   748  		 */
   749  		function searchWithSort(newSort) {
   750  			let sortValue = "";
   751  			let orderValue = "";
   752  
   753  			if (CurrentSort !== newSort) {
   754  				// Change sort type and set order to 'asc'
   755  				sortValue = newSort;
   756  				orderValue = "asc";
   757  			} else {
   758  				// Change sort order
   759  				if (CurrentOrder === "asc") {
   760  					orderValue = "desc";
   761  				} else if (CurrentOrder === "desc") {
   762  					orderValue = "asc";
   763  				} else {
   764  					console.error(`invalid current order value: '${CurrentOrder}'`);
   765  					orderValue = "asc";
   766  				}
   767  
   768  				sortValue = CurrentSort;
   769  			}
   770  
   771  			const sortInput = document.getElementById("filters__types__hidden__sort");
   772  			sortInput.value = sortValue;
   773  			const orderInput = document.getElementById("filters__types__hidden__order");
   774  			orderInput.value = orderValue;
   775  
   776  			// Submit form
   777  			const form = document.querySelector("#filters form");
   778  			if (form === null) {
   779  				console.error("couldn't get form")
   780  				return;
   781  			}
   782  			form.submit();
   783  		}
   784  
   785  		// Switch tab according to url hash
   786  		window.addEventListener("load", () => {
   787  			// Get hash without '#'
   788  			let tabID = window.location.hash.slice(1);
   789  			if (tabID != "spends" && tabID != "statistics") {
   790  				tabID = "spends";
   791  			}
   792  			switchTab(tabID);
   793  		})
   794  
   795  		/**
   796  		* @param {HTMLFormElement} form
   797  		*/
   798  		function updateFormActionHash(form) {
   799  			const action = form.action;
   800  			if (action.includes(window.location.hash)) {
   801  				return;
   802  			}
   803  			form.action = action + window.location.hash;
   804  		}
   805  
   806  		/**
   807  		* Reset form exclude date inputs
   808  		*
   809  		* @param {HTMLFormElement} form
   810  		*/
   811  		function resetForm(form) {
   812  			const dateInputs = form.querySelectorAll("input[type='date']");
   813  
   814  			// Store date input values
   815  			const values = {};
   816  			for (const input of dateInputs) {
   817  				values[input.name] = input.value;
   818  			}
   819  
   820  			// Reset form
   821  			form.reset();
   822  
   823  			// Restore date inputs
   824  			for (const input of dateInputs) {
   825  				input.value = values[input.name];
   826  			}
   827  		}
   828  
   829  		/**
   830  		* @param {string} tabID - tab id to display
   831  		*/
   832  		function switchTab(tabID) {
   833  			// Switch tab title
   834  			const chosenTabTitleClass = "result__tab-title--chosen";
   835  			const attrWithTabID = "data-title-for-tab";
   836  
   837  			const tabTitles = document.querySelectorAll("#result__tab-titles .result__tab-title");
   838  			for (let i = 0; i < tabTitles.length; i++) {
   839  				tabTitles[i].classList.remove(chosenTabTitleClass);
   840  
   841  				const attr = tabTitles[i].getAttribute(attrWithTabID);
   842  				if (attr === tabID) {
   843  					tabTitles[i].classList.add(chosenTabTitleClass);
   844  				}
   845  			}
   846  
   847  			// Switch tab
   848  			const chosenTabClass = "result__tab--chosen";
   849  
   850  			const tabs = document.querySelectorAll("#result__tabs .result__tab");
   851  			for (let i = 0; i < tabs.length; i++) {
   852  				tabs[i].classList.remove(chosenTabClass);
   853  
   854  				if (tabs[i].id == tabID) {
   855  					tabs[i].classList.add(chosenTabClass);
   856  				}
   857  			}
   858  
   859  			// Update url hash
   860  			window.location.hash = tabID;
   861  		}
   862  	</script>
   863  
   864  	<!-- Statistics Charts -->
   865  	{{ if .Spends }}
   866  	<script src="{{ asStaticURL `/static/vendor/chart.js/chart.min.js` }}"></script>
   867  	<script src="{{ asStaticURL `/static/vendor/chartjs-plugin-datalabels/chartjs-plugin-datalabels.min.js` }}"></script>
   868  	<script src="{{ asStaticURL `/static/js/chart.js` }}"></script>
   869  	<script>
   870  		Chart.register(ChartDataLabels);
   871  		Chart.defaults.plugins.tooltip.displayColors = false;
   872  
   873  		const invisibleColor = "#00000000";
   874  
   875  		function getBackgroundColor() {
   876  			return isDarkTheme() ? "#26292f" : "#dddddd";
   877  		}
   878  
   879  		function getHoverBackgroundColor() {
   880  			return isDarkTheme() ? "#31343a" : "#d4d4d4";
   881  		}
   882  
   883  		function getBorderColor() {
   884  			return isDarkTheme() ? "#0d1117" : "#ffffff";
   885  		}
   886  
   887  		// Spent by Spend Type chart
   888  
   889  		const spentBySpendTypeStatistics = JSON.parse(`{{ .SpentBySpendTypeDatasets }}`);
   890  		let spendBySpendTypeDatasets = [];
   891  		for (const data of spentBySpendTypeStatistics) {
   892  			spendBySpendTypeDatasets.push({
   893  				data: data.map(v => v["spent"]),
   894  				labels: data.map(v => v["spend_type_name"]),
   895  				backgroundColor: data.map(v => v["spend_type_name"] ? getBackgroundColor() : invisibleColor),
   896  				hoverBackgroundColor: data.map(v => v["spend_type_name"] ? getHoverBackgroundColor() : invisibleColor),
   897  				borderColor: data.map(v => v["spend_type_name"] ? getBorderColor() : invisibleColor),
   898  				hoverBorderColor: data.map(v => invisibleColor), // Hide hover border colors
   899  			});
   900  		}
   901  
   902  		const totalSpent = Math.abs(`{{ .TotalCost }}`);
   903  		// labelSeparator is En dash - https://en.wikipedia.org/wiki/Dash#En_dash
   904  		const labelSeparator = " – ";
   905  
   906  		const spentBySpendTypeCtx = document.getElementById("spent-by-spend-type__chart").getContext("2d");
   907  		const spentBySpendTypeChart = new Chart(spentBySpendTypeCtx, {
   908  			type: "doughnut",
   909  			data: {
   910  				datasets: spendBySpendTypeDatasets,
   911  			},
   912  			options: {
   913  				maintainAspectRatio: true,
   914  				// Tune doughnut
   915  				cutout: "30%",
   916  				rotation: 225,
   917  				// Plugins
   918  				plugins: {
   919  					tooltip: {
   920  						filter: function (tooltipItem, index, tooltipItems, data) {
   921  							const labels = data.datasets[tooltipItem.datasetIndex].labels;
   922  							return labels ? labels[tooltipItem.dataIndex] !== "" : true;
   923  						},
   924  						callbacks: {
   925  							label: function (tooltipItem) {
   926  								const dataset = tooltipItem.dataset;
   927  								const index = tooltipItem.dataIndex;
   928  								return dataset.labels[index] + labelSeparator + dataset.data[index];
   929  							},
   930  						}
   931  					},
   932  					datalabels: {
   933  						textAlign: "center",
   934  						formatter: function (value, context) {
   935  							const dataset = context.chart.data.datasets[context.datasetIndex];
   936  							if (!dataset || !dataset.labels) return "";
   937  
   938  							const label = dataset.labels[context.dataIndex];
   939  							if (!label) return "";
   940  
   941  							return `${label}\n${(value / totalSpent * 100).toFixed(2)}%`;
   942  						},
   943  						display: function (context) {
   944  							const dataset = context.chart.data.datasets[context.datasetIndex];
   945  							if (!dataset || !dataset.labels) return false;
   946  
   947  							const label = dataset.labels[context.dataIndex];
   948  							if (!label) return false;
   949  
   950  							return "auto";
   951  						}
   952  					}
   953  				}
   954  			}
   955  		});
   956  
   957  		// Cost Distribution chart
   958  
   959  		const costIntervals = JSON.parse(`{{ .CostIntervals }}`);
   960  		let costIntervalLabels = [];
   961  		let costIntervalData = [];
   962  		for (const data of costIntervals) {
   963  			costIntervalLabels.push(data.from);
   964  			costIntervalData.push(data.total);
   965  		}
   966  		costIntervalLabels.push(costIntervals[costIntervals.length - 1].to);
   967  
   968  		const costIntervalsCtx = document.getElementById("cost-distribution__chart").getContext("2d");
   969  		const costIntervalsChart = new Chart(costIntervalsCtx, {
   970  			type: "bar",
   971  			data: {
   972  				labels: new Array(costIntervalLabels.length - 1).fill(""),
   973  				datasets: [{
   974  					data: costIntervalData,
   975  					backgroundColor: getBackgroundColor(),
   976  					hoverBackgroundColor: getHoverBackgroundColor(),
   977  				}],
   978  			},
   979  			plugins: [
   980  				{
   981  					// Update right padding to be able to display last interval
   982  					afterInit: chart => {
   983  						const longestLabel = costIntervalLabels[costIntervalLabels.length - 1];
   984  						const textWidth = chart.ctx.measureText(longestLabel).width;
   985  
   986  						// Add additional 5px
   987  						chart.options.layout.padding.right = textWidth / 2 + 5;
   988  						chart.update();
   989  					},
   990  					// Draw intervals
   991  					afterDraw: chart => {
   992  						const axis = chart.scales["x"];
   993  						const tickDistance = axis.width / (costIntervalLabels.length - 1);
   994  
   995  						const y = chart.height - 10;
   996  						const offset = axis.left;
   997  
   998  						const ctxFont = chart.ctx.font;
   999  						const fillStyle = chart.ctx.fillStyle;
  1000  
  1001  						// Sometimes canvas font can be bold (for example, on hover). Change it to normal while drawing intervals
  1002  						chart.ctx.font = chart.ctx.font.replace("bold", "");
  1003  						chart.ctx.fillStyle = chart.options.color;
  1004  
  1005  						for (let i = 0; i < costIntervalLabels.length; i++) {
  1006  							const text = costIntervalLabels[i];
  1007  							const textWidth = chart.ctx.measureText(text).width;
  1008  							const x = offset + tickDistance * i - textWidth / 2;
  1009  
  1010  							chart.ctx.save();
  1011  							chart.ctx.fillText(text, x, y);
  1012  							chart.ctx.restore();
  1013  						}
  1014  
  1015  						// Revert canvas options
  1016  						chart.ctx.font = ctxFont;
  1017  						chart.ctx.fillStyle = fillStyle;
  1018  					}
  1019  				}
  1020  			],
  1021  			options: {
  1022  				plugins: {
  1023  					tooltip: {
  1024  						callbacks: {
  1025  							label: function (tooltipItem) {
  1026  								const interval = costIntervals[tooltipItem.dataIndex];
  1027  								return `${tooltipItem.raw} – ${interval.count} Spends`;
  1028  							},
  1029  						}
  1030  					},
  1031  					datalabels: {
  1032  						display: function (context) {
  1033  							const dataset = context.chart.data.datasets[context.datasetIndex];
  1034  							if (!dataset) return false;
  1035  
  1036  							const value = dataset.data[context.dataIndex];
  1037  							if (!value) return false;
  1038  
  1039  							return "auto";
  1040  						}
  1041  					}
  1042  				},
  1043  				scales: {
  1044  					y: { ticks: { callback: Chart.Ticks.formatters.money } }
  1045  				}
  1046  			}
  1047  		});
  1048  
  1049  		// Spent by Day chart
  1050  
  1051  		const spentByDayStatistics = JSON.parse(`{{ .SpentByDayDataset }}`);
  1052  		let spentByDayLabels = [];
  1053  		let spentByDayData = [];
  1054  		for (const data of spentByDayStatistics) {
  1055  			const month = data.month < 10 ? "0" + data.month : data.month;
  1056  			const day = data.day < 10 ? "0" + data.day : data.day;
  1057  			spentByDayLabels.push(`${data.year}-${month}-${day}`)
  1058  
  1059  			spentByDayData.push(data.spent);
  1060  		}
  1061  
  1062  		const spentByDayCtx = document.getElementById("spent-by-day__chart").getContext("2d");
  1063  		const spentByDayChart = new Chart(spentByDayCtx, {
  1064  			type: "bar",
  1065  			data: {
  1066  				labels: spentByDayLabels,
  1067  				datasets: [{
  1068  					data: spentByDayData,
  1069  					backgroundColor: getBackgroundColor(),
  1070  					hoverBackgroundColor: getHoverBackgroundColor(),
  1071  				}],
  1072  			},
  1073  			options: {
  1074  				plugins: {
  1075  					tooltip: {
  1076  						callbacks: {
  1077  							title: function (tooltipItem) { return "" },
  1078  							label: function (tooltipItem) {
  1079  								const date = convertDateToHumanReadableFormat(tooltipItem.label)
  1080  								return date + labelSeparator + tooltipItem.raw;
  1081  							},
  1082  						}
  1083  					},
  1084  					datalabels: {
  1085  						display: function (context) {
  1086  							const dataset = context.chart.data.datasets[context.datasetIndex];
  1087  							if (!dataset) return false;
  1088  
  1089  							const value = dataset.data[context.dataIndex];
  1090  							if (!value) return false;
  1091  
  1092  							return "auto";
  1093  						}
  1094  					}
  1095  				},
  1096  				scales: {
  1097  					y: { ticks: { callback: Chart.Ticks.formatters.money } }
  1098  				}
  1099  			}
  1100  		});
  1101  
  1102  		window.addEventListener(themeChangeEventName, () => {
  1103  			const backgroundColor = getBackgroundColor();
  1104  			const hoverBackgroundColor = getHoverBackgroundColor();
  1105  			const borderColor = getBorderColor();
  1106  			const gridLinesColor = getGridLinesColor();
  1107  
  1108  			function getDatasetColor(color, newColor) {
  1109  				if (color instanceof Array) {
  1110  					for (let i = 0; i < color.length; i++) {
  1111  						if (color[i] === invisibleColor) {
  1112  							continue;
  1113  						}
  1114  						color[i] = newColor;
  1115  					}
  1116  					return color;
  1117  				}
  1118  
  1119  				if (typeof color === "string" || color instanceof String) {
  1120  					if (color === invisibleColor) {
  1121  						return color;
  1122  					}
  1123  					return newColor;
  1124  				}
  1125  
  1126  				// Indicate something went wrong
  1127  				return "red";
  1128  			}
  1129  
  1130  			for (const chart of [spentBySpendTypeChart, costIntervalsChart, spentByDayChart]) {
  1131  				// Update dataset color
  1132  				for (const dataset of chart.data.datasets) {
  1133  					dataset.backgroundColor = getDatasetColor(dataset.backgroundColor, backgroundColor);
  1134  					dataset.hoverBackgroundColor = getDatasetColor(dataset.hoverBackgroundColor, hoverBackgroundColor);
  1135  					dataset.borderColor = getDatasetColor(dataset.borderColor, borderColor);
  1136  				}
  1137  
  1138  				// Update color of grid lines
  1139  				if (chart.options.scales) {
  1140  					for (const axis of [chart.options.scales.x, chart.options.scales.y]) {
  1141  						if (axis) {
  1142  							axis.grid.color = gridLinesColor
  1143  						}
  1144  					}
  1145  				}
  1146  
  1147  				chart.update();
  1148  			}
  1149  		})
  1150  	</script>
  1151  	{{ end }}
  1152  
  1153  	<!-- Link Formatter -->
  1154  	{{ if .Spends }}
  1155  	<script src="{{ asStaticURL `/static/js/link-formatter.js` }}"></script>
  1156  	<script>
  1157  		window.addEventListener("load", () => {
  1158  			formatLinks(".spends__table__notes");
  1159  		})
  1160  	</script>
  1161  	{{ end }}
  1162  </body>
  1163  
  1164  </html>