making my way downtown~

This commit is contained in:
2024-05-29 22:23:51 +02:00
parent 2e73e69bf1
commit db3b1e6a11
28 changed files with 2895 additions and 21 deletions

71
frontend/README.md Normal file
View File

@@ -0,0 +1,71 @@
# Frontend
Es soll eine Webseite mit HTML, CSS und JavaScript erstellt werden. Die Webseite soll hierbei eine Finanzverwaltung mit doppelter Buchführung abbilden.
**Anforderungen:**
- **Inhalt:**
- Es sind zwei Unterseiten vorzusehen:
- Übersicht über alle Konten
- Liste der Buchungen eines Kontos
- Die Übersichtsseite enthält je Konto mindestens folgende Angaben:
- Name des Kontos
- Beschreibung des Kontos
- Kontonummer
- Aktuelle Summe
Die Konten sollen hierbei hierarchisch angeordnet werden (beispielsweise Aufwendungen mit den Unterkonten Miete, Lebensmittel und Semesterbeitrag).
Die Übersicht soll nach Kontonummer sortiert sein.
- - Die Buchungsliste für ein Konto enthält je Buchung mindestens folgende Angaben:
- Titel der Buchung
- Betrag der Buchung auf dem Konto
- Gegenkonto
- **Navigation:**
- Zwischen Übersichtsseite und Buchungslisten soll eine Navigation über Hyperlinks möglich sein.
- **Struktur:**
- Es gibt auf allen Seiten einen einheitlichen Header und Footer.
- **Stylesheets:**
- CSS-Stylesheets sollen als separate Datei eingebunden werden
- Für das Styling des Headers und Footers ist ein gemeinsames Stylesheet zu nutzen.
- Die aktuelle Summe ist grün bei positiven, rot bei negativen und schwarz beim Betrag null zu formatieren.
- Es werden keine CSS-Frameworks eingesetzt (Bootstrap o.Ä.)
- Die Darstellung auf Smartphones und Desktop-Geräten soll sich unterscheiden (Responsive Design).
- **Schnittstelle:**
- Für den Abruf der Daten soll die bereitgestellte API genutzt werden.
- Unter der URL, die ihr im ersten Labor erhaltet, sind die Endpunkte der API mit OpenAPI dokumentiert und können dort auch getestet werden.
- Liefert die Schnittstelle einen Fehler zurück, soll dieser angemessen behandelt werden (d.h. Anzeige einer für den Benutzer sichtbaren und verständlichen Fehlermeldung).
- **Bearbeitung:**
- Buchungen sollen erstellt, bearbeitet und gelöscht werden können.
- Konten sollen erstellt, bearbeitet und gelöscht werden können.
- **Mehrteilige Buchungen:**
- Buchungen sollen auch so erstellt werden können, dass in einer
Buchung mehr als zwei Konten aufgeführt sind.
- **Gesamtbuchungsübersicht:**
- Es ist eine zusätzliche Seite zu erstellen, welche die Buchungen
aller Konten enthält.
- **Sortieren und Filtern:**
- Auf der Buchungsübersicht und der Gesamtbuchungsübersicht ist eine Sortier- und Filterfunktion zu implementieren.
- Die Sortierung muss auf- und absteigend erfolgen können nach:
- Buchungsdatum
- Wertstellungsdatum
- Die Filterung muss erfolgen können nach:
- Datum oder Zeitraum (Datum von - bis) für
- Buchungsdatum
- Wertstellungsdatum
- Titel der Buchung
- case-insensitive (Groß-/Kleinschreibung ignorieren)
- auch Teilausdrücke finden (beispielsweise soll "Brot" den Eintrag "Abendbrot für Sonntag" finden)
- Der Filter soll direkt beim Tippen angewandt werden, ohne dass eine Bestätigung notwendig ist.
- Betrag der Buchung (größer als .../kleiner als ...)
Sind mehrere Filter gleichzeitig aktiv, werden diese logisch und-verknüpft.
- - Standardmäßig sollen nur Buchungen des laufenden Kalenderjahres absteigend sortiert nach Buchungsdatum angezeigt werden.
- **Paginierung:**
- Die Anzeige auf der Buchungsübersicht und der Gesamtbuchungsübersicht soll paginierbar sein, das heißt, es werden auf einer Seite nur eine bestimmte Menge (initial 10) an Buchungen angezeigt.
- Die Seitengröße soll hierbei auf 10, 20, 50, 100 und „Alle“ einstellbar sein.
- Sind weniger als 10 Einträge vorhanden, sollen die Einstellungen zur Paginierung (Seitenauswahl, Seitengröße) verborgen werden.
- **Echtzeitupdates:**
- Die Anwendung soll Änderungen bei den Buchungen über SSE empfangen und in Echtzeit anzeigen. Hierfür dient der Endpunkt /live des Backends.

BIN
frontend/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Financial Accounting Tool Project" />
<meta
name="author"
content="Maximilian Ruhm, Tarek Dafay, Christoph van Deest"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Financial Accounting Tool</title>
<link rel="icon" href="assets/favicon.ico" />
<link rel="stylesheet" href="../style/index.css" />
<link rel="stylesheet" href="../style/account.css" />
<script type="module" src="../js/account.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this Website.</noscript>
<div id="root">
<div class="header">
<span class="title" onclick="window.location.href='../index.html'"
>Account-Editor</span
><span class="nav" onclick="window.location.href='overview.html'"
>Gesamtübersicht</span
>
</div>
<div id="creation-container" class="center">
<div class="center form-data">
<div class="form-data-label">Kontonummer:</div>
<input class="form-data-element" type="text" id="number" />
</div>
<div class="center form-data">
<div class="form-data-label">Kontoname:</div>
<input class="form-data-element" type="text" id="name" />
</div>
<div class="center form-data">
<div class="form-data-label">Beschreibung:</div>
<input class="form-data-element" type="text" id="description" />
</div>
<div class="center form-data">
<div class="form-data-label">Kontoart:</div>
<select class="form-data-element" id="type">
<option disabled selected>--- Bitte Option wählen ---</option>
<option>default</option>
<option>meta</option>
</select>
</div>
<div class="center form-data">
<div class="form-data-label">Übergeordnetes Konto:</div>
<select class="form-data-element" id="account-select">
<option disabled selected>--- Wurzel Konto ---</option>
</select>
</div>
<div class="save-buttons center">
<input class="save-button" type="button" value="abbrechen" id="cancel" />
<input class="save-button" type="button" value="löschen" id="delete" style="display: none;"/>
<input class="save-button" type="button" value="erstellen" id="submit" />
</div>
</div>
<div class="footer">
Entwicklung des Frontends - Maximilian Ruhm, Tarek Dafay, Christoph van
Deest
</div>
</div>
</body>
</html>

107
frontend/html/detail.html Normal file
View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Financial Accounting Tool Project" />
<meta
name="author"
content="Maximilian Ruhm, Tarek Dafay, Christoph van Deest"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Financial Accounting Tool</title>
<link rel="icon" href="assets/favicon.ico" />
<link rel="stylesheet" href="../style/index.css" />
<link rel="stylesheet" href="../style/detail.css" />
<script type="module" src="../js/detail.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this Website.</noscript>
<div id="root">
<div class="header">
<span class="title" onclick="window.location.href='../index.html'"
>Detailansicht</span
><span class="nav" onclick="window.location.href='overview.html'"
>Gesamtübersicht</span
>
</div>
<div class="link-container">
<a href="../index.html">zurück zu Startseite</a>
</div>
<div id="detail-container"></div>
<div id="table-container" style="display: flex">
<div class="filter">
<div class="filter-div">
<span class="filter-text">Zeitraum von</span>
<input type="date" id="beginDate" name="dateFilter" />
<span class="filter-text">bis</span>
<input type="date" id="endDate" name="dateFilter" />
<input type="button" id="resetDates" value="X" />
</div>
<div class="center filter-div">
<input
class="search-text"
id="searchText"
name="textFilter"
type="text"
placeholder="Suche...."
/>
</div>
<div class="center filter-div">
<input
class="amount-text"
type="text"
placeholder="Betrag ab...."
id="beginAmount"
name="textFilter"
/>
<input
class="amount-text"
type="text"
placeholder="Betrag bis...."
id="endAmount"
name="textFilter"
/>
</div>
</div>
<div class="table">
<table class="entry-table">
<thead>
<tr class="entry-table-tr">
<th class="entry-table-th clickable" id="bookingDate" width="15%">
Buchungsdatum
</th>
<th class="entry-table-th clickable" id="valuta" width="15%">Valuta</th>
<th class="entry-table-th" width="20%">Titel</th>
<th class="entry-table-th" width="20%">Gegenkonto</th>
<th class="entry-table-th" width="20%">Betrag</th>
<th class="entry-table-th" width="10%" style="text-align: center;"><input type="button" class="action-button" value="+" id="add-transaction" /></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="pagination-div" class="pagination">
<div class="pagination-part center">
<input type="button" id="backward-page" class="page-button" value="Vorherige" />
<input type="button" id="forward-page" class="page-button" value="Nächste" />
</div>
<div class="pagination-part center"><span id="page-info">1</span></div>
<div class="pagination-part center">
<span>Buchungen pro Seite:</span>
<select id="table-range" class="dropdown">
<option>10</option>
<option>20</option>
<option>50</option>
<option>100</option>
<option>Alle</option>
</select>
</div>
</div>
</div>
<div class="footer">
Entwicklung des Frontends - Maximilian Ruhm, Tarek Dafay, Christoph van
Deest
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Financial Accounting Tool Project" />
<meta
name="author"
content="Maximilian Ruhm, Tarek Dafay, Christoph van Deest"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Financial Accounting Tool</title>
<link rel="icon" href="assets/favicon.ico" />
<link rel="stylesheet" href="../style/index.css" />
<link rel="stylesheet" href="../style/detail.css" />
<script type="module" src="../js/overview.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this Website.</noscript>
<div id="root">
<div class="header">
<span class="title" onclick="window.location.href='../index.html'"
>Buchhaltungs-Gesamtübersicht</span
>
</div>
<div class="link-container">
<a href="../index.html">zurück zu Startseite</a>
</div>
<div id="table-container" style="display: flex">
<div class="filter">
<div class="filter-div">
<span class="filter-text">Zeitraum von</span>
<input type="date" id="beginDate" name="dateFilter" />
<span class="filter-text">bis</span>
<input type="date" id="endDate" name="dateFilter" />
<input type="button" id="resetDates" value="X" />
</div>
<div class="center filter-div">
<input
class="search-text"
id="searchText"
name="textFilter"
type="text"
placeholder="Suche...."
/>
</div>
</div>
<div class="table">
<table class="entry-table">
<thead>
<tr class="entry-table-tr">
<th class="entry-table-th clickable" id="bookingDate">
Buchungsdatum
</th>
<th class="entry-table-th clickable" id="valuta">Valuta</th>
<th class="entry-table-th">Titel</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div id="pagination-div" class="pagination">
<div class="pagination-part center">
<input type="button" id="backward-page" class="page-button" value="Vorherige" />
<input type="button" id="forward-page" class="page-button" value="Nächste" />
</div>
<div class="pagination-part center"><span id="page-info">1</span></div>
<div class="pagination-part center">
<span>Buchungen pro Seite:</span>
<select id="table-range" class="dropdown">
<option>10</option>
<option>20</option>
<option>50</option>
<option>100</option>
<option>Alle</option>
</select>
</div>
</div>
</div>
<div class="footer">
Entwicklung des Frontends - Maximilian Ruhm, Tarek Dafay, Christoph van
Deest
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Financial Accounting Tool Project" />
<meta
name="author"
content="Maximilian Ruhm, Tarek Dafay, Christoph van Deest"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Financial Accounting Tool</title>
<link rel="icon" href="assets/favicon.ico" />
<link rel="stylesheet" href="../style/index.css" />
<link rel="stylesheet" href="../style/transaction.css" />
<script type="module" src="../js/transaction.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this Website.</noscript>
<div id="root">
<div class="header">
<span class="title" onclick="window.location.href='../index.html'"
>Transaktions-Editor</span
><span class="nav" onclick="window.location.href='overview.html'"
>Gesamtübersicht</span
>
</div>
<div id="creation-container" class="center">
<div class="center form-data">
<div class="form-data-label">Buchungsdatum:</div>
<input class="form-data-element" type="date" id="postingDate" />
</div>
<div class="center form-data">
<div class="form-data-label">Wertstellungsdatum (Valuta):</div>
<input class="form-data-element" type="date" id="valueDate" />
</div>
<div class="center form-data">
<div class="form-data-label">Buchungstitel:</div>
<input class="form-data-element" type="text" id="title" />
</div>
<table class="transaction-table">
<thead>
<tr class="transaction-tr">
<th class="transaction-th" width="30%">Konto</th>
<th class="transaction-th" width="25%">Betrag</th>
<th class="transaction-th" width="30%">Buchungstext</th>
<th class="transaction-th" id="action-row" width="15%" style="text-align: center;"><input type="button" class="action-button" value="+" id="add-transaction" /></th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="save-buttons center">
<input
class="save-button"
type="button"
value="abbrechen"
id="cancel"
/>
<input
class="save-button"
type="button"
value="speichern"
id="submit"
/>
</div>
</div>
<div class="footer">
Entwicklung des Frontends - Maximilian Ruhm, Tarek Dafay, Christoph van
Deest
</div>
</div>
</body>
</html>

39
frontend/index.html Normal file
View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="description" content="Financial Accounting Tool Project" />
<meta
name="author"
content="Maximilian Ruhm, Tarek Dafay, Christoph van Deest"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Financial Accounting Tool</title>
<link rel="icon" href="assets/favicon.ico" />
<link rel="stylesheet" href="style/index.css" />
<link rel="stylesheet" href="style/accordion.css" />
<script type="module" src="index.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this Website.</noscript>
<div id="root">
<div class="header">
<span class="title" onclick="window.location.href='./index.html'"
>Buchhaltung</span
><span class="nav" onclick="window.location.href='html/overview.html'"
>Gesamtübersicht</span
>
</div>
<div class="content-wrap">
<div class="link-container">
<a href="./html/account.html">Konto hinzufügen</a>
</div>
<div id="accordion-container"></div>
</div>
<div class="footer">
Entwicklung des Frontends - Maximilian Ruhm, Tarek Dafay, Christoph van
Deest
</div>
</div>
</body>
</html>

