diff options
Diffstat (limited to 'src/expense/static')
-rw-r--r-- | src/expense/static/script.js | 165 | ||||
-rw-r--r-- | src/expense/static/style.css | 129 |
2 files changed, 294 insertions, 0 deletions
diff --git a/src/expense/static/script.js b/src/expense/static/script.js new file mode 100644 index 0000000..810dc0e --- /dev/null +++ b/src/expense/static/script.js @@ -0,0 +1,165 @@ +/** + * Category type in Javascript + * @typedef {Object} Category + * @property {Number} id + * @property {string} name + */ + +/** + * Expense Type needed for summary + * @typedef {Object} Expense + * @property {Category} category + * @property {Number} amount + */ + +/*** + * The main function + */ +async function main() { + const data = await getData() + const root = document.getElementById("root") + // Extract Labels + const labels = uniq(data.map( + ({ category }) => category), + ({ id }) => id + ).sort((a, b) => a.id - b.id) + createGraphs(root, data, labels) + generateTable(root, data, labels) +} + +/** + * Crete the Table + * @param {HTMLElement} root + * @param {Expense[]} data + * @param {str[]} labels + */ +function generateTable(root, data, labels) { + const summary = group_cat(data, labels) + + const table = document.createElement("table") + table.style.margin = "1rem auto" + root.append(table) + + table.createTHead() + table.createTFoot() + + const row = table.tHead.insertRow() + row.append(...["Category", "Amount"].map(element => { + const header = document.createElement("th") + header.append(element) + return header + })) + + summary.forEach(element => { + const row = table.insertRow() + + const link = document.createElement("a") + row.insertCell().append(link) + link.href = `/cat/${element.category.id}` + link.append(element.category.name) + + row.insertCell().append(element.amount.toFixed(2)) + }) + + const footer = table.tFoot.insertRow(); + [ + "Overall", + summary.reduce((partialSum, { amount }) => partialSum + amount, 0).toFixed(2) + ].forEach(element => { + footer.insertCell().append(element) + }) +} + +/** + * Create The Pie Charts + * @param {HTMLElement} root + * @param {Expense[]} data + * @param {str[]} labels + */ +function createGraphs(root, data, labels) { + const graphBar = document.createElement("div") + root.append(graphBar) + graphBar.style.display = "grid" + graphBar.style.gridAutoFlow = "column" + pieChart(graphBar, group_cat(data.filter(({ amount }) => amount < 0), labels), "Inflow") + pieChart(graphBar, group_cat(data.filter(({ amount }) => amount > 0), labels), "Outflow") +} + +/** + * Fetches the summary Data + * @returns {Promise<Expense[]>} + */ +async function getData() { + const res = await fetch("/summary.json") + const raw = await res.json() + + const data = raw.map(({ amount, category }) => ({ amount, category })) + return data +} + +/** + * Group the data by category + * @param {Expense[]} data + * @param {Category[]} labels + * @returns {Expense[]} + */ +function group_cat(data, labels = []) { + return data.reduce((accumulator, { category, amount }) => { + const index = accumulator.find(element => element.category.id == category.id) + if (index !== undefined) { + index.amount += amount + return accumulator + } else { + return [...accumulator, { category, amount }] + } + }, labels.map(category => ({ category, amount: 0 }))) +} + +/** + * Create a pie chart from the data + * @param {element} root + * @param {any[]} data + * @param {str} title + */ +function pieChart(root, data, title = "") { + const ctx = document.createElement('canvas') + + new Chart(ctx, { + type: "pie", + data: { + labels: data.map(({ category }) => category.name), + datasets: [{ + data: data.map(({ amount }) => amount) + }] + }, + options: { + plugins: { + legend: { + position: 'right', + }, + title: { + display: true, + text: title + } + } + } + }) + root.append(ctx) +} + +/** + * get the unique value in an array + * @param {any[]} arr + * @param {(any) => any} key + * @returns any[] + */ +function uniq(arr, key) { + var seen = new Set() + return arr.filter((item) => { + var k = key(item) + return seen.has(k) ? false : seen.add(k) + }) +} + + +window.onload = main
\ No newline at end of file diff --git a/src/expense/static/style.css b/src/expense/static/style.css new file mode 100644 index 0000000..4438cf6 --- /dev/null +++ b/src/expense/static/style.css @@ -0,0 +1,129 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +:root { + color-scheme: light dark; + width: 100dvw; + margin: auto; +} + +nav { + background: rgb(117, 117, 117); + display: flex; + align-items: center; +} + +nav>h1 { + flex: auto; +} + +nav>* { + display: inline; + padding: 0.5rem; + margin: 0; +} + +.content { + background: rgb(117, 117, 117); + display: none; + position: absolute; + right: 0; + min-width: 10rem; +} + +nav :is(span, a):hover { + font-style: italic; +} + +.dropdown { + position: relative; + display: inline; +} + +.dropdown>span { + display: block; + height: 100%; + padding: 0 0.5rem; +} + +.dropdown:hover .content { + display: block; +} + +.content>* { + display: block; + padding: 0.5rem; + width: 100%; + overflow: auto; +} + +nav *:any-link { + color: inherit; + text-decoration: inherit; +} + +form { + display: grid; + grid-gap: 2rem; + margin: auto; + width: max-content; +} + +form>label { + grid-column: 1; +} + +form>* { + grid-column: 2; + width: auto; +} + +form>button, +form>input[type="submit"] { + width: max-content; +} + +table, +th, +td { + border: 1px solid; + border-collapse: collapse; + padding: 1rem; +} + +thead th, +tfoot th, +tfoot td { + background: rgb(117, 117, 117); + border: 3px solid; +} + +th, +td { + padding: 1.5rem; + border: 1px solid; +} + +h1 { + font-size: xxx-large; +} + +h2 { + font-size: xx-large; +} + +main { + margin: 0 auto; + width: fit-content +} + +.button-group>* { + margin: 0 1rem; +} + +textarea { + min-height: 10rem; +} |