From 67d27ffe91c600e84774aee12256f381a33a1219 Mon Sep 17 00:00:00 2001
From: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Date: Tue, 2 Sep 2025 20:57:20 -0500
Subject: [PATCH] Accounting Glowup: Advances, Pay raises and cuts, and more
(#92419)
## About The Pull Request
Fixes #92188
1. The Accounting console now has a new UI
2. The Accounting console can now dish out advances on paychecks, giving
you for full paycheck early. You can give up to 3 advances. This of
course means your next paycheck is not paid out when relevant.
3. The Accounting console can now change paycheck sizes of crewmembers -
up to 1.5x and down to 0.5x.
## Why It's Good For The Game
- Gives the HoP some more duties, now being able to dish out money on
request, reward good behavior, or punish bad behavior.
## Changelog
:cl: Melbert
add: Accounting Console: New UI!
add: Accounting Console: Now can give advances to crewmembers
add: Accounting Console: Can now give pay raises or pay cuts
add: Accounting Console: Now only printable in the security lathe
add: Accounting Console: A spare board is now now found in secure tech
storage.
fix: Fix vending machines adding payments to audit log twice.
fix: Non-crewmembers are no longer shown in the accounting console
/:cl:
---------
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
---
code/controllers/subsystem/economy.dm | 7 +-
code/datums/components/payment.dm | 2 +-
code/game/machinery/computer/accounting.dm | 63 ++++++-
.../effects/spawners/random/techstorage.dm | 5 +-
code/modules/economy/account.dm | 37 ++--
code/modules/jobs/job_types/_job.dm | 2 +-
.../job_types/station_trait/cargo_gorilla.dm | 2 +-
.../research/designs/comp_board_designs.dm | 4 +-
.../shuttle/mobile_port/variants/supply.dm | 2 +-
code/modules/vending/vendor/inventory.dm | 1 -
.../tgui/interfaces/AccountingConsole.tsx | 132 --------------
.../AccountingConsole/AuditScreen.tsx | 43 +++++
.../AccountingConsole/FakeDesktopButton.tsx | 35 ++++
.../AccountingConsole/FakeToolbarButton.tsx | 34 ++++
.../AccountingConsole/FakeWindow.tsx | 64 +++++++
.../interfaces/AccountingConsole/Sort.tsx | 39 +++++
.../AccountingConsole/UsersScreen.tsx | 151 ++++++++++++++++
.../interfaces/AccountingConsole/helpers.ts | 15 ++
.../interfaces/AccountingConsole/index.tsx | 162 ++++++++++++++++++
.../interfaces/AccountingConsole/types.ts | 42 +++++
.../tgui/styles/interfaces/Accounting.scss | 42 +++++
tgui/packages/tgui/styles/main.scss | 1 +
22 files changed, 720 insertions(+), 165 deletions(-)
delete mode 100644 tgui/packages/tgui/interfaces/AccountingConsole.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/AuditScreen.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/FakeDesktopButton.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/FakeToolbarButton.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/FakeWindow.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/Sort.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/UsersScreen.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/helpers.ts
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/index.tsx
create mode 100644 tgui/packages/tgui/interfaces/AccountingConsole/types.ts
create mode 100644 tgui/packages/tgui/styles/interfaces/Accounting.scss
diff --git a/code/controllers/subsystem/economy.dm b/code/controllers/subsystem/economy.dm
index bbc72ee4470..9125936600f 100644
--- a/code/controllers/subsystem/economy.dm
+++ b/code/controllers/subsystem/economy.dm
@@ -157,7 +157,7 @@ SUBSYSTEM_DEF(economy)
var/datum/bank_account/bank_account = cached_processing[cached_processing[i]]
if(bank_account?.account_job && !ispath(bank_account.account_job))
temporary_total += (bank_account.account_job.paycheck * STARTING_PAYCHECKS)
- bank_account.payday(1)
+ bank_account.payday(1, skippable = TRUE)
station_total += bank_account.account_balance
if(MC_TICK_CHECK)
cached_processing.Cut(1, i + 1)
@@ -214,14 +214,15 @@ SUBSYSTEM_DEF(economy)
* * price_to_use: The cost of the purchase made for this transaction.
* * vendor: The object or structure medium that is charging the user. For Vending machines that's the machine, for payment component that's the parent, cargo that's the crate, etc.
*/
-/datum/controller/subsystem/economy/proc/track_purchase(datum/bank_account/account, price_to_use, vendor)
- if(!account || isnull(price_to_use) || !vendor)
+/datum/controller/subsystem/economy/proc/add_audit_entry(datum/bank_account/account, price_to_use, vendor)
+ if(isnull(account) || isnull(price_to_use) || !vendor)
CRASH("Track purchases was missing an argument! (Account, Price, or Vendor.)")
audit_log += list(list(
"account" = "[account.account_holder]",
"cost" = price_to_use,
"vendor" = "[vendor]",
+ "stationtime" = station_time_timestamp("hh:mm"),
))
/**
diff --git a/code/datums/components/payment.dm b/code/datums/components/payment.dm
index 37839f1599c..edb9762ba7e 100644
--- a/code/datums/components/payment.dm
+++ b/code/datums/components/payment.dm
@@ -168,7 +168,7 @@
log_econ("[total_cost] credits were spent on [parent] by [user] via [idcard.registered_account.account_holder]'s card.")
idcard.registered_account.bank_card_talk("[total_cost] credits deducted from your account.")
playsound(src, 'sound/effects/cashregister.ogg', 20, TRUE)
- SSeconomy.track_purchase(idcard.registered_account, total_cost, parent)
+ SSeconomy.add_audit_entry(idcard.registered_account, total_cost, parent)
return TRUE
/**
diff --git a/code/game/machinery/computer/accounting.dm b/code/game/machinery/computer/accounting.dm
index d804b8efe5d..75a890d382a 100644
--- a/code/game/machinery/computer/accounting.dm
+++ b/code/game/machinery/computer/accounting.dm
@@ -1,3 +1,7 @@
+#define MAX_ADVANCES 3
+#define MIN_PAY_MOD 0.5
+#define MAX_PAY_MOD 1.5
+
/obj/machinery/computer/accounting
name = "account lookup console"
desc = "Used to view crew member accounts and purchases."
@@ -17,17 +21,60 @@
. = ..()
var/list/data = list()
var/list/player_accounts = list()
- var/list/audit_list = SSeconomy.audit_log
- for(var/current_account as anything in SSeconomy.bank_accounts_by_id)
- var/datum/bank_account/current_bank_account = SSeconomy.bank_accounts_by_id[current_account]
+ for(var/id in SSeconomy.bank_accounts_by_id)
+ var/datum/bank_account/current_bank_account = SSeconomy.bank_accounts_by_id[id]
+ if(!(current_bank_account.account_job?.job_flags & JOB_CREW_MANIFEST))
+ continue
player_accounts += list(list(
"name" = current_bank_account.account_holder,
- "job" = current_bank_account.account_job?.title || "No job", // because this can be null
+ "job" = current_bank_account.account_job.title,
"balance" = round(current_bank_account.account_balance),
- "modifier" = round((current_bank_account.payday_modifier * 0.9), 0.1),
+ "modifier" = current_bank_account.payday_modifier,
+ "num_advances" = current_bank_account.paydays_to_skip,
+ "id" = id,
))
- data["PlayerAccounts"] = player_accounts
- data["AuditLog"] = audit_list
- data["Crashing"] = HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)
+ data["accounts"] = player_accounts
+ data["audit_log"] = SSeconomy.audit_log
+ data["crashing"] = HAS_TRAIT(SSeconomy, TRAIT_MARKET_CRASHING)
+ data["station_time"] = station_time_timestamp("hh:mm")
return data
+
+/obj/machinery/computer/accounting/ui_static_data(mob/user)
+ var/list/data = list()
+ var/static/ian_format = pick("png", "jpg", "jpeg", "webp", "bmp")
+ data["pic_file_format"] = ian_format
+ data["young_ian"] = check_holidays(IAN_HOLIDAY)
+ data["max_advances"] = MAX_ADVANCES
+ data["max_pay_mod"] = MAX_PAY_MOD
+ data["min_pay_mod"] = MIN_PAY_MOD
+ return data
+
+/obj/machinery/computer/accounting/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ playsound(src, SFX_TERMINAL_TYPE, 50, FALSE)
+ var/datum/bank_account/bank_account = SSeconomy.bank_accounts_by_id[params["account_id"]]
+ if(isnull(bank_account) || !(bank_account.account_job?.job_flags & JOB_CREW_MANIFEST))
+ return
+
+ switch(action)
+ if("paycheck_advance")
+ if(bank_account.paydays_to_skip < MAX_ADVANCES)
+ bank_account.payday(1, event = "Paycheck advance")
+ bank_account.paydays_to_skip += 1
+ return TRUE
+ if("change_pay_mod")
+ var/old_modifier = bank_account.payday_modifier
+ bank_account.payday_modifier = clamp(round(text2num(params["pay_mod"]), 0.05), MIN_PAY_MOD, MAX_PAY_MOD)
+ var/new_check_total = bank_account.payday_modifier * bank_account.account_job.paycheck
+ var/raise_or_cut = new_check_total > old_modifier * bank_account.account_job.paycheck ? "raised" : "cut"
+ bank_account.bank_card_talk("Paycheck [raise_or_cut] to [new_check_total]cr.")
+ SSeconomy.add_audit_entry(bank_account, new_check_total, "Paycheck [raise_or_cut]")
+ return TRUE
+
+#undef MAX_ADVANCES
+#undef MIN_PAY_MOD
+#undef MAX_PAY_MOD
diff --git a/code/game/objects/effects/spawners/random/techstorage.dm b/code/game/objects/effects/spawners/random/techstorage.dm
index 2fd7808847c..d114e071901 100644
--- a/code/game/objects/effects/spawners/random/techstorage.dm
+++ b/code/game/objects/effects/spawners/random/techstorage.dm
@@ -126,9 +126,10 @@
/obj/effect/spawner/random/techstorage/command_all
name = "secure command circuit board spawner"
loot = list(
- /obj/item/circuitboard/computer/crew,
- /obj/item/circuitboard/computer/communications,
+ /obj/item/circuitboard/computer/accounting,
/obj/item/circuitboard/computer/bankmachine,
+ /obj/item/circuitboard/computer/communications,
+ /obj/item/circuitboard/computer/crew,
)
/obj/effect/spawner/random/techstorage/rnd_secure_all
diff --git a/code/modules/economy/account.dm b/code/modules/economy/account.dm
index 8ec8e5b2d8a..986def70031 100644
--- a/code/modules/economy/account.dm
+++ b/code/modules/economy/account.dm
@@ -37,6 +37,8 @@
var/list/transaction_history = list()
///A lazylist of coupons redeemed with the Coupon Master pda app associated with this account.
var/list/redeemed_coupons
+ /// How many paychecks to skip when payday is called.
+ var/paydays_to_skip = 0
/datum/bank_account/New(newname, job, modifier = 1, player_account = TRUE)
account_holder = newname
@@ -188,10 +190,21 @@
* Arguments:
* * amount_of_paychecks - literally the number of salaries, 1 for issuing one salary, 5 for issuing five salaries.
* * free - issuance of free funds, if TRUE then takes funds from the void, if FALSE (default) tries to send from the department's account.
+ * * skippable - if TRUE, this proc may pay out nothing if the account has paydays_to_skip
+ * * event - the name of the event that is being processed, used for bank card messages.
*/
-/datum/bank_account/proc/payday(amount_of_paychecks, free = FALSE)
+/datum/bank_account/proc/payday(amount_of_paychecks, free = FALSE, skippable = FALSE, event = "Payday")
if(!account_job)
- return
+ return FALSE
+
+ if(skippable && !free)
+ while(paydays_to_skip > 0 && amount_of_paychecks > 0)
+ amount_of_paychecks -= 1
+ paydays_to_skip -= 1
+
+ if(amount_of_paychecks <= 0)
+ return FALSE
+
var/money_to_transfer = round(account_job.paycheck * payday_modifier * amount_of_paychecks)
if(amount_of_paychecks == 1)
money_to_transfer = clamp(money_to_transfer, 0, PAYCHECK_CREW) //We want to limit single, passive paychecks to regular crew income.
@@ -201,17 +214,15 @@
SSeconomy.station_target += money_to_transfer
log_econ("[money_to_transfer] credits were given to [src.account_holder]'s account from income.")
return TRUE
- else
- var/datum/bank_account/department_account = SSeconomy.get_dep_account(account_job.paycheck_department)
- if(department_account)
- if(!transfer_money(department_account, money_to_transfer))
- bank_card_talk("ERROR: Payday aborted, departmental funds insufficient.")
- return FALSE
- else
- bank_card_talk("Payday processed, account now holds [account_balance] cr.")
- return TRUE
- bank_card_talk("ERROR: Payday aborted, unable to contact departmental account.")
- return FALSE
+ var/datum/bank_account/department_account = SSeconomy.get_dep_account(account_job.paycheck_department)
+ if(isnull(department_account))
+ bank_card_talk("ERROR: [event] aborted, unable to contact departmental account.")
+ return FALSE
+ if(!transfer_money(department_account, money_to_transfer))
+ bank_card_talk("ERROR: [event] aborted, departmental funds insufficient.")
+ return FALSE
+ bank_card_talk("[event] processed, account now holds [account_balance] cr.")
+ return TRUE
/**
* This sends a local chat message to the owner of a bank account, on all ID cards registered to the bank_account.
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index 869c59c7044..449392abe26 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -203,7 +203,7 @@
/mob/living/carbon/human/on_job_equipping(datum/job/equipping, client/player_client)
if(equipping.paycheck_department)
var/datum/bank_account/bank_account = new(real_name, equipping, dna.species.payday_modifier)
- bank_account.payday(STARTING_PAYCHECKS, TRUE)
+ bank_account.payday(STARTING_PAYCHECKS, free = TRUE)
account_id = bank_account.account_id
bank_account.replaceable = FALSE
add_mob_memory(/datum/memory/key/account, remembered_id = account_id)
diff --git a/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm b/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm
index 4f0af0d2d06..c11c931a3ef 100644
--- a/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm
+++ b/code/modules/jobs/job_types/station_trait/cargo_gorilla.dm
@@ -34,7 +34,7 @@
. = ..()
// Gorilla with a wage, what's he buyin?
var/datum/bank_account/bank_account = new(spawned.real_name, src)
- bank_account.payday(STARTING_PAYCHECKS, TRUE)
+ bank_account.payday(STARTING_PAYCHECKS, free = TRUE)
bank_account.replaceable = FALSE
spawned.add_mob_memory(/datum/memory/key/account, remembered_id = bank_account.account_id)
diff --git a/code/modules/research/designs/comp_board_designs.dm b/code/modules/research/designs/comp_board_designs.dm
index 078e3aabcc0..2daa2404c08 100644
--- a/code/modules/research/designs/comp_board_designs.dm
+++ b/code/modules/research/designs/comp_board_designs.dm
@@ -392,9 +392,9 @@
build_type = IMPRINTER
build_path = /obj/item/circuitboard/computer/accounting
category = list(
- RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_RECORDS
+ RND_CATEGORY_COMPUTER + RND_SUBCATEGORY_COMPUTER_COMMAND
)
- departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+ departmental_flags = DEPARTMENT_BITFLAG_SECURITY //Honestly should have a bridge techfab for this sometime.
/datum/design/board/shuttle
category = list("Computer Boards", "Shuttle Machinery")
diff --git a/code/modules/shuttle/mobile_port/variants/supply.dm b/code/modules/shuttle/mobile_port/variants/supply.dm
index c48ab01178e..a6c5030605a 100644
--- a/code/modules/shuttle/mobile_port/variants/supply.dm
+++ b/code/modules/shuttle/mobile_port/variants/supply.dm
@@ -218,7 +218,7 @@ GLOBAL_LIST_INIT(blacklisted_cargo_types, typecacheof(list(
if(spawning_order.charge_on_purchase)
receiver_message += " [price] credits have been charged to your bank account"
paying_for_this.bank_card_talk(receiver_message)
- SSeconomy.track_purchase(paying_for_this, price, spawning_order.pack.name)
+ SSeconomy.add_audit_entry(paying_for_this, price, spawning_order.pack.name)
var/datum/bank_account/department/cargo = SSeconomy.get_dep_account(ACCOUNT_CAR)
cargo.adjust_money(price - pack_cost) //Cargo gets the handling fee
value += pack_cost
diff --git a/code/modules/vending/vendor/inventory.dm b/code/modules/vending/vendor/inventory.dm
index 0777ea2b160..f180058f8a6 100644
--- a/code/modules/vending/vendor/inventory.dm
+++ b/code/modules/vending/vendor/inventory.dm
@@ -278,7 +278,6 @@
var/datum/bank_account/paying_id_account = SSeconomy.get_dep_account(payment_department)
if(paying_id_account)
SSblackbox.record_feedback("amount", "vending_spent", price_to_use)
- SSeconomy.track_purchase(account, price_to_use, name)
log_econ("[price_to_use] credits were inserted into [src] by [account.account_holder] to buy [product_to_vend].")
credits_contained += round(price_to_use * VENDING_CREDITS_COLLECTION_AMOUNT)
return TRUE
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole.tsx b/tgui/packages/tgui/interfaces/AccountingConsole.tsx
deleted file mode 100644
index 11d1fbdd0c1..00000000000
--- a/tgui/packages/tgui/interfaces/AccountingConsole.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import { useState } from 'react';
-import {
- Blink,
- BlockQuote,
- Collapsible,
- Modal,
- Section,
- Stack,
- Tabs,
-} from 'tgui-core/components';
-import type { BooleanLike } from 'tgui-core/react';
-
-import { useBackend } from '../backend';
-import { Window } from '../layouts';
-
-type Data = {
- PlayerAccounts: PlayerAccount[];
- AuditLog: AuditLog[];
- Crashing: BooleanLike;
-};
-
-type PlayerAccount = {
- index: number;
- name: string;
- balance: number;
- job: string;
- modifier: number;
-};
-
-type AuditLog = {
- index: number;
- account: number;
- cost: number;
- vendor: string;
-};
-
-enum SCREENS {
- users,
- audit,
-}
-
-export const AccountingConsole = (props) => {
- const [screenmode, setScreenmode] = useState(SCREENS.users);
-
- return (
-
-
-
-
-
-
- setScreenmode(SCREENS.users)}
- >
- Users
-
- setScreenmode(SCREENS.audit)}
- >
- Audit
-
-
-
-
- {screenmode === SCREENS.users && }
- {screenmode === SCREENS.audit && }
-
-
-
-
- );
-};
-
-const UsersScreen = (props) => {
- const { data } = useBackend();
- const { PlayerAccounts } = data;
-
- return (
-
- {PlayerAccounts.map((account) => (
-
-
-
-
- {account.balance} credit balance
-
-
- Employee has {account.modifier * 100}% pay modifier
-
-
-
-
- ))}
-
- );
-};
-
-const AuditScreen = (props) => {
- const { data } = useBackend();
- const { AuditLog } = data;
-
- return (
-
- {AuditLog.map((purchase) => (
-
- {purchase.account} spent {purchase.cost} cr at{' '}
- {purchase.vendor}.
-
- ))}
-
- );
-};
-
-/** The modal menu that contains the prompts to making new channels. */
-const MarketCrashing = (props) => {
- const { data } = useBackend();
-
- const { Crashing } = data;
- if (!Crashing) {
- return null;
- }
- return (
-
-
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/AuditScreen.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/AuditScreen.tsx
new file mode 100644
index 00000000000..d6799d9291f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/AuditScreen.tsx
@@ -0,0 +1,43 @@
+import { Blink, Modal, Section, Table } from 'tgui-core/components';
+import { useBackend } from '../../backend';
+import { getRandomDoomMessage } from './helpers';
+import type { Data } from './types';
+
+export const AuditScreen = () => {
+ const { data } = useBackend();
+ const { crashing, audit_log } = data;
+
+ return (
+
+ {!!crashing && (
+
+
+
+ )}
+
+
+ Account
+ Cost
+ Location
+ Timestamp
+
+ {audit_log.map((purchase, index) => (
+
+ {purchase.account}
+
+ {purchase.cost} cr
+
+
+ {purchase.vendor}
+
+
+ {purchase.stationtime || '00:00'} ST
+
+
+ ))}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/FakeDesktopButton.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/FakeDesktopButton.tsx
new file mode 100644
index 00000000000..b429fa33d23
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/FakeDesktopButton.tsx
@@ -0,0 +1,35 @@
+import { Button, Stack } from 'tgui-core/components';
+
+import { useBackend } from '../../backend';
+import type { SCREENS } from './types';
+
+type FakeDesktopButtonProps = {
+ children: React.ReactNode;
+ name: string;
+ setScreenmode: (mode: SCREENS) => void;
+ ownerScreenMode: SCREENS;
+};
+
+export const FakeDesktopButton = (props: FakeDesktopButtonProps) => {
+ const { act } = useBackend();
+ const { children, name, setScreenmode, ownerScreenMode } = props;
+
+ return (
+ <>
+
+
+
+
+ {name}
+
+ >
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/FakeToolbarButton.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/FakeToolbarButton.tsx
new file mode 100644
index 00000000000..edea840372e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/FakeToolbarButton.tsx
@@ -0,0 +1,34 @@
+import { Button } from 'tgui-core/components';
+import { useBackend } from '../../backend';
+import type { SCREENS } from './types';
+
+type FakeToolbarButtonProps = {
+ name: string;
+ currentScreenMode: SCREENS;
+ setScreenmode: (mode: SCREENS) => void;
+ ownerScreenMode: SCREENS;
+};
+
+export const FakeToolbarButton = (props: FakeToolbarButtonProps) => {
+ const { act } = useBackend();
+ const { name, currentScreenMode, setScreenmode, ownerScreenMode } = props;
+
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/FakeWindow.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/FakeWindow.tsx
new file mode 100644
index 00000000000..da9ee0865bc
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/FakeWindow.tsx
@@ -0,0 +1,64 @@
+import { Box, Button, DmIcon, Stack } from 'tgui-core/components';
+import { useBackend } from '../../backend';
+import { type Data, SCREENS } from './types';
+
+type FakeWindowProps = {
+ name: string;
+ setScreenmode: (mode: SCREENS) => void;
+};
+export const FakeWindowIan = (props: FakeWindowProps) => {
+ const { data } = useBackend();
+ const { young_ian } = data;
+
+ return (
+
+
+
+ );
+};
+export const FakeWindow = (
+ props: FakeWindowProps & {
+ children: React.ReactNode;
+ },
+) => {
+ const { act } = useBackend();
+ const { name, children, setScreenmode } = props;
+
+ return (
+
+
+
+
+ {name}
+
+
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/Sort.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/Sort.tsx
new file mode 100644
index 00000000000..c8c8cc49ec7
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/Sort.tsx
@@ -0,0 +1,39 @@
+import { Button } from 'tgui-core/components';
+import { useBackend } from '../../backend';
+import { SORTING } from './types';
+
+type SortButtonProps = {
+ sorting: SORTING;
+ setSorting: (sorting: SORTING) => void;
+ otherSorters: ((sorting: SORTING) => void)[];
+};
+
+export const SortButton = (props: SortButtonProps) => {
+ const { act } = useBackend();
+ const { sorting, setSorting, otherSorters } = props;
+
+ return (
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/UsersScreen.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/UsersScreen.tsx
new file mode 100644
index 00000000000..271ecfb014a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/UsersScreen.tsx
@@ -0,0 +1,151 @@
+import {
+ Blink,
+ Button,
+ Modal,
+ NumberInput,
+ Section,
+ Stack,
+ Table,
+} from 'tgui-core/components';
+import { useBackend, useSharedState } from '../../backend';
+import { getRandomDoomMessage } from './helpers';
+import { SortButton } from './Sort';
+import { type Data, SORTING } from './types';
+
+export const UsersScreen = () => {
+ const { act, data } = useBackend();
+ const { crashing, accounts, max_pay_mod, min_pay_mod, max_advances } = data;
+
+ const [accountNameSorting, setAccountNameSorting] = useSharedState(
+ 'sorting_account_name',
+ SORTING.ascending,
+ );
+ const [balanceSorting, setBalanceSorting] = useSharedState(
+ 'sorting_balance',
+ SORTING.none,
+ );
+ const [jobSorting, setJobSorting] = useSharedState(
+ 'sorting_job',
+ SORTING.none,
+ );
+
+ const accountsSorted = accounts.sort((a, b) => {
+ if (accountNameSorting === SORTING.ascending) {
+ return a.name > b.name ? 1 : -1;
+ } else if (accountNameSorting === SORTING.descending) {
+ return a.name > b.name ? -1 : 1;
+ } else if (balanceSorting === SORTING.ascending) {
+ return a.balance - b.balance;
+ } else if (balanceSorting === SORTING.descending) {
+ return b.balance - a.balance;
+ } else if (jobSorting === SORTING.ascending) {
+ return a.job > b.job ? 1 : -1;
+ } else if (jobSorting === SORTING.descending) {
+ return a.job > b.job ? -1 : 1;
+ }
+ return 0;
+ });
+
+ return (
+
+ {!!crashing && (
+
+
+
+ )}
+
+
+
+
+ Account
+
+
+
+
+
+
+
+ Balance
+
+
+
+
+
+
+
+ Job
+
+
+
+
+
+ Pay
+ Advances
+
+ {accountsSorted.map((account, index) => (
+
+ {account.name}
+
+ {account.balance} cr
+
+
+ {account.job}
+
+
+
+ act('change_pay_mod', {
+ account_id: account.id,
+ pay_mod: value,
+ })
+ }
+ />
+
+
+
+ {account.num_advances}
+
+
+
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/helpers.ts b/tgui/packages/tgui/interfaces/AccountingConsole/helpers.ts
new file mode 100644
index 00000000000..6b75c73ffba
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/helpers.ts
@@ -0,0 +1,15 @@
+const doomMessages = [
+ 'BUY GOLD!',
+ 'BUY LOW, SELL HIGH!',
+ 'INVEST IN CRYPTO!',
+ 'SELL EVERYTHING!',
+ 'THE ECONOMY IS COLLAPSING!',
+ 'THE ECONOMY IS RUINED!',
+ 'THE MARKET IS CRASHING!',
+ 'THE STATION IS GOING BANKRUPT!',
+];
+
+// Used when the economy is crashing to get a random funny message.
+export function getRandomDoomMessage(): string {
+ return doomMessages[Math.floor(Math.random() * doomMessages.length)];
+}
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/index.tsx b/tgui/packages/tgui/interfaces/AccountingConsole/index.tsx
new file mode 100644
index 00000000000..064e3eff952
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/index.tsx
@@ -0,0 +1,162 @@
+import { Button, DmIcon, Stack } from 'tgui-core/components';
+
+import { useBackend, useSharedState } from '../../backend';
+import { Window } from '../../layouts';
+import { AuditScreen } from './AuditScreen';
+import { FakeDesktopButton } from './FakeDesktopButton';
+import { FakeToolbarButton } from './FakeToolbarButton';
+import { FakeWindow, FakeWindowIan } from './FakeWindow';
+import { type Data, SCREENS } from './types';
+import { UsersScreen } from './UsersScreen';
+
+export const AccountingConsole = () => {
+ const { data } = useBackend();
+ const {
+ station_time = '00:00',
+ pic_file_format = 'png',
+ young_ian = false,
+ } = data;
+ const [screenmode, setScreenmode] = useSharedState('screen', SCREENS.none);
+
+ const ianFileName = young_ian
+ ? `Ian's first birthday.${pic_file_format}`
+ : `Ian.${pic_file_format}`;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {screenmode === SCREENS.users && (
+
+
+
+
+
+ )}
+ {screenmode === SCREENS.audit && (
+
+
+
+
+
+ )}
+ {screenmode === SCREENS.ian && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/AccountingConsole/types.ts b/tgui/packages/tgui/interfaces/AccountingConsole/types.ts
new file mode 100644
index 00000000000..5d714b180ed
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AccountingConsole/types.ts
@@ -0,0 +1,42 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+export type Data = {
+ accounts: PlayerAccount[];
+ audit_log: AuditLog[];
+ crashing: BooleanLike;
+ pic_file_format: string;
+ max_pay_mod: number;
+ min_pay_mod: number;
+ max_advances: number;
+ station_time: string;
+ young_ian: BooleanLike;
+};
+
+type PlayerAccount = {
+ id: number;
+ name: string;
+ balance: number;
+ job: string;
+ modifier: number;
+ num_advances: number;
+};
+
+type AuditLog = {
+ account: number;
+ cost: number;
+ vendor: string;
+ stationtime: string;
+};
+
+export enum SCREENS {
+ none,
+ users,
+ audit,
+ ian,
+}
+
+export enum SORTING {
+ ascending,
+ descending,
+ none,
+}
diff --git a/tgui/packages/tgui/styles/interfaces/Accounting.scss b/tgui/packages/tgui/styles/interfaces/Accounting.scss
new file mode 100644
index 00000000000..d48ef846d73
--- /dev/null
+++ b/tgui/packages/tgui/styles/interfaces/Accounting.scss
@@ -0,0 +1,42 @@
+.Accounting__Toolbar {
+ outline: solid 1px;
+ outline-style: outset;
+ outline-width: 2px;
+ outline-color: hsl(0, 0%, 85%);
+ background-color: rgb(195, 195, 195);
+ color: grey;
+}
+
+.Accounting__Window {
+ flex-direction: column;
+ outline: solid 1px black;
+ outline-style: outset;
+ outline-width: 2px;
+ outline-color: hsl(0, 0%, 85%);
+ background-color: rgb(195, 195, 195);
+}
+
+.Accounting__WindowContent {
+ background-color: white;
+ outline: solid;
+ outline-style: outset;
+ outline-width: 2px;
+ outline-color: hsl(0, 0%, 85%);
+ background-color: rgb(195, 195, 195);
+}
+
+.Accounting__TableHeader {
+ border-style: solid;
+ border-width: 2px;
+ border-left: 0px;
+ border-right: 0px;
+ border-bottom: 0px;
+}
+
+.Accounting__TableCellSides {
+ border-style: solid;
+ border-width: 1px;
+ border-top: 0px;
+ border-right: 0px;
+ border-bottom: 0px;
+}
diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss
index ad5d9498735..67289ecb747 100644
--- a/tgui/packages/tgui/styles/main.scss
+++ b/tgui/packages/tgui/styles/main.scss
@@ -16,6 +16,7 @@
@include meta.load-css('~tgui-core/styles/all-themes.scss');
// Interfaces
+@include meta.load-css('./interfaces/Accounting.scss');
@include meta.load-css('./interfaces/AlertModal.scss');
@include meta.load-css('./interfaces/Changelog.scss');
@include meta.load-css('./interfaces/CrewManifest.scss');