44
frontend/index.js Normal file
View File

@@ -0,0 +1,44 @@
import { fetchData } from "./js/api.js";
import { createAccordion } from "./js/accordion.js";
function start() {
fetchData("accounts").then((data) => {
const container = document.getElementById("accordion-container");
container.innerHTML = "";
data = data.sort((a, b) => (a.number > b.number) ? 1 : ((b.number > a.number) ? -1 : 0));
data.forEach(element => {
const linkName = element.name;
const childrenContainer = createChildrens(element.subaccounts, linkName);
createAccordion(container, element, childrenContainer, linkName);
});
});
}
/**
* Funktion mit rekusiven Aufruf um verschachtelte Accordions zu erzeugen
* @param {array} subaccounts
* @param {string} linkName
* @returns
*/
function createChildrens(subaccounts, linkName) {
const childrenContainer = document.createElement("div");
childrenContainer.setAttribute("class", "children-container");
if (subaccounts.length > 0) {
subaccounts.forEach(account => {
const linkHref = `${linkName}:${account.name}`;
const container = createChildrens(account.subaccounts, linkHref);
createAccordion(childrenContainer, account, container, linkHref);
})
return childrenContainer;
}
return null;
}
start();

109
frontend/js/accordion.js Normal file
View File

@@ -0,0 +1,109 @@
const euroFormat = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
/**
* Erstellt ein Accordion an einem vorgegebenen Punkt, mit einem Inhalt der übergeben wird
* @param {DOMElement} rootElement
* @param {object} element
* @param {DOMElement} body
* @param {string} linkName
*/
export function createAccordion(rootElement, element, body, linkName) {
// accordion
const accordion = document.createElement("div");
accordion.setAttribute("class", "accordion clickable");
if (body !== null)
accordion.setAttribute("class", "accordion clickable");
else
accordion.setAttribute("class", "accordion");
// accordion Titel
const accordionTitle = document.createElement("div");
accordionTitle.setAttribute("class", "accordion-title");
// accordion Titel Nummer
const numberText = document.createElement("div");
numberText.innerHTML = element.number;
numberText.setAttribute("class", "title-number");
accordionTitle.appendChild(numberText);
// accordion Titel Name
const titleText = document.createElement("div");
titleText.setAttribute("class", "title-name");
const titleLink = document.createElement("a");
titleLink.innerHTML = element.name;
titleLink.setAttribute("class", "title-link");
titleLink.href = `html/detail.html?account=${linkName}`;
//titleLink.target = "_blank";
titleText.appendChild(titleLink);
accordionTitle.appendChild(titleText);
// accordion Titel Beschreibung
const descriptionText = document.createElement("div");
descriptionText.innerHTML = element.description ? element.description : "";
descriptionText.setAttribute("class", "title-description");
accordionTitle.appendChild(descriptionText);
// accordion Titel Wert
const balanceText = document.createElement("div");
balanceText.innerHTML = euroFormat.format(element.balance);
if (element.balance < 0)
balanceText.setAttribute("class", "title-balance negative-number");
else if (element.balance > 0)
balanceText.setAttribute("class", "title-balance positive-number");
else
balanceText.setAttribute("class", "title-balance neutral-number");
accordionTitle.appendChild(balanceText);
// accordion Anzeige, ob Unterkonten vorhanden
const indicatorText = document.createElement("div");
indicatorText.innerHTML = element.subaccounts.length > 0 ? "↓" : "";
indicatorText.setAttribute("class", "title-indicator");
accordionTitle.appendChild(indicatorText);
// accordion onclick handler
accordionTitle.onclick = () => {
if (accordion.querySelector(".accordion-content")) {
closeAccordion(accordion);
} else {
const accordions = document.querySelectorAll(".accordion");
//accordions.forEach((accordion) => closeAccordion(accordion));
if (body !== null) openAccordion(accordion, body);
}
}
accordion.appendChild(accordionTitle);
rootElement.appendChild(accordion);
}
/**
* Öffnet das Accordion
* @param {DOMElement} accordion
* @param {DOMElement} body
*/
function openAccordion(accordion, body) {
// accordion Inhalt
const accordionContent = document.createElement("div");
accordionContent.setAttribute("class", "accordion-content");
accordionContent.appendChild(body);
accordion.appendChild(accordionContent);
accordion.setAttribute("class", "accordion clickable open");
};
/**
* Schließt das Accordion
* @param {DOMElement} accordion
* @param {DOMElement} body
*/
function closeAccordion(accordion) {
accordion.setAttribute("class", "accordion clickable");
const content = accordion.querySelector(".accordion-content");
if (content)
content.remove();
};

110
frontend/js/account.js Normal file
View File

@@ -0,0 +1,110 @@
import { fetchData, postData, putData, deleteData } from "./api.js";
const urlParams = new URLSearchParams(window.location.search);
let accounts = [];
let editAccount = null;
function start() {
fetchData("accounts").then((data) => {
extractAccounts(data, "");
const selectElement = document.getElementById("account-select");
accounts.forEach(element => {
const option = document.createElement("option");
option.text = `${element.id} - ${element.name}`;
option.value = element.value;
selectElement.append(option);
});
// Prüft ob ein Paramter vorhanden ist, wenn ja dann wird das Konto geladen und die Daten zum bearbeiten befüllt
if (urlParams.get('account')) {
fetchData(`accounts/${urlParams.get('account')}`).then((data) => {
document.getElementById("number").value = data.number;
document.getElementById("name").value = data.name;
document.getElementById("description").value = data.description ? data.description : "";
document.getElementById("type").value = data.type;
document.getElementById("account-select").value = data.qualifiedName.replace(`:${data.name}`, "");
console.log(data.qualifiedName.replace(`:${data.name}`, ""))
editAccount = data;
document.getElementById("submit").value = "speichern";
document.getElementById("delete").style.display = "block";
});
}
});
}
/**
* Extrahiert die Kontodetails/namen und erstellt dazu ein neues Array
* @param {array} data
* @param {string} parentName
*/
function extractAccounts(data, parentName) {
data.forEach(element => {
let parentAccount = element.name;
if (parentName !== "")
parentAccount = `${parentName}:${parentAccount}`
accounts.push({ id: element.number, name: element.name, value: parentAccount });
if (element.subaccounts.length > 0) {
extractAccounts(element.subaccounts, parentAccount);
}
});
}
/**
* Eventhandler um den Vorgang abzubrechen und zurück zu Startseite zu navigieren
*/
document.getElementById("cancel").addEventListener("click", () => {
window.location.href = "../index.html";
});
/**
* Eventhandler um ein Konto zu löschen
*/
document.getElementById("delete").addEventListener("click", () => {
if (editAccount && editAccount.entries.length > 0) {
alert("Löschen nur möglich, wenn keine Buchungen stattgefunden haben und keine Unterkonten existieren");
return;
}
deleteData(`accounts/${urlParams.get('account')}`).then((response) => {
if (response)
window.location.href = "../index.html";
});
});
/**
* Eventhandler um ein neues Konto anzulegen bzw. das Konto zu bearbeiten
*/
document.getElementById("submit").addEventListener("click", () => {
if (editAccount && editAccount.entries.length > 0) {
alert("Accountbearbeitung nur möglich, wenn keine Buchungen stattgefunden haben");
return;
}
const number = document.getElementById("number").value;
const name = document.getElementById("name").value;
const description = document.getElementById("description").value;
const type = document.getElementById("type").value;
const parentAccount = document.getElementById("account-select").value;
const data = {
"number": Number.parseInt(number),
"name": name,
"description": description,
"type": type,
"parentAccount": parentAccount
}
if (editAccount) {
putData(`accounts/${urlParams.get('account')}`, data).then((response) => {
if (response)
window.location.href = "../index.html";
});
} else {
postData("accounts", data).then((response) => {
if (response)
window.location.href = "../index.html";
});
}
});
start();

