mirror of
https://github.com/dancojocaru2000/foxbank.git
synced 2025-06-19 11:02:28 +03:00
Compare commits
12 commits
c7c8f3c765
...
1c56bf2a60
Author | SHA1 | Date | |
---|---|---|---|
|
1c56bf2a60 | ||
|
57d9ff8ed0 | ||
649e2c729f | |||
|
ab53e05287 | ||
|
e8477db7b8 | ||
1250b12049 | |||
|
5e19ab457b | ||
c429506bdf | |||
a0a3fe774d | |||
|
e369315034 | ||
|
b76cbe77c7 | ||
|
0e27defbdc |
14 changed files with 2867 additions and 150 deletions
15
client/.vscode/launch.json
vendored
Normal file
15
client/.vscode/launch.json
vendored
Normal 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
2257
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
4
client/src/utils.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export function amountToString(amount) {
|
||||
amount = amount.toString().padStart(3, "0");
|
||||
return amount.replace(/(.{2})$/, ".$1");
|
||||
}
|
1
server/.vscode/launch.json
vendored
1
server/.vscode/launch.json
vendored
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue