Skip to content

Commit 8bd22d1

Browse files
authored
review board ui with mock data (#529)
1 parent 642855c commit 8bd22d1

File tree

10 files changed

+907
-23
lines changed

10 files changed

+907
-23
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
Box,
3+
CircularProgress,
4+
FormControlLabel,
5+
Typography,
6+
} from "@mui/material";
7+
import ExternalTableFilter from "@/components/Shared/FilterSelect/ExternalTableFilter";
8+
import { useStaffUsersData } from "@/hooks/useStaff";
9+
import { useCallback, useEffect, useMemo, useState } from "react";
10+
import CustomSwitch from "@/components/Shared/Controlled/CustomSwitch";
11+
import { useAuth } from "react-oidc-context";
12+
13+
interface ReviewBoardFiltersProps {
14+
onFilterChange: (filterId: string, value: string[] | string) => void;
15+
externalFilters: Record<string, string[] | string>;
16+
initialChecked: boolean;
17+
}
18+
19+
const ReviewBoardFilters: React.FC<ReviewBoardFiltersProps> = ({
20+
onFilterChange,
21+
externalFilters,
22+
initialChecked,
23+
}) => {
24+
const { user: currentUser, isLoading: authLoading } = useAuth();
25+
const { data: staffUsers, isLoading: staffLoading } = useStaffUsersData();
26+
27+
// Internal state management
28+
const [checked, setChecked] = useState(initialChecked);
29+
30+
// Update internal state when initialChecked changes (for restoration)
31+
useEffect(() => {
32+
setChecked(initialChecked);
33+
}, [initialChecked]);
34+
35+
const primaryOfficerOptions = useMemo(
36+
() =>
37+
staffUsers?.map((user) => ({
38+
text: user.name,
39+
value: user.id.toString(),
40+
})) ?? [],
41+
[staffUsers]
42+
);
43+
44+
// Find current user in staff list
45+
const currentStaff = useMemo(() => {
46+
if (!currentUser?.profile?.preferred_username || !staffUsers)
47+
return undefined;
48+
return staffUsers.find(
49+
(staff) => staff.auth_user_guid === currentUser.profile.preferred_username
50+
);
51+
}, [currentUser?.profile?.preferred_username, staffUsers]);
52+
53+
// Determine if switch should be disabled
54+
const isSwitchDisabled = useMemo(() => {
55+
return authLoading || staffLoading || !currentStaff;
56+
}, [authLoading, staffLoading, currentStaff]);
57+
58+
// Handle switch change
59+
const handleSwitchChange = useCallback(
60+
(newChecked: boolean) => {
61+
setChecked(newChecked);
62+
63+
if (newChecked && currentStaff) {
64+
// When turning ON, set the primary officer filter to current user
65+
onFilterChange("primary_officer_id", [currentStaff.id.toString()]);
66+
} else {
67+
// When turning OFF, clear the primary officer filter
68+
onFilterChange("primary_officer_id", []);
69+
}
70+
},
71+
[currentStaff, onFilterChange]
72+
);
73+
74+
if (authLoading || staffLoading) {
75+
return <CircularProgress size={24} />;
76+
}
77+
78+
return (
79+
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
80+
<ExternalTableFilter
81+
filterId="primary_officer_id"
82+
filterOptions={primaryOfficerOptions}
83+
onFilterChange={onFilterChange}
84+
placeholder="Primary"
85+
variant="inline-standalone"
86+
isMulti={true}
87+
name="primaryOfficerFilter"
88+
currentValue={externalFilters.primary_officer_id}
89+
/>
90+
<FormControlLabel
91+
control={
92+
<CustomSwitch
93+
checked={checked}
94+
onChange={(_, value) => handleSwitchChange(value)}
95+
size="small"
96+
disabled={isSwitchDisabled}
97+
/>
98+
}
99+
label={
100+
<Typography variant="body1" mr={1}>
101+
<strong>{`${currentUser?.profile?.given_name}'s Files`}</strong>
102+
</Typography>
103+
}
104+
labelPlacement="start"
105+
sx={{
106+
marginRight: -1,
107+
}}
108+
/>
109+
</Box>
110+
);
111+
};
112+
113+
export default ReviewBoardFilters;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Box, Typography, Chip } from "@mui/material";
2+
import { BCDesignTokens } from "epic.theme";
3+
import { ReviewBoardSection as ReviewBoardSectionType } from "@/models/ReviewBoard";
4+
import ReviewBoardSectionItem from "@/components/App/ReviewBoard/ReviewBoardSectionItem";
5+
6+
const ReviewBoardSection = ({
7+
section,
8+
}: {
9+
section: ReviewBoardSectionType;
10+
}) => {
11+
return (
12+
<Box
13+
key={section.id}
14+
sx={{
15+
display: "flex",
16+
flexDirection: "column",
17+
gap: 1,
18+
backgroundColor: BCDesignTokens.surfaceColorBackgroundLightGray,
19+
borderRadius: BCDesignTokens.layoutBorderRadiusMedium,
20+
p: 1,
21+
width: 208,
22+
height: "calc(100% - 1rem)", // 1rem is the margin bottom of the main page box
23+
}}
24+
>
25+
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, pb: 1 }}>
26+
<Typography
27+
variant="caption"
28+
color={BCDesignTokens.typographyColorDisabled}
29+
fontWeight={BCDesignTokens.typographyFontWeightsBold}
30+
>
31+
{section.section}
32+
</Typography>
33+
<Chip
34+
size="small"
35+
color="default"
36+
label={section.items.length}
37+
sx={{
38+
backgroundColor: BCDesignTokens.typographyColorSecondaryInvert,
39+
color: BCDesignTokens.typographyColorPlaceholder,
40+
fontWeight: BCDesignTokens.typographyFontWeightsBold,
41+
borderRadius: BCDesignTokens.layoutBorderRadiusMedium,
42+
}}
43+
/>
44+
</Box>
45+
<Box
46+
sx={{
47+
display: "flex",
48+
flexDirection: "column",
49+
gap: 1,
50+
overflow: "auto",
51+
flex: 1,
52+
}}
53+
>
54+
{section.items.map((item) => (
55+
<ReviewBoardSectionItem key={item.id} item={item} />
56+
))}
57+
</Box>
58+
</Box>
59+
);
60+
};
61+
62+
export default ReviewBoardSection;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { ReviewBoardItem } from "@/models/ReviewBoard";
2+
import { APPROVAL_STATUS } from "@/utils/constants";
3+
import dateUtils from "@/utils/dateUtils";
4+
import { CalendarMonthRounded } from "@mui/icons-material";
5+
import { Box, Chip, Typography } from "@mui/material";
6+
import { BCDesignTokens } from "epic.theme";
7+
8+
const ReviewBoardSectionItem = ({ item }: { item: ReviewBoardItem }) => {
9+
const approvalCardColor = (approvalStatus: string) => {
10+
if (approvalStatus === APPROVAL_STATUS.APPROVED) {
11+
return "success";
12+
} else if (approvalStatus === APPROVAL_STATUS.APPROVAL_PENDING) {
13+
return "warning";
14+
} else if (approvalStatus === APPROVAL_STATUS.NOT_APPROVED) {
15+
return "error";
16+
}
17+
return "default";
18+
};
19+
20+
return (
21+
<Box
22+
key={item.id}
23+
sx={{
24+
display: "flex",
25+
flexDirection: "column",
26+
p: 1,
27+
backgroundColor: BCDesignTokens.surfaceColorBackgroundWhite,
28+
borderRadius: BCDesignTokens.layoutBorderRadiusMedium,
29+
border: `1px solid ${BCDesignTokens.surfaceColorBorderDefault}`,
30+
// height: 400,
31+
flexShrink: 0,
32+
}}
33+
>
34+
<Box
35+
sx={{
36+
display: "flex",
37+
flexWrap: "wrap",
38+
gap: 0.5,
39+
mb: 1,
40+
}}
41+
>
42+
<Chip
43+
variant="outlined"
44+
size="small"
45+
color={item.card_type.name === "IR" ? "default" : "warning"}
46+
label={`${item.card_type.name}${item.card_type.sub_type ? `: ${item.card_type.sub_type}` : ""}`}
47+
sx={{
48+
width: "fit-content",
49+
fontSize: "0.75rem",
50+
}}
51+
/>
52+
{item.approval_status ? (
53+
<Chip
54+
variant="outlined"
55+
size="small"
56+
color={approvalCardColor(item.approval_status.id)}
57+
label={item.approval_status.name}
58+
sx={{
59+
width: "fit-content",
60+
fontSize: "0.75rem",
61+
}}
62+
/>
63+
) : null}
64+
</Box>
65+
<Typography
66+
variant="body2"
67+
fontWeight={BCDesignTokens.typographyFontWeightsBold}
68+
color={BCDesignTokens.typographyColorLink}
69+
sx={{
70+
wordWrap: "break-word",
71+
overflowWrap: "break-word",
72+
whiteSpace: "normal",
73+
width: "100%",
74+
}}
75+
>
76+
{item.number}
77+
</Typography>
78+
<Typography
79+
variant="caption"
80+
color={BCDesignTokens.typographyColorPlaceholder}
81+
>
82+
{item.name}
83+
</Typography>
84+
<Box sx={{ display: "flex", alignItems: "center", mt: 1 }}>
85+
<CalendarMonthRounded
86+
sx={{
87+
fontSize: "1rem",
88+
marginRight: 0.25,
89+
color: BCDesignTokens.typographyColorDisabled,
90+
}}
91+
/>
92+
<Typography variant="caption">
93+
{dateUtils.formatDate(item.card_date)}
94+
</Typography>
95+
{item.types ? (
96+
<Typography
97+
variant="caption"
98+
sx={{
99+
backgroundColor: BCDesignTokens.surfaceColorBackgroundLightGray,
100+
padding: 0.5,
101+
marginLeft: 0.5,
102+
borderRadius: BCDesignTokens.layoutBorderRadiusMedium,
103+
}}
104+
>
105+
{item.types.map((type) => type.name).join(", ")}
106+
</Typography>
107+
) : null}
108+
</Box>
109+
<Typography
110+
variant="caption"
111+
sx={{
112+
width: "fit-content",
113+
backgroundColor: BCDesignTokens.surfaceColorBackgroundLightBlue,
114+
padding: 0.5,
115+
borderRadius: BCDesignTokens.layoutBorderRadiusMedium,
116+
}}
117+
>
118+
{item.primary_officer.last_name}
119+
</Typography>
120+
</Box>
121+
);
122+
};
123+
124+
export default ReviewBoardSectionItem;

compliance-web/src/components/Shared/SideNav/RouteItemsList.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
FormatListBulletedRounded,
33
SettingsRounded,
4+
ViewKanbanRounded,
45
} from "@mui/icons-material";
56

67
export interface RouteMenuItem {
@@ -34,6 +35,11 @@ export default function RouteItemsList() {
3435
},
3536
],
3637
},
38+
{
39+
routeName: "Review Board",
40+
icon: <ViewKanbanRounded />,
41+
path: "/review-board",
42+
},
3743
{
3844
routeName: "Admin",
3945
icon: <SettingsRounded />,

compliance-web/src/hooks/useRestorativeJustice.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const deleteRestorativeJustice = ({
5555
restorativeJusticeId,
5656
}: {
5757
restorativeJusticeId: number;
58-
}): Promise<void> => {
58+
}) => {
5959
return request({
6060
url: `/restorative-justices/${restorativeJusticeId}`,
6161
method: "delete",

0 commit comments

Comments
 (0)