shoc-frontend-new/src/app/(protected)/team-members/index.tsx
2026-09-16 23:14:51 -03:00

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>
);
}