mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 16:02:08 +00:00
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
import { useState } from "react";
|
|
import { useDebounce } from "@/hooks/use-debounce";
|
|
import { useTeamMembersList } from "@/domain/team-members/use-cases/use-team-members-list";
|
|
import type { TeamMemberListItem } from "@/domain/team-members/types/team-member";
|
|
import {
|
|
Alert,
|
|
Avatar,
|
|
Box,
|
|
Button,
|
|
Chip,
|
|
Drawer,
|
|
IconButton,
|
|
Stack,
|
|
Tab,
|
|
Tabs,
|
|
TextField,
|
|
Typography,
|
|
} from "@mui/material";
|
|
import CloseIcon from "@mui/icons-material/Close";
|
|
import { TeamMembersTable } from "./_components/team-members-table";
|
|
import { AddTeamMemberDialog } from "./_components/add-team-member-dialog";
|
|
|
|
type StatusTab = "active" | "inactive";
|
|
|
|
const STATUS_LABELS: Record<StatusTab, "Active" | "Inactive"> = {
|
|
active: "Active",
|
|
inactive: "Inactive",
|
|
};
|
|
|
|
function MemberStatus({ status }: { status: TeamMemberListItem["status"] }) {
|
|
return <Chip size="small" label={status} color={status === "Active" ? "success" : "default"} />;
|
|
}
|
|
|
|
function TeamMemberDrawer({
|
|
member,
|
|
onClose,
|
|
}: {
|
|
member: TeamMemberListItem | null;
|
|
onClose: () => void;
|
|
}) {
|
|
return (
|
|
<Drawer anchor="right" open={Boolean(member)} onClose={onClose}>
|
|
<Box sx={{ width: { xs: "100vw", sm: 380 }, p: 3 }}>
|
|
<Stack
|
|
direction="row"
|
|
sx={{ alignItems: "center", justifyContent: "space-between", mb: 3 }}
|
|
>
|
|
<Typography variant="h6">Team Member</Typography>
|
|
<IconButton aria-label="Close team member details" onClick={onClose}>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</Stack>
|
|
{member !== null && (
|
|
<Stack spacing={2}>
|
|
<Stack direction="row" spacing={2} sx={{ alignItems: "center" }}>
|
|
<Avatar sx={{ bgcolor: member.color || "primary.main" }}>
|
|
{member.name.slice(0, 1).toUpperCase() || "?"}
|
|
</Avatar>
|
|
<Box>
|
|
<Typography variant="h6">{member.name || "—"}</Typography>
|
|
<Typography color="text.secondary">{member.role || "—"}</Typography>
|
|
</Box>
|
|
</Stack>
|
|
<MemberStatus status={member.status} />
|
|
<Typography>
|
|
<strong>Service Area:</strong> {member.serviceArea || "—"}
|
|
</Typography>
|
|
<Typography>
|
|
<strong>Phone:</strong> {member.phone || "—"}
|
|
</Typography>
|
|
<Typography>
|
|
<strong>Email:</strong> {member.email || "—"}
|
|
</Typography>
|
|
</Stack>
|
|
)}
|
|
</Box>
|
|
</Drawer>
|
|
);
|
|
}
|
|
|
|
export default function TeamMembersListPage() {
|
|
const [search, setSearch] = useState("");
|
|
const [tab, setTab] = useState<StatusTab>("active");
|
|
const [selectedMember, setSelectedMember] = useState<TeamMemberListItem | null>(null);
|
|
const [addMemberOpen, setAddMemberOpen] = useState(false);
|
|
const debouncedSearch = useDebounce(search, 300).trim().toLowerCase();
|
|
const { data, isLoading, error } = useTeamMembersList();
|
|
const allMembers = data?.items ?? [];
|
|
|
|
const activeCount = allMembers.filter((member) => member.status === "Active").length;
|
|
const inactiveCount = allMembers.length - activeCount;
|
|
const rows = allMembers
|
|
.filter((member) => member.status === STATUS_LABELS[tab])
|
|
.filter((member) => !debouncedSearch || member.name.toLowerCase().includes(debouncedSearch));
|
|
|
|
return (
|
|
<Box
|
|
className="flex h-full flex-col overflow-hidden"
|
|
sx={{ backgroundColor: "var(--color-bg-page)" }}
|
|
>
|
|
<Stack
|
|
direction={{ xs: "column", sm: "row" }}
|
|
spacing={2}
|
|
sx={{
|
|
alignItems: { xs: "stretch", sm: "center" },
|
|
justifyContent: "space-between",
|
|
p: 2.5,
|
|
backgroundColor: "var(--color-bg-surface)",
|
|
borderBottom: "1px solid var(--color-border)",
|
|
}}
|
|
>
|
|
<Box>
|
|
<Typography variant="h4" component="h1">
|
|
Team Members
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.75 }}>
|
|
{allMembers.length} team member{allMembers.length === 1 ? "" : "s"} · {activeCount}{" "}
|
|
active
|
|
</Typography>
|
|
</Box>
|
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
|
<TextField
|
|
size="small"
|
|
value={search}
|
|
onChange={(event) => setSearch(event.target.value)}
|
|
placeholder="Search team member…"
|
|
aria-label="Search team member"
|
|
sx={{ width: 224 }}
|
|
/>
|
|
<Button variant="contained" size="small" onClick={() => setAddMemberOpen(true)}>
|
|
Add Member
|
|
</Button>
|
|
</Stack>
|
|
</Stack>
|
|
|
|
<Tabs
|
|
value={tab}
|
|
onChange={(_event, value: StatusTab) => setTab(value)}
|
|
aria-label="Team member status"
|
|
>
|
|
<Tab value="active" label={`Active ${activeCount}`} />
|
|
<Tab value="inactive" label={`Inactive ${inactiveCount}`} />
|
|
</Tabs>
|
|
|
|
{Boolean(error) && (
|
|
<Alert severity="error">
|
|
{error instanceof Error ? error.message : "Failed to load team members"}
|
|
</Alert>
|
|
)}
|
|
|
|
<Box sx={{ flex: 1, overflow: "auto", p: 2 }}>
|
|
<TeamMembersTable
|
|
rows={rows}
|
|
isLoading={isLoading}
|
|
hasError={Boolean(error)}
|
|
tab={tab}
|
|
onOpen={setSelectedMember}
|
|
/>
|
|
</Box>
|
|
|
|
<Stack
|
|
direction="row"
|
|
sx={{
|
|
justifyContent: "space-between",
|
|
px: 2,
|
|
py: 1,
|
|
borderTop: "1px solid var(--color-border)",
|
|
backgroundColor: "var(--color-bg-surface)",
|
|
}}
|
|
>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{rows.length} {tab} team member{rows.length === 1 ? "" : "s"}
|
|
{debouncedSearch ? " · filtered" : ""}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{activeCount} active · {inactiveCount} inactive
|
|
</Typography>
|
|
</Stack>
|
|
|
|
<TeamMemberDrawer member={selectedMember} onClose={() => setSelectedMember(null)} />
|
|
<AddTeamMemberDialog open={addMemberOpen} onClose={() => setAddMemberOpen(false)} />
|
|
</Box>
|
|
);
|
|
}
|