mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 04:30:44 +01:00
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 | | - | - | |  |  | Express Console | Before | After | | - | - | |  |  | Other | Content preview | Express login | | - | - | |  |  | </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:
@@ -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)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user