Supply Consoles restyle & order content preview (#89198)

## About The Pull Request
In a nutshell, everything will be visible in the `Demo` section.
But if you want some details...

- Filling of static_data was divided into procs, for better
understanding and possibility to use the same actions in two order
consoles
- Added an image of the first item in crate at each order (almost)
- Added the ability to see what is inside an order, the information is
not always accurate due to the nature of some orders such as random hats
- Slightly remaked layout, making everything look more cohesive and
eliminating the empty space at the bottom of the screen
- When buying anonymously, the original price does not disappear, but is
crossed out, and the new price is written below it

## Demo
<details><summary> Screenshots </summary>

Supply Console
| Before | After |
| - | - |
|
![image](https://github.com/user-attachments/assets/6067fc76-de5a-4d32-929a-239074514767)
|
![image](https://github.com/user-attachments/assets/81b3ab2a-f6d6-4f7a-8fff-0d5f459bcef9)
|

Express Console
| Before | After |
| - | - |
|
![image](https://github.com/user-attachments/assets/934554e5-ec42-422f-b6ce-f66a137bd280)
|
![image](https://github.com/user-attachments/assets/0d00c364-eece-4aa4-8f7d-f40ab7b9bb27)
|

Other 
| Content preview | Express login | 
| - | - |
|
![image](https://github.com/user-attachments/assets/62b3374e-5f46-4113-9da9-f481aa25739c)
|
![image](https://github.com/user-attachments/assets/8468f191-a5da-4de4-86f1-e0aca10256b3)
|

</details>

<details><summary> Video </summary>


https://github.com/user-attachments/assets/c7e16206-39f5-413c-9eea-c0284350c4a3

</details>

## Why It's Good For The Game
More convenience plus a bit prettier.
The ability to view the contents of an order, even if some madman didn't
describe its contents in the description (those who did this are really
crazy)
Section with tabs scrolls on both 515 and 516
And little cleaner data receiving code

## Changelog

🆑
qol: Supply console got little redesign, and ability to see what can be
inside a order.
code: Supply console and express supply console, now use a unified
method of acquiring data
/🆑

---------

Co-authored-by: Ivory <distributivgesetz93@gmail.com>
Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
This commit is contained in:
Aylong
2025-01-30 08:00:12 -08:00
committed by GitHub
co-authored by Ivory Jeremiah
parent d5bc76c273
commit d773f4db2f
9 changed files with 494 additions and 314 deletions
+12 -21
View File
@@ -92,26 +92,17 @@
/obj/machinery/computer/cargo/express/proc/packin_up(forced = FALSE) // oh shit, I'm sorry
meme_pack_data = list() // sorry for what?
if (!forced && !SSshuttle.initialized) // Subsystem is still sleeping, add ourselves to its buffer and abort
SSshuttle.express_consoles += src
return
for(var/pack in SSshuttle.supply_packs) // our quartermaster taught us not to be ashamed of our supply packs
var/datum/supply_pack/P = SSshuttle.supply_packs[pack] // specially since they're such a good price and all
if(!meme_pack_data[P.group]) // yeah, I see that, your quartermaster gave you good advice
meme_pack_data[P.group] = list( // it gets cheaper when I return it
"name" = P.group, // mmhm
"packs" = list() // sometimes, I return it so much, I rip the manifest
) // see, my quartermaster taught me a few things too
if((P.hidden) || (P.special)) // like, how not to rip the manifest
continue// by using someone else's crate
if(P.contraband && !contraband) // will you show me?
continue // i'd be right happy to
meme_pack_data[P.group]["packs"] += list(list(
"name" = P.name,
"cost" = P.get_cost() * get_discount(),
"id" = pack,
"desc" = P.desc || P.name // If there is a description, use it. Otherwise use the pack's name.
))
if(!forced && !SSshuttle.initialized) // our quartermaster taught us not to be ashamed of our supply packs
SSshuttle.express_consoles += src // specially since they're such a good price and all
return // yeah, I see that, your quartermaster gave you good advice
// it gets cheaper when I return it
for(var/pack_id in SSshuttle.supply_packs) // mmhm
var/datum/supply_pack/pack = SSshuttle.supply_packs[pack_id] // sometimes, I return it so much, I rip the manifest
if(!meme_pack_data[pack.group]) // see, my quartermaster taught me a few things too
meme_pack_data[pack.group] = list( // like, how not to rip the manifest
"name" = pack.group, // by using someone else's crate
"packs" = get_packs_data(pack.group, express = TRUE), // will you show me?
) // i'd be right happy to
/obj/machinery/computer/cargo/express/ui_data(mob/user)
var/canBeacon = beacon && (isturf(beacon.loc) || ismob(beacon.loc))//is the beacon in a valid location?
@@ -146,7 +137,7 @@
data["supplies"] = meme_pack_data
return data
/obj/machinery/computer/cargo/express/proc/get_discount()
/obj/machinery/computer/cargo/express/get_discount()
return (obj_flags & EMAGGED) ? EXPRESS_EMAG_DISCOUNT : 1
/obj/machinery/computer/cargo/express/ui_act(action, params, datum/tgui/ui)
+72 -20
View File
@@ -95,9 +95,9 @@
var/list/data = list()
data["department"] = "Cargo" // Hardcoded here, for customization in budgetordering.dm AKA NT IRN
data["location"] = SSshuttle.supply.getStatusText()
var/datum/bank_account/D = SSeconomy.get_dep_account(cargo_account)
if(D)
data["points"] = D.account_balance
var/datum/bank_account/bank = SSeconomy.get_dep_account(cargo_account)
if(bank)
data["points"] = bank.account_balance
data["grocery"] = SSshuttle.chef_groceries.len
data["away"] = SSshuttle.supply.getDockedId() == docking_away
data["self_paid"] = self_paid
@@ -162,26 +162,78 @@
var/list/data = list()
data["max_order"] = CARGO_MAX_ORDER
data["supplies"] = list()
for(var/pack in SSshuttle.supply_packs)
var/datum/supply_pack/P = SSshuttle.supply_packs[pack]
if(!data["supplies"][P.group])
data["supplies"][P.group] = list(
"name" = P.group,
"packs" = list()
for(var/pack_id in SSshuttle.supply_packs)
var/datum/supply_pack/pack = SSshuttle.supply_packs[pack_id]
if(!data["supplies"][pack.group])
data["supplies"][pack.group] = list(
"name" = pack.group,
"packs" = get_packs_data(pack.group),
)
if((P.hidden && !(obj_flags & EMAGGED)) || (P.contraband && !contraband) || (P.special && !P.special_enabled) || P.drop_pod_only)
continue
data["supplies"][P.group]["packs"] += list(list(
"name" = P.name,
"cost" = P.get_cost(),
"id" = pack,
"desc" = P.desc || P.name, // If there is a description, use it. Otherwise use the pack's name.
"goody" = P.goody,
"access" = P.access,
"contraband" = P.contraband,
))
return data
/**
* returns a list of supply packs for a certain group
* * group - the group of packs to return
* * express - if this is an express console
*/
/obj/machinery/computer/cargo/proc/get_packs_data(group, express = FALSE)
var/list/packs = list()
for(var/pack_id in SSshuttle.supply_packs)
var/datum/supply_pack/pack = SSshuttle.supply_packs[pack_id]
if(pack.group != group)
continue
// Express console packs check
if(express && (pack.hidden || pack.special))
continue
if(!express && ((pack.hidden && !(obj_flags & EMAGGED)) || (pack.special && !pack.special_enabled) || pack.drop_pod_only))
continue
if(pack.contraband && !contraband)
continue
var/obj/item/first_item = length(pack.contains) > 0 ? pack.contains[1] : null
packs += list(list(
"name" = pack.name,
"cost" = pack.get_cost() * get_discount(),
"id" = pack_id,
"desc" = pack.desc || pack.name, // If there is a description, use it. Otherwise use the pack's name.
"first_item_icon" = first_item?.icon,
"first_item_icon_state" = first_item?.icon_state,
"goody" = pack.goody,
"access" = pack.access,
"contraband" = pack.contraband,
"contains" = get_pack_contains(pack),
))
return packs
/**
* returns a list of the contents of a supply pack
* * pack - the pack to get the contents of
*/
/obj/machinery/computer/cargo/proc/get_pack_contains(datum/supply_pack/pack)
var/list/contains = list()
for(var/obj/item/item as anything in pack.contains)
contains += list(list(
"name" = item.name,
"icon" = item.greyscale_config ? null : item.icon,
"icon_state" = item.greyscale_config ? null : item.icon_state,
"amount" = pack.contains[item]
))
return contains
/**
* returns the discount multiplier applied to all supply packs,
* the discount is calculated as follows: pack_cost * get_discount()
*/
/obj/machinery/computer/cargo/proc/get_discount()
return 1
/**
* adds an supply pack to the checkout cart
* * user - the mobe doing this order
@@ -11,7 +11,6 @@ import {
import { formatMoney } from 'tgui-core/format';
import { useBackend } from '../../backend';
import { CargoCartButtons } from './CargoButtons';
import { CargoData } from './types';
export function CargoCart(props) {
@@ -23,16 +22,18 @@ export function CargoCart(props) {
return (
<Stack fill vertical>
<Stack.Item grow>
<Section fill scrollable title="Cart" buttons={<CargoCartButtons />}>
<Section fill scrollable>
<CheckoutItems />
</Section>
</Stack.Item>
{cart.length > 0 && !!can_send && (
<Stack.Item>
<Section align="right">
<Stack.Item m={0}>
<Section textAlign="right">
<Stack fill align="center">
<Stack.Item grow>
{!sendable && <Icon color="blue" name="toolbox" spin />}
{!sendable && (
<Icon mr={0.5} size={1.5} color="blue" name="toolbox" spin />
)}
</Stack.Item>
<Stack.Item>
<Button
@@ -82,25 +83,24 @@ function CheckoutItems(props) {
<Table.Cell>{entry.object}</Table.Cell>
<Table.Cell width={11}>
{can_send && entry.can_be_cancelled ? (
<RestrictedInput
width={5}
minValue={0}
maxValue={max_order}
value={entry.amount}
onEnter={(e, value) =>
act('modify', {
order_name: entry.object,
amount: value,
})
}
/>
) : (
<Input width="40px" value={entry.amount} disabled />
)}
{!!can_send && !!entry.can_be_cancelled && (
{!!can_send && !!entry.can_be_cancelled ? (
<>
<Button
icon="minus"
onClick={() => act('remove', { order_name: entry.object })}
/>
<RestrictedInput
width={5}
minValue={0}
maxValue={max_order}
value={entry.amount}
onEnter={(e, value) =>
act('modify', {
order_name: entry.object,
amount: value,
})
}
/>
<Button
icon="plus"
disabled={amount_by_name[entry.object] >= max_order}
@@ -108,11 +108,9 @@ function CheckoutItems(props) {
act('add_by_name', { order_name: entry.object })
}
/>
<Button
icon="minus"
onClick={() => act('remove', { order_name: entry.object })}
/>
</>
) : (
<Input width="40px" value={entry.amount} disabled />
)}
</Table.Cell>
@@ -1,19 +1,20 @@
import { sortBy } from 'common/collections';
import { useMemo } from 'react';
import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import {
BlockQuote,
Button,
Icon,
Input,
ImageButton,
Modal,
Section,
Stack,
Table,
Tabs,
Tooltip,
} from 'tgui-core/components';
import { formatMoney } from 'tgui-core/format';
import { useBackend, useSharedState } from '../../backend';
import { CargoCartButtons } from './CargoButtons';
import { SearchBar } from '../common/SearchBar';
import { searchForSupplies } from './helpers';
import { CargoData, Supply, SupplyCategory } from './types';
@@ -22,19 +23,17 @@ type Props = {
};
export function CargoCatalog(props: Props) {
const { data } = useBackend<CargoData>();
const { express } = props;
const { act, data } = useBackend<CargoData>();
const { self_paid } = data;
const supplies = Object.values(data.supplies);
const [showContents, setShowContents] = useState('');
const [searchText, setSearchText] = useSharedState('search_text', '');
const [activeSupplyName, setActiveSupplyName] = useSharedState(
'supply',
supplies[0]?.name,
);
const [searchText, setSearchText] = useSharedState('search_text', '');
const packs = useMemo(() => {
let fetched: Supply[] | undefined;
@@ -54,42 +53,35 @@ export function CargoCatalog(props: Props) {
}, [activeSupplyName, supplies, searchText]);
return (
<Section
fill
title="Catalog"
buttons={
!express && (
<>
<CargoCartButtons />
<Button
color={self_paid ? 'caution' : 'transparent'}
icon={self_paid ? 'check-square-o' : 'square-o'}
ml={2}
onClick={() => act('toggleprivate')}
tooltip="Use your own funds to purchase items."
>
Buy Privately
</Button>
</>
)
}
>
<>
{showContents && (
<CatalogPackInfo
packs={packs}
name={showContents}
closeContents={setShowContents}
/>
)}
<Stack fill>
<Stack.Item grow>
<CatalogTabs
activeSupplyName={activeSupplyName}
categories={supplies}
searchText={searchText}
setActiveSupplyName={setActiveSupplyName}
setSearchText={setSearchText}
/>
<Stack.Item grow mr={-1.33}>
<Section fill>
<CatalogTabs
express={express}
activeSupplyName={activeSupplyName}
categories={supplies}
searchText={searchText}
setActiveSupplyName={setActiveSupplyName}
setSearchText={setSearchText}
/>
</Section>
</Stack.Item>
<Stack.Divider />
<Stack.Item grow={express ? 2 : 3}>
<CatalogList packs={packs} />
<Stack.Item grow={express ? 2 : 3} m={0}>
<Section fill scrollable>
<CatalogList packs={packs} openContents={setShowContents} />
</Section>
</Stack.Item>
</Stack>
</Section>
</>
);
}
@@ -101,143 +93,245 @@ type CatalogTabsProps = {
setSearchText: (text: string) => void;
};
function CatalogTabs(props: CatalogTabsProps) {
function CatalogTabs(props: CatalogTabsProps & Props) {
const { act, data } = useBackend<CargoData>();
const {
activeSupplyName,
categories,
searchText,
setActiveSupplyName,
setSearchText,
express,
} = props;
const { self_paid } = data;
const sorted = sortBy(categories, (supply) => supply.name);
return (
<Tabs vertical>
<Tabs.Tab
key="search_results"
selected={activeSupplyName === 'search_results'}
>
<Stack align="center">
<Stack.Item>
<Icon name="search" />
</Stack.Item>
<Stack.Item grow>
<Input
fluid
placeholder="Search..."
value={searchText}
onInput={(e, value) => {
if (value === searchText) {
return;
}
<Stack fill vertical>
<Stack.Item>
<SearchBar
query={searchText}
onSearch={(value) => {
if (value === searchText) {
return;
}
if (value.length) {
// Start showing results
setActiveSupplyName('search_results');
} else if (activeSupplyName === 'search_results') {
// return to normal category
setActiveSupplyName(sorted[0]?.name);
}
setSearchText(value);
}}
/>
</Stack.Item>
</Stack>
</Tabs.Tab>
{sorted.map((supply) => (
<Tabs.Tab
className="candystripe"
color={supply.name === activeSupplyName ? 'green' : undefined}
key={supply.name}
selected={supply.name === activeSupplyName}
onClick={() => {
setActiveSupplyName(supply.name);
setSearchText('');
if (value.length) {
// Start showing results
setActiveSupplyName('search_results');
} else if (activeSupplyName === 'search_results') {
// return to normal category
setActiveSupplyName(sorted[0]?.name);
}
setSearchText(value);
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{supply.name}</span>
<span> {supply.packs.length}</span>
</div>
</Tabs.Tab>
))}
</Tabs>
/>
</Stack.Item>
<Stack.Item grow p={1} m={-1} mt={1} overflowY="auto">
<Tabs vertical>
<Tabs.Tab
key="search_results"
selected={activeSupplyName === 'search_results'}
style={{ display: 'none' }}
/>
{sorted.map((supply) => (
<Tabs.Tab
className="candystripe"
color={supply.name === activeSupplyName ? 'green' : undefined}
key={supply.name}
selected={supply.name === activeSupplyName}
onClick={() => {
setActiveSupplyName(supply.name);
setSearchText('');
}}
>
<Stack justify="space-between">
<span>{supply.name}</span>
<span> {supply.packs.length}</span>
</Stack>
</Tabs.Tab>
))}
</Tabs>
</Stack.Item>
<Stack.Item>
{!express && (
<Button
fluid
color={self_paid ? 'caution' : 'transparent'}
icon={self_paid ? 'check-square-o' : 'square-o'}
onClick={() => act('toggleprivate')}
tooltip="Use your own funds to purchase items."
tooltipPosition="top"
>
Buy Privately
</Button>
)}
</Stack.Item>
</Stack>
);
}
type CatalogListProps = {
packs: SupplyCategory['packs'];
openContents: Dispatch<SetStateAction<string>>;
};
function CatalogList(props: CatalogListProps) {
const { act, data } = useBackend<CargoData>();
const { amount_by_name = {}, max_order, self_paid, app_cost } = data;
const { packs = [] } = props;
const { packs = [], openContents } = props;
return (
<Section fill scrollable>
<Table>
{packs.map((pack) => {
let color = '';
const digits = Math.floor(Math.log10(pack.cost) + 1);
if (self_paid) {
color = 'caution';
} else if (digits >= 5 && digits <= 6) {
color = 'yellow';
} else if (digits > 6) {
color = 'bad';
}
<>
{packs.map((pack) => {
let color = '';
const digits = Math.floor(Math.log10(pack.cost) + 1);
if (self_paid) {
color = 'yellow';
} else if (digits >= 5 && digits <= 6) {
color = 'orange';
} else if (digits > 6) {
color = 'bad';
}
return (
<Table.Row key={pack.name} className="candystripe">
<Table.Cell color="label">{pack.name}</Table.Cell>
<Table.Cell collapsing>
{!!pack.small_item && (
<Tooltip content="Small Item">
<Icon color="purple" name="compress-alt" />
</Tooltip>
)}
</Table.Cell>
<Table.Cell collapsing>
{!!pack.access && (
<Tooltip content="Restricted">
<Icon color="average" name="lock" />
</Tooltip>
)}
{!!pack.contraband && (
<Tooltip content="Contraband">
<Icon color="bad" name="pastafarianism" />
</Tooltip>
)}
</Table.Cell>
<Table.Cell collapsing textAlign="right">
<Button
color={color}
tooltip={pack.desc}
tooltipPosition="left"
disabled={(amount_by_name[pack.name] || 0) >= max_order}
onClick={() =>
act('add', {
id: pack.id,
})
}
minWidth={5}
mb={0.5}
>
{formatMoney(
(self_paid && !pack.goody) || app_cost
? Math.round(pack.cost * 1.1)
: pack.cost,
)}{' '}
cr
</Button>
</Table.Cell>
</Table.Row>
);
})}
</Table>
</Section>
const privateBuy = (self_paid && !pack.goody) || app_cost;
const tooltipIcon = (content: string, icon: string, color: string) => (
<Stack.Item>
<Tooltip content={content}>
<Icon color={color} name={icon} />
</Tooltip>
</Stack.Item>
);
return (
<ImageButton
key={pack.id}
fluid
dmIcon={pack.first_item_icon}
dmIconState={pack.first_item_icon_state}
imageSize={32}
color={color}
disabled={(amount_by_name[pack.name] || 0) >= max_order}
buttonsAlt={
<Button
color="transparent"
icon="info"
onClick={() => openContents(pack.name)}
/>
}
onClick={() =>
act('add', {
id: pack.id,
})
}
>
<Stack fill textAlign="right">
<Stack.Item grow textAlign="left">
{pack.name}
</Stack.Item>
{(!!pack.small_item || !!pack.access || !!pack.contraband) && (
<Stack.Item>
<Stack reverse>
{!!pack.small_item &&
tooltipIcon('Small Item', 'compress-alt', 'purple')}
{!!pack.access &&
tooltipIcon('Restricted', 'lock', 'average')}
{!!pack.contraband &&
tooltipIcon('Contraband', 'pastafarianism', 'bad')}
</Stack>
</Stack.Item>
)}
<Stack.Item align="center" width={5.5} mt={-0.75} mb={-0.75}>
<Stack vertical color="gold" lineHeight={0.75} fontSize={0.85}>
<Stack.Item
opacity={privateBuy && 0.75}
style={{ textDecoration: privateBuy && 'red line-through' }}
>
{formatMoney(pack.cost)} cr
</Stack.Item>
{!!privateBuy && (
<Stack.Item>
{formatMoney(Math.round(pack.cost * 1.1))} cr
</Stack.Item>
)}
</Stack>
</Stack.Item>
</Stack>
</ImageButton>
);
})}
</>
);
}
type CatalogContentsProps = {
name: string;
closeContents: Dispatch<SetStateAction<string>>;
packs: SupplyCategory['packs'];
};
function CatalogPackInfo(props: CatalogContentsProps) {
const { name, packs, closeContents } = props;
const pack = packs.find((pack) => pack.name === name);
const contains = pack?.contains;
return (
<Modal p={1} width="50vw" height="50vh">
<Stack fill vertical>
<Stack.Item>
<Section
fill
title={name}
buttons={
<Button
icon="close"
color="bad"
onClick={() => closeContents('')}
/>
}
>
<BlockQuote>{pack?.desc || 'No description available.'}</BlockQuote>
</Section>
</Stack.Item>
<Stack.Item m={0} grow>
<Section fill scrollable>
{contains && contains.length > 0 ? (
contains.map((item) => (
<ImageButton
key={item.name}
fluid
dmIcon={item.icon}
dmIconState={item.icon_state}
buttonsAlt={
!!item.amount && (
<Stack.Item backgroundColor="rgba(255, 255, 255, 0.1">
x{item.amount}
</Stack.Item>
)
}
imageSize={32}
>
<Stack fill>
<Stack.Item textAlign="left">{item.name}</Stack.Item>
</Stack>
</ImageButton>
))
) : (
<Stack fill vertical align="center" justify="center">
<Stack.Item>
<Icon name="triangle-exclamation" size={6} color="orange" />
</Stack.Item>
<Stack.Item mt={2} color="label" textAlign="center">
{`We can't find information about even the approximate contents
of this order.`}
</Stack.Item>
</Stack>
)}
</Section>
</Stack.Item>
</Stack>
</Modal>
);
}
@@ -10,22 +10,7 @@ export function CargoRequests(props) {
const { requests = [], requestonly, can_send, can_approve_requests } = data;
return (
<Section
fill
scrollable
title="Active Requests"
buttons={
!requestonly && (
<Button
icon="times"
color="transparent"
onClick={() => act('denyall')}
>
Clear
</Button>
)
}
>
<Section fill scrollable>
{requests.length === 0 && <NoticeBox success>No Requests</NoticeBox>}
{requests.length > 0 && (
<Table>
@@ -30,7 +30,7 @@ export function CargoStatus(props) {
<Section
title={department}
buttons={
<Box inline bold>
<Box inline bold verticalAlign="middle">
<AnimatedNumber
value={points}
format={(value) => formatMoney(value)}
+62 -40
View File
@@ -1,7 +1,9 @@
import { Stack, Tabs } from 'tgui-core/components';
import { Button, Section, Stack, Tabs } from 'tgui-core/components';
import { toTitleCase } from 'tgui-core/string';
import { useBackend, useSharedState } from '../../backend';
import { Window } from '../../layouts';
import { CargoCartButtons } from './CargoButtons';
import { CargoCart } from './CargoCart';
import { CargoCatalog } from './CargoCatalog';
import { CargoHelp } from './CargoHelp';
@@ -11,7 +13,7 @@ import { CargoData } from './types';
enum TAB {
Catalog = 'catalog',
Requests = 'requests',
Requests = 'active requests',
Cart = 'cart',
Help = 'help',
}
@@ -27,10 +29,8 @@ export function Cargo(props) {
}
export function CargoContent(props) {
const { data } = useBackend<CargoData>();
const { act, data } = useBackend<CargoData>();
const { cart = [], requests = [], requestonly } = data;
const [tab, setTab] = useSharedState('cargotab', TAB.Catalog);
let amount = 0;
@@ -44,44 +44,66 @@ export function CargoContent(props) {
<CargoStatus />
</Stack.Item>
<Stack.Item>
<Tabs fluid>
<Tabs.Tab
icon="list"
selected={tab === TAB.Catalog}
onClick={() => setTab(TAB.Catalog)}
>
Catalog
</Tabs.Tab>
<Tabs.Tab
icon="envelope"
textColor={tab !== TAB.Requests && requests.length > 0 && 'yellow'}
selected={tab === TAB.Requests}
onClick={() => setTab(TAB.Requests)}
>
Requests ({requests.length})
</Tabs.Tab>
{!requestonly && (
<Section
title={toTitleCase(tab || '')}
buttons={
<>
<Tabs.Tab
icon="shopping-cart"
textColor={tab !== TAB.Cart && amount > 0 && 'yellow'}
selected={tab === TAB.Cart}
onClick={() => setTab(TAB.Cart)}
>
Checkout ({amount})
</Tabs.Tab>
<Tabs.Tab
icon="question"
selected={tab === TAB.Help}
onClick={() => setTab(TAB.Help)}
>
Help
</Tabs.Tab>
{tab === TAB.Requests && !requestonly && (
<Button
icon="times"
color="transparent"
onClick={() => act('denyall')}
>
Clear
</Button>
)}
{(tab === TAB.Catalog || tab === TAB.Cart) && (
<CargoCartButtons />
)}
</>
)}
</Tabs>
}
>
<Tabs fluid m={-1}>
<Tabs.Tab
icon="list"
selected={tab === TAB.Catalog}
onClick={() => setTab(TAB.Catalog)}
>
Catalog
</Tabs.Tab>
<Tabs.Tab
icon="envelope"
textColor={
tab !== TAB.Requests && requests.length > 0 && 'yellow'
}
selected={tab === TAB.Requests}
onClick={() => setTab(TAB.Requests)}
>
Requests ({requests.length})
</Tabs.Tab>
{!requestonly && (
<>
<Tabs.Tab
icon="shopping-cart"
textColor={tab !== TAB.Cart && amount > 0 && 'yellow'}
selected={tab === TAB.Cart}
onClick={() => setTab(TAB.Cart)}
>
Checkout ({amount})
</Tabs.Tab>
<Tabs.Tab
icon="question"
selected={tab === TAB.Help}
onClick={() => setTab(TAB.Help)}
>
Help
</Tabs.Tab>
</>
)}
</Tabs>
</Section>
</Stack.Item>
<Stack.Item grow mt={0}>
<Stack.Item grow m={0}>
{tab === TAB.Catalog && <CargoCatalog />}
{tab === TAB.Requests && <CargoRequests />}
{tab === TAB.Cart && <CargoCart />}
@@ -31,11 +31,21 @@ export type Supply = {
access: BooleanLike;
cost: number;
desc: string;
first_item_icon: string | null;
first_item_icon_state: string | null;
goody: BooleanLike;
id: string;
name: string;
small_item: BooleanLike;
contraband: BooleanLike;
contains: SupplyItem[];
};
type SupplyItem = {
name: string;
icon: string | null;
icon_state: string | null;
amount: number;
};
type CartEntry = {
+65 -37
View File
@@ -2,15 +2,17 @@ import {
AnimatedNumber,
Box,
Button,
Icon,
LabeledList,
NoticeBox,
Section,
Stack,
} from 'tgui-core/components';
import { BooleanLike } from 'tgui-core/react';
import { useBackend } from '../backend';
import { Window } from '../layouts';
import { CargoCatalog } from './Cargo/CargoCatalog';
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
type Data = {
locked: BooleanLike;
@@ -18,21 +20,41 @@ type Data = {
using_beacon: BooleanLike;
beaconzone: string;
beaconName: string;
beaconError: BooleanLike;
canBuyBeacon: BooleanLike;
hasBeacon: BooleanLike;
canBeacon: BooleanLike;
printMsg: string;
message: string;
};
export function CargoExpress(props) {
const { data } = useBackend<Data>();
const { locked } = data;
const { beaconError, canBeacon, message, locked } = data;
return (
<Window width={600} height={700}>
<Window.Content scrollable>
<InterfaceLockNoticeBox accessText="a Cargo Technician-level ID card" />
{!locked && <CargoExpressContent />}
<Window.Content>
{locked ? (
<Section fill>
<Stack fill vertical textAlign={'center'} justify={'center'}>
<Stack.Item bold color={'red'}>
<Icon mb={3} name={'lock'} size={7.5} />
<br />
{`Swipe a Cargo Technician-level ID card to unlock this interface.`}
</Stack.Item>
</Stack>
</Section>
) : (
<Stack fill vertical>
<NoticeBox color={beaconError || !canBeacon ? 'red' : 'blue'}>
{message}
</NoticeBox>
<Stack.Item grow m={0}>
<CargoExpressContent />
</Stack.Item>
</Stack>
)}
</Window.Content>
</Window>
);
@@ -42,7 +64,6 @@ function CargoExpressContent(props) {
const { act, data } = useBackend<Data>();
const {
hasBeacon,
message,
points,
using_beacon,
beaconzone,
@@ -52,36 +73,43 @@ function CargoExpressContent(props) {
} = data;
return (
<>
<Section
title="Cargo Express"
buttons={
<Box inline bold>
<AnimatedNumber value={Math.round(points)} />
{' credits'}
</Box>
}
>
<LabeledList>
<LabeledList.Item label="Landing Location">
<Button selected={!using_beacon} onClick={() => act('LZCargo')}>
Cargo Bay
</Button>
<Button
selected={using_beacon}
disabled={!hasBeacon}
onClick={() => act('LZBeacon')}
>
{beaconzone} ({beaconName})
</Button>
<Button disabled={!canBuyBeacon} onClick={() => act('printBeacon')}>
{printMsg}
</Button>
</LabeledList.Item>
<LabeledList.Item label="Notice">{message}</LabeledList.Item>
</LabeledList>
</Section>
<CargoCatalog express />
</>
<Stack fill vertical>
<Stack.Item>
<Section
title="Cargo Express"
buttons={
<Box inline bold verticalAlign={'middle'}>
<AnimatedNumber value={Math.round(points)} />
{' credits'}
</Box>
}
>
<LabeledList>
<LabeledList.Item label="Landing Location">
<Button selected={!using_beacon} onClick={() => act('LZCargo')}>
Cargo Bay
</Button>
<Button
selected={using_beacon}
disabled={!hasBeacon}
tooltip={beaconzone}
onClick={() => act('LZBeacon')}
>
{beaconName}
</Button>
<Button
disabled={!canBuyBeacon}
onClick={() => act('printBeacon')}
>
{printMsg}
</Button>
</LabeledList.Item>
</LabeledList>
</Section>
</Stack.Item>
<Stack.Item grow m={0}>
<CargoCatalog express />
</Stack.Item>
</Stack>
);
}