1
0
Fork 0
mirror of https://github.com/dancojocaru2000/foxbank.git synced 2025-06-19 11:02:28 +03:00

Compare commits

...

12 commits

Author SHA1 Message Date
DariusTFox24
1c56bf2a60
Merge pull request #11 from dancojocaru2000/Frontend
Finished frontend implementation with api
2022-01-04 01:25:25 +02:00
DariusTFox24
57d9ff8ed0 Finished frontend implementation with api 2022-01-04 01:22:27 +02:00
649e2c729f
Merge pull request #10 from dancojocaru2000/Backend
Currency exchange in Backend
2022-01-03 21:57:28 +02:00
DariusTFox24
ab53e05287
Merge pull request #9 from dancojocaru2000/Frontend
Frontend updates
2022-01-03 14:28:47 +02:00
DariusTFox24
e8477db7b8 Implemented api stuff 2022-01-03 14:25:32 +02:00
1250b12049
Merge pull request #8 from dancojocaru2000/Backend
Hotfix: notification for received transfer has wrong id
2022-01-03 14:25:32 +02:00
DariusTFox24
5e19ab457b
Merge pull request #7 from dancojocaru2000/Backend
Backend updates
2022-01-03 09:53:40 +02:00
c429506bdf
Merge pull request #6 from dancojocaru2000/Frontend
Frontend Accounts functionality
2021-12-30 01:50:39 +02:00
a0a3fe774d
Merge pull request #5 from dancojocaru2000/Backend
Accounts functionality
2021-12-30 01:49:36 +02:00
DariusTFox24
e369315034 Updated state management and implemented new account functionality 2021-12-30 01:45:14 +02:00
DariusTFox
b76cbe77c7 Partial implementation of select instead of text for currency type 2021-12-08 16:21:06 +02:00
DariusTFox
0e27defbdc Partial API frontend implementation 2021-12-08 13:54:31 +02:00
14 changed files with 2867 additions and 150 deletions

15
client/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-msedge",
"request": "launch",
"name": "Launch Edge against localhost",
"url": "http://localhost:5000",
"webRoot": "${workspaceFolder}/public"
}
]
}

2257
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0",
"svelte-keydown": "^0.4.0",
"svelte-preprocess": "^4.9.8"
},
"dependencies": {

View file

@ -5,20 +5,24 @@
import CardBG from "./CardBG.svelte";
import DetailField from './DetailField.svelte';
import GreenButton from './GreenButton.svelte';
import {createEventDispatcher} from 'svelte';
import {createEventDispatcher, onMount, getContext} from 'svelte';
import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { gettransactions } from './api';
import { amountToString } from './utils';
const dispatch = createEventDispatcher();
const token = getContext("token");
export let type="RON Account";
export let name="RON Account";
export let currency="RON";
export let balance="5425";
export let iban="RONFOX62188921";
export let isExpanded=false;
export let transactions=[];
export let accountId;
let copied = false;
@ -42,20 +46,22 @@
dispatch("createPopup",{
type: 'send_money',
account: {
type,
id: accountId,
type: name,
currency,
balance,
iban,
}
});
}
</script>
<CardBG class="flex-shrink flex flex-col items-stretch md:self-start mt-16 mb-6 px-6 pt-6 pb-0 max-h-full overflow-clip min-h-0">
<div class="flex flex-col flex-shrink">
<div class='font-sans mt-2 mx-2 border-b-1'>
<h3 class="text-gray-50 inline mr-4">{type}</h3>
<h3 class="text-gray-50 inline mr-4">{name}</h3>
<span class="text-gray-100">IBAN: {iban}</span>
<button on:click={copyIban} class="inline {copied ? "cursor-default" : ""}"> <Icon icon={copied ? "akar-icons:check" : "akar-icons:copy"} color="rgba(249, 250, 251, 1)"/></button>
</div>
@ -68,7 +74,7 @@
<div>
<DetailField class="p-1 flex-shrink">
<h2 class='font-sans mt-3 mb-2 pl-2 text-4xl text-gray-50'>Balance: <span style="color: #264D59">{balance}</span>{currency}</h2>
<h2 class='font-sans mt-3 mb-2 pl-2 text-4xl text-gray-50'>Balance: <span style="color: #264D59">{amountToString(balance)}</span>{currency}</h2>
</DetailField>
</div>
@ -80,18 +86,27 @@
<DetailField class="my-3 py-1 flex-shrink min-w-transaction">
<div class='font-sans text-gray-50 mt-2 mx-4 border-b-1'>
<h3 class="inline mr-3">{transaction.title}: </h3>
<span class="text-4xl {transaction.type == "send" ? "text-red-c" : "text-lime-c"}">{transaction.amount}</span>
<span class="text-4xl">{currency}</span>
{#if transaction.transactionType == "send_transfer"}
<h3 class="inline mr-3">Sent money: </h3>
{:else if transaction.transactionType == "receive_transfer"}
<h3 class="inline mr-3">Received money: </h3>
{:else if transaction.transactionType == "card_payment"}
<h3 class="inline mr-3">{transaction.otherParty.store}: </h3>
{:else if transaction.transactionType == "fee"}
<h3 class="inline mr-3">Fees: </h3>
{/if}
<span class="text-4xl {transaction.transactionType == "receive_transfer" ? "text-lime-c" : "text-red-c"}">{amountToString(transaction.extra.amount)}</span>
<span class="text-4xl">{transaction.extra.currency}</span>
</div>
<div class='font-sans text-2xl text-gray-100 mt-2 mx-6 border-b-1'>
<p class="inline">at {transaction.time} </p>
<p class="inline">at {new Date(transaction.datetime).toLocaleString()} </p>
{#if transaction.status == "PROCESSED"}
{#if transaction.status == "processed"}
<span>
<Icon class="inline mb-1" icon="akar-icons:circle-check" color="#6DE25ACC"/>
</span>
{:else if transaction.status == "PENDING"}
{:else if transaction.status == "pending"}
<span>
<Icon class="inline mb-1" icon="akar-icons:arrow-cycle" color="#F6AF43"/>
</span>
@ -114,18 +129,27 @@
{:else if transactions.length > 0}
<DetailField class="my-3 py-2 flex-shrink min-w-transaction">
<div class='font-sans text-gray-50 mt-2 mx-4 border-b-1'>
<h3 class="inline mr-3">{transactions[0].title}: </h3>
<span class="text-4xl {transactions[0].type == "send" ? "text-red-c" : "text-lime-c"}">{transactions[0].amount}</span>
<span class="text-4xl">{currency}</span>
{#if transactions[0].transactionType == "send_transfer"}
<h3 class="inline mr-3">Sent money: </h3>
{:else if transactions[0].transactionType == "receive_transfer"}
<h3 class="inline mr-3">Received money: </h3>
{:else if transactions[0].transactionType == "card_payment"}
<h3 class="inline mr-3">{transactions[0].otherParty.store}: </h3>
{:else if transactions[0].transactionType == "fee"}
<h3 class="inline mr-3">Fees: </h3>
{/if}
<span class="text-4xl {transactions[0].transactionType == "receive_transfer" ? "text-lime-c" : "text-red-c"}">{amountToString(transactions[0].extra.amount)}</span>
<span class="text-4xl">{transactions[0].extra.currency}</span>
</div>
<div class='font-sans text-2xl text-gray-100 mt-2 mx-6 border-b-1'>
<p class="inline">at {transactions[0].time} </p>
<p class="inline">at {new Date(transactions[0].datetime).toLocaleString()} </p>
{#if transactions[0].status == "PROCESSED"}
{#if transactions[0].status == "processed"}
<span>
<Icon class="inline mb-1" icon="akar-icons:circle-check" color="#6DE25ACC"/>
</span>
{:else if transactions[0].status == "PENDING"}
{:else if transactions[0].status == "pending"}
<span>
<Icon class="inline mb-1" icon="akar-icons:arrow-cycle" color="#F6AF43"/>
</span>

View file

@ -1,6 +1,7 @@
<script>
import { onMount } from "svelte";
import { whoami } from "./api";
import { onMount, setContext } from "svelte";
import { writable, readable } from "svelte/store";
import { whoami, createnotification, getaccountlist, getnotificationlist } from "./api";
import BottomBorder from "./BottomBorder.svelte";
import CheckNotifications from "./CheckNotifications.svelte";
@ -12,16 +13,115 @@ import { whoami } from "./api";
import SendMoney from "./SendMoney.svelte";
import TopBorder from "./TopBorder.svelte";
let loggedin = false;
function toggleLoggedIn() {
loggedin = !loggedin;
}
const userToken = writable(sessionStorage.getItem("token"));
userToken.subscribe(newToken => {
sessionStorage.setItem("token", newToken);
})
setContext("token", userToken);
const user = readable(null, set => {
const unsubscribe = userToken.subscribe(token => {
if(token == null) {
set(null);
}else{
whoami(token)
.then(result =>{
if(result.status != "success") {
$userToken = null;
}else {
set(result);
}
})
}
})
return () => {
unsubscribe();
}
})
setContext("user", user);
const refreshAccounts = writable(null);
setContext("refreshAccounts", refreshAccounts);
const accounts = readable(null, set => {
function getAccounts(token){
if(token==null){
set(null);
}else{
getaccountlist(token)
.then(result => {
set(result);
})
}
}
let token = null;
refreshAccounts.set( () => {
getAccounts(token);
});
const unsubscribe = userToken.subscribe(newToken => {
token = newToken;
getAccounts(token);
})
const intervalId = setInterval(() => {
getAccounts(token);
}, 10000);
return () => {
unsubscribe();
clearInterval(intervalId);
}
})
setContext("accounts", accounts);
const refreshNotifications = writable(null);
setContext("refreshNotifications", refreshNotifications);
const notifications = readable(null, set=> {
function getNotifications(token){
if(token==null){
set(null);
}else{
getnotificationlist(token)
.then(result => {
set(result);
})
}
}
let token = null;
refreshNotifications.set( () => {
getNotifications(token);
});
const unsubscribe = userToken.subscribe(newToken => {
token = newToken;
getNotifications(token);
})
const intervalId = setInterval(() => {
getNotifications(token);
}, 10000);
return () => {
unsubscribe();
clearInterval(intervalId);
}
})
setContext("notifications", notifications);
let isCreatingAccount = false;
let isCheckingNotifications = false;
let isSendingMoney = false;
let sendingAccount = "";
let notifications = [];
function onCreatePopup(event) {
const eventType = event.detail.type;
@ -34,12 +134,18 @@ import { whoami } from "./api";
var today = new Date();
var date = today.getDate()+'/'+(today.getMonth()+1)+'/'+today.getFullYear();
var time = today.getHours() + ":" + today.getMinutes();
var body = "The "+ eventType.currency + " account with the name " + eventType.type + " was created succesfully!";
//add notification about created account
createnotification(async function() {
const result = await createnotification($userToken, body, date+time);
if(result.status == "success") {
console.log("Succesfully created notification.");
}else{
console.log("Failed to create notification.");
}
})
notifications.push(
{
text: "The new account '" + event.detail.type+ "' was created successfully!",
time: time + " " + date,
});
isCreatingAccount = false;
break;
@ -48,7 +154,7 @@ import { whoami } from "./api";
break;
case "create_acc_failed":
isCreatingAccount = false;
// isCreatingAccount = false;
alert(`Account creation failed! Reason: ${event.detail.reason}`);
break;
@ -66,7 +172,6 @@ import { whoami } from "./api";
break;
case "check_notifications":
notifications = event.detail.notifications;
isCheckingNotifications = true;
break;
@ -79,26 +184,13 @@ import { whoami } from "./api";
}
}
onMount(async function() {
const token = sessionStorage.getItem("token");
if(token == null){
loggedin = false;
}else {
const result = await whoami(token);
if (result.status == "success") {
loggedin = true;
}else {
loggedin = false;
}
}
})
</script>
<main class="flex flex-col items-stretch bg-banner bg-cover bg-center bg-fixed h-screen font-sans">
<TopBorder class="flex-shrink"></TopBorder>
<div class="flex-grow max-h-full overflow-hidden">
{#if loggedin}
{#if $user}
{#if isCreatingAccount}
<Overlay>
@ -109,7 +201,7 @@ import { whoami } from "./api";
{:else if isCheckingNotifications}
<Overlay>
<div class="flex items-center justify-center h-full">
<CheckNotifications on:createPopup={onCreatePopup} notifications={notifications}></CheckNotifications>
<CheckNotifications on:createPopup={onCreatePopup}></CheckNotifications>
</div>
</Overlay>
{:else if isSendingMoney}
@ -120,10 +212,10 @@ import { whoami } from "./api";
</Overlay>
{/if}
<MainPage on:createPopup={onCreatePopup} on:logOut={toggleLoggedIn}></MainPage>
<MainPage on:createPopup={onCreatePopup} on:logOut={()=>$userToken=null}></MainPage>
{:else}
<Login on:loginSuccess={toggleLoggedIn}></Login>
<Login on:loginSuccess={(e)=> $userToken = e.detail.token}></Login>
{/if}

View file

@ -1,21 +1,32 @@
<script>
import CardBG from "./CardBG.svelte";
import {createEventDispatcher} from 'svelte';
import {createEventDispatcher, getContext} from 'svelte';
import Icon from "@iconify/svelte";
import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
import DetailField from "./DetailField.svelte";
import { marknotificationread } from "./api";
const dispatch = createEventDispatcher();
const notificationsStore = getContext("notifications");
const refreshNotifications = getContext("refreshNotifications");
const token = getContext("token");
export let notifications = [{time: "15:38 27/11/2021", text: "A notification's text."}];
$: notifications = $notificationsStore ? $notificationsStore.notifications : [];
function cancelCheckNotifications(){
dispatch("createPopup",{type:"check_notifications_cancelled"});
}
async function onNotificationClick(id){
await marknotificationread($token, id);
if($refreshNotifications){
$refreshNotifications();
}
}
</script>
<div class="h-full self-center">
@ -28,23 +39,44 @@
<div class="w-full max-w-md self-start border-solid border mb-3"></div>
<div class="flex flex-col flex-grow pl-8 pr-10 relative scroller overflow-auto overflow-x-hidden max-h-full min-h-0">
{#each notifications as notification,i (i)}
<div in:slide={{delay:100*i}} out:slide={{delay:50*(notifications.length-i)}}>
<DetailField class="my-3 py-1 flex-shrink min-w-transaction max-w-4xl">
<div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'>
{notification.text}
</div>
<div class="flex flex-row">
<div class='inline font-sans ml-auto mr-4 text-xl text-gray-100 mt-2 mx-6 border-b-1'>
<span> at {notification.time} </span>
{#if notifications.length > 0}
{#each notifications as notification,i (i)}
<div on:click={() => onNotificationClick(notification.id)} in:slide={{delay:100*i}} out:slide={{delay:50*(notifications.length-i)}}>
<DetailField class="relative my-3 py-1 flex-shrink min-w-transaction max-w-4xl">
<div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'>
{notification.body}
</div>
<div class="flex flex-row">
<div class='inline font-sans ml-auto mr-4 text-xl text-gray-100 mt-2 mx-6 border-b-1'>
<span> at {new Date(notification.datetime).toLocaleString()} </span>
</div>
</div>
<div class='fixed font-sans text-6xl text-gray-100 left-2 bottom-2'>
{#if !notification.read}
<svg class="fill-current text-gray-100" viewBox="0 0 16 16" width="16" height="16">
<circle cx="8" cy="8" r="8"></circle>
</svg>
{:else}
<svg class="fill-current text-gray-100" viewBox="0 0 24 24" width="16" height="16">
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"/>
</svg>
{/if}
</div>
</DetailField>
</div>
{/each}
{:else}
<DetailField class="relative my-3 py-1 flex-shrink min-w-transaction max-w-4xl">
<div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'>
No notifications.
</div>
</DetailField>
</div>
{/each}
</DetailField>
{/if}
</div>
<div class="m-2"></div>

View file

@ -3,31 +3,42 @@
import CardBG from "./CardBG.svelte";
import InputField from "./InputField.svelte";
import {createEventDispatcher} from 'svelte';
import {createEventDispatcher, onMount} from 'svelte';
import Icon from "@iconify/svelte";
import Overlay from "./Overlay.svelte";
import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { createaccount, getcurrencies, getaccounttypes } from "./api";
import { getContext } from "svelte";
const token = getContext("token");
const refreshAccounts = getContext("refreshAccounts");
const dispatch = createEventDispatcher();
let type = "";
let currencies = ["RON", "EUR"];
let currency = currencies[0];
let type = null;
let name = "";
let currency = null;
let termsAccepted = false;
$: placeholder = type==null ? "Checking Account" : `${type} Account`;
function create(){
if(type == "" || type == null) {
alert("Account Name field can not be empty!");
console.debug(`account name: ${type}`)
}else if(!currencies.includes(currency)){
alert("Currency is not supported!");
async function create(){
if(type == null){
alert("Type is not selected!");
}else if(currency == null){
alert("Currency is not selected!");
}else if (!termsAccepted){
alert("Terms of Service not accepted!");
}else{
//TODO Create account with provided details on the server
dispatch("createPopup",{type:"create_acc_success", account:{type:type, currency:currency, transactions:[]}});
const result = await createaccount($token, name, currency, type);
if(result.status == "success") {
dispatch("createPopup",{type:"create_acc_success"});
if($refreshAccounts){
$refreshAccounts();
}
}else{
dispatch("createPopup",{type:"create_acc_failed", reason:"Failed to create account. Error:"+result.status});
}
}
}
@ -43,6 +54,11 @@
termsAccepted = !termsAccepted;
}
onMount(() => {
getcurrencies().then(result => currency = result.currencies[0]);
getaccounttypes().then(result => type = result.accountTypes[0]);
})
</script>
@ -58,12 +74,31 @@
<div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>Account name:</h2>
<InputField placeholder="New Account" isPassword={false} bind:value={type}></InputField>
<InputField placeholder={placeholder} isPassword={false} bind:value={name}></InputField>
</div>
<div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>Type:</h2>
<select bind:value={type}>
{#await getaccounttypes() then result}
{#each result.accountTypes as option}
<option class="custom-option" value={option}>{option}</option>
{/each}
{/await}
</select>
</div>
<div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>Currency:</h2>
<InputField placeholder="RON" isPassword={false} bind:value={currency}></InputField>
<select bind:value={currency}>
{#await getcurrencies() then result}
{#each result.currencies as option}
<option class="custom-option" value={option}>{option}</option>
{/each}
{/await}
</select>
</div>
<div class="mx-1 flex-shrink max-w-2xl">
@ -85,3 +120,28 @@
</div>
</div>
<style>
select{
min-width: 120px;
min-height: 32px;
color: rgba(233, 231, 231, 0.842);
background: linear-gradient(92.55deg, rgba(76, 172, 135, 0.95) -28.27%, rgba(249, 224, 127, 0.096) 115.79%);
filter: drop-shadow(0px 8px 4px rgba(0, 0, 0, 0.25));
border-radius: 3px;
}
select option{
min-width: 120px;
min-height: 32px;
color: rgba(233, 231, 231, 0.842);
background: linear-gradient(92.55deg, rgba(76, 172, 135, 0.95) -28.27%, rgba(249, 224, 127, 0.096) 115.79%);
filter: drop-shadow(0px 8px 4px rgba(0, 0, 0, 0.25));
border-radius: 3px;
}
</style>

View file

@ -9,7 +9,7 @@
</script>
<main>
<input type={isPassword ? "password" : "text"} placeholder={placeholder} value={value} on:input={handleInput} class="placeholder-gray-300 p-3 text-gray-50 w-full text-3xl">
<input on:keydown type={isPassword ? "password" : "text"} placeholder={placeholder} value={value} on:input={handleInput} class="placeholder-gray-300 p-3 text-gray-50 w-full text-3xl">
</main>
<style>

View file

@ -15,14 +15,28 @@
async function checkLogin(){
const result = await login(username, code);
if(result.status == "success") {
sessionStorage.setItem("token", result.token);
dispatch("loginSuccess",null);
dispatch("loginSuccess",{
token: result.token,
});
}else{
alert(result.code);
}
}
async function enter(event) {
if (event.key === 'Enter') {
const result = await login(username, code);
if(result.status == "success") {
dispatch("loginSuccess",{
token: result.token,
});
}else{
alert(result.code);
}
}
}
</script>
<main class="h-full">
@ -34,11 +48,11 @@
</div>
<div class="m-3 flex-shrink">
<InputField placeholder="Code" isPassword={true} bind:value={code}></InputField>
<InputField on:keydown={enter} placeholder="Code" isPassword={true} bind:value={code}></InputField>
</div>
<div class="m-3 flex-shrink">
<OrangeButton on:click={checkLogin}>Login</OrangeButton>
<OrangeButton on:click={checkLogin}>Login</OrangeButton>
</div>
<div class="flex-grow">

View file

@ -1,66 +1,58 @@
<script>
import Icon from '@iconify/svelte';
import CardBG from "./CardBG.svelte";
import {createEventDispatcher, onMount} from 'svelte';
import {createEventDispatcher, onMount, getContext} from 'svelte';
import AccountCard from './AccountCard.svelte';
import GreenButton from './GreenButton.svelte';
import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { logout, whoami } from './api';
import { logout, whoami, getaccountlist, getnotificationlist, getForex } from './api';
import { amountToString } from './utils';
const token = getContext("token");
const user = getContext("user");
const accountsStore = getContext("accounts");
const notificationsStore = getContext("notifications");
const dispatch = createEventDispatcher();
let fullname = "";
let username = "";
let email = "";
let code = "";
let totalbalance = "2455.22";
$: fullname = $user.user.fullname;
$: username = $user.user.username;
$: email = $user.user.email;
$: notifications = $notificationsStore ? $notificationsStore.notifications : [];
let totalbalance = "0.00";
let maincurrency = "RON";
let expandedAccount = null;
let showAllAccounts = true;
let notifications = [
{time: "15:38 27/11/2021", text: "A notification's text."},
{time: "15:38 27/11/2021", text: "A notification's text but longer aaaaaaaaaaaa asddagagfabsdhubaiufbau bdauhsbabsdbayub badysabdyba ybbdbasbd bbdabsdb aybdbaysbdya bybdabs bdabsdbadbua."},
{time: "15:38 27/11/2021", text: "A notification's text but way longer absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd."},
{time: "15:38 27/11/2021", text: "A notification's text."},
{time: "15:38 27/11/2021", text: "A notification's text."},
];
$: notifications_unread = notifications.filter(n => !n.read).length;
let accounts = [
{type:"RON Account", currency:"RON", balance:"420.42", iban:"RONFOX62188921",
transactions: [
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"},
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"},
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"},
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
]
},
{type:"EUR Account", currency:"EUR", balance:"620,42", iban:"EURFOX62188921",
transactions: [
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"},
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
]
},
];
$: accounts = $accountsStore ? $accountsStore.accounts.map(account => {
return {
name: account.customName ? account.customName : `${account.accountType} Account`,
currency: account.currency,
balance: account.balance,
iban: account.iban.replace(/(.{4})/g, "$1 "),
id: account.id,
transactions: account.transactions,
}
}) : [];
$: {
Promise.all(accounts.map(account => {
return getForex(account.currency, maincurrency, account.balance);
})).then( balances => {
const sum = balances.reduce((acc, current) => acc+current, 0);
totalbalance = amountToString(Math.round(sum));
})
}
function dispatchLogout(){
//todo: CHeck here
if (confirm("Log out?")) {
logout(sessionStorage.getItem("token"));
sessionStorage.removeItem("token");
logout($token);
dispatch("logOut",null);
}
}
function expanded(index) {
if (!expandedAccount && expandedAccount !== 0) {
expandedAccount = index;
@ -92,34 +84,24 @@
});
}
onMount( async function() {
const token = sessionStorage.getItem("token");
const result = await whoami(token);
if(result.status == "success") {
fullname = result.user.fullname;
email = result.user.email;
username = result.user.username;
}
})
</script>
<main class="h-full flex flex-col items-stretch md:flex-row">
<div class="flex flex-col items-stretch max-h-full">
{#if expandedAccount || expandedAccount === 0}
<AccountCard type={accounts[expandedAccount].type} currency={accounts[expandedAccount].currency} balance={accounts[expandedAccount].balance} iban={accounts[expandedAccount].iban} transactions={accounts[expandedAccount].transactions} isExpanded={true} on:expanded={() => expanded(null)}></AccountCard>
<AccountCard accountId={accounts[expandedAccount].id} name={accounts[expandedAccount].name} currency={accounts[expandedAccount].currency} balance={accounts[expandedAccount].balance} iban={accounts[expandedAccount].iban} transactions={accounts[expandedAccount].transactions} isExpanded={true} on:expanded={() => expanded(null)}></AccountCard>
{:else}
{#if showAllAccounts}
{#each accounts as account,i}
<div in:slide={{delay:500*i, duration:250*(i==0 ? 1 : i) }}>
<AccountCard type={account.type} currency={account.currency} balance={account.balance} iban={account.iban} transactions={account.transactions} isExpanded={false} on:expanded={() => expanded(i)} on:createPopup></AccountCard>
<AccountCard accountId={account.id} name={account.name} currency={account.currency} balance={account.balance} iban={account.iban} transactions={account.transactions} isExpanded={false} on:expanded={() => expanded(i)} on:createPopup></AccountCard>
</div>
{/each}
{/if}
{/if}
<div class="self-center m-0">
{#if showAllAccounts}
{#if showAllAccounts && (accounts.length < 3) }
<div in:slide={{delay:500*accounts.length, duration:250*accounts.length}}>
<GreenButton on:click={createAccount}><Icon icon="akar-icons:plus" color="rgba(249, 250, 251, 1)" width="26" height="26" /></GreenButton>
</div>
@ -134,7 +116,7 @@
<CardBG class="flex-shrink flex flex-col min-w-transaction items-stretch md:self-start p-6">
<div class="flex flex-row">
<h1 class='font-sans flex-grow text-5xl text-gray-50 m-6 border-b-2'>{fullname}</h1>
<button on:click={checkNotifications} style=" filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));"> <Icon icon="akar-icons:envelope" color="#FB6666" width="36" height="36" /></button>
<button on:click={checkNotifications} style=" filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));"> <Icon icon={notifications_unread==0 ? "akar-icons:envelope" : "akar-icons:open-envelope"} color="#FB6666" width="36" height="36" /></button>
</div>
<div class="m-3 flex-shrink">

View file

@ -3,37 +3,46 @@
import CardBG from "./CardBG.svelte";
import InputField from "./InputField.svelte";
import {createEventDispatcher} from 'svelte';
import {createEventDispatcher, getContext} from 'svelte';
import Icon from "@iconify/svelte";
import Overlay from "./Overlay.svelte";
import TextareaField from "./TextareaField.svelte";
import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { createnotification, createtransaction } from "./api";
const dispatch = createEventDispatcher();
export let account={type: "", currency:"", balance:0};
export let account={id: -1, type: "", currency:"", balance:0};
let receivername="";
let receiveriban="";
let amount=0.00;
let description="";
const token = getContext("token");
const refreshAccounts = getContext("refreshAccounts");
let send_details={receivername:"", receiveriban:"", amount:0, description:""};
function create(){
async function create(){
if(receivername == "" || receivername == null) {
alert("Receiver's name field can not be empty!");
}else if(receiveriban == "" || receiveriban == null){
alert("Receiver's iBan field can not be empty!");
}else if (amount > parseFloat(account.balance) ){
}else if (parseFloat(amount) > parseFloat(account.balance) ){
alert("Not enough money in your account!");
}else if (amount <= 0.00 ){
alert("Insert a valid amount!");
}else{
//TODO Create account with provided details on the server
send_details={receivername:receivername, receiveriban:receiveriban, amount:amount, description:description}
dispatch("createPopup",{type:"send_money_success", send_details:{send_details}});
await createtransaction($token, receiveriban, Math.round(amount*100), account.id, description).then( result => {
if(result.status == "success") {
dispatch("createPopup",{type:"send_money_success"});
if($refreshAccounts){
$refreshAccounts();
}
}
});
}
}
@ -63,7 +72,7 @@
<div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>IBAN:</h2>
<InputField placeholder={account.currency +"-0000-0000-0000-0000"} isPassword={false} bind:value={receiveriban}></InputField>
<InputField placeholder={"RO00 FOXB 0"+account.currency +" 0000 0000 0000"} isPassword={false} bind:value={receiveriban}></InputField>
</div>
<div class="mx-1 flex-shrink">

View file

@ -54,4 +54,234 @@ export async function logout(token) {
code: "request/failure"
}
}
}
}
export async function getaccountlist(token) {
try {
const result = await fetch(new URL("/accounts/", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
const data = await result.json();
if(data.status == "success"){
for(let i=0; i < data.accounts.length; i++){
const transactionsreq = await gettransactions(token, data.accounts[i].id);
if (transactionsreq.status == "success") {
data.accounts[i].transactions = transactionsreq.transactions;
}
}
}
return data;
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getcurrencies() {
try {
const result = await fetch(new URL("/accounts/meta/currencies", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getaccounttypes() {
try {
const result = await fetch(new URL("/accounts/meta/account_types", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function createaccount(token, name, currency, type) {
try {
const result = await fetch(new URL("/accounts/", baseURL), {
method: "POST",
body: JSON.stringify({
customName: name,
currency: currency,
accountType: type,
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getnotificationlist(token) {
try {
const result = await fetch(new URL("/notifications/", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
const data = await result.json();
if(data.status == "success") {
data.notifications.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
return data;
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function createnotification(token, body, read) {
try {
const result = await fetch(new URL("/notifications/", baseURL), {
method: "POST",
body: JSON.stringify({
body: body,
read: read,
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function marknotificationread(token, id) {
try {
const result = await fetch(new URL("/notifications/"+id+"/mark_read", baseURL), {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function createtransaction(token, otherparty, amount, accountId, description) {
try {
const result = await fetch(new URL("/transactions/", baseURL), {
method: "POST",
body: JSON.stringify({
description: description,
account_id: accountId,
destination_iban: otherparty,
amount: amount,
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function gettransactions(token, id) {
try {
const result = await fetch(new URL("/transactions/?account_id="+id, baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
const data = await result.json();
if(data.status == "success") {
data.transactions.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
return data;
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getForex(from, to, amount) {
try {
const result = await fetch(new URL("/forex/"+from+"/"+to, baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await result.json();
if(data.status == "success") {
const exchanged = amount * data.rate;
return exchanged;
}
return null;
} catch (error) {
return null;
}
}

4
client/src/utils.js Normal file
View file

@ -0,0 +1,4 @@
export function amountToString(amount) {
amount = amount.toString().padStart(3, "0");
return amount.replace(/(.{2})$/, ".$1");
}

View file

@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Flask",
"type": "python",