github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/templates/month.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>{{ .Month.Month }} {{ .Year }} | 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  		/* | Header */
    16  
    17  		#header {
    18  			position: relative;
    19  		}
    20  
    21  		#header__buttons {
    22  			column-gap: 5px;
    23  			display: grid;
    24  			grid-template-columns: repeat(2, 1fr);
    25  			height: 100%;
    26  			position: absolute;
    27  			right: 0px;
    28  			top: 0px;
    29  		}
    30  
    31  		#header__buttons svg {
    32  			height: 30px;
    33  			width: 30px;
    34  		}
    35  
    36  		/* | Content */
    37  
    38  		#content {
    39  			column-gap: 30px;
    40  			display: grid;
    41  			height: 100%;
    42  			row-gap: 20px;
    43  		}
    44  
    45  		.card {
    46  			overflow-x: auto;
    47  		}
    48  
    49  		.card__body .copy-from-previous-month {
    50  			height: 30px;
    51  			margin-top: 30px;
    52  			text-align: center;
    53  		}
    54  
    55  		.card__body .copy-from-previous-month>input {
    56  			width: auto;
    57  			padding: 6px;
    58  		}
    59  
    60  		.table-shrink-cell {
    61  			width: 1px;
    62  			white-space: nowrap;
    63  		}
    64  
    65  		th.money,
    66  		td.money {
    67  			min-width: 80px;
    68  			text-align: right;
    69  		}
    70  
    71  		.card__title>.money {
    72  			font-size: 1rem;
    73  		}
    74  
    75  		/* || Income */
    76  
    77  		#incomes {
    78  			grid-area: incomes;
    79  		}
    80  
    81  		/* || Monthly Payments */
    82  
    83  		#monthly-payments {
    84  			grid-area: monthly-payments;
    85  		}
    86  
    87  		/* || Days */
    88  
    89  		#days-info {
    90  			display: flex;
    91  			grid-area: days;
    92  			flex-direction: column;
    93  			height: 100%;
    94  			overflow-x: auto;
    95  		}
    96  
    97  		#calendar {
    98  			display: grid;
    99  			grid-template-columns: repeat(7, 1fr);
   100  			column-gap: 10px;
   101  			row-gap: 10px;
   102  		}
   103  
   104  		.calendar__week-day-name {
   105  			border-bottom: 1px solid var(--border-color);
   106  			font-size: 14px;
   107  			margin: auto;
   108  			padding: 0 10px;
   109  			text-align: center;
   110  			width: min-content;
   111  		}
   112  
   113  		.calendar__day {
   114  			border: 1px solid var(--border-color);
   115  			padding: 5px 7px;
   116  		}
   117  
   118  		.calendar__day.disabled {
   119  			cursor: default;
   120  			opacity: 0.2;
   121  		}
   122  
   123  		.calendar__day__date {
   124  			margin-bottom: 5px;
   125  		}
   126  
   127  		.calendar__day__info {
   128  			white-space: nowrap;
   129  		}
   130  
   131  		.day {
   132  			/* Hidden by default */
   133  			display: none;
   134  			margin: 20px;
   135  		}
   136  
   137  		.day__result {
   138  			column-gap: 20px;
   139  			display: grid;
   140  			grid-template-columns: repeat(2, max-content);
   141  			font-size: 18px;
   142  			justify-content: right;
   143  			margin: 10px 10px 10px 0;
   144  		}
   145  
   146  		/* | Modal windows */
   147  
   148  		#modal-window__background {
   149  			background-color: #00000040;
   150  			display: none;
   151  			height: 100%;
   152  			left: 0;
   153  			overflow-y: auto;
   154  			position: fixed;
   155  			top: 0;
   156  			width: 100%;
   157  			z-index: 2;
   158  		}
   159  
   160  		.modal-window {
   161  			box-shadow: 0px 0px 10px 10px #00000020;
   162  			display: none;
   163  			margin: 9vh auto 5vh;
   164  			max-width: 80%;
   165  			min-width: 700px;
   166  			overflow-x: auto;
   167  			width: max-content;
   168  		}
   169  
   170  		.modal-window>.card__title {
   171  			font-size: 1.4rem;
   172  			padding: 10px 20px;
   173  		}
   174  
   175  		.modal-window>.card__body {
   176  			padding: 10px 20px 20px;
   177  		}
   178  
   179  		.modal-window__edit-field {
   180  			display: grid;
   181  			grid-template-columns: 100px auto;
   182  			margin-bottom: 5px;
   183  		}
   184  
   185  		.modal-window__edit-field>select {
   186  			min-width: 150px;
   187  			width: 25%;
   188  		}
   189  
   190  		.modal-window__save-button {
   191  			margin-top: 20px;
   192  			text-align: center;
   193  		}
   194  
   195  		.modal-window__manage-types__spend-type {
   196  			column-gap: 10px;
   197  			display: grid;
   198  			grid-template-columns: max-content 5px 200px auto;
   199  			width: min-content;
   200  			margin: 0 auto 5px;
   201  		}
   202  
   203  		.modal-window__manage-types__spend-type:last-child {
   204  			margin-bottom: 20px;
   205  		}
   206  
   207  		.modal-window__manage-types__spend-type>span {
   208  			opacity: 0.5;
   209  		}
   210  
   211  		.modal-window__manage-types__spend-type>.feather-icon>svg {
   212  			height: 20px;
   213  			vertical-align: middle;
   214  			width: 20px;
   215  		}
   216  
   217  		#modal-window__manage-types__saved-msg {
   218  			display: inline-flex;
   219  			font-size: 20px;
   220  			justify-content: center;
   221  			margin-top: 5px;
   222  			opacity: 0;
   223  			transition: opacity 0.25s;
   224  			width: 100%;
   225  		}
   226  
   227  		.modal-window__manage-types__saved-msg-animation {
   228  			opacity: 1 !important;
   229  		}
   230  
   231  		#modal-window__manage-types__saved-msg>div.feather-icon {
   232  			margin-right: 3px;
   233  			position: relative;
   234  			width: 20px;
   235  		}
   236  
   237  		#modal-window__manage-types__saved-msg>div.feather-icon>svg {
   238  			height: 20px;
   239  			position: absolute;
   240  			stroke: green;
   241  			top: 50%;
   242  			transform: translateY(-43%);
   243  			width: 20px;
   244  		}
   245  
   246  		#modal-window__manage-types__saved-msg span {
   247  			color: green;
   248  		}
   249  
   250  		/* | Other */
   251  
   252  		.actions-horizontal-list {
   253  			display: flex;
   254  		}
   255  
   256  		.add-button>svg {
   257  			transform: scale(1.2);
   258  		}
   259  
   260  
   261  		/* | Layouts */
   262  
   263  		/* For medium and large screens (> 1350px) */
   264  		@media (min-width: 1351px) {
   265  			#content {
   266  				grid-template-columns: 1fr 2fr;
   267  				grid-template-rows: 1fr 1fr;
   268  				grid-template-areas:
   269  					"incomes days"
   270  					"monthly-payments days";
   271  			}
   272  		}
   273  
   274  		/* For medium screens (<= 1550px) */
   275  		@media (max-width: 1550px) {
   276  
   277  			th.notes,
   278  			td.notes {
   279  				display: none;
   280  			}
   281  		}
   282  
   283  		/* For small screens (<= 1350px) */
   284  		@media (max-width: 1350px) {
   285  			#content {
   286  				grid-template-columns: 1fr 1fr;
   287  				grid-template-rows: 2fr 5fr;
   288  				grid-template-areas:
   289  					"incomes monthly-payments"
   290  					"days days";
   291  				row-gap: 30px;
   292  			}
   293  
   294  			.card__title,
   295  			.day__date {
   296  				font-size: 18px;
   297  			}
   298  
   299  			.calendar__day__date {
   300  				text-align: center;
   301  			}
   302  
   303  			.calendar__day__info {
   304  				display: none;
   305  			}
   306  		}
   307  	</style>
   308  </head>
   309  
   310  <body>
   311  	<div id="app">
   312  		<div id="header">
   313  			<div>
   314  				<span class="header__path__element"> <a href="/months">Months</a> </span>
   315  				<span class="header__path__element"> {{ .Year }} </span>
   316  				<span class="header__path__element"> {{ .Month.Month }} </span>
   317  			</div>
   318  
   319  			<div id="header__buttons">
   320  				<!-- Manage Spend Types -->
   321  				<button class="feather-icon" title="Manage Types" onclick="showModalWindowToEditTypes()">
   322  					{{ template "components/icon" "tag" }}
   323  				</button>
   324  
   325  				<!-- Search for Spends -->
   326  				{{ $after := printf `%d-%02d-01` .Year .Month.Month }}
   327  				{{ $before := printf `%d-%02d-%d` .Year .Month.Month (len .Month.Days) }}
   328  				<a href="/search/spends?after={{ $after }}&before={{ $before }}" class="feather-icon" title="Search & Statistics">
   329  					{{ template "components/icon" "bar-chart-2" }}
   330  				</a>
   331  			</div>
   332  		</div>
   333  
   334  		<div id="content">
   335  			<!-- Incomes -->
   336  			<div id="incomes" class="card">
   337  				<div class="card__title noselect">
   338  					<span>Incomes</span>
   339  					<span class="money money--gain">{{ .TotalIncome }}</span>
   340  				</div>
   341  				<div class="card__body">
   342  					<table>
   343  						{{ if .Incomes }}
   344  						<thead>
   345  							<tr class="noselect">
   346  								<th>Title</th>
   347  								<th class="notes">Notes</th>
   348  								<th class="money">Income</th>
   349  								<th></th>
   350  							</tr>
   351  						</thead>
   352  						{{ end }}
   353  
   354  						<tbody>
   355  							{{ range .Incomes }}
   356  							<tr>
   357  								<td>{{ .Title }}</td>
   358  								<td class="notes">{{ .Notes }}</td>
   359  								<td class="money table-shrink-cell">{{ .Income }}</td>
   360  								<td class="table-shrink-cell">
   361  									<div class="actions-horizontal-list">
   362  										<button class="feather-icon" title="Edit"
   363  											onclick="showModalWindowToEditIncome('{{ .ID }}', '{{ .Title }}', '{{ .Notes }}', '{{ printf `%f` .Income }}')">
   364  											{{ template "components/icon" "edit-2" }}
   365  										</button>
   366  										<button class="feather-icon" title="Remove" onclick="removeIncome(Number('{{ .ID }}'))">
   367  											{{ template "components/icon" "trash" }}
   368  										</button>
   369  									</div>
   370  								</td>
   371  							</tr>
   372  							{{ end }}
   373  						</tbody>
   374  
   375  						<tfoot>
   376  							<tr>
   377  								<form onsubmit="addIncome()" autocomplete="off">
   378  									<td>
   379  										<input type="text" id="new-income-title" placeholder="Title">
   380  									</td>
   381  									<td class="notes">
   382  										<input type="text" id="new-income-notes" placeholder="Notes">
   383  									</td>
   384  									<td class="money table-shrink-cell">
   385  										<input type="text" id="new-income-income" placeholder="Income">
   386  									</td>
   387  									<td class="table-shrink-cell">
   388  										<div class="actions-horizontal-list">
   389  											<button type="submit" class="feather-icon add-button" title="Add">
   390  												{{ template "components/icon" "plus" }}
   391  											</button>
   392  										</div>
   393  									</td>
   394  								</form>
   395  							</tr>
   396  						</tfoot>
   397  					</table>
   398  
   399  					{{ if not .Incomes }}
   400  					<div class="copy-from-previous-month">
   401  						<input type="button" value="Copy from previous Month" onclick="copyIncomesFromPreviousMonth()">
   402  					</div>
   403  					{{ end }}
   404  				</div>
   405  			</div>
   406  
   407  			<!-- Monthly Payments -->
   408  			<div id="monthly-payments" class="card">
   409  				<div class="card__title noselect">
   410  					<span>Monthly Payments</span>
   411  					<span class="money money--lose">{{ .MonthlyPaymentsTotalCost }}</span>
   412  				</div>
   413  				<div class="card__body">
   414  					<table>
   415  						{{ if .MonthlyPayments }}
   416  						<thead>
   417  							<tr class="noselect">
   418  								<th>Title</th>
   419  								<th class="notes">Notes</th>
   420  								<th>Type</th>
   421  								<th class="money">Cost</th>
   422  								<th></th>
   423  							</tr>
   424  						</thead>
   425  						{{ end }}
   426  
   427  						<tbody>
   428  							{{ range .MonthlyPayments }}
   429  							<tr>
   430  								<td>{{ .Title }}</td>
   431  								<td class="notes">{{ .Notes }}</td>
   432  								<td class="table-shrink-cell">
   433  									{{ if .Type }}
   434  									{{ .Type.Name }}
   435  									{{ else }}
   436  									-
   437  									{{ end }}
   438  								</td>
   439  								<td class="money table-shrink-cell">{{ .Cost }}</td>
   440  								<td class="table-shrink-cell">
   441  									<div class="actions-horizontal-list">
   442  										<button class="feather-icon" title="Edit"
   443  											onclick="showModalWindowToEditMonthlyPayment('{{ .ID }}', '{{ .Title }}', '{{ .Notes }}',
   444  												'{{ if .Type }}{{ .Type.ID }}{{ else }}0{{ end }}', '{{ printf `%f` .Cost }}')">
   445  											{{ template "components/icon" "edit-2" }}
   446  										</button>
   447  										<button class="feather-icon" title="Remove" onclick="removeMonthlyPayment(Number('{{ .ID }}'))">
   448  											{{ template "components/icon" "trash" }}
   449  										</button>
   450  									</div>
   451  								</td>
   452  							</tr>
   453  							{{ end }}
   454  						</tbody>
   455  
   456  						<tfoot>
   457  							<!-- Inputs for new Monthly Payment -->
   458  							<tr>
   459  								<form onsubmit="addMonthlyPayment()" autocomplete="off">
   460  									<td>
   461  										<input type="text" id="new-monthly-payment-title" placeholder="Title">
   462  									</td>
   463  									<td class="notes">
   464  										<input type="text" id="new-monthly-payment-notes" placeholder="Notes">
   465  									</td>
   466  									<td class="table-shrink-cell">
   467  										<select id="new-monthly-payment-type">
   468  											<option value="0">-</option>
   469  											{{ range $.SpendTypes }}
   470  											<option value="{{ .ID }}">{{ .FullName }}</option>
   471  											{{ end }}
   472  										</select>
   473  									</td>
   474  									<td class="money table-shrink-cell">
   475  										<input type="text" id="new-monthly-payment-cost" placeholder="Cost">
   476  									</td>
   477  									<td class="table-shrink-cell">
   478  										<div class="actions-horizontal-list">
   479  											<button type="submit" class="feather-icon add-button" title="Add">
   480  												{{ template "components/icon" "plus" }}
   481  											</button>
   482  										</div>
   483  									</td>
   484  								</form>
   485  							</tr>
   486  						</tfoot>
   487  					</table>
   488  
   489  					{{ if not .MonthlyPayments }}
   490  					<div class="copy-from-previous-month">
   491  						<input type="button" value="Copy from previous Month" onclick="copyMonthlyPaymentsFromPreviousMonth()">
   492  					</div>
   493  					{{ end }}
   494  				</div>
   495  			</div>
   496  
   497  			<!-- Days -->
   498  			<div id="days-info">
   499  				<!-- Calendar -->
   500  				<div id="calendar">
   501  					{{ range .Days }}
   502  					<a href="#{{ .Day }}" id="id-calendar-day-{{ .Day }}" class="calendar__day card--hover" onclick="selectDay('{{ .Day }}');">
   503  						<div class="calendar__day__date">
   504  							<span>{{ .Day }} {{ call $.ToShortMonth $.Month.Month }}</span>
   505  						</div>
   506  
   507  						<div class="calendar__day__info">
   508  							<span title="Saldo">
   509  								{{ if ge .Saldo 0 }}
   510  								<span class="money--gain">{{ .Saldo }}</span>
   511  								{{ else }}
   512  								<span class="money--lose">{{ .Saldo }}</span>
   513  								{{ end }}
   514  							</span>
   515  							<span> · </span>
   516  							<span title="Spends">{{ len .Spends }}</span>
   517  						</div>
   518  					</a>
   519  					{{ end }}
   520  				</div>
   521  
   522  				<!-- Spends -->
   523  				{{ range .Days }}
   524  				<div id="id-day-{{ .Day }}" class="card day">
   525  					<div class="card__title">
   526  						<span>{{ .Day }} {{ $.Month.Month }}</span>
   527  
   528  						<!-- Spends must be always <= 0 -->
   529  						{{ $totalCost := call $.SumSpendCosts .Spends }}
   530  						{{ if eq $totalCost 0 }}
   531  						<span class="money money--gain">{{ $totalCost }}</span>
   532  						{{ else }}
   533  						<span class="money money--lose">{{ $totalCost }}</span>
   534  						{{ end }}
   535  					</div>
   536  
   537  					<div class="card__body">
   538  						<table>
   539  							{{ if .Spends }}
   540  							<thead>
   541  								<tr class="noselect">
   542  									<th>Title</th>
   543  									<th class="notes">Notes</th>
   544  									<th>Type</th>
   545  									<th class="money">Cost</th>
   546  									<th></th>
   547  								</tr>
   548  							</thead>
   549  							{{ end }}
   550  
   551  							<tbody>
   552  								{{ range .Spends }}
   553  								<tr>
   554  									<td>{{ .Title }}</td>
   555  									<td class="notes">{{ .Notes }}</td>
   556  									<td class="table-shrink-cell">
   557  										{{ if .Type }}
   558  										{{ .Type.Name }}
   559  										{{ else }}
   560  										-
   561  										{{ end }}
   562  									</td>
   563  									<td class="money table-shrink-cell">{{ .Cost }}</td>
   564  									<td class="table-shrink-cell">
   565  										<div class="actions-horizontal-list">
   566  											<button class="feather-icon" title="Edit"
   567  												onclick="showModalWindowToEditSpend('{{ .ID }}', '{{ .Title }}', '{{ .Notes }}',
   568  													'{{ if .Type }}{{ .Type.ID }}{{ else }}0{{ end }}', '{{ printf `%f` .Cost }}')">
   569  												{{ template "components/icon" "edit-2" }}
   570  											</button>
   571  											<button class="feather-icon" title="Remove" onclick="removeSpend(Number('{{ .ID }}'))">
   572  												{{ template "components/icon" "trash" }}
   573  											</button>
   574  										</div>
   575  									</td>
   576  								</tr>
   577  								{{ end }}
   578  							</tbody>
   579  
   580  							<tfoot>
   581  								<!--
   582  										Inputs for new Spend
   583  
   584  										We use '-{{ .ID }}' suffix because we render all days, just hide them.
   585  										So, ids without this suffix wouldn't be unique. We use day id instead of
   586  										day number because we pass into 'addSpend' function day id.
   587  									-->
   588  								<tr class="day__new-spend" data-day-id="{{ .ID }}">
   589  									<form onsubmit="addSpend(Number('{{ .ID }}'))" autocomplete="off">
   590  										<td>
   591  											<input type="text" id="new-spend-title-{{ .ID }}" placeholder="Title" list="spend-datalist-{{ .ID }}">
   592  											<datalist id="spend-datalist-{{ .ID }}"></datalist>
   593  										</td>
   594  										<td class="notes">
   595  											<input type="text" id="new-spend-notes-{{ .ID }}" placeholder="Notes">
   596  										</td>
   597  										<td class="table-shrink-cell">
   598  											<select id="new-spend-type-{{ .ID }}">
   599  												<option value="0">-</option>
   600  												{{ range $.SpendTypes }}
   601  												<option value="{{ .ID }}">{{ .FullName }}</option>
   602  												{{ end }}
   603  											</select>
   604  										</td>
   605  										<td class="money table-shrink-cell">
   606  											<input type="text" id="new-spend-cost-{{ .ID }}" placeholder="Cost">
   607  										</td>
   608  										<td class="table-shrink-cell">
   609  											<div class="actions-horizontal-list">
   610  												<button type="submit" class="feather-icon add-button" title="Add">
   611  													{{ template "components/icon" "plus" }}
   612  												</button>
   613  											</div>
   614  										</td>
   615  									</form>
   616  								</tr>
   617  							</tfoot>
   618  						</table>
   619  					</div>
   620  				</div>
   621  				{{ end }}
   622  			</div>
   623  		</div>
   624  
   625  		<!-- Modal windows (hidden by default) -->
   626  		<div id="modal-window__background">
   627  			<!-- Edit Income -->
   628  			<form id="modal-window__edit-income" class="card modal-window">
   629  				<div class="card__title noselect">Edit Income</div>
   630  
   631  				<div class="card__body">
   632  					<div class="modal-window__edit-field">
   633  						<span class="noselect">Title:</span>
   634  						<input id="modal-window__edit-income__title" type="text">
   635  					</div>
   636  
   637  					<div class="modal-window__edit-field">
   638  						<span class="noselect">Notes:</span>
   639  						<input id="modal-window__edit-income__notes" type="text">
   640  					</div>
   641  
   642  					<div class="modal-window__edit-field">
   643  						<span class="noselect">Income:</span>
   644  						<input id="modal-window__edit-income__income" type="text">
   645  					</div>
   646  
   647  					<div class="modal-window__save-button">
   648  						<input type="button" value="Cancel" onclick="hideAllModalWindows()">
   649  						<input type="submit" id="modal-window__edit-income__save-button" value="Save">
   650  					</div>
   651  				</div>
   652  			</form>
   653  
   654  			<!-- Edit Monthly Payment -->
   655  			<form id="modal-window__edit-monthly-payment" class="card modal-window">
   656  				<div class="card__title noselect">Edit Monthly Payment</div>
   657  
   658  				<div class="card__body">
   659  					<div class="modal-window__edit-field">
   660  						<span class="noselect">Title:</span>
   661  						<input id="modal-window__edit-monthly-payment__title" type="text">
   662  					</div>
   663  
   664  					<div class="modal-window__edit-field">
   665  						<span class="noselect">Notes:</span>
   666  						<input id="modal-window__edit-monthly-payment__notes" type="text">
   667  					</div>
   668  
   669  					<div class="modal-window__edit-field">
   670  						<span class="noselect">Type:</span>
   671  						<select id="modal-window__edit-monthly-payment__type">
   672  							<option value="0">-</option>
   673  							{{ range $.SpendTypes }}
   674  							<option value="{{ .ID }}">{{ .FullName }}</option>
   675  							{{ end }}
   676  						</select>
   677  					</div>
   678  
   679  					<div class="modal-window__edit-field">
   680  						<span class="noselect">Cost:</span>
   681  						<input id="modal-window__edit-monthly-payment__cost" type="text">
   682  					</div>
   683  
   684  					<div class="modal-window__save-button">
   685  						<input type="button" value="Cancel" onclick="hideAllModalWindows()">
   686  						<input type="submit" id="modal-window__edit-monthly-payment__save-button" value="Save">
   687  					</div>
   688  				</div>
   689  			</form>
   690  
   691  			<!-- Edit Spend -->
   692  			<form id="modal-window__edit-spend" class="card modal-window">
   693  				<div class="card__title noselect">Edit Spend</div>
   694  
   695  				<div class="card__body">
   696  					<div class="modal-window__edit-field">
   697  						<span class="noselect">Title:</span>
   698  						<input id="modal-window__edit-spend__title" type="text">
   699  					</div>
   700  
   701  					<div class="modal-window__edit-field">
   702  						<span class="noselect">Notes:</span>
   703  						<input id="modal-window__edit-spend__notes" type="text">
   704  					</div>
   705  
   706  					<div class="modal-window__edit-field">
   707  						<span class="noselect">Type:</span>
   708  						<select id="modal-window__edit-spend__type">
   709  							<option value="0">-</option>
   710  							{{ range $.SpendTypes }}
   711  							<option value="{{ .ID }}">{{ .FullName }}</option>
   712  							{{ end }}
   713  						</select>
   714  					</div>
   715  
   716  					<div class="modal-window__edit-field">
   717  						<span class="noselect">Cost:</span>
   718  						<input id="modal-window__edit-spend__cost" type="text">
   719  					</div>
   720  
   721  					<div class="modal-window__save-button">
   722  						<input type="button" value="Cancel" onclick="hideAllModalWindows()">
   723  						<input type="submit" id="modal-window__edit-spend__save-button" value="Save">
   724  					</div>
   725  				</div>
   726  			</form>
   727  
   728  			<!-- Manage types -->
   729  			<div id="modal-window__manage-types" class="card modal-window">
   730  				<div class="card__title noselect">Manage Spend Types</div>
   731  
   732  				<div class="card__body">
   733  					{{ range .SpendTypes }}
   734  					<div id="modal-window__manage-types__spend-type-{{ .ID }}" class="modal-window__manage-types__spend-type">
   735  						{{ $currentType := . }}
   736  						<select id="edit-spend-type-parent-id-{{ .ID }}" class="reverse" autocomplete="off" title="Parent Spend Type">
   737  							<option value="0"></option>
   738  							{{ range $.SpendTypes }}
   739  
   740  							{{ $attributes := "" }}
   741  							{{ if eq $currentType.ParentID .ID }}
   742  							{{ $attributes = (printf "%s %s" $attributes "selected") }}
   743  							{{ end }}
   744  							{{ if not (call $.ShouldSuggestSpendType $currentType .) }}
   745  							{{ $attributes = (printf "%s %s" $attributes "disabled style='color: var(--font-color--disabled)'") }}
   746  							{{ end }}
   747  
   748  							<option value="{{ .ID }}" {{ toHTMLAttr $attributes }}>{{ .FullName }}</option>
   749  							{{ end }}
   750  						</select>
   751  
   752  						<span class="noselect">/</span>
   753  
   754  						<input type="text" id="edit-spend-type-name-{{ .ID }}" value="{{ .Name }}" autocomplete="off"
   755  							placeholder="{{ .Name }}">
   756  
   757  						<div class="actions-horizontal-list">
   758  							<button class="feather-icon" title="Save" onclick="editSpendType('{{ .ID }}')">
   759  								{{ template "components/icon" "check" }}
   760  							</button>
   761  							<button class="feather-icon" title="Remove" onclick="removeSpendType(Number('{{ .ID }}'))">
   762  								{{ template "components/icon" "trash" }}
   763  							</button>
   764  						</div>
   765  					</div>
   766  					{{ end }}
   767  
   768  					<!-- New Spend Type -->
   769  					<form class="modal-window__manage-types__spend-type" onsubmit="addSpendType()">
   770  						<select id="new-spend-type-parent-id" class="reverse" autocomplete="off" title="Parent Spend Type">
   771  							<option value="0"></option>
   772  							{{ range $.SpendTypes }}
   773  							<option value="{{ .ID }}">{{ .FullName }}</option>
   774  							{{ end }}
   775  						</select>
   776  
   777  						<span class="noselect">/</span>
   778  
   779  						<input type="text" id="new-spend-type-name" placeholder="New Spend Type" autocomplete="off">
   780  
   781  						<div class="actions-horizontal-list">
   782  							<button type="submit" class="feather-icon" title="Add">
   783  								{{ template "components/icon" "plus" }}
   784  							</button>
   785  							<!-- Use this disabled and hidden element to align 'Add' button -->
   786  							<button class="feather-icon" style="opacity: 0; pointer-events: none;">
   787  								{{ template "components/icon" "plus" }}
   788  							</button>
   789  						</div>
   790  					</form>
   791  				</div>
   792  
   793  				<div id="modal-window__manage-types__saved-msg">
   794  					<div class="feather-icon">
   795  						{{ template "components/icon" "check" }}
   796  					</div>
   797  
   798  					<span>Saved</span>
   799  				</div>
   800  			</div>
   801  		</div>
   802  
   803  		{{ template "components/footer.html" .Footer }}
   804  	</div>
   805  
   806  	<script>
   807  		// ----------------------------------------------------
   808  		// Global variables
   809  		// ----------------------------------------------------
   810  
   811  		const Year = Number("{{ .Year }}")
   812  		const MonthID = Number("{{ .ID }}");
   813  		const MonthNumber = Number("{{ printf `%d` .Month.Month }}")
   814  
   815  		// ----------------------------------------------------
   816  		// Days
   817  		// ----------------------------------------------------
   818  
   819  		// Fill weeks in calendar: add days of previous and next months if needed
   820  		window.addEventListener("load", () => {
   821  			const calendar = document.getElementById("calendar");
   822  			if (calendar === null) {
   823  				console.error("couldn't get element with id 'calendar'");
   824  				return;
   825  			}
   826  
   827  			const createCalendarDay = (dayNumber, monthName) => {
   828  				const div = document.createElement("div");
   829  				div.classList.add("calendar__day", "disabled", "noselect");
   830  
   831  				const info = document.createElement("div");
   832  				info.classList.add("calendar__day__date");
   833  
   834  				if (monthName.length > 4) {
   835  					monthName = monthName.slice(0, 3);
   836  				}
   837  				const date = `${dayNumber} ${monthName}`;
   838  				const dateElem = document.createElement("div");
   839  				dateElem.textContent = date;
   840  				info.append(dateElem);
   841  
   842  				div.append(info);
   843  
   844  				return div;
   845  			}
   846  
   847  			// Add days of the previous month
   848  
   849  			const previousMonth = new Date(Year, MonthNumber - 1, 0);
   850  			const daysInPreviousMonth = previousMonth.getDate();
   851  			const previousMonthName = previousMonth.toLocaleString("default", { month: "long" });
   852  
   853  			// Weeks are started from Monday
   854  			const monthBeginning = new Date(Year, MonthNumber - 1);
   855  			const firstDayNumber = (monthBeginning.getDay() + 6) % 7;
   856  
   857  			for (let i = 0; i < firstDayNumber; i++) {
   858  				const div = createCalendarDay(daysInPreviousMonth - i, previousMonthName);
   859  				calendar.prepend(div);
   860  			}
   861  
   862  			// Add days of the next month
   863  
   864  			const nextMonth = new Date(Year, MonthNumber);
   865  			const nextMonthName = nextMonth.toLocaleString("default", { month: "long" });
   866  
   867  			// Weeks are started from Monday
   868  			let lastDayNumber = (nextMonth.getDay() + 6) % 7;
   869  			if (lastDayNumber === 0) {
   870  				// Special case: month ends on Sunday
   871  				lastDayNumber = 7;
   872  			}
   873  
   874  			for (let i = 0; i < 7 - lastDayNumber; i++) {
   875  				const div = createCalendarDay(i + 1, nextMonthName);
   876  				calendar.append(div);
   877  			}
   878  
   879  			// Add name of week days
   880  			const weekDayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
   881  			const weekDayNamesShort = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
   882  			for (let i = weekDayNames.length - 1; i >= 0; i--) {
   883  				let div = document.createElement("div");
   884  				div.classList.add("calendar__week-day-name", "noselect")
   885  				div.textContent = weekDayNames[i];
   886  				calendar.prepend(div);
   887  			}
   888  		})
   889  
   890  		// Select day
   891  		window.addEventListener("load", () => {
   892  			// Check whether day is passed in URL
   893  			const passedDay = Number(location.hash.slice(1));
   894  			if (passedDay) {
   895  				const daysInMonth = new Date(Year, MonthNumber, 0).getDate();
   896  				if (0 < passedDay && passedDay <= daysInMonth) {
   897  					// Select passed day
   898  					selectDay(passedDay);
   899  					return;
   900  				}
   901  			}
   902  
   903  			const urlParams = new URLSearchParams(window.location.search);
   904  			const year = urlParams.get("year");
   905  			const month = urlParams.get("month");
   906  
   907  			// Check whether we display the current month
   908  			const now = new Date();
   909  			if (year == now.getFullYear() && month == now.getMonth() + 1) {
   910  				// Select the current day
   911  				selectDay(now.getDate());
   912  				return;
   913  			}
   914  
   915  			// Just choose the first day
   916  			selectDay(1);
   917  		});
   918  
   919  		const calendarDayIDPrefix = "id-calendar-day-";
   920  		const calendarDayClass = "calendar__day";
   921  		//
   922  		const dayIDPrefix = "id-day-";
   923  		const dayClass = "day";
   924  
   925  		function selectDay(dayNumber) {
   926  			// Calendar
   927  
   928  			// Remove highlights
   929  			const calendarDays = document.getElementsByClassName(calendarDayClass);
   930  			for (let i = 0; i < calendarDays.length; i++) {
   931  				calendarDays[i].style.backgroundColor = "";
   932  			}
   933  
   934  			// Highlight day in calendar
   935  			const calendarDayElemID = calendarDayIDPrefix + dayNumber;
   936  			const calendarDayElem = document.getElementById(calendarDayElemID);
   937  			if (calendarDayElem === undefined) {
   938  				console.error(`element with id '${calendarDayElem}' doesn't exist`);
   939  			} else {
   940  				calendarDayElem.style.backgroundColor = "var(--hover-color)";
   941  			}
   942  
   943  			// Chosen day
   944  
   945  			// Hide all days at first
   946  			const days = document.getElementsByClassName(dayClass);
   947  			for (let i = 0; i < days.length; i++) {
   948  				days[i].style.display = "none";
   949  			}
   950  
   951  			// Display chosen day
   952  			const dayElemID = dayIDPrefix + dayNumber;
   953  			const dayElem = document.getElementById(dayElemID);
   954  			if (dayElem === undefined) {
   955  				console.error(`element with id '${dayElemID}' doesn't exist`);
   956  				return;
   957  			}
   958  
   959  			dayElem.style.display = "block";
   960  			// Set day number in URL
   961  			location.hash = dayNumber;
   962  
   963  			// Focus new Spend title input
   964  			const newSpendForm = dayElem.querySelector(".day__new-spend");
   965  			const dayID = Number(newSpendForm.getAttribute("data-day-id"));
   966  			const newSpendTitleInput = newSpendForm.querySelector(`#new-spend-title-${dayID}`)
   967  			newSpendTitleInput.focus();
   968  
   969  			// Scroll to bottom
   970  			dayElem.scrollTop = dayElem.scrollHeight;
   971  		}
   972  
   973  		// ----------------------------------------------------
   974  		// Requests
   975  		// ----------------------------------------------------
   976  
   977  		// Incomes
   978  
   979  		function addIncome() {
   980  			preventDefault(this);
   981  
   982  			const income = replaceCommas(getValue("new-income-income"));
   983  			if (isNaN(income)) {
   984  				processError("income must be a number");
   985  				return;
   986  			}
   987  
   988  			const fields = {
   989  				"month_id": MonthID,
   990  				"title": getValue("new-income-title"),
   991  				"notes": getValue("new-income-notes"),
   992  				"income": Number(income),
   993  			}
   994  
   995  			sendRequest("POST", "/api/incomes", fields);
   996  		}
   997  
   998  		function editIncome(id) {
   999  			preventDefault(this);
  1000  
  1001  			const income = replaceCommas(getValue("modal-window__edit-income__income"));
  1002  			if (isNaN(income)) {
  1003  				processError("income must be a number");
  1004  				return;
  1005  			}
  1006  
  1007  			const fields = {
  1008  				"id": Number(id),
  1009  				"title": getValue("modal-window__edit-income__title"),
  1010  				"notes": getValue("modal-window__edit-income__notes"),
  1011  				"income": Number(income),
  1012  			}
  1013  
  1014  			sendRequest("PUT", "/api/incomes", fields);
  1015  		}
  1016  
  1017  		function removeIncome(id) {
  1018  			preventDefault(this);
  1019  
  1020  			if (!confirm("Do you really want to delete the Income?")) {
  1021  				return;
  1022  			}
  1023  
  1024  			const fields = { "id": Number(id) };
  1025  			sendRequest("DELETE", "/api/incomes", fields);
  1026  		}
  1027  
  1028  		async function copyIncomesFromPreviousMonth() {
  1029  			const previousMonth = await getPreviousMonth();
  1030  			if (!previousMonth) return;
  1031  			const incomes = previousMonth.incomes;
  1032  
  1033  			for (const income of incomes) {
  1034  				const fields = {
  1035  					"month_id": MonthID,
  1036  					"title": income.title,
  1037  					"notes": income.notes || "",
  1038  					"income": income.income,
  1039  				};
  1040  				await sendRequest("POST", "/api/incomes", fields, () => { });
  1041  			}
  1042  			location.reload()
  1043  		}
  1044  
  1045  		// Monthly Payments
  1046  
  1047  		function addMonthlyPayment() {
  1048  			preventDefault(this);
  1049  
  1050  			// Skip check because user can't specify typeID as not a number
  1051  			const typeID = getValue("new-monthly-payment-type");
  1052  
  1053  			const cost = replaceCommas(getValue("new-monthly-payment-cost"));
  1054  			if (isNaN(cost)) {
  1055  				processError("cost must be a number");
  1056  				return;
  1057  			}
  1058  
  1059  			const fields = {
  1060  				"month_id": MonthID,
  1061  				"title": getValue("new-monthly-payment-title"),
  1062  				"notes": getValue("new-monthly-payment-notes"),
  1063  				"type_id": Number(typeID),
  1064  				"cost": Number(cost),
  1065  			};
  1066  
  1067  			sendRequest("POST", "/api/monthly-payments", fields);
  1068  		}
  1069  
  1070  		function editMonthlyPayment(id) {
  1071  			preventDefault(this);
  1072  
  1073  			// Skip check because user can't specify typeID as not a number
  1074  			const typeID = getValue("modal-window__edit-monthly-payment__type");
  1075  
  1076  			const cost = replaceCommas(getValue("modal-window__edit-monthly-payment__cost"));
  1077  			if (isNaN(cost)) {
  1078  				processError("cost must be a number");
  1079  				return;
  1080  			}
  1081  
  1082  			const fields = {
  1083  				"id": Number(id),
  1084  				"title": getValue("modal-window__edit-monthly-payment__title"),
  1085  				"notes": getValue("modal-window__edit-monthly-payment__notes"),
  1086  				"type_id": Number(typeID),
  1087  				"cost": Number(cost),
  1088  			}
  1089  
  1090  			sendRequest("PUT", "/api/monthly-payments", fields);
  1091  		}
  1092  
  1093  		function removeMonthlyPayment(id) {
  1094  			preventDefault(this);
  1095  
  1096  			if (!confirm("Do you really want to delete the Monthly Payment?")) {
  1097  				return;
  1098  			}
  1099  
  1100  			const fields = { "id": Number(id) };
  1101  			sendRequest("DELETE", "/api/monthly-payments", fields);
  1102  		}
  1103  
  1104  		async function copyMonthlyPaymentsFromPreviousMonth() {
  1105  			const previousMonth = await getPreviousMonth();
  1106  			if (!previousMonth) return;
  1107  			const monthlyPayments = previousMonth.monthly_payments;
  1108  
  1109  			for (const payment of monthlyPayments) {
  1110  				const fields = {
  1111  					"month_id": MonthID,
  1112  					"title": payment.title,
  1113  					"notes": payment.notes || "",
  1114  					"type_id": payment.type ? payment.type.id : 0,
  1115  					"cost": payment.cost,
  1116  				};
  1117  				await sendRequest("POST", "/api/monthly-payments", fields, () => { });
  1118  			}
  1119  			location.reload()
  1120  		}
  1121  
  1122  		// Spends
  1123  
  1124  		function addSpend(dayID) {
  1125  			preventDefault(this);
  1126  
  1127  			// Skip check because user can't specify typeID as not a number
  1128  			const typeID = getValue(`new-spend-type-${dayID}`);
  1129  
  1130  			const cost = replaceCommas(getValue(`new-spend-cost-${dayID}`));
  1131  			if (isNaN(cost)) {
  1132  				processError("cost must be a number");
  1133  				return;
  1134  			}
  1135  
  1136  			const fields = {
  1137  				"day_id": Number(dayID),
  1138  				"title": getValue(`new-spend-title-${dayID}`),
  1139  				"notes": getValue(`new-spend-notes-${dayID}`),
  1140  				"type_id": Number(typeID),
  1141  				"cost": Number(cost),
  1142  			};
  1143  
  1144  			sendRequest("POST", "/api/spends", fields);
  1145  		}
  1146  
  1147  		function editSpend(id) {
  1148  			preventDefault(this);
  1149  
  1150  			// Skip check because user can't specify typeID as not a number
  1151  			const typeID = getValue("modal-window__edit-spend__type");
  1152  
  1153  			const cost = replaceCommas(getValue("modal-window__edit-spend__cost"));
  1154  			if (isNaN(cost)) {
  1155  				processError("cost must be a number");
  1156  				return;
  1157  			}
  1158  
  1159  			const fields = {
  1160  				"id": Number(id),
  1161  				"title": getValue("modal-window__edit-spend__title"),
  1162  				"notes": getValue("modal-window__edit-spend__notes"),
  1163  				"type_id": Number(typeID),
  1164  				"cost": Number(cost),
  1165  			}
  1166  
  1167  			sendRequest("PUT", "/api/spends", fields);
  1168  		}
  1169  
  1170  		function removeSpend(id) {
  1171  			preventDefault(this);
  1172  
  1173  			if (!confirm("Do you really want to delete the Spend?")) {
  1174  				return;
  1175  			}
  1176  
  1177  			const fields = { "id": Number(id) };
  1178  			sendRequest("DELETE", "/api/spends", fields);
  1179  		}
  1180  
  1181  		// Spend Types
  1182  
  1183  		// If spendTypeChanged is true, page will be reloaded after closing Modal Window
  1184  		var spendTypeChanged = false;
  1185  
  1186  		function addSpendType() {
  1187  			preventDefault(this);
  1188  
  1189  			const name = getValue("new-spend-type-name");
  1190  			if (name === "") {
  1191  				processError("Name of Spend Type can't be empty");
  1192  				return;
  1193  			}
  1194  
  1195  			// Skip check because user can't specify parent id as not a number
  1196  			const parentID = getValue("new-spend-type-parent-id");
  1197  
  1198  			const fields = {
  1199  				"name": name,
  1200  				"parent_id": Number(parentID)
  1201  			}
  1202  
  1203  			// Reload on success
  1204  			sendRequest("POST", "/api/spend-types", fields);
  1205  		}
  1206  
  1207  		function editSpendType(id) {
  1208  			preventDefault(this);
  1209  
  1210  			const name = getValue(`edit-spend-type-name-${id}`);
  1211  			if (name === "") {
  1212  				processError("Name of Spend Type can't be empty");
  1213  				return;
  1214  			}
  1215  
  1216  			// Skip check because user can't specify parent id as not a number
  1217  			const parentID = getValue(`edit-spend-type-parent-id-${id}`);
  1218  
  1219  			const fields = {
  1220  				"id": Number(id),
  1221  				"name": name,
  1222  				"parent_id": Number(parentID),
  1223  			}
  1224  
  1225  			const handler = () => {
  1226  				spendTypeChanged = true;
  1227  				showSpendTypeSavedMessage();
  1228  			}
  1229  			sendRequest("PUT", "/api/spend-types", fields, handler);
  1230  		}
  1231  
  1232  		function removeSpendType(id) {
  1233  			if (!confirm("Do you really want to delete the Spend Type?")) {
  1234  				return;
  1235  			}
  1236  
  1237  			const fields = {
  1238  				"id": Number(id),
  1239  			}
  1240  
  1241  			const hideRemovedSpendType = () => {
  1242  				spendTypeChanged = true;
  1243  
  1244  				const elemID = `modal-window__manage-types__spend-type-${id}`;
  1245  				const elem = document.getElementById(elemID);
  1246  				if (!elem) {
  1247  					return;
  1248  				}
  1249  				elem.style.display = "none";
  1250  			}
  1251  			sendRequest("DELETE", "/api/spend-types", fields, hideRemovedSpendType);
  1252  		}
  1253  
  1254  		async function getPreviousMonth() {
  1255  			let year = Year, month = MonthNumber - 1;
  1256  			if (month === 0) {
  1257  				year--;
  1258  				month = 12;
  1259  			}
  1260  
  1261  			const params = new URLSearchParams({ "year": year, "month": month });
  1262  			return fetch("/api/months/date?" + params.toString()).
  1263  				then(rawResp => rawResp.json()).
  1264  				then(resp => {
  1265  					if (!resp.success) throw resp.error;
  1266  					return resp.month;
  1267  				}).
  1268  				catch(err => processError(err))
  1269  		}
  1270  
  1271  		/**
  1272  		 * @param {string} method - HTTP method (POST or PUT)
  1273  		 * @param {string} url - request url
  1274  		 * @param {Object} fields - additional json fields (like month id, day id and etc.)
  1275  		 * @param {Function} successHandler - success handler (page are reloaded by default)
  1276  		 */
  1277  		async function sendRequest(method, url, fields, successHandler = () => { location.reload() }) {
  1278  			return fetch(url, {
  1279  				method: method,
  1280  				headers: { "Content-Type": "application/json" },
  1281  				body: JSON.stringify(fields || null)
  1282  			}).
  1283  				then(rawResp => rawResp.json()).
  1284  				then(resp => {
  1285  					if (!resp.success) throw resp.error;
  1286  
  1287  					successHandler();
  1288  
  1289  				}).catch(err => processError(err));
  1290  		}
  1291  
  1292  		// ----------------------------------------------------
  1293  		// Modal windows
  1294  		// ----------------------------------------------------
  1295  
  1296  		// Add event listener to hide modal window on click outside of Modal Window
  1297  		window.addEventListener("load", () => {
  1298  			const elem = document.getElementById(modalWindowID);
  1299  			if (!elem) {
  1300  				console.error(`element with id '${modalWindowID}' doesn't exist`);
  1301  				return
  1302  			}
  1303  
  1304  			// Hide on click
  1305  			elem.addEventListener("click", (ev) => {
  1306  				if (ev.target.id !== modalWindowID) {
  1307  					return;
  1308  				}
  1309  				hideAllModalWindows();
  1310  			})
  1311  		});
  1312  
  1313  		// Add event listener to hide modal window on Esc key pressing
  1314  		window.addEventListener("keydown", (ev) => {
  1315  			const escKeyCode = 27;
  1316  
  1317  			if (ev.keyCode === escKeyCode) {
  1318  				hideAllModalWindows();
  1319  			}
  1320  		})
  1321  
  1322  		const modalWindowID = "modal-window__background";
  1323  		const editIncomeModalWindowID = "modal-window__edit-income";
  1324  		const editMonthlyPaymentModalWindowID = "modal-window__edit-monthly-payment";
  1325  		const editSpendModalWindowID = "modal-window__edit-spend";
  1326  		const manageSpendTypesModalWindowID = "modal-window__manage-types";
  1327  
  1328  		/**
  1329  		 * @param {string} id - Income id
  1330  		 * @param {string} title - current Income title
  1331  		 * @param {string} notes - current Income notes
  1332  		 * @param {string} income - current income
  1333  		 */
  1334  		function showModalWindowToEditIncome(id, title, notes, income) {
  1335  			hideAllModalWindows();
  1336  			blurBackground();
  1337  
  1338  			// Set current values
  1339  			setValue("modal-window__edit-income__title", title);
  1340  			setValue("modal-window__edit-income__notes", notes);
  1341  			setValue("modal-window__edit-income__income", income);
  1342  
  1343  			// Reset event listener
  1344  			const form = document.getElementById(editIncomeModalWindowID);
  1345  			form.onsubmit = () => { editIncome(id); }
  1346  
  1347  			// Show Modal Window
  1348  			showElement(editIncomeModalWindowID);
  1349  			showElement(modalWindowID);
  1350  		}
  1351  
  1352  		/**
  1353  		 * @param {string} id - Monthly Payment id
  1354  		 * @param {string} title - current Monthly Payment title
  1355  		 * @param {string} notes - current Monthly Payment notes
  1356  		 * @param {string} typeID - current Monthly Payment type id
  1357  		 * @param {string} cost - current Monthly Payment cost
  1358  		 */
  1359  		function showModalWindowToEditMonthlyPayment(id, title, notes, typeID, cost) {
  1360  			hideAllModalWindows();
  1361  			blurBackground();
  1362  
  1363  			// Set current values
  1364  			setValue("modal-window__edit-monthly-payment__title", title);
  1365  			setValue("modal-window__edit-monthly-payment__notes", notes);
  1366  			setValue("modal-window__edit-monthly-payment__type", typeID);
  1367  			setValue("modal-window__edit-monthly-payment__cost", cost);
  1368  
  1369  			// Reset event listener
  1370  			const form = document.getElementById(editMonthlyPaymentModalWindowID);
  1371  			form.onsubmit = () => { editMonthlyPayment(id); }
  1372  
  1373  			// Show Modal Window
  1374  			showElement(editMonthlyPaymentModalWindowID);
  1375  			showElement(modalWindowID);
  1376  		}
  1377  
  1378  		/**
  1379  		 * @param {string} id - Spend id
  1380  		 * @param {string} title - current Spend title
  1381  		 * @param {string} notes - current Spend notes
  1382  		 * @param {string} typeID - current Spend type id
  1383  		 * @param {string} cost - current Spend cost
  1384  		 */
  1385  		function showModalWindowToEditSpend(id, title, notes, typeID, cost) {
  1386  			hideAllModalWindows();
  1387  			blurBackground();
  1388  
  1389  			// Set current values
  1390  			setValue("modal-window__edit-spend__title", title);
  1391  			setValue("modal-window__edit-spend__notes", notes);
  1392  			setValue("modal-window__edit-spend__type", typeID);
  1393  			setValue("modal-window__edit-spend__cost", cost);
  1394  
  1395  			// Reset event listener
  1396  			const form = document.getElementById(editSpendModalWindowID);
  1397  			form.onsubmit = () => { editSpend(id); }
  1398  
  1399  			// Show Modal Window
  1400  			showElement(editSpendModalWindowID);
  1401  			showElement(modalWindowID);
  1402  		}
  1403  
  1404  		var spendTypeSaveMessageAnimationTimeoutID = 0;
  1405  
  1406  		function showSpendTypeSavedMessage() {
  1407  			const elemID = "modal-window__manage-types__saved-msg";
  1408  			const animationClassName = elemID + "-animation";
  1409  
  1410  			const elem = document.getElementById(elemID);
  1411  			if (!elem) return;
  1412  
  1413  			if (spendTypeSaveMessageAnimationTimeoutID) {
  1414  				// Reset timeout
  1415  				clearTimeout(spendTypeSaveMessageAnimationTimeoutID);
  1416  			}
  1417  
  1418  			elem.classList.add(animationClassName);
  1419  
  1420  			spendTypeSaveMessageAnimationTimeoutID = setTimeout(() => {
  1421  				elem.classList.remove(animationClassName);
  1422  				spendTypeSaveMessageAnimationTimeoutID = 0;
  1423  			}, 1000)
  1424  		}
  1425  
  1426  		function showModalWindowToEditTypes() {
  1427  			hideAllModalWindows();
  1428  			blurBackground();
  1429  
  1430  			// Show Modal Window
  1431  			showElement(manageSpendTypesModalWindowID);
  1432  			showElement(modalWindowID);
  1433  		}
  1434  
  1435  		/**
  1436  		 * hideAllModalWindows hides all Modal Windows
  1437  		 */
  1438  		function hideAllModalWindows() {
  1439  			unblurBackground();
  1440  
  1441  			hideElement(modalWindowID);
  1442  			hideElement(editIncomeModalWindowID);
  1443  			hideElement(editMonthlyPaymentModalWindowID);
  1444  			hideElement(editSpendModalWindowID);
  1445  			hideElement(manageSpendTypesModalWindowID);
  1446  
  1447  			if (spendTypeChanged) {
  1448  				// Reload page to display updated Spend Types
  1449  				location.reload();
  1450  			}
  1451  		}
  1452  
  1453  		function blurBackground() {
  1454  			document.getElementById("header").classList.add("blurred");
  1455  			document.getElementById("content").classList.add("blurred");
  1456  			document.getElementById("footer").classList.add("blurred");
  1457  		}
  1458  
  1459  		function unblurBackground() {
  1460  			document.getElementById("header").classList.remove("blurred");
  1461  			document.getElementById("content").classList.remove("blurred");
  1462  			document.getElementById("footer").classList.remove("blurred");
  1463  		}
  1464  
  1465  		// ----------------------------------------------------
  1466  		// Helpers
  1467  		// ----------------------------------------------------
  1468  
  1469  		/**
  1470  		 * preventDefault prevent the default action if it is possible
  1471  		 */
  1472  		function preventDefault(that) {
  1473  			if (that.event && that.event.preventDefault) {
  1474  				that.event.preventDefault();
  1475  			}
  1476  		}
  1477  
  1478  		/**
  1479  		 * @param {string} elemID - id of an element
  1480  		 * @return {Object} value of an element or empty string
  1481  		 */
  1482  		function getValue(elemID) {
  1483  			const elem = document.getElementById(elemID);
  1484  			if (elem === undefined) {
  1485  				console.error(`element with id '${elemID}' doesn't exist`);
  1486  				return "";
  1487  			}
  1488  
  1489  			return elem.value || "";
  1490  		}
  1491  
  1492  		/**
  1493  		 * @param {string} elemID - id of an element 
  1494  		 * @param {string} value - new value element
  1495  		 */
  1496  		function setValue(elemID, value) {
  1497  			const elem = document.getElementById(elemID);
  1498  			if (elem === undefined) {
  1499  				console.error(`element with id '${elemID}' doesn't exist`);
  1500  				return;
  1501  			}
  1502  
  1503  			elem.value = value;
  1504  		}
  1505  
  1506  		/** resetValues resets values of passed elements
  1507  		 *
  1508  		 * @param {...string} elemIDs - list of element ids
  1509  		 */
  1510  		function resetValues(...elemIDs) {
  1511  			for (let i = 0; i < elemIDs.length; i++) {
  1512  				/**
  1513  				* @type {HTMLInputElement}
  1514  				*/
  1515  				const elem = document.getElementById(elemIDs[i]);
  1516  				if (elem === undefined) {
  1517  					console.error(`element with id '${elemIDs[i]}' doesn't exist`)
  1518  					continue;
  1519  				}
  1520  
  1521  				elem.value = "";
  1522  			}
  1523  		}
  1524  
  1525  		/** showElement sets 'display' to 'block'
  1526  		 *
  1527  		 * @param {string} elemID - element id
  1528  		 */
  1529  		function showElement(elemID) {
  1530  			const elem = document.getElementById(elemID);
  1531  			if (!elem) {
  1532  				console.error(`element with id '${elemID}' doesn't exist`);
  1533  				return
  1534  			}
  1535  
  1536  			elem.style.display = "block";
  1537  		}
  1538  
  1539  		/** hideElement sets 'display' to 'none'
  1540  		 *
  1541  		 * @param {string} elemID - element id
  1542  		 */
  1543  		function hideElement(elemID) {
  1544  			const elem = document.getElementById(elemID);
  1545  			if (!elem) {
  1546  				console.error(`element with id '${elemID}' doesn't exist`);
  1547  				return
  1548  			}
  1549  
  1550  			elem.style.display = "none";
  1551  		}
  1552  
  1553  		function processError(error) {
  1554  			console.error(error);
  1555  			alert("Error: " + error);
  1556  		}
  1557  
  1558  		/** replaceCommas replaces commas in passed string with points: 'qwe,rty,uio' -> 'qwe.rty,uio'.
  1559  		 * It should be used to fix decimal separator: '15,5' -> '15.5'
  1560  		 *
  1561  		 * @param {string} s
  1562  		 * @return {string} string with replaced commas
  1563  		 */
  1564  		function replaceCommas(s) {
  1565  			return s.replace(",", ".");
  1566  		}
  1567  
  1568  	</script>
  1569  
  1570  	<!-- Link Formatter -->
  1571  	<script src="{{ asStaticURL `/static/js/link-formatter.js` }}"></script>
  1572  	<script>
  1573  		window.addEventListener("load", () => {
  1574  			formatLinks("td.notes");
  1575  		})
  1576  	</script>
  1577  
  1578  	<!-- Spend Autocompletion -->
  1579  	<script>
  1580  		// A special character which is added to option values in <datalist>.
  1581  		// Use 'three-per-em space' - https://en.wikipedia.org/wiki/Whitespace_character
  1582  		const suggestionMark = " ";
  1583  		const suggestionRegexp = new RegExp(`${suggestionMark}•${suggestionMark}\\d+`);
  1584  
  1585  		/**
  1586  		 * @param {string} title
  1587  		 * @return {boolean}
  1588  		 */
  1589  		function isSuggestion(title) {
  1590  			return suggestionRegexp.test(title);
  1591  		}
  1592  
  1593  		/**
  1594  		 * @param {string} title
  1595  		 * @param {number} count
  1596  		 * @return {string}
  1597  		 */
  1598  		function addSuggestionMark(title, count) {
  1599  			return title + `${suggestionMark}•${suggestionMark}${count}`;
  1600  		}
  1601  
  1602  		/**
  1603  		 * @param {string} title
  1604  		 * @return {string}
  1605  		 */
  1606  		function removeSuggestionMark(title) {
  1607  			return title.replace(suggestionRegexp, "");
  1608  		}
  1609  
  1610  		class SpendAutocompleter {
  1611  			/**
  1612  			 * @typedef {Object} Spend
  1613  			 * @property {string} title
  1614  			 * @property {Object} type
  1615  			 * @property {number} type.id
  1616  			 * @property {string} type.name
  1617  			 * @property {number} cost
  1618  			 */
  1619  
  1620  			/**
  1621  			 * @typedef {Object} Counter
  1622  			 * @property {number} count
  1623  			 *
  1624  			 * @typedef {Spend & Counter} SpendWithCounter
  1625  			 */
  1626  
  1627  			/**
  1628  			 * @param {number} dayID
  1629  			 */
  1630  			constructor(dayID) {
  1631  				this.dayID = dayID;
  1632  				/** @type {SpendWithCounter[]} */
  1633  				this.cachedSpends = [];
  1634  				/** @type {?number} */
  1635  				this.getSpendsTimeoutID = null;
  1636  
  1637  				this.titleInput = document.getElementById("new-spend-title-" + dayID);
  1638  				this.titleInput.addEventListener("input", this.oninputListener.bind(this));
  1639  
  1640  				this.notesInput = document.getElementById("new-spend-notes-" + dayID);
  1641  				this.typeSelector = document.getElementById("new-spend-type-" + dayID);
  1642  				this.costInput = document.getElementById("new-spend-cost-" + dayID);
  1643  
  1644  				this.spendDatalist = document.getElementById("spend-datalist-" + this.dayID);
  1645  			}
  1646  
  1647  			/**
  1648  			 * @param ev {Event}
  1649  			 */
  1650  			oninputListener(ev) {
  1651  				const title = ev.target.value;
  1652  
  1653  				if (isSuggestion(title)) {
  1654  					// User chose a suggestion. Remove the suggestion mark and complete it
  1655  					ev.target.value = removeSuggestionMark(title);
  1656  					this.completeSuggestion(ev.target.value)
  1657  					return;
  1658  				}
  1659  
  1660  				if (title.length < 3) {
  1661  					this.clearSuggestionList();
  1662  					return;
  1663  				}
  1664  
  1665  				if (this.getSpendsTimeoutID) {
  1666  					clearTimeout(this.getSpendsTimeoutID);
  1667  				}
  1668  				this.getSpendsTimeoutID = setTimeout(async () => {
  1669  					this.cachedSpends = await this.getSpends(title);
  1670  					this.updateSuggestionList();
  1671  				}, 100);
  1672  			}
  1673  
  1674  			/**
  1675  			 * Get spends with passed title and unite duplicates (leave only newest). Spends are sorted by frequency
  1676  			 * @param {string} title
  1677  			 * @return {SpendWithCounter[]}
  1678  			 */
  1679  			async getSpends(title) {
  1680  				const spends = await this.searchSpendsViaAPI(title);
  1681  
  1682  				// Unite duplicates
  1683  
  1684  				/** @type {Object.<string, SpendWithCounter>} */
  1685  				const uniqueSpends = {};
  1686  				for (let spend of spends) {
  1687  					if (uniqueSpends[spend.title]) {
  1688  						uniqueSpends[spend.title].count++;
  1689  						continue;
  1690  					}
  1691  					uniqueSpends[spend.title] = spend;
  1692  					uniqueSpends[spend.title].count = 1;
  1693  				}
  1694  
  1695  				// Sort by frequency
  1696  
  1697  				/** @type {SpendWithCounter[]} */
  1698  				const sortedSpends = [];
  1699  				for (let title in uniqueSpends) {
  1700  					sortedSpends.push(uniqueSpends[title])
  1701  				}
  1702  				sortedSpends.sort((a, b) => {
  1703  					if (a.count !== b.count) {
  1704  						return a.count < b.count;
  1705  					}
  1706  					return a.title > b.title;
  1707  				});
  1708  
  1709  				return sortedSpends;
  1710  			}
  1711  
  1712  			/**
  1713  			 * Search spends with passed title via API. Spends are sorted by date from newest to oldest
  1714  			 * @param {string} title
  1715  			 * @return {Spend[]}
  1716  			 */
  1717  			async searchSpendsViaAPI(title) {
  1718  				const params = new URLSearchParams({ "title": title, "sort": "date", "order": "desc" });
  1719  
  1720  				const spends = await fetch("/api/search/spends?" + params.toString()).
  1721  					then(rawResp => rawResp.json()).
  1722  					then(resp => {
  1723  						if (!resp.success) throw resp.error;
  1724  						return resp.spends;
  1725  					}).
  1726  					catch(err => console.error(err));
  1727  
  1728  				return spends || [];
  1729  			}
  1730  
  1731  			/**
  1732  			 * Update <datalist> with cached spends. Option values are marked with suggestion mark
  1733  			 */
  1734  			updateSuggestionList() {
  1735  				this.clearSuggestionList();
  1736  
  1737  				for (let spend of this.cachedSpends) {
  1738  					const option = document.createElement("option");
  1739  					option.value = addSuggestionMark(spend.title, spend.count);
  1740  					this.spendDatalist.appendChild(option);
  1741  				}
  1742  			}
  1743  
  1744  			clearSuggestionList() {
  1745  				this.spendDatalist.innerHTML = "";
  1746  			}
  1747  
  1748  			/**
  1749  			 * Complete the suggestion by setting spend type and cost
  1750  			 * @param {string} title - Title without suggestion mark
  1751  			 */
  1752  			completeSuggestion(title) {
  1753  				for (let spend of this.cachedSpends) {
  1754  					if (spend.title != title) continue;
  1755  
  1756  					this.typeSelector.value = spend.type ? spend.type.id : 0;
  1757  					this.notesInput.value = spend.notes || "";
  1758  					this.costInput.value = spend.cost;
  1759  					break;
  1760  				}
  1761  			}
  1762  		}
  1763  
  1764  		window.addEventListener("load", () => {
  1765  			const elements = document.getElementsByClassName("day__new-spend");
  1766  			for (let elem of elements) {
  1767  				const dayID = Number(elem.getAttribute("data-day-id"));
  1768  				new SpendAutocompleter(dayID);
  1769  			}
  1770  		})
  1771  	</script>
  1772  </body>
  1773  
  1774  </html>