522 lines
16 KiB
HTML
Executable File
522 lines
16 KiB
HTML
Executable File
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>BudgetBear</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
body {font-family: Arial, Helvetica, sans-serif;}
|
|
/* The Modal (background) */
|
|
.modal {
|
|
display: none; /* Hidden by default */
|
|
position: fixed; /* Stay in place */
|
|
z-index: 1; /* Sit on top */
|
|
padding-top: 50px; /* Location of the box */
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%; /* Full width */
|
|
height: 100%; /* Full height */
|
|
overflow: auto; /* Enable scroll if needed */
|
|
background-color: rgb(0,0,0); /* Fallback color */
|
|
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
|
|
}
|
|
|
|
/* Modal Content */
|
|
.modal-content {
|
|
background-color: #fefefe;
|
|
margin: auto;
|
|
padding: 30px;
|
|
border: 1px solid #888;
|
|
width: 40%;
|
|
}
|
|
|
|
/* The Close Button */
|
|
.close {
|
|
float: left;
|
|
margin: auto;
|
|
}
|
|
|
|
/* The Next Button */
|
|
.next {
|
|
float: right;
|
|
margin: auto;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>BudgetBear</h1>
|
|
<p>The app for bears on a budget</p>
|
|
<div id="login_form">
|
|
<label for="login_input">Login: </label>
|
|
<input id="login_input" name="login">
|
|
<input id="login_submit" type="submit" value="Submit">
|
|
</div>
|
|
<div id="login_success" style="display:none">
|
|
<p>Logged in as: <span id="login_username"></span></p>
|
|
</div>
|
|
<div id="login_error" style="display:none">
|
|
<p style="color:red">Failed to login</p>
|
|
</div>
|
|
<h2>Transactions</h2>
|
|
<input id="transactions_refresh" type="submit" value="Refresh" disabled>
|
|
<!-- Transaction Modal -->
|
|
<button id="transaction_modal_btn">Let's Process!</button>
|
|
<div id="transaction_error" style="display:none">
|
|
<p style="color:blue">No transactions to process!</p>
|
|
</div>
|
|
<div id="transaction_modal" class="modal">
|
|
<div class="modal-content">
|
|
<h3>Transaction ID <span id="transaction_id"></span></h3>
|
|
<p><b>Remaining:</b> <span id="transactions_unprocessed"></span></p>
|
|
<p><b>Transaction date:</b> <span id="transaction_date"></span></p>
|
|
<p><b>Description:</b> <span id="transaction_description"></span></p>
|
|
<p><b>Amount:</b> $<span id="transaction_amount"></span></p>
|
|
<label for="transaction_category">Category</label>
|
|
<select id="transaction_category"></select>
|
|
<label for="transaction_subcategory">Subcategory</label>
|
|
<select id="transaction_subcategory" disabled></select>
|
|
<br><br>
|
|
<label for="transaction_notes">Notes:</label>
|
|
<textarea id="transaction_notes" rows="4" cols="50"></textarea>
|
|
<br><br>
|
|
<label for="transaction_split">Split with others?</label>
|
|
<input type="checkbox" id="transaction_split" value="true">
|
|
<br><br>
|
|
<div id="transaction_split_content" style="display:none">
|
|
<h3>Split Transaction</h3>
|
|
<label for="transaction_split_amount">Amount reimbursed:</label>
|
|
<input type="number" id="transaction_split_amount">
|
|
<br><br>
|
|
<label for="transaction_split_type">Type: </label>
|
|
<select id="transaction_split_type">
|
|
<option value="2">Splitwise</option>
|
|
<option value="3">SpaceX</option>
|
|
</select>
|
|
<br><br>
|
|
<label for="transaction_split_notes">Notes: </label>
|
|
<textarea id="transaction_split_notes" rows="4" cols="50"></textarea>
|
|
<br><br>
|
|
</div>
|
|
<button id="modal_close" class="close">Exit</button>
|
|
<button id="modal_next" class="next">Submit</button>
|
|
</div>
|
|
</div>
|
|
<h2>Add category</h2>
|
|
<label for="category_parent">Parent: </label>
|
|
<select id="category_parent"></select>
|
|
<label for="category_name">Name: </label>
|
|
<input id="category_name">
|
|
<button id="category_add">Add</button>
|
|
<!-- Transaction Modal End -->
|
|
</body>
|
|
<script>
|
|
// Login HTML elements.
|
|
const login =
|
|
{
|
|
form : document.getElementById('login_form'),
|
|
input : document.getElementById('login_input'),
|
|
submit : document.getElementById('login_submit'),
|
|
success : document.getElementById('login_success'),
|
|
error : document.getElementById('login_error'),
|
|
username : document.getElementById('login_username'),
|
|
};
|
|
|
|
// Transaction HTML elements.
|
|
const x_action =
|
|
{
|
|
refresh : document.getElementById('transactions_refresh'),
|
|
modal_btn : document.getElementById('transaction_modal_btn'),
|
|
error : document.getElementById('transaction_error'),
|
|
modal : document.getElementById('transaction_modal'),
|
|
id : document.getElementById('transaction_id'),
|
|
exit: document.getElementById('modal_close'),
|
|
submit: document.getElementById('modal_next'),
|
|
unprocessed : document.getElementById('transactions_unprocessed'),
|
|
date: document.getElementById('transaction_date'),
|
|
description: document.getElementById('transaction_description'),
|
|
amount: document.getElementById('transaction_amount'),
|
|
category: document.getElementById('transaction_category'),
|
|
subcategory: document.getElementById('transaction_subcategory'),
|
|
notes: document.getElementById('transaction_notes'),
|
|
split:
|
|
{
|
|
enabled: document.getElementById('transaction_split'),
|
|
container: document.getElementById('transaction_split_content'),
|
|
notes: document.getElementById('transaction_split_notes'),
|
|
amount: document.getElementById('transaction_split_amount'),
|
|
type: document.getElementById('transaction_split_type'),
|
|
|
|
},
|
|
};
|
|
|
|
const category =
|
|
{
|
|
name_input: document.getElementById('category_name'),
|
|
parent_select: document.getElementById('category_parent'),
|
|
add_button: document.getElementById('category_add'),
|
|
};
|
|
|
|
// Global variables.
|
|
const global_data =
|
|
{
|
|
current_user: null,
|
|
transaction:
|
|
{
|
|
unprocessed_indices: [],
|
|
list: [],
|
|
process_index: 0,
|
|
unprocessed_cnt: function()
|
|
{
|
|
const diff = this.unprocessed_indices.length - 1 - this.process_index
|
|
return diff >= 0 ? diff : 0;
|
|
},
|
|
get_current: function()
|
|
{
|
|
if (this.process_index < this.unprocessed_indices.length)
|
|
{
|
|
return this.list[this.unprocessed_indices[this.process_index]]
|
|
}
|
|
|
|
return null
|
|
}
|
|
},
|
|
categories: {},
|
|
update: function()
|
|
{
|
|
let new_indices = []
|
|
for (let i = 0; i < this.transaction.list.length; i++)
|
|
{
|
|
if (this.transaction.list[i].subcategory === null)
|
|
{
|
|
// Save index of first unprocessed transaction.
|
|
new_indices.push(i);
|
|
}
|
|
}
|
|
this.transaction.unprocessed_indices = new_indices;
|
|
}
|
|
};
|
|
|
|
login.input.onkeypress = function(event) {
|
|
if (event.key === "Enter")
|
|
{
|
|
event.preventDefault();
|
|
login.submit.click();
|
|
}
|
|
};
|
|
|
|
login.submit.onclick = function(_) {
|
|
const username = login.input.value;
|
|
const request_url = "/users?username=" + username;
|
|
const handler =
|
|
{
|
|
success: function (data) {
|
|
// Cache data in browser.
|
|
global_data.current_user = data;
|
|
|
|
// Retrieve transactions.
|
|
get_transactions(global_data.current_user.uuid);
|
|
|
|
// Update UI.
|
|
login.form.style.display = 'none';
|
|
login.error.style.display = 'none';
|
|
login.username.innerText = data.username;
|
|
login.success.style.display = 'block';
|
|
},
|
|
error: function () {
|
|
// Update UI.
|
|
login.error.style.display = 'block';
|
|
}
|
|
};
|
|
http_request(request_url, handler);
|
|
};
|
|
|
|
x_action.refresh.onclick = function() {
|
|
get_transactions(global_data.current_user.uuid);
|
|
}
|
|
|
|
x_action.modal_btn.onclick = function() {
|
|
if (global_data.transaction.unprocessed_cnt() === 0)
|
|
{
|
|
x_action.error.style.display = "block";
|
|
}
|
|
else
|
|
{
|
|
update_transaction_ui(global_data.transaction.get_current());
|
|
x_action.error.style.display = "none";
|
|
x_action.modal.style.display = "block";
|
|
}
|
|
}
|
|
|
|
x_action.exit.onclick = function()
|
|
{
|
|
reset_transaction_ui();
|
|
}
|
|
|
|
x_action.submit.onclick = function()
|
|
{
|
|
const body = {}
|
|
if (x_action.subcategory.value !== "")
|
|
{
|
|
body.category = x_action.subcategory.value;
|
|
}
|
|
else
|
|
{
|
|
body.category = x_action.category.value;
|
|
}
|
|
body.notes = x_action.notes.value;
|
|
const transaction = global_data.transaction.get_current()
|
|
const url = "/transactions/" + transaction.primary_key;
|
|
const handler =
|
|
{
|
|
success: function() {
|
|
console.log("Updated transaction successfully!");
|
|
},
|
|
error: function() {
|
|
alert("Failed to update transaction");
|
|
}
|
|
}
|
|
http_request(url, handler, "PUT", body);
|
|
|
|
const is_split = x_action.split.enabled.checked;
|
|
if (is_split)
|
|
{
|
|
const split_body = {}
|
|
split_body.category = body.category;
|
|
split_body.notes = x_action.split.notes.value;
|
|
split_body.amount = -1 * parseFloat(x_action.split.amount.value);
|
|
split_body.type = parseInt(x_action.split.type.value);
|
|
split_body.transaction_date = transaction.transaction_date;
|
|
split_body.description = transaction.description;
|
|
split_body.user_id = global_data.current_user.uuid;
|
|
|
|
const url = "/transactions";
|
|
const handler =
|
|
{
|
|
success: function() {
|
|
},
|
|
error: function() {
|
|
alert("Failed to add split transaction");
|
|
}
|
|
}
|
|
http_request(url, handler, "POST", split_body);
|
|
}
|
|
|
|
global_data.transaction.process_index++;
|
|
if (global_data.transaction.get_current() !== null)
|
|
{
|
|
update_transaction_ui(global_data.transaction.get_current());
|
|
}
|
|
else
|
|
{
|
|
reset_transaction_ui();
|
|
}
|
|
}
|
|
|
|
x_action.split.enabled.onclick = function()
|
|
{
|
|
if (x_action.split.enabled.checked == true)
|
|
{
|
|
x_action.split.container.style.display = 'block';
|
|
const transaction = global_data.transaction.get_current();
|
|
x_action.split.type.onchange();
|
|
}
|
|
else
|
|
{
|
|
x_action.split.container.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
x_action.split.type.onchange = function()
|
|
{
|
|
const selected = x_action.split.type.value;
|
|
const transaction = global_data.transaction.get_current();
|
|
if (selected == 2) // Splitwise
|
|
{
|
|
x_action.split.amount.value = transaction.amount / 2;
|
|
}
|
|
else if (selected == 3) //SpaceX
|
|
{
|
|
x_action.split.amount.value = transaction.amount;
|
|
}
|
|
}
|
|
|
|
x_action.category.onchange = function()
|
|
{
|
|
x_action.subcategory.innerHTML = '';
|
|
const selected = x_action.category.value;
|
|
const subcategories = global_data.categories[selected]
|
|
if (subcategories.length == 0)
|
|
{
|
|
x_action.subcategory.disabled = true
|
|
}
|
|
else
|
|
{
|
|
x_action.subcategory.disabled = undefined
|
|
for (let i = 0; i < subcategories.length; i++)
|
|
{
|
|
const opt = document.createElement("option");
|
|
opt.value = subcategories[i];
|
|
opt.innerText = subcategories[i];
|
|
|
|
x_action.subcategory.appendChild(opt);
|
|
}
|
|
}
|
|
}
|
|
|
|
category.add_button.onclick = function()
|
|
{
|
|
const body = {};
|
|
body.name = category.name_input.value;
|
|
const selected = category.parent_select.value;
|
|
if (selected != "none" )
|
|
{
|
|
body.parent = selected;
|
|
}
|
|
const url = "/categories";
|
|
const handler =
|
|
{
|
|
success: function()
|
|
{
|
|
get_categories();
|
|
},
|
|
error: function()
|
|
{
|
|
alert("Failed to add category");
|
|
}
|
|
}
|
|
http_request(url, handler, "POST", body);
|
|
|
|
category.parent_select.value = "none";
|
|
category.name_input.value = "";
|
|
}
|
|
|
|
category.name_input.onkeypress = function(event) {
|
|
if (event.key === "Enter")
|
|
{
|
|
event.preventDefault();
|
|
category.add_button.click();
|
|
}
|
|
};
|
|
|
|
function reset_transaction_ui(close_modal=true)
|
|
{
|
|
// Reset all stateful inputs.
|
|
if (close_modal)
|
|
{
|
|
x_action.modal.style.display = "none";
|
|
}
|
|
|
|
x_action.split.container.style.display = 'none';
|
|
x_action.split.enabled.checked = false;
|
|
x_action.split.notes.value = "";
|
|
x_action.category.innerHTML = "";
|
|
x_action.subcategory.innerHTML = "";
|
|
x_action.subcategory.disabled = "true";
|
|
x_action.notes.value = "";
|
|
}
|
|
|
|
function update_transaction_ui(transaction)
|
|
{
|
|
// Reset old state.
|
|
reset_transaction_ui(false);
|
|
|
|
// Global updates first.
|
|
x_action.unprocessed.innerText = global_data.transaction.unprocessed_cnt();
|
|
for (key in global_data.categories)
|
|
{
|
|
const opt = document.createElement("option");
|
|
opt.value = key;
|
|
opt.innerText = key;
|
|
|
|
x_action.category.appendChild(opt);
|
|
}
|
|
|
|
// Transaction specific updates.
|
|
x_action.id.innerText = transaction.primary_key;
|
|
x_action.date.innerText = transaction.transaction_date;
|
|
x_action.description.innerText = transaction.description;
|
|
x_action.amount.innerText = transaction.amount;
|
|
}
|
|
|
|
function get_transactions(uuid) {
|
|
const request_url = "/transactions?user_id=" + uuid;
|
|
const handler =
|
|
{
|
|
success: function(data) {
|
|
global_data.transaction.list = data;
|
|
x_action.refresh.disabled = undefined;
|
|
},
|
|
error: function() {
|
|
console.error("Failed to get transactions");
|
|
}
|
|
}
|
|
http_request(request_url, handler);
|
|
}
|
|
|
|
function get_categories() {
|
|
const request_url = "/categories";
|
|
const handler =
|
|
{
|
|
success: function(data) {
|
|
global_data.categories = data
|
|
update_category_ui();
|
|
},
|
|
error: function() {
|
|
console.error("Failed to get categories");
|
|
}
|
|
}
|
|
http_request(request_url, handler);
|
|
}
|
|
|
|
function update_category_ui()
|
|
{
|
|
function add_opt(key)
|
|
{
|
|
const opt = document.createElement("option");
|
|
opt.value = key;
|
|
opt.innerText = key;
|
|
|
|
category.parent_select.appendChild(opt);
|
|
}
|
|
|
|
category.parent_select.innerHTML = "";
|
|
add_opt("none");
|
|
for (key in global_data.categories) add_opt(key);
|
|
}
|
|
|
|
function http_request(url, response_handler, method="GET", body=null)
|
|
{
|
|
let request = new XMLHttpRequest();
|
|
request.addEventListener("load", function(){
|
|
if (request.status !== 200)
|
|
{
|
|
response_handler.error();
|
|
}
|
|
else
|
|
{
|
|
let response_body = ""
|
|
if (request.responseText !== "")
|
|
{
|
|
response_body = JSON.parse(request.responseText);
|
|
}
|
|
response_handler.success(response_body);
|
|
global_data.update();
|
|
}
|
|
})
|
|
|
|
const async = true;
|
|
request.open(method, url, async);
|
|
if (method == "POST" || method == "PUT")
|
|
{
|
|
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
request.send(JSON.stringify(body));
|
|
}
|
|
else
|
|
{
|
|
request.send(null);
|
|
}
|
|
}
|
|
|
|
// Do every page load.
|
|
get_categories();
|
|
</script>
|
|
</html> |