127
frontend/js/api.js Normal file
View File

@@ -0,0 +1,127 @@
// API URL
// const apiUrl = 'http://149.222.51.77/da4c59b9-43c4-4144-ae39-742c6ba3ad50/api/v1/';
const apiUrl = '/api/v1/';
//SSE URL
export const sseUrl = 'http://149.222.51.77/da4c59b9-43c4-4144-ae39-742c6ba3ad50/live';
export function fetchData(url) {
const uri = apiUrl + url;
return get(uri);
}
export function postData(url, data) {
const uri = apiUrl + url;
return post(uri, data);
}
export function putData(url, data) {
const uri = apiUrl + url;
return put(uri, data);
}
// Make a GET request
async function get(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => { alert("Verbindungsprobleme"); controller.abort(); }, 10000);
const response = await fetch(url, { signal: controller.signal });
if (response.ok) {
clearTimeout(timeoutId);
return response.json();
} else {
clearTimeout(timeoutId);
if (response.status === 400)
alert("Ungültige Anfrage");
else if (response.status === 404)
alert("Transaktion existiert nicht");
else (response.status === 500)
alert("Unerwarteter Fehler");
}
}
// Make a POST request
async function post(url, data) {
const controller = new AbortController();
const timeoutId = setTimeout(() => { alert("Verbindungsprobleme"); controller.abort(); }, 10000);
const response = await fetch(url, {
signal: controller.signal,
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
method: "POST",
body: JSON.stringify(data)
});
if (response.ok) {
clearTimeout(timeoutId);
alert("Gespeichert!");
return true;
} else {
clearTimeout(timeoutId);
if (response.status === 400)
alert("Ungültige Anfrage");
else if (response.status === 422)
alert("Nicht ausführbare Anfrage");
else (response.status === 500)
alert("Unerwarteter Fehler");
return false;
}
}
// Make a PUT request
async function put(url, data) {
const controller = new AbortController();
const timeoutId = setTimeout(() => { alert("Verbindungsprobleme"); controller.abort(); }, 10000);
const response = await fetch(url, {
signal: controller.signal,
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
method: "PUT",
body: JSON.stringify(data)
});
if (response.ok) {
clearTimeout(timeoutId);
alert("Gespeichert!");
return true;
} else {
clearTimeout(timeoutId);
if (response.status === 400)
alert("Ungültige Anfrage");
else if (response.status === 404)
alert("Konto existiert nicht");
else if (response.status === 422)
alert("Nicht ausführbare Anfrage");
else (response.status === 500)
alert("Unerwarteter Fehler");
return false;
}
}
// Make a DELETE request
export async function deleteData(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => { alert("Verbindungsprobleme"); controller.abort(); }, 10000);
const response = await fetch(apiUrl + url, {
signal: controller.signal,
method: "DELETE"
});
if (response.ok) {
clearTimeout(timeoutId);
alert("Gelöscht!");
return true;
} else {
clearTimeout(timeoutId);
if (response.status === 400)
alert("Ungültige Anfrage");
else if (response.status === 404)
alert("Konto existiert nicht");
else if (response.status === 422)
alert("Nicht ausführbare Anfrage");
else (response.status === 500)
alert("Unerwarteter Fehler");
return false;
}
}

350
frontend/js/detail.js Normal file
View File

@@ -0,0 +1,350 @@
import { fetchData, deleteData, sseUrl } from "./api.js";
const urlParams = new URLSearchParams(window.location.search);
const euroFormat = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
let backendEntries = [];
let entries = [];
// Verbindung zum SSE-Endpunkt herstellen
const eventSource = new EventSource(sseUrl);
// Auf Update reagieren
eventSource.addEventListener('update', () => {
console.log("refreshing...")
start();
});
function start() {
fetchData(`accounts/${urlParams.get('account')}`).then((data) => {
const root = document.getElementById("detail-container");
root.innerHTML = "";
const title = document.createElement("div");
title.innerHTML = `${data.number} - ${data.name}`
title.setAttribute("class", "detail-title");
root.append(title);
createDescriptionTable(root, data);
if (data.description) {
const description = document.createElement("div");
description.innerHTML = data.description;
root.append(description);
}
backendEntries = [...data.entries];
entries = [...backendEntries];
if (entries.length > 0) {
document.getElementById("table-container").style.display = "flex";
//Standardmäßig soll absteigend nach Buchungsdatum sortiert werden
entries = entries.sort((a, b) => (a.postingDate < b.postingDate) ? 1 : ((b.postingDate < a.postingDate) ? -1 : 0));
//Standardmäßig sollen nur Buchungen des laufenden Kalenderjahres angezeigt werden
entries = entries.filter(element => new Date(element.postingDate) >= new Date(new Date().getFullYear(), 0, 1) && new Date(element.valueDate) >= new Date(new Date().getFullYear(), 0, 1));
document.getElementById("beginDate").value = `${new Date().getFullYear()}-01-01`;
createEntryTable(entries);
}
if (entries.length < 10)
document.getElementById("pagination-div").style.display = "none";
});
}
/**
* Erzeugt die Tabelle und errechnet die anzuzeigenden Einträge
* @param {array} data
*/
function createEntryTable(data) {
const rootElement = document.querySelector(".table tbody");
rootElement.innerHTML = null;
const bookingDate = document.getElementById("bookingDate");
bookingDate.onclick = () => sortTable("postingDate", bookingDate.innerHTML, bookingDate);
const valuta = document.getElementById("valuta");
valuta.onclick = () => sortTable("valueDate", valuta.innerHTML, valuta);
const pageInfo = document.getElementById("page-info");
const arrayEntriesStart = (pageInfo.innerHTML - 1) * getBookingsPerPage();
const arrayEntriesEnd = (pageInfo.innerHTML) * getBookingsPerPage();
data = data.slice(arrayEntriesStart, arrayEntriesEnd);
data.forEach(element => {
rootElement.append(createEntryTableRow(element));
});
}
/**
* Erzeugt die einzelnen Tabellenzeilen
* @param {object} entry
* @returns DOMElement
*/
function createEntryTableRow(entry) {
const tr = document.createElement("tr");
tr.setAttribute("class", "entry-table-tr");
const bookingDate = document.createElement("td");
bookingDate.setAttribute("class", "entry-table-td");
bookingDate.innerHTML = new Date(entry.postingDate).toLocaleDateString("de-DE");
tr.append(bookingDate);
const valuta = document.createElement("td");
valuta.setAttribute("class", "entry-table-td");
valuta.innerHTML = new Date(entry.valueDate).toLocaleDateString("de-DE");
tr.append(valuta);
const title = document.createElement("td");
title.setAttribute("class", "entry-table-td");
title.innerHTML = entry.title;
tr.append(title);
const offsetAccount = document.createElement("td");
offsetAccount.setAttribute("class", "entry-table-td");
offsetAccount.innerHTML = entry.offsetAccounts;
tr.append(offsetAccount);
const amount = document.createElement("td");
amount.setAttribute("class", "entry-table-td");
amount.innerHTML = euroFormat.format(entry.amount);
tr.append(amount);
console.log(entry)
const action = document.createElement("td");
action.setAttribute("class", "entry-table-td");
action.style.textAlign="center";
const editBtn = document.createElement("input");
editBtn.setAttribute("class", "action-button-edit");
editBtn.type = "button";
editBtn.value = "edit";
editBtn.onclick = () => window.location.href = `transaction.html?account=${urlParams.get('account')}&id=${entry.id}`;
action.append(editBtn);
const deleteBtn = document.createElement("input");
deleteBtn.setAttribute("class", "action-button");
deleteBtn.type = "button";
deleteBtn.value = "x";
deleteBtn.onclick = () => deleteTransaction(entry);
action.append(deleteBtn);
tr.append(action);
return tr;
}
/**
* triggert das löschen der Transaktion
* @param {object} transaction
*/
function deleteTransaction(transaction) {
deleteData(`transactions/${transaction.id}`).then(response => {
if(response)
window.location.reload();
})
}
/**
* navigiert zur Seite, zum anlegen neuer Transaktionen
*/
document.getElementById("add-transaction").addEventListener("click", () => {
window.location.href = `transaction.html?account=${urlParams.get('account')}`
});
/**
* Erstellt die Detailtabelle
* @param {DOMElement} rootElement
* @param {object} data
*/
function createDescriptionTable(rootElement, data) {
const table = document.createElement("table");
table.setAttribute("class", "description-table");
table.setAttribute("border", "1px");
if (data.description)
table.append(createDescriptionTableRow("Kontoart:", data.type));
table.append(createDescriptionTableRow("Bilanz (mit Unterkonten):", euroFormat.format(data.balance)));
table.append(createDescriptionTableRow("Bilanz (ohne Unterkonten):", euroFormat.format(data.localBalance)));
table.append(createDescriptionTableRow("Aktionen", `<a href="account.html?account=${urlParams.get('account')}" class="edit-link">EDIT</a>`));
rootElement.append(table);
}
/**
* Erzeugt die einzelnen Tabellenzeilen
* @param {string} title
* @param {string} value
* @returns
*/
function createDescriptionTableRow(title, value) {
const accounttype = document.createElement("tr");
accounttype.setAttribute("class", "description-table-tr");
const accounttypeHeader = document.createElement("th");
accounttypeHeader.setAttribute("class", "description-table-th");
accounttypeHeader.innerHTML = title;
const accounttypeValue = document.createElement("td");
accounttypeValue.setAttribute("class", "description-table-td");
accounttypeValue.innerHTML = value;
accounttype.append(accounttypeHeader);
accounttype.append(accounttypeValue);
return accounttype;
}
/**
* Eventhandler für das sortieren der Tabellen Datumsspalten
* Managed die Pfeile zur Sortierrichtung und sortiert das entries array
* @param {string} column
* @param {string} columnName
* @param {DOMElement} docElement
*/
function sortTable(column, columnName, docElement) {
document.querySelectorAll(".clickable").forEach(element => {
if (element.innerHTML.slice(-1) === "↓" || element.innerHTML.slice(-1) === "↑")
element.innerHTML = element.innerHTML.slice(0, -1);
});
if (columnName.slice(-1) === "↓") { // ASC
docElement.innerHTML = `${docElement.innerHTML.slice(0, columnName.length - 1)}`;
entries = entries.sort((a, b) => (a[column] > b[column]) ? 1 : ((b[column] > a[column]) ? -1 : 0));
} else if (columnName.slice(-1) === "↑") { // default
docElement.innerHTML = `${docElement.innerHTML.slice(0, columnName.length - 1)}`;
entries = backendEntries;
} else { // DESC
docElement.innerHTML = `${docElement.innerHTML}`;
entries = entries.sort((a, b) => (a[column] < b[column]) ? 1 : ((b[column] < a[column]) ? -1 : 0));
}
createEntryTable(entries);
}
// Fügt den Eventhandler für die Filter Eingabefelder hinzu
document.getElementsByName("dateFilter").forEach(element => element.addEventListener("change", filterAll));
document.getElementsByName("textFilter").forEach(element => element.addEventListener("keyup", filterAll));
// Eventhandler um die Datumsfelder zurückzusetzen
document.getElementById("resetDates").addEventListener("click", () => {
document.getElementById("beginDate").value = "";
document.getElementById("endDate").value = "";
filterAll();
});
/**
* Filtert die Daten aus dem Backend nach den Filterfeldern
*/
function filterAll() {
let filteredEntries = backendEntries;
filteredEntries = filterDates(filteredEntries);
filteredEntries = filterText(filteredEntries);
filteredEntries = filterAmount(filteredEntries);
createEntryTable(filteredEntries);
}
/**
* Filtert die Daten anhand der Datumsfelder
* @param {array} array
* @returns array
*/
function filterDates(array) {
const begin = document.getElementById("beginDate").value;
const end = document.getElementById("endDate").value;
if (begin !== "" && end !== "") {
return array.filter(element => (new Date(element.postingDate) >= new Date(begin) && new Date(element.valueDate) >= new Date(begin)) && (new Date(element.postingDate) <= new Date(end) && new Date(element.valueDate) <= new Date(end)));
} else if (begin !== "") {
return array.filter(element => new Date(element.postingDate) >= new Date(begin) && new Date(element.valueDate) >= new Date(begin));
} else if (end !== "") {
return array.filter(element => new Date(element.postingDate) <= new Date(end) && new Date(element.valueDate) <= new Date(end));
}
return array;
}
/**
* Filtert die Daten anhand der des Freitextfeldes
* @param {array} array
* @returns array
*/
function filterText(array) {
const searchString = document.getElementById("searchText").value;
if (searchString !== "") {
return array.filter(element => element.title.toLowerCase().includes(searchString.toLowerCase()));
}
return array;
}
/**
* Filtert die Daten anhand der des min. und max. Betrags
* @param {array} array
* @returns array
*/
function filterAmount(array) {
const begin = document.getElementById("beginAmount").value;
const end = document.getElementById("endAmount").value;
if (begin !== "" && end !== "") {
return array.filter(element => (element.amount >= begin) && (element.amount <= end));
} else if (begin !== "") {
return array.filter(element => element.amount >= begin);
} else if (end !== "") {
return array.filter(element => element.amount <= end);
}
return array;
}
/**
* Eventhandler für Pagination "Seite Vorwärts Button"
*/
document.getElementById("forward-page").addEventListener("click", () => {
const pageInfo = document.getElementById("page-info");
let pageNumber = ++pageInfo.innerHTML;
const maxPage = Math.ceil(entries.length / getBookingsPerPage());
if (pageNumber > maxPage)
pageNumber = 1;
pageInfo.innerHTML = pageNumber;
createEntryTable(entries);
});
/**
* Eventhandler für Pagination "Seite Rückwärts Button"
*/
document.getElementById("backward-page").addEventListener("click", () => {
const pageInfo = document.getElementById("page-info");
let pageNumber = --pageInfo.innerHTML;
const maxPage = Math.ceil(entries.length / getBookingsPerPage());
if (pageNumber < 1)
pageNumber = maxPage;
pageInfo.innerHTML = pageNumber;
createEntryTable(entries);
});
/**
* Eventhandler für Selectbox Änderung
*/
document.getElementById("table-range").addEventListener("change", () => {
document.getElementById("page-info").innerHTML = 1; //reset page
createEntryTable(entries);
});
/**
* Liefert die Anzahl der Einträge aus der Selectbox zurück bzw. die Array länge, bei Auswahl von "Alle"
* @returns integer
*/
function getBookingsPerPage() {
if (document.getElementById("table-range").value === "Alle")
return entries.length;
return Number.parseInt(document.getElementById("table-range").value);
}
start();

