Added the capability to upload CSV files
This commit is contained in:
parent
90b02eef79
commit
d1e736d7a7
19 changed files with 678 additions and 99 deletions
|
|
@ -1,14 +1,19 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Account {{account.account_name}}{% endblock title %}
|
||||
{% block body %}
|
||||
<div>{{account.account_name}}</div>
|
||||
<div class="flex">
|
||||
<span class="text-lg grow">{{account.account_name}}</span>
|
||||
<div>
|
||||
<a href="/accounts/id/{{account.account_id}}/transactions/add">+</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Descripción</th>
|
||||
<th>Fecha</th>
|
||||
<th>Cantidad</th>
|
||||
<th>Categoría</th>
|
||||
<th>Description</th>
|
||||
<th>Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Category</th>
|
||||
</tr>
|
||||
{% for tx in transactions %}
|
||||
<tr>
|
||||
|
|
@ -19,7 +24,7 @@
|
|||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p>Cargados {{n_txs}} movimientos</p>
|
||||
<p>Loaded {{n_txs}} transactions</p>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
|
||||
|
|
|
|||
176
templates/accounts_add_txs.html
Normal file
176
templates/accounts_add_txs.html
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Account {{account.account_name}}{% endblock title %}
|
||||
{% block body %}
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<span class="text-lg grow">Add transactions to {{account.account_name}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<form id="file-form">
|
||||
<div><input id="file-input" type="file" name="file"></div>
|
||||
<div><input id="file-submit" type="submit" value="Upload transactions" disabled></div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="file-content">
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/static/csv.js"></script>
|
||||
<script type="module">
|
||||
import csv_parse from "/static/csv.js";
|
||||
|
||||
const form_elem = document.getElementById('file-form')
|
||||
form_elem.onsubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
console.log('Unable to send');
|
||||
}
|
||||
|
||||
const mappers = [
|
||||
['None', null],
|
||||
['Date dd/mm/yyyy', el => {
|
||||
let split = el.split('/');
|
||||
return new Date(
|
||||
parseInt(split[2], 10),
|
||||
parseInt(split[1], 10),
|
||||
parseInt(split[0], 10),
|
||||
);
|
||||
}],
|
||||
['Date yyyy/mm/dd', el => {
|
||||
let split = el.split('/');
|
||||
return new Date(
|
||||
parseInt(split[0], 10),
|
||||
parseInt(split[1], 10),
|
||||
parseInt(split[2], 10),
|
||||
);
|
||||
}],
|
||||
['Description', el => el],
|
||||
['Amount', el => parseFloat(el)]
|
||||
];
|
||||
|
||||
function appendOptions(el) {
|
||||
el.replaceChildren(...mappers.map((e, idx)=>{
|
||||
let option = document.createElement('option');
|
||||
option.setAttribute('value', idx);
|
||||
option.textContent = e[0];
|
||||
return option;
|
||||
}));
|
||||
}
|
||||
|
||||
document.getElementById('file-input').onchange = (evt) => {
|
||||
let files = evt.target.files;
|
||||
if(files.length > 0) {
|
||||
let file = files[0];
|
||||
if(file.type != 'text/csv') {
|
||||
window.alert("File not valid");
|
||||
return;
|
||||
}
|
||||
file.text().then(content => {
|
||||
let line_end = content.indexOf('\n');
|
||||
if(line_end == -1) {
|
||||
window.alert("File is not a valid CSV");
|
||||
return;
|
||||
}
|
||||
|
||||
let table_content = csv_parse(content);
|
||||
let table_header = table_content.splice(0,1)[0];
|
||||
|
||||
let table = document.createElement('table');
|
||||
let thead = document.createElement('thead');
|
||||
let trhead = document.createElement('tr');
|
||||
trhead.replaceChildren(...table_header.map(e =>{
|
||||
let elem = document.createElement('th');
|
||||
let text = document.createElement('div');
|
||||
text.textContent = e;
|
||||
elem.appendChild(text);
|
||||
|
||||
let container = document.createElement('div');
|
||||
let sel_el = document.createElement('select');
|
||||
sel_el.id = 'column_' + e;
|
||||
appendOptions(sel_el);
|
||||
container.appendChild(sel_el);
|
||||
elem.appendChild(container);
|
||||
|
||||
return elem;
|
||||
}));
|
||||
thead.appendChild(trhead);
|
||||
table.appendChild(thead);
|
||||
|
||||
form_elem.onsubmit = (evt) => {
|
||||
evt.preventDefault();
|
||||
console.log(table_header);
|
||||
console.log(table_content);
|
||||
|
||||
let mapper = {
|
||||
date: null,
|
||||
amount: null,
|
||||
description: null
|
||||
};
|
||||
|
||||
table_header.forEach((e, idx)=>{
|
||||
let option = document.getElementById('column_'+e).selectedIndex;
|
||||
switch(option){
|
||||
case 1:
|
||||
case 2:
|
||||
mapper.date = row => mappers[option][1](row[idx]);
|
||||
break;
|
||||
case 3:
|
||||
mapper.description = row => mappers[option][1](row[idx]);
|
||||
break;
|
||||
case 4:
|
||||
mapper.amount = row => mappers[option][1](row[idx]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
if(mapper.date == null) {
|
||||
alert('Missing date mapping');
|
||||
return;
|
||||
} else if(mapper.amount == null) {
|
||||
alert('Missing amount mapping');
|
||||
return;
|
||||
} else if(mapper.description == null) {
|
||||
alert('Missing description mapping');
|
||||
return;
|
||||
}
|
||||
let out = table_content.map(e=>{
|
||||
return {
|
||||
date: mapper.date(e),
|
||||
amount: mapper.amount(e),
|
||||
description: mapper.description(e)
|
||||
};
|
||||
});
|
||||
fetch('add', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(out)
|
||||
}).then(e=>window.location.href='..');
|
||||
};
|
||||
|
||||
document.getElementById('file-submit').removeAttribute('disabled');
|
||||
|
||||
let tbody = document.createElement('tbody');
|
||||
tbody.replaceChildren(...table_content.map(row => {
|
||||
let row_elem = document.createElement('tr');
|
||||
row_elem.replaceChildren(...row.map(cell => {
|
||||
let td = document.createElement('td');
|
||||
td.textContent = cell;
|
||||
return td;
|
||||
}));
|
||||
return row_elem;
|
||||
}))
|
||||
|
||||
table.appendChild(tbody);
|
||||
|
||||
let content_div = document.getElementById('file-content');
|
||||
content_div.replaceChildren(table);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
th, td {
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock body %}
|
||||
|
||||
|
|
@ -13,8 +13,7 @@
|
|||
}
|
||||
|
||||
.sidebar {
|
||||
max-width: 12rem;
|
||||
flex-grow: 1;
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
|
|
@ -26,10 +25,11 @@
|
|||
<body>
|
||||
<div class="flex h-full">
|
||||
<aside class="sidebar bg-stone-300 p-4 flex flex-col">
|
||||
<a class="hover:bg-stone-400" href="/">Inicio</a>
|
||||
<a class="hover:bg-stone-400" href="/rules">Reglas</a>
|
||||
<a class="hover:bg-stone-400" href="/">Start</a>
|
||||
<a class="hover:bg-stone-400" href="/rules">Rules</a>
|
||||
<a class="hover:bg-stone-400" href="/categories">Categories</a>
|
||||
</aside>
|
||||
<div class="p-4 grow">
|
||||
<div class="p-4 grow h-full overflow-auto">
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
</div>
|
||||
|
|
|
|||
25
templates/categories_list.html
Normal file
25
templates/categories_list.html
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Categories{% endblock title %}
|
||||
{% block body %}
|
||||
<div>
|
||||
<a href="/categories/new">New</a>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th span="col">Id</th>
|
||||
<th span="col">Name</th>
|
||||
<th span="col">Description</th>
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category in categories %}
|
||||
<tr>
|
||||
<td>{{category.category_id}}</td>
|
||||
<td>{{category.name}}</td>
|
||||
<td>{{category.description}}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock body %}
|
||||
28
templates/categories_new.html
Normal file
28
templates/categories_new.html
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Create category{% endblock title %}
|
||||
{% block body %}
|
||||
<form action="/categories/new" method="post" class="flex flex-col">
|
||||
<label class="grow">
|
||||
Name
|
||||
<input type="text" name="name" />
|
||||
</label>
|
||||
<label class="grow">
|
||||
Description
|
||||
<input type="text" name="description" />
|
||||
</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<style>
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid black;
|
||||
border-radius: 3px;
|
||||
padding: 0.25rem;
|
||||
margin: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock body %}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Index{% endblock title %}
|
||||
{% block body %}
|
||||
{% for account in accounts %}
|
||||
<a href="/accounts/id/{{account.account_id}}">{{account.account_name}}({{account.account_id}})</a>
|
||||
{% endfor %}
|
||||
<div><h2 class="text-lg">Accounts</h2></div>
|
||||
<div>
|
||||
{% for account in accounts %}
|
||||
<a class="p-2 hover:bg-stone-200" href="/accounts/id/{{account.account_id}}">{{account.account_name}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock body %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue