[MIRROR] Convert ATM to TGUI (#9212)

Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com>
Co-authored-by: CHOMPStation2 <chompsation2@gmail.com>
This commit is contained in:
CHOMPStation2
2024-10-12 06:03:24 -07:00
committed by GitHub
parent 8b58d2c577
commit ffb65bbc3e
4 changed files with 1061 additions and 319 deletions

View File

@@ -123,147 +123,176 @@ log transactions
else
..()
/obj/machinery/atm/attack_hand(mob/user as mob)
if(istype(user, /mob/living/silicon))
to_chat (user, span_warning("A firewall prevents you from interfacing with this device!"))
return
if(get_dist(src,user) <= 1)
/obj/machinery/atm/tgui_status(mob/user)
. = ..()
if(issilicon(user))
return STATUS_CLOSE
//js replicated from obj/machinery/computer/card
var/dat = "<h1>Automatic Teller Machine</h1>"
dat += "For all your monetary needs!<br>"
dat += "<i>This terminal is</i> [machine_id]. <i>Report this code when contacting IT Support</i><br/>"
/obj/machinery/atm/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "AutomatedTellerMachine", name)
ui.open()
/obj/machinery/atm/tgui_static_data(mob/user)
var/list/data = ..()
data["machine_id"] = machine_id
return data
/obj/machinery/atm/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state)
var/list/data = ..()
data["emagged"] = emagged
if(emagged > 0)
dat += "Card: <span style='color: red;'>LOCKED</span><br><br><span style='color: red;'>Unauthorized terminal access detected! This ATM has been locked. Please contact IT Support.</span>"
else
dat += "Card: <a href='?src=\ref[src];choice=insert_card'>[held_card ? held_card.name : "------"]</a><br><br>"
return data
data["held_card"] = held_card
data["locked_down"] = ticks_left_locked_down
if(ticks_left_locked_down > 0)
dat += span_warning("Maximum number of pin attempts exceeded! Access to this ATM has been temporarily disabled.")
else if(authenticated_account)
if(authenticated_account.suspended)
dat += span_red(span_bold("Access to this account has been suspended, and the funds within frozen."))
else
switch(view_screen)
if(CHANGE_SECURITY_LEVEL)
dat += "Select a new security level for this account:<br><hr>"
var/text = "Zero - Either the account number or card is required to access this account. EFTPOS transactions will require a card and ask for a pin, but not verify the pin is correct."
if(authenticated_account.security_level != 0)
text = "<A href='?src=\ref[src];choice=change_security_level;new_security_level=0'>[text]</a>"
dat += "[text]<hr>"
text = "One - An account number and pin must be manually entered to access this account and process transactions."
if(authenticated_account.security_level != 1)
text = "<A href='?src=\ref[src];choice=change_security_level;new_security_level=1'>[text]</a>"
dat += "[text]<hr>"
text = "Two - In addition to account number and pin, a card is required to access this account and process transactions."
if(authenticated_account.security_level != 2)
text = "<A href='?src=\ref[src];choice=change_security_level;new_security_level=2'>[text]</a>"
dat += "[text]<hr><br>"
dat += "<A href='?src=\ref[src];choice=view_screen;view_screen=0'>Back</a>"
if(VIEW_TRANSACTION_LOGS)
dat += "<b>Transaction logs</b><br>"
dat += "<A href='?src=\ref[src];choice=view_screen;view_screen=0'>Back</a>"
dat += "<table border=1 style='width:100%'>"
dat += "<tr>"
dat += "<td><b>Date</b></td>"
dat += "<td><b>Time</b></td>"
dat += "<td><b>Target</b></td>"
dat += "<td><b>Purpose</b></td>"
dat += "<td><b>Value</b></td>"
dat += "<td><b>Source terminal ID</b></td>"
dat += "</tr>"
for(var/datum/transaction/T in authenticated_account.transaction_log)
dat += "<tr>"
dat += "<td>[T.date]</td>"
dat += "<td>[T.time]</td>"
dat += "<td>[T.target_name]</td>"
dat += "<td>[T.purpose]</td>"
dat += "<td>$[T.amount]</td>"
dat += "<td>[T.source_terminal]</td>"
dat += "</tr>"
dat += "</table>"
dat += "<A href='?src=\ref[src];choice=print_transaction'>Print</a><br>"
if(TRANSFER_FUNDS)
dat += "<b>Account balance:</b> $[authenticated_account.money]<br>"
dat += "<A href='?src=\ref[src];choice=view_screen;view_screen=0'>Back</a><br><br>"
dat += "<form name='transfer' action='?src=\ref[src]' method='get'>"
dat += "<input type='hidden' name='src' value='\ref[src]'>"
dat += "<input type='hidden' name='choice' value='transfer'>"
dat += "Target account number: <input type='text' name='target_acc_number' value='' style='width:200px; background-color:white;'><br>"
dat += "Funds to transfer: <input type='text' name='funds_amount' value='' style='width:200px; background-color:white;'><br>"
dat += "Transaction purpose: <input type='text' name='purpose' value='Funds transfer' style='width:200px; background-color:white;'><br>"
dat += "<input type='submit' value='Transfer funds'><br>"
dat += "</form>"
else
dat += "Welcome, <b>[authenticated_account.owner_name].</b><br/>"
dat += "<b>Account balance:</b> $[authenticated_account.money]"
dat += "<form name='withdrawal' action='?src=\ref[src]' method='get'>"
dat += "<input type='hidden' name='src' value='\ref[src]'>"
dat += "<input type='radio' name='choice' value='withdrawal' checked> Cash <input type='radio' name='choice' value='e_withdrawal'> Chargecard<br>"
dat += "<input type='text' name='funds_amount' value='' style='width:200px; background-color:white;'><input type='submit' value='Withdraw'>"
dat += "</form>"
dat += "<A href='?src=\ref[src];choice=view_screen;view_screen=1'>Change account security level</a><br>"
dat += "<A href='?src=\ref[src];choice=view_screen;view_screen=2'>Make transfer</a><br>"
dat += "<A href='?src=\ref[src];choice=view_screen;view_screen=3'>View transaction log</a><br>"
dat += "<A href='?src=\ref[src];choice=balance_statement'>Print balance statement</a><br>"
dat += "<A href='?src=\ref[src];choice=logout'>Logout</a><br>"
else
dat += "<form name='atm_auth' action='?src=\ref[src]' method='get'>"
dat += "<input type='hidden' name='src' value='\ref[src]'>"
dat += "<input type='hidden' name='choice' value='attempt_auth'>"
dat += "<b>Account:</b> <input type='text' id='account_num' name='account_num' style='width:250px; background-color:white;'><br>"
dat += "<b>PIN:</b> <input type='text' id='account_pin' name='account_pin' style='width:250px; background-color:white;'><br>"
dat += "<input type='submit' value='Submit'><br>"
dat += "</form>"
return data
user << browse(dat,"window=atm;size=550x650")
else
user << browse(null,"window=atm")
/obj/machinery/atm/Topic(var/href, var/href_list)
if(href_list["choice"])
switch(href_list["choice"])
if("transfer")
data["authenticated_account"] = null
data["suspended"] = FALSE
if(authenticated_account)
var/transfer_amount = text2num(href_list["funds_amount"])
transfer_amount = round(transfer_amount, 0.01)
if(transfer_amount <= 0)
tgui_alert_async(usr, "That is not a valid amount.")
else if(transfer_amount <= authenticated_account.money)
var/target_account_number = text2num(href_list["target_acc_number"])
var/transfer_purpose = href_list["purpose"]
if(charge_to_account(target_account_number, authenticated_account.owner_name, transfer_purpose, machine_id, transfer_amount))
to_chat(usr, "[icon2html(src, usr.client)]<span class='info'>Funds transfer successful.</span>")
authenticated_account.money -= transfer_amount
if(authenticated_account.suspended)
data["suspended"] = TRUE
return data
//create an entry in the account transaction log
var/datum/transaction/T = new()
T.target_name = "Account #[target_account_number]"
T.purpose = transfer_purpose
T.source_terminal = machine_id
T.date = current_date_string
T.time = stationtime2text()
T.amount = "([transfer_amount])"
authenticated_account.transaction_log.Add(T)
else
to_chat(usr, "[icon2html(src, usr.client)]<span class='warning'>Funds transfer failed.</span>")
var/list/transactions = list()
for(var/datum/transaction/T as anything in authenticated_account.transaction_log)
UNTYPED_LIST_ADD(transactions, list(
"date" = T.date,
"time" = T.time,
"target_name" = T.target_name,
"purpose" = T.purpose,
"amount" = T.amount,
"source_terminal" = T.source_terminal
))
data["authenticated_account"] = list(
"owner_name" = authenticated_account.owner_name,
"money" = authenticated_account.money,
"security_level" = authenticated_account.security_level,
"transactions" = transactions,
)
return data
/obj/machinery/atm/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state)
. = ..()
if(.)
return
switch(action)
// This is also a logout
if("insert_card")
if(held_card)
release_held_id(usr)
else
to_chat(usr, "[icon2html(src, usr.client)]<span class='warning'>You don't have enough funds to do that!</span>")
if("view_screen")
view_screen = text2num(href_list["view_screen"])
if(emagged > 0)
to_chat(usr, span_red("[icon2html(src, usr.client)] The ATM card reader rejected your ID because this machine has been sabotaged!"))
else
var/obj/item/I = usr.get_active_hand()
if(istype(I, /obj/item/card/id))
usr.drop_item(src)
held_card = I
. = TRUE
if("logout")
if(held_card)
release_held_id(usr)
authenticated_account = null
. = TRUE
// Balance statement
if("balance_statement")
if(!authenticated_account)
return
var/obj/item/paper/R = new(loc)
R.name = "Account balance: [authenticated_account.owner_name]"
R.info = "<b>NT Automated Teller Account Statement</b><br><br>"
R.info += "<i>Account holder:</i> [authenticated_account.owner_name]<br>"
R.info += "<i>Account number:</i> [authenticated_account.account_number]<br>"
R.info += "<i>Balance:</i> $[authenticated_account.money]<br>"
R.info += "<i>Date and time:</i> [stationtime2text()], [current_date_string]<br><br>"
R.info += "<i>Service terminal ID:</i> [machine_id]<br>"
//stamp the paper
var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
stampoverlay.icon_state = "paper_stamp-cent"
if(!R.stamped)
R.stamped = new
R.stamped += /obj/item/stamp
R.add_overlay(stampoverlay)
R.stamps += "<HR><i>This paper has been stamped by the Automatic Teller Machine.</i>"
if(prob(50))
playsound(src, 'sound/items/polaroid1.ogg', 50, 1)
else
playsound(src, 'sound/items/polaroid2.ogg', 50, 1)
. = TRUE
// Transaction logs
if("print_transaction")
if(!authenticated_account)
return
var/obj/item/paper/R = new(loc)
R.name = "Transaction logs: [authenticated_account.owner_name]"
R.info = "<b>Transaction logs</b><br>"
R.info += "<i>Account holder:</i> [authenticated_account.owner_name]<br>"
R.info += "<i>Account number:</i> [authenticated_account.account_number]<br>"
R.info += "<i>Date and time:</i> [stationtime2text()], [current_date_string]<br><br>"
R.info += "<i>Service terminal ID:</i> [machine_id]<br>"
R.info += "<table border=1 style='width:100%'>"
R.info += "<tr>"
R.info += "<td><b>Date</b></td>"
R.info += "<td><b>Time</b></td>"
R.info += "<td><b>Target</b></td>"
R.info += "<td><b>Purpose</b></td>"
R.info += "<td><b>Value</b></td>"
R.info += "<td><b>Source terminal ID</b></td>"
R.info += "</tr>"
for(var/datum/transaction/T in authenticated_account.transaction_log)
R.info += "<tr>"
R.info += "<td>[T.date]</td>"
R.info += "<td>[T.time]</td>"
R.info += "<td>[T.target_name]</td>"
R.info += "<td>[T.purpose]</td>"
R.info += "<td>$[T.amount]</td>"
R.info += "<td>[T.source_terminal]</td>"
R.info += "</tr>"
R.info += "</table>"
//stamp the paper
var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
stampoverlay.icon_state = "paper_stamp-cent"
if(!R.stamped)
R.stamped = new
R.stamped += /obj/item/stamp
R.add_overlay(stampoverlay)
R.stamps += "<HR><i>This paper has been stamped by the Automatic Teller Machine.</i>"
if(prob(50))
playsound(src, 'sound/items/polaroid1.ogg', 50, 1)
else
playsound(src, 'sound/items/polaroid2.ogg', 50, 1)
. = TRUE
if("change_security_level")
if(authenticated_account)
var/new_sec_level = max( min(text2num(href_list["new_security_level"]), 2), 0)
var/new_sec_level = clamp(text2num(params["new_security_level"]), 0, 2)
authenticated_account.security_level = new_sec_level
. = TRUE
if("attempt_auth")
if(!ticks_left_locked_down)
var/tried_account_num = held_card ? held_card.associated_account_number : text2num(href_list["account_num"])
var/tried_pin = text2num(href_list["account_pin"])
if(ticks_left_locked_down)
return
var/tried_account_num = held_card ? held_card.associated_account_number : text2num(params["account_num"])
var/tried_pin = text2num(params["account_pin"])
// check if they have low security enabled
if(!tried_account_num)
scan_user(usr)
else
@@ -311,12 +340,48 @@ log transactions
to_chat(usr, span_blue("[icon2html(src, usr.client)] Access granted. Welcome user '[authenticated_account.owner_name].'"))
previous_account_number = tried_account_num
. = TRUE
if("transfer")
if(!authenticated_account)
return
var/transfer_amount = text2num(params["funds_amount"])
transfer_amount = round(transfer_amount, 0.01)
if(transfer_amount <= 0)
tgui_alert_async(usr, "That is not a valid amount.")
else if(transfer_amount <= authenticated_account.money)
var/target_account_number = text2num(params["target_acc_number"])
var/transfer_purpose = params["purpose"]
if(charge_to_account(target_account_number, authenticated_account.owner_name, transfer_purpose, machine_id, transfer_amount))
to_chat(usr, "[icon2html(src, usr.client)]<span class='info'>Funds transfer successful.</span>")
authenticated_account.money -= transfer_amount
//create an entry in the account transaction log
var/datum/transaction/T = new()
T.target_name = "Account #[target_account_number]"
T.purpose = transfer_purpose
T.source_terminal = machine_id
T.date = current_date_string
T.time = stationtime2text()
T.amount = "([transfer_amount])"
authenticated_account.transaction_log.Add(T)
else
to_chat(usr, "[icon2html(src, usr.client)]<span class='warning'>Funds transfer failed.</span>")
else
to_chat(usr, "[icon2html(src, usr.client)]<span class='warning'>You don't have enough funds to do that!</span>")
. = TRUE
if("e_withdrawal")
var/amount = max(text2num(href_list["funds_amount"]),0)
var/amount = max(text2num(params["funds_amount"]),0)
amount = round(amount, 0.01)
if(amount <= 0)
tgui_alert_async(usr, "That is not a valid amount.")
else if(authenticated_account && amount > 0)
return
if(!authenticated_account)
return
if(amount <= authenticated_account.money)
playsound(src, 'sound/machines/chime.ogg', 50, 1)
@@ -337,12 +402,18 @@ log transactions
authenticated_account.transaction_log.Add(T)
else
to_chat(usr, "[icon2html(src, usr.client)]<span class='warning'>You don't have enough funds to do that!</span>")
. = TRUE
if("withdrawal")
var/amount = max(text2num(href_list["funds_amount"]),0)
var/amount = max(text2num(params["funds_amount"]),0)
amount = round(amount, 0.01)
if(amount <= 0)
tgui_alert_async(usr, "That is not a valid amount.")
else if(authenticated_account && amount > 0)
return
if(!authenticated_account)
return
if(amount <= authenticated_account.money)
playsound(src, 'sound/machines/chime.ogg', 50, 1)
@@ -362,91 +433,17 @@ log transactions
authenticated_account.transaction_log.Add(T)
else
to_chat(usr, "[icon2html(src, usr.client)]<span class='warning'>You don't have enough funds to do that!</span>")
if("balance_statement")
if(authenticated_account)
var/obj/item/paper/R = new(src.loc)
R.name = "Account balance: [authenticated_account.owner_name]"
R.info = "<b>NT Automated Teller Account Statement</b><br><br>"
R.info += "<i>Account holder:</i> [authenticated_account.owner_name]<br>"
R.info += "<i>Account number:</i> [authenticated_account.account_number]<br>"
R.info += "<i>Balance:</i> $[authenticated_account.money]<br>"
R.info += "<i>Date and time:</i> [stationtime2text()], [current_date_string]<br><br>"
R.info += "<i>Service terminal ID:</i> [machine_id]<br>"
. = TRUE
//stamp the paper
var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
stampoverlay.icon_state = "paper_stamp-cent"
if(!R.stamped)
R.stamped = new
R.stamped += /obj/item/stamp
R.add_overlay(stampoverlay)
R.stamps += "<HR><i>This paper has been stamped by the Automatic Teller Machine.</i>"
if(.)
playsound(src, "keyboard", 50, TRUE)
if(prob(50))
playsound(src, 'sound/items/polaroid1.ogg', 50, 1)
else
playsound(src, 'sound/items/polaroid2.ogg', 50, 1)
if ("print_transaction")
if(authenticated_account)
var/obj/item/paper/R = new(src.loc)
R.name = "Transaction logs: [authenticated_account.owner_name]"
R.info = "<b>Transaction logs</b><br>"
R.info += "<i>Account holder:</i> [authenticated_account.owner_name]<br>"
R.info += "<i>Account number:</i> [authenticated_account.account_number]<br>"
R.info += "<i>Date and time:</i> [stationtime2text()], [current_date_string]<br><br>"
R.info += "<i>Service terminal ID:</i> [machine_id]<br>"
R.info += "<table border=1 style='width:100%'>"
R.info += "<tr>"
R.info += "<td><b>Date</b></td>"
R.info += "<td><b>Time</b></td>"
R.info += "<td><b>Target</b></td>"
R.info += "<td><b>Purpose</b></td>"
R.info += "<td><b>Value</b></td>"
R.info += "<td><b>Source terminal ID</b></td>"
R.info += "</tr>"
for(var/datum/transaction/T in authenticated_account.transaction_log)
R.info += "<tr>"
R.info += "<td>[T.date]</td>"
R.info += "<td>[T.time]</td>"
R.info += "<td>[T.target_name]</td>"
R.info += "<td>[T.purpose]</td>"
R.info += "<td>$[T.amount]</td>"
R.info += "<td>[T.source_terminal]</td>"
R.info += "</tr>"
R.info += "</table>"
//stamp the paper
var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
stampoverlay.icon_state = "paper_stamp-cent"
if(!R.stamped)
R.stamped = new
R.stamped += /obj/item/stamp
R.add_overlay(stampoverlay)
R.stamps += "<HR><i>This paper has been stamped by the Automatic Teller Machine.</i>"
if(prob(50))
playsound(src, 'sound/items/polaroid1.ogg', 50, 1)
else
playsound(src, 'sound/items/polaroid2.ogg', 50, 1)
if("insert_card")
if(!held_card)
//this might happen if the user had the browser window open when somebody emagged it
if(emagged > 0)
to_chat(usr, span_red("[icon2html(src, usr.client)] The ATM card reader rejected your ID because this machine has been sabotaged!"))
else
var/obj/item/I = usr.get_active_hand()
if (istype(I, /obj/item/card/id))
usr.drop_item()
I.loc = src
held_card = I
else
release_held_id(usr)
if("logout")
authenticated_account = null
//usr << browse(null,"window=atm")
src.attack_hand(usr)
/obj/machinery/atm/attack_hand(mob/user as mob)
if(istype(user, /mob/living/silicon))
to_chat (user, span_warning("A firewall prevents you from interfacing with this device!"))
return
if(get_dist(src,user) <= 1)
tgui_interact(user)
//stolen wholesale and then edited a bit from newscasters, which are awesome and by Agouri
/obj/machinery/atm/proc/scan_user(mob/living/carbon/human/human_user as mob)

View File

@@ -0,0 +1,702 @@
import { useEffect, useState } from 'react';
import { useBackend } from 'tgui/backend';
import { Window } from 'tgui/layouts';
import {
Box,
Button,
DmIcon,
Icon,
Input,
LabeledList,
Section,
Stack,
Table,
} from 'tgui-core/components';
export type Transaction = {
date: string;
time: number;
target_name: string;
purpose: string;
amount: number;
source_terminal: string;
};
export type Account = {
owner_name: string;
money: number;
security_level: number;
transactions: Transaction[];
};
export type AutomatedTellerMachineData = {
machine_id: string;
emagged: number;
held_card: string | null;
locked_down: number;
suspended: boolean;
authenticated_account: Account;
};
export const AutomatedTellerMachine = (props) => {
const { act, data } = useBackend<AutomatedTellerMachineData>();
return (
<Window width={680} height={550}>
<Window.Content>
<Section title={'Automatic Teller Machine - ' + data.machine_id} fill>
{data.authenticated_account ? (
<AuthenticatedScreen />
) : (
<LoginScreen machine_id={data.machine_id} card={data.held_card} />
)}
</Section>
</Window.Content>
</Window>
);
};
enum Menu {
Main = 'main',
Withdraw = 'withdraw',
Balance = 'balance',
Transfer = 'transfer',
More = 'more',
}
const AuthenticatedScreen = (props) => {
const [menu, setMenu] = useState(Menu.Main);
switch (menu) {
case Menu.Main: {
return <MainMenu setMenu={setMenu} />;
}
case Menu.Withdraw: {
return <WithdrawMenu setMenu={setMenu} />;
}
case Menu.Balance: {
return <BalanceMenu setMenu={setMenu} />;
}
case Menu.Transfer: {
return <TransferMenu setMenu={setMenu} />;
}
case Menu.More: {
return <MoreMenu setMenu={setMenu} />;
}
}
};
const MainMenu = (props: {
setMenu: React.Dispatch<React.SetStateAction<Menu>>;
}) => {
const { setMenu } = props;
const { act, data } = useBackend<AutomatedTellerMachineData>();
return (
<Stack fill fontSize={2}>
<Stack.Item grow>
<Stack vertical fill>
<Stack.Item>
<Button fluid onClick={() => setMenu(Menu.Withdraw)}>
<Icon name="money-bill-wave" width={2} mr={2} />
Withdraw
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => setMenu(Menu.More)}>
<Icon name="bars" width={2} mr={2} />
More Services
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow textAlign="right">
<Stack vertical fill>
<Stack.Item>
<Button fluid onClick={() => setMenu(Menu.Balance)}>
Balance
<Icon name="dollar-sign" width={2} ml={2} />
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => setMenu(Menu.Transfer)}>
Transfer
<Icon name="money-check-alt" width={2} ml={2} />
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => act('logout')}>
{data.held_card ? 'Return Card' : 'Logout'}
<Icon name="sign-out-alt" width={2} ml={2} />
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
);
};
const WithdrawMenu = (props: {
setMenu: React.Dispatch<React.SetStateAction<Menu>>;
}) => {
const { act, data } = useBackend<AutomatedTellerMachineData>();
const { setMenu } = props;
const [custom, setCustom] = useState(false);
const [useCard, setUseCard] = useState(false);
const withdrawFunction = (val) => {
if (useCard) {
act('e_withdrawal', { funds_amount: val });
} else {
act('withdrawal', { funds_amount: val });
}
setMenu(Menu.Main);
};
if (custom) {
return (
<CustomWithdrawal
withdrawFunction={withdrawFunction}
useCard={useCard}
setCustom={setCustom}
setMenu={setMenu}
/>
);
}
return (
<Section
title="Withdrawals"
buttons={
<Button icon="arrow-left" onClick={() => setMenu(Menu.Main)}>
Back To Main Menu
</Button>
}
>
<Box fontSize={2} mb={1}>
Account Balance: ${data.authenticated_account.money}
</Box>
<Box fontSize={2} mb={4}>
<Stack align="center">
<Stack.Item grow>
<Button
fluid
icon="money-bill-wave"
selected={!useCard}
onClick={() => setUseCard(false)}
>
Cash
</Button>
</Stack.Item>
<Stack.Item grow>
<Button
fluid
icon="credit-card"
selected={useCard}
onClick={() => setUseCard(true)}
>
Chargecard
</Button>
</Stack.Item>
</Stack>
</Box>
<Stack fontSize={2}>
<Stack.Item grow>
<Stack vertical fill>
<Stack.Item>
<Button fluid onClick={() => withdrawFunction(10)}>
$10
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => withdrawFunction(100)}>
$100
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => withdrawFunction(500)}>
$500
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow textAlign="right">
<Stack vertical fill>
<Stack.Item>
<Button fluid onClick={() => withdrawFunction(50)}>
$50
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => withdrawFunction(200)}>
$200
</Button>
</Stack.Item>
<Stack.Item>
<Button fluid onClick={() => setCustom(true)}>
Other
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const CustomWithdrawal = (props: {
withdrawFunction: (val: number) => void;
useCard: boolean;
setCustom: React.Dispatch<React.SetStateAction<boolean>>;
setMenu: React.Dispatch<React.SetStateAction<Menu>>;
}) => {
const { act, data } = useBackend<AutomatedTellerMachineData>();
const { withdrawFunction, useCard, setCustom, setMenu } = props;
const [money, setMoney] = useState(1000);
return (
<Section
title="Withdrawal"
buttons={
<Button icon="arrow-left" onClick={() => setCustom(false)}>
Back To Withdrawals
</Button>
}
fill
>
<Stack
align="center"
justify="center"
fill
fontSize={4}
textAlign="center"
>
<Stack.Item>
<Stack vertical>
<Stack.Item fontSize={3}>
Available: ${data.authenticated_account.money}
</Stack.Item>
<Stack.Item>
<Input
fluid
value={money}
maxLength={10}
onInput={(e, val) => {
let value = parseInt(val, 10);
if (isNaN(value)) {
setMoney(0);
} else {
setMoney(value);
}
}}
onChange={(e, val) => {
let value = parseInt(val, 10);
if (isNaN(value)) {
setMoney(0);
} else {
setMoney(value);
}
}}
/>
</Stack.Item>
<Stack.Item>
<Button
icon={useCard ? 'credit-card' : 'money-bill-wave'}
fluid
fontSize={3}
onClick={() => withdrawFunction(money)}
>
{useCard ? 'Load' : 'Withdraw'} ${money}
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};
const BalanceMenu = (props: {
setMenu: React.Dispatch<React.SetStateAction<Menu>>;
}) => {
const { act, data } = useBackend<AutomatedTellerMachineData>();
const { setMenu } = props;
return (
<Section
title="Balance"
buttons={
<Button icon="arrow-left" onClick={() => setMenu(Menu.Main)}>
Back To Main Menu
</Button>
}
fill
>
<Stack justify="space-between">
<Stack.Item>
<Box fontSize={1.5} mb={1}>
Current Funds: ${data.authenticated_account.money}
</Box>
</Stack.Item>
<Stack.Item>
<Button
icon="money-bill-alt"
onClick={() => act('balance_statement')}
>
Print Balance Statement
</Button>
<Button icon="money-check" onClick={() => act('print_transaction')}>
Print Transactions
</Button>
</Stack.Item>
</Stack>
<TransactionLog transactions={data.authenticated_account.transactions} />
</Section>
);
};
const TransactionLog = (props: { transactions: Transaction[] }) => {
const { transactions } = props;
return (
<Section fill scrollable height="95%">
<Table collapsing={false} className="AutomatedTellerMachine__Table">
<Table.Row header>
<Table.Cell header>Date</Table.Cell>
<Table.Cell header>Time</Table.Cell>
<Table.Cell header>Target</Table.Cell>
<Table.Cell header>Purpose</Table.Cell>
<Table.Cell header>Value</Table.Cell>
<Table.Cell header>Source Terminal ID</Table.Cell>
</Table.Row>
{transactions.map((transaction, index) => (
<Table.Row key={index}>
<Table.Cell>{transaction.date}</Table.Cell>
<Table.Cell>{transaction.time}</Table.Cell>
<Table.Cell>{transaction.target_name}</Table.Cell>
<Table.Cell>{transaction.purpose}</Table.Cell>
<Table.Cell>${transaction.amount}</Table.Cell>
<Table.Cell>{transaction.source_terminal}</Table.Cell>
</Table.Row>
))}
</Table>
</Section>
);
};
const MoreMenu = (props: {
setMenu: React.Dispatch<React.SetStateAction<Menu>>;
}) => {
const { act, data } = useBackend<AutomatedTellerMachineData>();
const { setMenu } = props;
return (
<Section
title="More Services"
buttons={
<Button icon="arrow-left" onClick={() => setMenu(Menu.Main)}>
Back To Main Menu
</Button>
}
>
<Box fontSize={2} mb={1}>
Account Security Settings
</Box>
<Button
fluid
style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
selected={data.authenticated_account.security_level === 0}
onClick={() => act('change_security_level', { new_security_level: 0 })}
>
Zero - Either the account number or card is required to access this
account. EFTPOS transactions will require a card and ask for a pin, but
not verify the pin is correct.
</Button>
<Button
fluid
style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
selected={data.authenticated_account.security_level === 1}
onClick={() => act('change_security_level', { new_security_level: 1 })}
>
One - An account number and pin must be manually entered to access this
account and process transactions.
</Button>
<Button
fluid
style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
selected={data.authenticated_account.security_level === 2}
onClick={() => act('change_security_level', { new_security_level: 2 })}
>
Two - In addition to account number and pin, a card is required to
access this account and process transactions.
</Button>
</Section>
);
};
const TransferMenu = (props: {
setMenu: React.Dispatch<React.SetStateAction<Menu>>;
}) => {
const { act, data } = useBackend<AutomatedTellerMachineData>();
const [accountNum, setAccountNum] = useState(100000);
const updateAccountNum = (val) => {
let newVal = parseInt(val, 10);
if (isNaN(newVal)) {
setAccountNum(100000);
} else {
setAccountNum(newVal);
}
};
const [money, setMoney] = useState(0);
const updateMoney = (val) => {
let newVal = parseInt(val, 10);
if (isNaN(newVal)) {
setMoney(0);
} else {
setMoney(newVal);
}
};
const [purpose, setPurpose] = useState('Funds transfer');
const { setMenu } = props;
return (
<Section
title="Transfer Money"
buttons={
<Button icon="arrow-left" onClick={() => setMenu(Menu.Main)}>
Back To Main Menu
</Button>
}
fill
textAlign="center"
>
<Box fontSize={2}>
<Box fontSize={3} mb={2}>
Available: ${data.authenticated_account.money}
</Box>
<LabeledList>
<LabeledList.Item label="Target Account Number">
<Input
fluid
maxLength={6}
value={accountNum}
onChange={(e, val) => updateAccountNum(val)}
onInput={(e, val) => updateAccountNum(val)}
/>
</LabeledList.Item>
<LabeledList.Item label="Funds To Transfer">
<Input
fluid
maxLength={10}
value={money}
onChange={(e, val) => updateMoney(val)}
onInput={(e, val) => updateMoney(val)}
/>
</LabeledList.Item>
<LabeledList.Item label="Transaction Purpose">
<Input
fluid
maxLength={20}
value={purpose}
onChange={(e, val) => setPurpose(val)}
onInput={(e, val) => setPurpose(val)}
/>
</LabeledList.Item>
</LabeledList>
<Button
icon="exchange-alt"
fluid
mt={1}
onClick={() =>
act('transfer', {
funds_amount: money,
target_acc_number: accountNum,
})
}
>
Transfer Funds
</Button>
</Box>
</Section>
);
};
const LoginScreen = (props: { machine_id: string; card: string | null }) => {
const { act } = useBackend();
const { machine_id, card } = props;
const [account, setAccount] = useState('');
const [pin, setPin] = useState('');
return (
<Stack align="flex-start" justify="space-between" fill>
<Stack.Item grow height="100%">
<Stack vertical fill>
<Stack.Item>
<Button
fontSize={2}
fluid
height={8}
verticalAlignContent="middle"
onClick={() => act('insert_card')}
>
<Stack align="center" justify="center">
<Stack.Item>
<Icon name="id-card" />
</Stack.Item>
<Stack.Item
grow
style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
textAlign="center"
>
{card ? card : 'Insert ID'}
</Stack.Item>
</Stack>
</Button>
</Stack.Item>
<Stack.Item fontSize={1.5} mt={10}>
<LabeledList>
<LabeledList.Item label="Account">
<Input
fluid
value={account}
onChange={(e, val) => setAccount(val)}
onInput={(e, val) => setAccount(val)}
/>
</LabeledList.Item>
<LabeledList.Item label="PIN">
<Input
fluid
value={pin}
onChange={(e, val) => setPin(val)}
onInput={(e, val) => setPin(val)}
/>
</LabeledList.Item>
</LabeledList>
<Button
fontSize={4}
fluid
mt={2}
textAlign="center"
onClick={() =>
act('attempt_auth', {
account_num: account,
account_pin: pin,
})
}
>
<Stack>
<Stack.Item>
<Icon name="sign-in-alt" />
</Stack.Item>
<Stack.Item grow textAlign="center">
Log In
</Stack.Item>
</Stack>
</Button>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item
ml={-5}
mr={-5}
onClick={() => act('insert_card')}
style={{ cursor: 'pointer' }}
>
<Stack align="center" justify="center" fill vertical>
<Stack.Item>
<Box height={10}>
<CardSlot />
</Box>
</Stack.Item>
<Stack.Item>
<AnimatedIDCard />
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
);
};
export const CardSlot = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 100 40">
{/* Top */}
<rect
rx="5"
x="0"
y="0"
width="100"
height="20"
strokeWidth={0}
fill="grey"
/>
{/* Faceplate */}
<rect x="0" y="10" width="100" height="30" fill="#aaa" />
{/* Slot */}
<rect
rx="5"
ry="5"
x="10"
y="25"
width="80"
height="10"
fill="#000"
stroke="#444"
/>
{/* Arrows */}
<polyline transform="translate(30, 15)" points="0 5, 5 0, 10 5" />
<polyline transform="translate(60, 15)" points="0 5, 5 0, 10 5" />
{/* Card */}
<g transform="translate(45, 11)">
{/* Slot */}
<rect rx="2" x="0" y="0" width="10" height="2.5" />
{/* Card Outline */}
<rect
rx="1"
x="2"
y="2"
width="6"
height="8"
fill="transparent"
stroke="black"
/>
{/* Chip */}
<rect rx="0.5" x="4" y="3" width="2" height="3" />
</g>
</svg>
);
};
export const AnimatedIDCard = (props) => {
const [glitch, setGlitch] = useState(false);
useEffect(() => {
if (Math.random() * 100 < 1) {
// 1% chance
setGlitch(true);
}
}, []);
return (
<Box
className={
glitch
? 'AutomatedTellerMachine__Card--glitch'
: 'AutomatedTellerMachine__Card'
}
mt={-8}
>
<DmIcon
icon="icons/obj/card_new.dmi"
icon_state="civilian-id"
width={32}
height={32}
style={{
transform: 'rotate(90deg)',
transformOrigin: 'center',
}}
/>
</Box>
);
};

View File

@@ -0,0 +1,42 @@
.AutomatedTellerMachine__Card {
animation: cardAnim 0.75s linear 0s infinite normal both;
}
@keyframes cardAnim {
0% {
transform: translate(0, 40px);
}
25% {
transform: translate(0, -32px);
}
100% {
transform: translate(0, 40px);
}
}
.AutomatedTellerMachine__Card--glitch {
animation: cardAnimGlitch 0.2s linear 0s infinite normal both;
transform-origin: 'center';
}
@keyframes cardAnimGlitch {
0% {
transform: translate(0, 40px) rotate(-45deg);
}
25% {
transform: translate(0, -32px) rotate(45deg);
}
100% {
transform: translate(0, 40px) rotate(-45deg);
}
}
.AutomatedTellerMachine__Table,
.AutomatedTellerMachine__Table td {
border: 4px solid #aaa;
border-style: ridge;
}

View File

@@ -45,6 +45,7 @@
@include meta.load-css('./components/Tooltip.scss');
// Interfaces
@include meta.load-css('./interfaces/AutomatedTellerMachine.scss');
@include meta.load-css('./interfaces/ListInput.scss');
@include meta.load-css('./interfaces/InputModal.scss');
@include meta.load-css('./interfaces/IntegratedCircuit.scss');