214
frontend/js/overview.js Normal file
View File

@@ -0,0 +1,214 @@
import { fetchData, sseUrl } from "./api.js";
let backendEntries = [];
let entries = [];
// Verbindung zum SSE-Endpunkt herstellen
const eventSource = new EventSource(sseUrl);
// Auf Update reagieren
eventSource.addEventListener('update', () => {
console.log("refreshing...")
start();
});
function start() {
fetchData(`transactions`).then((data) => {
backendEntries = [...data];
entries = [...backendEntries];
if (entries.length > 0) {
document.getElementById("table-container").style.display = "flex";
createTableBody(entries);
}
if (entries.length < 10)
document.getElementById("pagination-div").style.display = "none";
});
}
/**
* Generiert den Tabelleninhalt
* Anhand der angaben aus Seitenzahl und Zeilenangaben werden entsprechend viele Zeilen erstellt
* @param {array} data
*/
function createTableBody(data) {
const rootElement = document.querySelector(".table tbody");
rootElement.innerHTML = null;
const bookingDate = document.getElementById("bookingDate");
bookingDate.onclick = () => sortTable("postingDate", bookingDate.innerHTML, bookingDate);
const valuta = document.getElementById("valuta");
valuta.onclick = () => sortTable("valueDate", valuta.innerHTML, valuta);
const pageInfo = document.getElementById("page-info");
const arrayEntriesStart = (pageInfo.innerHTML-1) * getBookingsPerPage();
const arrayEntriesEnd = (pageInfo.innerHTML) * getBookingsPerPage();
data = data.slice(arrayEntriesStart, arrayEntriesEnd);
data.forEach(element => {
rootElement.append(createEntryTableRow(element));
});
}
/**
* Erzeugt die einzelne Tabellenzeile
* @param {object} entry
* @returns DOMElement
*/
function createEntryTableRow(entry) {
const tr = document.createElement("tr");
tr.setAttribute("class", "entry-table-tr");
const bookingDate = document.createElement("td");
bookingDate.setAttribute("class", "entry-table-td");
bookingDate.innerHTML = new Date(entry.postingDate).toLocaleDateString("de-DE");
tr.append(bookingDate);
const valuta = document.createElement("td");
valuta.setAttribute("class", "entry-table-td");
valuta.innerHTML = new Date(entry.valueDate).toLocaleDateString("de-DE");
tr.append(valuta);
const title = document.createElement("td");
title.setAttribute("class", "entry-table-td");
title.innerHTML = entry.title;
tr.append(title);
return tr;
}
/**
* Eventhandler für das sortieren der Tabellen Datumsspalten
* Managed die Pfeile zur Sortierrichtung und sortiert das entries array
* @param {string} column
* @param {string} columnName
* @param {DOMElement} docElement
*/
function sortTable(column, columnName, docElement) {
document.querySelectorAll(".clickable").forEach(element => {
if(element.innerHTML.slice(-1) === "↓" || element.innerHTML.slice(-1) === "↑")
element.innerHTML = element.innerHTML.slice(0, -1);
});
if (columnName.slice(-1) === "↓") { // ASC
docElement.innerHTML = `${columnName.slice(0, columnName.length - 1)}`;
entries = entries.sort((a, b) => (a[column] > b[column]) ? 1 : ((b[column] > a[column]) ? -1 : 0));
} else if (columnName.slice(-1) === "↑") { // default
docElement.innerHTML = `${columnName.slice(0, columnName.length - 1)}`;
entries = backendEntries;
} else { // DESC
docElement.innerHTML = `${columnName}`;
entries = entries.sort((a, b) => (a[column] < b[column]) ? 1 : ((b[column] < a[column]) ? -1 : 0));
}
createTableBody(entries);
}
// Fügt den Eventhandler für die Filter Eingabefelder hinzu
document.getElementsByName("dateFilter").forEach(element => element.addEventListener("change", filterAll));
document.getElementsByName("textFilter").forEach(element => element.addEventListener("keyup", filterAll));
// Eventhandler um die Datumsfelder zurückzusetzen
document.getElementById("resetDates").addEventListener("click", () => {
document.getElementById("beginDate").value = "";
document.getElementById("endDate").value = "";
filterAll();
});
/**
* Filtert die Daten aus dem Backend nach den Filterfeldern
*/
function filterAll() {
let filteredEntries = backendEntries;
filteredEntries = filterDates(filteredEntries);
filteredEntries = filterText(filteredEntries);
createTableBody(filteredEntries);
}
/**
* Filtert die Daten anhand der Datumsfelder
* @param {array} array
* @returns array
*/
function filterDates(array) {
const begin = document.getElementById("beginDate").value;
const end = document.getElementById("endDate").value;
if (begin !== "" && end !== "") {
return array.filter(element => (new Date(element.postingDate) >= new Date(begin) && new Date(element.valueDate) >= new Date(begin)) && (new Date(element.postingDate) <= new Date(end) && new Date(element.valueDate) <= new Date(end)));
} else if (begin !== "") {
return array.filter(element => new Date(element.postingDate) >= new Date(begin) && new Date(element.valueDate) >= new Date(begin));
} else if (end !== "") {
return array.filter(element => new Date(element.postingDate) <= new Date(end) && new Date(element.valueDate) <= new Date(end));
}
return array;
}
/**
* Filtert die Daten anhand der des Freitextfeldes
* @param {array} array
* @returns array
*/
function filterText(array) {
const searchString = document.getElementById("searchText").value;
if (searchString !== "") {
return array.filter(element => element.title.toLowerCase().includes(searchString.toLowerCase()));
}
return array;
}
/**
* Eventhandler für Pagination "Seite Vorwärts Button"
*/
document.getElementById("forward-page").addEventListener("click", () => {
const pageInfo = document.getElementById("page-info");
let pageNumber = ++pageInfo.innerHTML;
const maxPage = Math.ceil(entries.length / getBookingsPerPage());
if(pageNumber > maxPage)
pageNumber = 1;
pageInfo.innerHTML = pageNumber;
createTableBody(entries);
});
/**
* Eventhandler für Pagination "Seite Rückwärts Button"
*/
document.getElementById("backward-page").addEventListener("click", () => {
const pageInfo = document.getElementById("page-info");
let pageNumber = --pageInfo.innerHTML;
const maxPage = Math.ceil(entries.length / getBookingsPerPage());
if(pageNumber < 1)
pageNumber = maxPage;
pageInfo.innerHTML = pageNumber;
createTableBody(entries);
});
/**
* Eventhandler für Selectbox Änderung
*/
document.getElementById("table-range").addEventListener("change", () => {
document.getElementById("page-info").innerHTML = 1; //reset page
createTableBody(entries);
});
/**
* Liefert die Anzahl der Einträge aus der Selectbox zurück bzw. die Array länge, bei Auswahl von "Alle"
* @returns integer
*/
function getBookingsPerPage() {
if(document.getElementById("table-range").value === "Alle")
return entries.length;
return Number.parseInt(document.getElementById("table-range").value);
}
start();

