diff --git a/tgui/packages/tgui/interfaces/CrewConsole.jsx b/tgui/packages/tgui/interfaces/CrewConsole.jsx
deleted file mode 100644
index ef3f8c31a38..00000000000
--- a/tgui/packages/tgui/interfaces/CrewConsole.jsx
+++ /dev/null
@@ -1,195 +0,0 @@
-import { sortBy } from 'common/collections';
-
-import { useBackend } from '../backend';
-import { Box, Button, Icon, Section, Table } from '../components';
-import { COLORS } from '../constants';
-import { Window } from '../layouts';
-
-const HEALTH_COLOR_BY_LEVEL = [
- '#17d568',
- '#c4cf2d',
- '#e67e22',
- '#ed5100',
- '#e74c3c',
- '#801308',
-];
-
-const STAT_LIVING = 0;
-const STAT_DEAD = 4;
-
-const jobIsHead = (jobId) => jobId % 10 === 0;
-
-const jobToColor = (jobId) => {
- if (jobId === 0) {
- return COLORS.department.captain;
- }
- if (jobId >= 10 && jobId < 20) {
- return COLORS.department.security;
- }
- if (jobId >= 20 && jobId < 30) {
- return COLORS.department.medbay;
- }
- if (jobId >= 30 && jobId < 40) {
- return COLORS.department.science;
- }
- if (jobId >= 40 && jobId < 50) {
- return COLORS.department.engineering;
- }
- if (jobId >= 50 && jobId < 60) {
- return COLORS.department.cargo;
- }
- if (jobId >= 60 && jobId < 200) {
- return COLORS.department.service;
- }
- if (jobId >= 200 && jobId < 230) {
- return COLORS.department.centcom;
- }
- return COLORS.department.other;
-};
-
-const statToIcon = (life_status) => {
- switch (life_status) {
- case STAT_LIVING:
- return 'heart';
- case STAT_DEAD:
- return 'skull';
- }
- return 'heartbeat';
-};
-
-const healthToAttribute = (oxy, tox, burn, brute, attributeList) => {
- const healthSum = oxy + tox + burn + brute;
- const level = Math.min(Math.max(Math.ceil(healthSum / 25), 0), 5);
- return attributeList[level];
-};
-
-const HealthStat = (props) => {
- const { type, value } = props;
- return (
-
- {value}
-
- );
-};
-
-export const CrewConsole = () => {
- return (
-
-
-
-
-
- );
-};
-
-const CrewTable = (props) => {
- const { act, data } = useBackend();
- const sensors = sortBy(data.sensors ?? [], (s) => s.ijob);
- return (
-
-
- Name
-
-
- Vitals
-
-
- Position
-
- {!!data.link_allowed && (
-
- Tracking
-
- )}
-
- {sensors.map((sensor) => (
-
- ))}
-
- );
-};
-
-const CrewTableEntry = (props) => {
- const { act, data } = useBackend();
- const { link_allowed } = data;
- const { sensor_data } = props;
- const {
- name,
- assignment,
- ijob,
- life_status,
- oxydam,
- toxdam,
- burndam,
- brutedam,
- area,
- can_track,
- } = sensor_data;
-
- return (
-
-
- {name}
- {assignment !== undefined ? ` (${assignment})` : ''}
-
-
- {oxydam !== undefined ? (
-
- ) : life_status !== STAT_DEAD ? (
-
- ) : (
-
- )}
-
-
- {oxydam !== undefined ? (
-
-
- {'/'}
-
- {'/'}
-
- {'/'}
-
-
- ) : life_status !== STAT_DEAD ? (
- 'Alive'
- ) : (
- 'Dead'
- )}
-
-
- {area !== undefined ? (
- area
- ) : (
-
- )}
-
- {!!link_allowed && (
-
-
- )}
-
- );
-};
diff --git a/tgui/packages/tgui/interfaces/CrewConsole.tsx b/tgui/packages/tgui/interfaces/CrewConsole.tsx
new file mode 100644
index 00000000000..aa4f272f248
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/CrewConsole.tsx
@@ -0,0 +1,307 @@
+import { BooleanLike } from 'common/react';
+import { createSearch } from 'common/string';
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import { Box, Button, Icon, Input, Section, Table } from '../components';
+import { COLORS } from '../constants';
+import { Window } from '../layouts';
+
+const HEALTH_COLOR_BY_LEVEL = [
+ '#17d568',
+ '#c4cf2d',
+ '#e67e22',
+ '#ed5100',
+ '#e74c3c',
+ '#801308',
+];
+
+const SORT_NAMES = {
+ ijob: 'Job',
+ name: 'Name',
+ area: 'Position',
+ health: 'Vitals',
+};
+
+const STAT_LIVING = 0;
+const STAT_DEAD = 4;
+
+const SORT_OPTIONS = ['ijob', 'name', 'area', 'health'];
+
+const jobIsHead = (jobId: number) => jobId % 10 === 0;
+
+const jobToColor = (jobId: number) => {
+ if (jobId === 0) {
+ return COLORS.department.captain;
+ }
+ if (jobId >= 10 && jobId < 20) {
+ return COLORS.department.security;
+ }
+ if (jobId >= 20 && jobId < 30) {
+ return COLORS.department.medbay;
+ }
+ if (jobId >= 30 && jobId < 40) {
+ return COLORS.department.science;
+ }
+ if (jobId >= 40 && jobId < 50) {
+ return COLORS.department.engineering;
+ }
+ if (jobId >= 50 && jobId < 60) {
+ return COLORS.department.cargo;
+ }
+ if (jobId >= 60 && jobId < 200) {
+ return COLORS.department.service;
+ }
+ if (jobId >= 200 && jobId < 230) {
+ return COLORS.department.centcom;
+ }
+ return COLORS.department.other;
+};
+
+const statToIcon = (life_status: number) => {
+ switch (life_status) {
+ case STAT_LIVING:
+ return 'heart';
+ case STAT_DEAD:
+ return 'skull';
+ }
+ return 'heartbeat';
+};
+
+const healthSort = (a: CrewSensor, b: CrewSensor) => {
+ if (a.life_status < b.life_status) return -1;
+ if (a.life_status > b.life_status) return 1;
+ if (a.health > b.health) return -1;
+ if (a.health < b.health) return 1;
+ return 0;
+};
+
+const areaSort = (a: CrewSensor, b: CrewSensor) => {
+ a.area ??= '~';
+ b.area ??= '~';
+ if (a.area < b.area) return -1;
+ if (a.area > b.area) return 1;
+ return 0;
+};
+
+const healthToAttribute = (
+ oxy: number,
+ tox: number,
+ burn: number,
+ brute: number,
+ attributeList: string[],
+) => {
+ const healthSum = oxy + tox + burn + brute;
+ const level = Math.min(Math.max(Math.ceil(healthSum / 25), 0), 5);
+ return attributeList[level];
+};
+
+type HealthStatProps = {
+ type: string;
+ value: number;
+};
+
+const HealthStat = (props: HealthStatProps) => {
+ const { type, value } = props;
+ return (
+
+ {value}
+
+ );
+};
+
+export const CrewConsole = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+type CrewSensor = {
+ name: string;
+ assignment: string | undefined;
+ ijob: number;
+ life_status: number;
+ oxydam: number;
+ toxdam: number;
+ burndam: number;
+ brutedam: number;
+ area: string | undefined;
+ health: number;
+ can_track: BooleanLike;
+ ref: string;
+};
+
+type CrewConsoleData = {
+ sensors: CrewSensor[];
+ link_allowed: BooleanLike;
+};
+
+const CrewTable = () => {
+ const { data } = useBackend();
+ const { sensors } = data;
+
+ const [sortAsc, setSortAsc] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortBy, setSortBy] = useState(SORT_OPTIONS[0]);
+
+ const cycleSortBy = () => {
+ let idx = SORT_OPTIONS.indexOf(sortBy) + 1;
+ if (idx === SORT_OPTIONS.length) idx = 0;
+ setSortBy(SORT_OPTIONS[idx]);
+ };
+
+ const nameSearch = createSearch(searchQuery, (crew: CrewSensor) => crew.name);
+
+ const sorted = sensors.filter(nameSearch).sort((a, b) => {
+ switch (sortBy) {
+ case 'name':
+ return sortAsc ? +(a.name > b.name) : +(b.name > a.name);
+ case 'ijob':
+ return sortAsc ? a.ijob - b.ijob : b.ijob - a.ijob;
+ case 'health':
+ return sortAsc ? healthSort(a, b) : healthSort(b, a);
+ case 'area':
+ return sortAsc ? areaSort(a, b) : areaSort(b, a);
+ default:
+ return 0;
+ }
+ });
+
+ return (
+
+ );
+};
+
+type CrewTableEntryProps = {
+ sensor_data: CrewSensor;
+};
+
+const CrewTableEntry = (props: CrewTableEntryProps) => {
+ const { act, data } = useBackend();
+ const { link_allowed } = data;
+ const { sensor_data } = props;
+ const {
+ name,
+ assignment,
+ ijob,
+ life_status,
+ oxydam,
+ toxdam,
+ burndam,
+ brutedam,
+ area,
+ can_track,
+ } = sensor_data;
+
+ return (
+
+
+ {name}
+ {assignment !== undefined ? ` (${assignment})` : ''}
+
+
+ {oxydam !== undefined ? (
+
+ ) : life_status !== STAT_DEAD ? (
+
+ ) : (
+
+ )}
+
+
+ {oxydam !== undefined ? (
+
+
+ {'/'}
+
+ {'/'}
+
+ {'/'}
+
+
+ ) : life_status !== STAT_DEAD ? (
+ 'Alive'
+ ) : (
+ 'Dead'
+ )}
+
+
+ {area !== '~' && area !== undefined ? (
+ area
+ ) : (
+
+ )}
+
+ {!!link_allowed && (
+
+
+
+ )}
+
+ );
+};