Skip to content

Commit 560e4c2

Browse files
committed
Converted more pages to smarttabs
1 parent bb7a17c commit 560e4c2

File tree

6 files changed

+201
-229
lines changed

6 files changed

+201
-229
lines changed

src/attendance/components/Tabs.tsx

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,30 @@
11
import React from "react";
22
import { Permissions, UserHelper, Locale } from "@churchapps/apphelper";
3-
import { Box, Paper, Tabs as MaterialTabs, Tab } from "@mui/material";
3+
import { Box, Paper } from "@mui/material";
44
import { ReportWithFilter } from "../../components/reporting";
5+
import { SmartTabs } from "../../components/ui";
56

67
export const Tabs: React.FC = () => {
7-
const [selectedTab, setSelectedTab] = React.useState("");
8-
const [tabIndex, setTabIndex] = React.useState(0);
9-
10-
// Determine a sensible default tab based on access
11-
const defaultTab = React.useMemo(() => {
12-
if (UserHelper.checkAccess(Permissions.attendanceApi.attendance.view)) return "attendance";
13-
if (UserHelper.checkAccess(Permissions.attendanceApi.attendance.view)) return "groups";
14-
return "setup";
15-
}, []);
16-
17-
// Initialize selected tab via effect to avoid state updates during render
18-
React.useEffect(() => {
19-
if (!selectedTab && defaultTab) setSelectedTab(defaultTab);
20-
}, [selectedTab, defaultTab]);
21-
22-
const getTab = (index: number, keyName: string, icon: string, text: string) => (
23-
<Tab
24-
key={index}
25-
style={{ textTransform: "none", color: "#000" }}
26-
onClick={() => {
27-
setSelectedTab(keyName);
28-
setTabIndex(index);
29-
}}
30-
label={<>{text}</>}
31-
/>
32-
);
33-
34-
const tabs = [];
35-
let currentTab = null;
36-
if (UserHelper.checkAccess(Permissions.attendanceApi.attendance.view)) {
37-
tabs.push(getTab(0, "attendance", "calendar_month", Locale.label("attendance.tabs.attTrend")));
38-
}
39-
if (UserHelper.checkAccess(Permissions.attendanceApi.attendance.view)) {
40-
tabs.push(getTab(1, "groups", "person", Locale.label("attendance.tabs.groupAtt")));
41-
}
42-
43-
switch (selectedTab) {
44-
case "attendance":
45-
currentTab = <ReportWithFilter keyName="attendanceTrend" autoRun={true} />;
46-
break;
47-
case "groups":
48-
currentTab = <ReportWithFilter keyName="groupAttendance" autoRun={true} />;
49-
break;
50-
default:
51-
currentTab = <div>{Locale.label("attendance.tabs.noImplement")}</div>;
52-
break;
53-
}
8+
const canView = UserHelper.checkAccess(Permissions.attendanceApi.attendance.view);
9+
const tabs = [
10+
{
11+
key: "attendance",
12+
label: Locale.label("attendance.tabs.attTrend"),
13+
content: <ReportWithFilter keyName="attendanceTrend" autoRun={true} />,
14+
hidden: !canView,
15+
},
16+
{
17+
key: "groups",
18+
label: Locale.label("attendance.tabs.groupAtt"),
19+
content: <ReportWithFilter keyName="groupAttendance" autoRun={true} />,
20+
hidden: !canView,
21+
},
22+
];
5423

5524
return (
5625
<Paper>
5726
<Box>
58-
<MaterialTabs value={tabIndex} style={{ borderBottom: "1px solid #CCC" }} data-cy="group-tabs">
59-
{tabs}
60-
</MaterialTabs>
61-
{currentTab}
27+
<SmartTabs tabs={tabs} ariaLabel="attendance-tabs" />
6228
</Box>
6329
</Paper>
6430
);

src/components/ui/SmartTabs.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from "react";
2+
import { Box, Tabs as MuiTabs, Tab } from "@mui/material";
3+
4+
export interface SmartTabItem {
5+
key: string;
6+
label: React.ReactNode;
7+
content: React.ReactNode;
8+
disabled?: boolean;
9+
hidden?: boolean;
10+
}
11+
12+
interface SmartTabsProps {
13+
tabs: SmartTabItem[];
14+
value?: string; // controlled key
15+
onChange?: (key: string) => void;
16+
ariaLabel?: string;
17+
}
18+
19+
export const SmartTabs: React.FC<SmartTabsProps> = ({ tabs, value, onChange, ariaLabel }) => {
20+
const visibleTabs = React.useMemo(() => tabs.filter((t) => !t.hidden), [tabs]);
21+
22+
const defaultKey = React.useMemo(() => visibleTabs[0]?.key ?? "", [visibleTabs]);
23+
const [internalKey, setInternalKey] = React.useState<string>(value ?? defaultKey);
24+
25+
React.useEffect(() => {
26+
// If controlled value changes, sync internal
27+
if (value !== undefined) setInternalKey(value);
28+
}, [value]);
29+
30+
React.useEffect(() => {
31+
// If tabs change and current key is no longer visible, fall back to first
32+
if (!visibleTabs.find((t) => t.key === internalKey)) {
33+
setInternalKey(defaultKey);
34+
if (onChange && defaultKey) onChange(defaultKey);
35+
}
36+
}, [visibleTabs, internalKey, defaultKey, onChange]);
37+
38+
const selectedKey = value ?? internalKey;
39+
const selectedIndex = Math.max(0, visibleTabs.findIndex((t) => t.key === selectedKey));
40+
const handleChange = (_: React.SyntheticEvent, newIndex: number) => {
41+
const newKey = visibleTabs[newIndex]?.key;
42+
if (!newKey) return;
43+
if (value === undefined) setInternalKey(newKey);
44+
onChange?.(newKey);
45+
};
46+
47+
const current = visibleTabs.find((t) => t.key === selectedKey) ?? visibleTabs[0];
48+
49+
return (
50+
<Box>
51+
<MuiTabs value={selectedIndex} onChange={handleChange} aria-label={ariaLabel} sx={{ borderBottom: "1px solid #CCC" }}>
52+
{visibleTabs.map((t) => (
53+
<Tab key={t.key} label={t.label} disabled={t.disabled} />
54+
))}
55+
</MuiTabs>
56+
<Box sx={{ mt: 2 }}>{current?.content}</Box>
57+
</Box>
58+
);
59+
};

src/components/ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { StatusChip } from "./StatusChip";
33
export { EmptyState } from "./EmptyState";
44
export { CardWithHeader } from "./CardWithHeader";
55
export { LoadingButton } from "./LoadingButton";
6+
export { SmartTabs } from "./SmartTabs";

src/forms/FormsPage.tsx

Lines changed: 53 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { type FormInterface } from "@churchapps/helpers";
44
import { ApiHelper, UserHelper, Permissions, Loading, Locale } from "@churchapps/apphelper";
55
import { Link } from "react-router-dom";
66
import {
7-
Icon, Table, TableBody, TableCell, TableRow, TableHead, Box, Typography, Stack, Button, Card, Tab
7+
Icon, Table, TableBody, TableCell, TableRow, TableHead, Box, Typography, Stack, Button, Card
88
} from "@mui/material";
99
import { Description as DescriptionIcon, Add as AddIcon, Archive as ArchiveIcon } from "@mui/icons-material";
1010
import { SmallButton } from "@churchapps/apphelper";
1111
import { PageHeader } from "@churchapps/apphelper";
1212
import { useQuery } from "@tanstack/react-query";
13+
import { SmartTabs } from "../components/ui";
1314

1415
export const FormsPage = () => {
1516
const [selectedFormId, setSelectedFormId] = React.useState("notset");
@@ -147,33 +148,63 @@ export const FormsPage = () => {
147148

148149
if (forms.isLoading || archivedForms.isLoading) return <Loading />;
149150

150-
const contents = (
151+
const renderTable = (rows: JSX.Element[]) => (
151152
<Table>
152153
<TableHead>{getTableHeader()}</TableHead>
153-
<TableBody>{selectedTab === "forms" ? getRows() : getArchivedRows()}</TableBody>
154+
<TableBody>{rows}</TableBody>
154155
</Table>
155156
);
156157

157-
const getTab = (keyName: string, icon: string, text: string) => (
158-
<Tab
159-
key={keyName}
160-
style={{ textTransform: "none", color: "#000" }}
161-
onClick={() => {
162-
setSelectedTab(keyName);
163-
}}
164-
label={<>{text}</>}
165-
/>
158+
const formsCount = forms.data?.length || 0;
159+
const archivedCount = archivedForms.data?.length || 0;
160+
161+
const formsCard = (
162+
<Card sx={{ mt: getSidebar() ? 2 : 0 }}>
163+
<Box sx={{ p: 2, borderBottom: 1, borderColor: "divider" }}>
164+
<Stack direction="row" justifyContent="space-between" alignItems="center">
165+
<Stack direction="row" spacing={1} alignItems="center">
166+
<DescriptionIcon />
167+
<Typography variant="h6">{Locale.label("forms.formsPage.forms")}</Typography>
168+
</Stack>
169+
<Typography variant="body2" color="text.secondary">
170+
{`${formsCount} ${formsCount === 1 ? "form" : "forms"}`}
171+
</Typography>
172+
</Stack>
173+
</Box>
174+
<Box sx={{ p: 0 }}>{renderTable(getRows())}</Box>
175+
</Card>
176+
);
177+
178+
const archivedCard = (
179+
<Card>
180+
<Box sx={{ p: 2, borderBottom: 1, borderColor: "divider" }}>
181+
<Stack direction="row" justifyContent="space-between" alignItems="center">
182+
<Stack direction="row" spacing={1} alignItems="center">
183+
<ArchiveIcon />
184+
<Typography variant="h6">{Locale.label("forms.formsPage.archForms")}</Typography>
185+
</Stack>
186+
<Typography variant="body2" color="text.secondary">
187+
{`${archivedCount} archived ${archivedCount === 1 ? "form" : "forms"}`}
188+
</Typography>
189+
</Stack>
190+
</Box>
191+
<Box sx={{ p: 0 }}>{renderTable(getArchivedRows())}</Box>
192+
</Card>
166193
);
167194

168-
const tabs = [];
169-
let defaultTab = "";
170-
tabs.push(getTab("forms", "format_align_left", Locale.label("forms.formsPage.forms")));
171-
if (defaultTab === "") defaultTab = "forms";
172-
if (archivedForms.data?.length > 0) {
173-
tabs.push(getTab("archived", "archive", Locale.label("forms.formsPage.archForms")));
174-
if (defaultTab === "") defaultTab = "archived";
175-
}
176-
// Default tab is initialized via useState; avoid setting state during render.
195+
const tabs = [
196+
{
197+
key: "forms",
198+
label: Locale.label("forms.formsPage.forms"),
199+
content: (
200+
<>
201+
{getSidebar()}
202+
{formsCard}
203+
</>
204+
)
205+
},
206+
{ key: "archived", label: Locale.label("forms.formsPage.archForms"), content: archivedCard, hidden: archivedForms.data?.length === 0 },
207+
];
177208

178209
return (
179210
<>
@@ -194,50 +225,10 @@ export const FormsPage = () => {
194225
{Locale.label("forms.formsPage.addForm") || "Add Form"}
195226
</Button>
196227
)}
197-
{archivedForms.data?.length > 0 && (
198-
<Button
199-
variant="outlined"
200-
onClick={() => {
201-
setSelectedTab("archived");
202-
}}
203-
sx={{
204-
color: "#FFF",
205-
backgroundColor: "transparent",
206-
borderColor: "#FFF",
207-
fontWeight: selectedTab === "archived" ? 600 : 400,
208-
"&:hover": {
209-
backgroundColor: "rgba(255,255,255,0.1)",
210-
color: "#FFF",
211-
borderColor: "#FFF",
212-
},
213-
}}>
214-
{Locale.label("forms.formsPage.archForms")}
215-
</Button>
216-
)}
217228
</PageHeader>
218-
219229
{/* Tab Content */}
220230
<Box sx={{ p: 3 }}>
221-
{getSidebar()}
222-
<Card sx={{ mt: getSidebar() ? 2 : 0 }}>
223-
{/* Card Header */}
224-
<Box sx={{ p: 2, borderBottom: 1, borderColor: "divider" }}>
225-
<Stack direction="row" justifyContent="space-between" alignItems="center">
226-
<Stack direction="row" spacing={1} alignItems="center">
227-
{selectedTab === "forms" ? <DescriptionIcon /> : <ArchiveIcon />}
228-
<Typography variant="h6">{selectedTab === "forms" ? Locale.label("forms.formsPage.forms") : Locale.label("forms.formsPage.archForms")}</Typography>
229-
</Stack>
230-
<Typography variant="body2" color="text.secondary">
231-
{selectedTab === "forms"
232-
? `${forms.data?.length || 0} ${forms.data?.length === 1 ? "form" : "forms"}`
233-
: `${archivedForms.data?.length || 0} archived ${archivedForms.data?.length === 1 ? "form" : "forms"}`}
234-
</Typography>
235-
</Stack>
236-
</Box>
237-
238-
{/* Card Content */}
239-
<Box sx={{ p: 0 }}>{contents}</Box>
240-
</Card>
231+
<SmartTabs tabs={tabs} value={selectedTab} onChange={setSelectedTab} ariaLabel="forms-tabs" />
241232
</Box>
242233
</>
243234
);

0 commit comments

Comments
 (0)