178
frontend/js/transaction.js Normal file
View File

@@ -0,0 +1,178 @@
import { fetchData, postData, putData } from "./api.js";
const urlParams = new URLSearchParams(window.location.search);
let accounts = [];
let account = null;
let transaction = null;
function start() {
fetchData("accounts").then((data) => {
extractAccounts(data, "");
if (urlParams.get('account')) {
fetchData(`accounts/${urlParams.get('account')}`).then((data) => {
account = data;
if (urlParams.get('id')) {
fetchData(`transactions/${urlParams.get('id')}`).then((transactionData) => {
transaction = transactionData;
console.log(transaction)
document.getElementById("submit").value = "bearbeiten";
document.getElementById("postingDate").value = transaction.postingDate;
document.getElementById("valueDate").value = transaction.valueDate;
document.getElementById("title").value = transaction.title;
transaction.entries.forEach(entry => {
addTableRow(entry);
});
});
}
});
}
});
}
/**
* extract account hierarchy and create arraylist of all accounts
* @param {array} data
* @param {string} parentName
*/
function extractAccounts(data, parentName) {
data.forEach(element => {
let parentAccount = element.name;
if (parentName !== "")
parentAccount = `${parentName}:${parentAccount}`
accounts.push({ id: element.number, name: element.name, value: parentAccount });
if (element.subaccounts.length > 0) {
extractAccounts(element.subaccounts, parentAccount);
}
});
}
/**
* Eventhandler um Tabellenzeilen mit leeren Textfeldern hinzuzufügen
*/
document.getElementById("add-transaction").addEventListener("click", () => {
addTableRow();
});
/**
* Fügt eine neue Tabellenzeile mit leeren Textfeldern hinzu
* @param {array} data
*/
function addTableRow(data) {
const table = document.querySelector(".transaction-table tbody");
const tr = document.createElement("tr");
tr.className = "transaction-tr";
const accountTd = document.createElement("td");
accountTd.className = "transaction-td";
const accountSelect = createAccountSelect()
accountTd.appendChild(accountSelect);
const amountTd = document.createElement("td");
amountTd.className = "transaction-td";
const amountInput = document.createElement("input");
amountInput.className = "transaction-input";
amountInput.type = "number";
amountInput.name = "amount";
amountTd.appendChild(amountInput);
const bookingTextTd = document.createElement("td");
bookingTextTd.className = "transaction-td";
const bookingInput = document.createElement("input");
bookingInput.className = "transaction-input";
bookingInput.name = "text";
bookingTextTd.appendChild(bookingInput);
const actionTd = document.createElement("td");
actionTd.className = "transaction-td";
const actionButton = document.createElement("input");
actionButton.className = "action-button";
actionButton.type = "button";
actionButton.value = "x";
actionButton.onclick = () => tr.remove();
actionTd.appendChild(actionButton);
if(data) {
accountSelect.value = data.account;
amountInput.value = data.amount;
bookingInput.value = data.label;
}
tr.appendChild(accountTd);
tr.appendChild(amountTd);
tr.appendChild(bookingTextTd);
tr.appendChild(actionTd);
table.append(tr);
}
/**
* Erstellt die Selectbox für Kontenauswahl
* @returns DOMElement
*/
function createAccountSelect() {
const selectElement = document.createElement("select");
selectElement.className = "transaction-input";
selectElement.name = "account";
const option = document.createElement("option");
option.text = "--- Konto wählen ---";
option.disabled = true;
option.selected = true;
selectElement.append(option);
accounts.forEach(element => {
const option = document.createElement("option");
option.text = `${element.id} - ${element.value}`;
option.value = element.value;
selectElement.append(option);
});
return selectElement;
}
/**
* Eventhandler für das abbrechen und zurücknavigieren zur Homepage
*/
document.getElementById("cancel").addEventListener("click", () => {
window.location.href = "../index.html";
});
/**
* Eventhandeler zum senden der Transaktionsdaten
* Liest die Werte aus den Eingabefenstern, erzeugt das Objekt und sendet sie zur API
*/
document.getElementById("submit").addEventListener("click", () => {
const postingDate = document.getElementById("postingDate").value;
const valueDate = document.getElementById("valueDate").value;
const title = document.getElementById("title").value;
const data = {
"postingDate": postingDate,
"valueDate": valueDate,
"title": title,
"entries": []
}
document.querySelectorAll(".transaction-table tbody tr").forEach(tr => {
const account = tr.querySelector("[name=account]").value;
const amount = Number.parseFloat(tr.querySelector("[name=amount]").value);
const text = tr.querySelector("[name=text]").value;
data.entries.push({ account: account, amount: amount, label: text });
});
if(transaction) {
putData(`transactions/${urlParams.get('id')}`, data).then((response) => {
if (response)
window.location.href = `./detail.html?account=${urlParams.get('account')}`;
});
} else {
postData("transactions", data).then((response) => {
if (response)
window.location.href = `./detail.html?account=${urlParams.get('account')}`;
});
}
});
start();

View File

@@ -0,0 +1,130 @@
#accordion-container {
width: 95%;
display: flex;
align-items: center;
flex-direction: column;
margin-top: 20px;
}
.accordion {
width: 100%;
margin-top: 10px;
}
.clickable {
cursor: pointer;
}
.open > .accordion-title {
background-color: #00305d;
color: #fff;
border-radius: 4px 4px 0px 0px;
/* diable text selection */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.accordion-title {
width: 100%;
display: flex;
flex-direction: row;
background-color: #00305d;
color: #fff;
border-radius: 4px 4px 4px 4px;
/* diable text selection */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.title-number {
padding: 10px;
width: 20%;
}
.title-name {
padding: 10px;
width: 20%;
}
.title-link {
color: hotpink;
}
.title-description {
padding: 10px;
width: 35%;
}
.title-balance {
padding: 10px;
width: 20%;
text-align: right;
}
.title-indicator {
padding: 10px;
width: 5%;
padding-right: 20px;
text-align: right;
}
.positive-number {
color: rgb(64, 207, 64);
}
.negative-number {
color: rgb(255, 51, 10);
}
.neutral-number {
color: #fff;
}
.accordion-content {
background-color: #b6b6b6;
color: #000;
padding: 5px 10px 10px 10px;
border-radius: 0px 0px 4px 4px;
cursor: auto;
}
@media all and (max-width: 1000px) {
.accordion-title {
text-align: center;
background-color: #00305d;
color: #fff;
display: block;
}
.title-number {
font-weight: 600;
}
.title-number,
.title-name,
.title-link,
.title-description,
.title-balance {
padding: 0px;
padding-top: 5px;
padding-bottom: 5px;
width: 100%;
text-align: center;
}
.title-indicator {
padding: 0px;
padding-top: 5px;
padding-bottom: 5px;
width: 100%;
text-align: center;
}
}

View File

@@ -0,0 +1,32 @@
#creation-container {
width: 80%;
margin-top: 30px;
flex-direction: column;
}
.save-buttons {
width: 75%;
margin-top: 30px;
}
.save-button {
padding: 10px;
width: 40%;
margin-left: 20px;
margin-right: 20px;
border-radius: 5px;
}
.form-data {
width: 100%;
flex-direction: row;
margin-top: 10px;
}
.form-data-label {
width: 30%;
}
.form-data-element {
width: 60%;
}

186
frontend/style/detail.css Normal file
View File

@@ -0,0 +1,186 @@
#detail-container {
margin-top: 30px;
width: 80%;
display: flex;
flex-direction: column;
align-items: center;
}
.detail-title {
font-size: larger;
font-weight: 600;
}
.detail-description {
}
.description-table {
margin-top: 20px;
}
.description-table,
.description-table-th,
.description-table-td {
border: 1px solid black;
border-collapse: collapse;
text-align: left;
}
.description-table-th {
width: 35%;
padding: 5px 20px;
}
.description-table-td {
width: 65%;
padding: 5px 20px;
}
.description-table-td:has(> .edit-link) {
width: 65%;
padding: 10px 20px;
}
.edit-link {
background-color: rgb(226, 226, 226);
border: 1px solid rgb(176, 176, 176);
border-radius: 2.5px;
padding: 5px;
height: 20px;
text-decoration: none;
color: #000;
}
.edit-link:hover,
.edit-link:active {
background-color: rgb(204, 204, 204);
border: 1px solid rgb(158, 158, 158);
color: #000;
text-decoration: none;
}
#table-container {
width: 90%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 20px;
margin-top: 25px;
}
.entry-table {
width: 100%;
margin-top: 20px;
}
.entry-table,
.entry-table-th,
.entry-table-td {
border: 1px solid black;
border-collapse: collapse;
text-align: left;
}
.entry-table-th {
padding: 5px 20px;
}
.entry-table-td {
padding: 5px 20px;
}
.filter {
width: 50%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.filter-text {
margin-left: 10px;
margin-right: 10px;
}
.filter-div {
margin-top: 7.5px;
text-align: center;
}
.search-text {
width: 80%;
}
.amount-text {
width: 45%;
margin-left: 10px;
margin-right: 10px;
}
.table {
width: 100%;
}
.pagination {
width: 100%;
margin-top: 10px;
text-align: right;
display: flex;
justify-content: right;
align-items: center;
}
.pagination-part {
height: 50px;
width: 33.33%;
}
.page-button {
margin-left: 10px;
margin-right: 10px;
}
.dropdown {
border: 1px solid black;
margin-left: 10px;
}
.clickable {
cursor: pointer;
/* diable text selection */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
th > .action-button {
font-size: larger;
background-color: rgb(0, 84, 6);
color: #fff;
border: 1px solid #fff;
border-radius: 5px;
height: 35px;
width: 50px;
}
td > .action-button {
background-color: rgb(151, 0, 0);
color: #fff;
border: 1px solid #fff;
border-radius: 5px;
height: 35px;
width: 50px;
}
td > .action-button-edit {
background-color: rgb(228, 228, 228);
color: #000;
border: 1px solid #8c8c8c;
border-radius: 5px;
height: 35px;
width: 50px;
}

130
frontend/style/index.css Normal file
View File

@@ -0,0 +1,130 @@
body {
font-family: Verdana, Geneva, Tahoma, sans-serif;
margin: 0px;
}
#root {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.content-wrap {
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
margin-bottom: 25px;
}
.link-container {
margin-top: 20px;
cursor: pointer;
}
.link-container > a {
background-color: #c2c2c2;
padding: 7.5px 10px 7.5px 10px;
border: 0.5px solid black;
border-radius: 5px;
color: #000;
text-decoration: none;
}
.link-container > a:hover,
.link-container > a:active {
background-color: #969696;
color: #000;
text-decoration: none;
}
.header {
width: 100%;
padding: 15px 0px 15px 0px;
margin-bottom: 10px;
border-bottom: 2px solid rgb(254, 93, 0);
background-color: #00305d;
color: #fff;
}
.title {
font-size: x-large;
font-weight: 500;
text-align: left;
cursor: pointer;
margin-left: 25px;
}
.nav {
margin-left: 5%;
cursor: pointer;
/* diable text selection */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.nav:hover,
nav:active {
color: rgb(170, 170, 170);
}
.footer {
width: 100%;
font-size: x-small;
text-align: center;
padding-top: 10px;
padding-bottom: 10px;
margin-top: auto;
border-top: 2px solid rgb(254, 93, 0);
background-color: #00305d;
color: #fff;
}
.center {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
input {
padding: 5px 12.5px;
border: 1px solid rgb(182, 182, 182);
border-radius: 2.5px;
color: #000;
font-size: 14px;
height: 25px;
-ms-box-sizing: content-box;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
select {
padding: 5px 12.5px;
border: 1px solid rgb(182, 182, 182);
border-radius: 2.5px;
color: #000;
font-size: 14px;
height: 25px;
-ms-box-sizing: content-box;
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box;
box-sizing: content-box;
}
input[type="button"] {
cursor: pointer;
padding: 5px;
}
input[type="button"]:hover, input[type="button"]:active {
background-color: rgb(164, 164, 164);
border-color: rgb(81, 81, 81);
}

View File

@@ -0,0 +1,83 @@
#creation-container {
width: 80%;
margin-top: 30px;
flex-direction: column;
}
.save-buttons {
width: 75%;
margin-top: 30px;
}
.save-button {
padding: 10px;
width: 40%;
margin-left: 20px;
margin-right: 20px;
border-radius: 5px;
}
.form-data {
width: 100%;
flex-direction: row;
margin-top: 10px;
}
.form-data-label {
width: 30%;
}
.form-data-element {
width: 70%;
}
.transaction-table {
width: 100%;
margin-top: 20px;
}
.transaction-table,
.transaction-th,
.transaction-td {
border: 1px solid rgb(105, 105, 105);
border-collapse: collapse;
text-align: center;
}
.transaction-th {
padding: 5px 20px;
}
.transaction-td {
padding: 10px;
}
.transaction-input {
width: 100%;
display: block;
width: 100%;
height: 35px;
-ms-box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
th > .action-button {
font-size: larger;
background-color: rgb(0, 84, 6);
color: #fff;
border: 1px solid #fff;
border-radius: 5px;
height: 35px;
width: 50px;
}
td > .action-button {
background-color: rgb(151, 0, 0);
color: #fff;
border: 1px solid #fff;
border-radius: 5px;
height: 35px;
width: 50px;
}