mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 02:22:06 +00:00
refactor(vendors): satisfy changed-file governance
This commit is contained in:
parent
b61d46ce4b
commit
ff2e04b30e
3 changed files with 318 additions and 260 deletions
252
src/app/(protected)/vendors/_components/vendor-filter-options.tsx
vendored
Normal file
252
src/app/(protected)/vendors/_components/vendor-filter-options.tsx
vendored
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
import type { Dispatch, ReactNode, SetStateAction } from "react";
|
||||||
|
import BusinessOutlinedIcon from "@mui/icons-material/BusinessOutlined";
|
||||||
|
import BuildOutlinedIcon from "@mui/icons-material/BuildOutlined";
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
||||||
|
import TagOutlinedIcon from "@mui/icons-material/TagOutlined";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Chip,
|
||||||
|
Stack,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import type {
|
||||||
|
VendorFacets,
|
||||||
|
VendorFacetCompany,
|
||||||
|
VendorFacetJobBucket,
|
||||||
|
VendorFacetLocation,
|
||||||
|
} from "@/domain/vendors/types/vendor";
|
||||||
|
import type { VendorFilters } from "./vendor-filters";
|
||||||
|
|
||||||
|
type DraftSetter = Dispatch<SetStateAction<VendorFilters>>;
|
||||||
|
|
||||||
|
function FilterSection({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
icon: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Accordion disableGutters elevation={0} square sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`${title}-filters-content`}>
|
||||||
|
<Stack direction="row" spacing={1.5} sx={{ alignItems: "center" }}>
|
||||||
|
<Box sx={{ color: "primary.main", display: "flex" }}>{icon}</Box>
|
||||||
|
<Typography variant="subtitle1" className="font-semibold">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails id={`${title}-filters-content`} sx={{ pt: 0, pb: 2.5 }}>
|
||||||
|
{children}
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompanyFilter({
|
||||||
|
options,
|
||||||
|
draft,
|
||||||
|
setDraft,
|
||||||
|
}: {
|
||||||
|
options: VendorFacetCompany[];
|
||||||
|
draft: VendorFilters;
|
||||||
|
setDraft: DraftSetter;
|
||||||
|
}) {
|
||||||
|
const allSelected = draft.companies.length === options.length && options.length > 0;
|
||||||
|
return (
|
||||||
|
<FilterSection title="Company" icon={<BusinessOutlinedIcon />}>
|
||||||
|
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between" }}>
|
||||||
|
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||||
|
Select vendor companies
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() =>
|
||||||
|
setDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
companies: allSelected ? [] : options.map((company) => company.name),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{allSelected ? "Clear all" : "Select all"}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={options}
|
||||||
|
getOptionLabel={(option) => (typeof option === "string" ? option : option.name)}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
typeof option === "string" || typeof value === "string"
|
||||||
|
? option === value
|
||||||
|
: option.name === value.name
|
||||||
|
}
|
||||||
|
value={options.filter((option) => draft.companies.includes(option.name))}
|
||||||
|
onChange={(_event, value) => {
|
||||||
|
const next = (value as (string | VendorFacetCompany)[]).map((entry) =>
|
||||||
|
typeof entry === "string" ? entry : entry.name,
|
||||||
|
);
|
||||||
|
setDraft((current) => ({ ...current, companies: next }));
|
||||||
|
}}
|
||||||
|
renderOption={(props, option, { selected }) => {
|
||||||
|
const { key, ...optionProps } = props;
|
||||||
|
return (
|
||||||
|
<li key={key} {...optionProps}>
|
||||||
|
<Checkbox checked={selected} size="small" sx={{ mr: 1 }} />
|
||||||
|
{option.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
renderValue={(value, getItemProps) =>
|
||||||
|
value.map((option, index) => {
|
||||||
|
const label = typeof option === "string" ? option : option.name;
|
||||||
|
const { key, ...chipProps } = getItemProps({ index });
|
||||||
|
return <Chip key={key} size="small" label={label} {...chipProps} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder={draft.companies.length === 0 ? "All companies" : ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FilterSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TradeFilter({ options, draft, setDraft }: FilterProps<string>) {
|
||||||
|
return (
|
||||||
|
<FilterSection title="Trade" icon={<BuildOutlinedIcon />}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={options}
|
||||||
|
value={draft.trades}
|
||||||
|
onChange={(_event, value) =>
|
||||||
|
setDraft((current) => ({ ...current, trades: value as string[] }))
|
||||||
|
}
|
||||||
|
renderValue={(value, getItemProps) =>
|
||||||
|
value.map((option, index) => {
|
||||||
|
const { key, ...chipProps } = getItemProps({ index });
|
||||||
|
return <Chip key={key} size="small" label={option} {...chipProps} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField {...params} placeholder={draft.trades.length === 0 ? "All trades" : ""} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FilterSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterProps<T> {
|
||||||
|
options: T[];
|
||||||
|
draft: VendorFilters;
|
||||||
|
setDraft: DraftSetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LocationFilter({ options, draft, setDraft }: FilterProps<VendorFacetLocation>) {
|
||||||
|
return (
|
||||||
|
<FilterSection title="Location" icon={<LocationOnOutlinedIcon />}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={options}
|
||||||
|
getOptionLabel={(option) => (typeof option === "string" ? option : option.label)}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
typeof option === "string" || typeof value === "string"
|
||||||
|
? option === value
|
||||||
|
: option.label === value.label
|
||||||
|
}
|
||||||
|
value={options.filter((option) => draft.locations.includes(option.label))}
|
||||||
|
onChange={(_event, value) => {
|
||||||
|
const next = (value as (string | VendorFacetLocation)[]).map((entry) =>
|
||||||
|
typeof entry === "string" ? entry : entry.label,
|
||||||
|
);
|
||||||
|
setDraft((current) => ({ ...current, locations: next }));
|
||||||
|
}}
|
||||||
|
renderValue={(value, getItemProps) =>
|
||||||
|
value.map((option, index) => {
|
||||||
|
const label = typeof option === "string" ? option : option.label;
|
||||||
|
const { key, ...chipProps } = getItemProps({ index });
|
||||||
|
return <Chip key={key} size="small" label={label} {...chipProps} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder={draft.locations.length === 0 ? "All locations" : ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FilterSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function JobCountFilter({ options, draft, setDraft }: FilterProps<VendorFacetJobBucket>) {
|
||||||
|
return (
|
||||||
|
<FilterSection title="Total Jobs" icon={<TagOutlinedIcon />}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
size="small"
|
||||||
|
options={options}
|
||||||
|
getOptionLabel={(option) => (typeof option === "string" ? option : option.label)}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
typeof option === "string" || typeof value === "string"
|
||||||
|
? option === value
|
||||||
|
: option.id === value.id
|
||||||
|
}
|
||||||
|
value={options.filter((option) => draft.jobBuckets.includes(option.id))}
|
||||||
|
onChange={(_event, value) => {
|
||||||
|
const next = (value as (string | VendorFacetJobBucket)[]).map((entry) =>
|
||||||
|
typeof entry === "string" ? entry : entry.id,
|
||||||
|
);
|
||||||
|
setDraft((current) => ({ ...current, jobBuckets: next }));
|
||||||
|
}}
|
||||||
|
renderValue={(value, getItemProps) =>
|
||||||
|
value.map((option, index) => {
|
||||||
|
const label = typeof option === "string" ? option : option.label;
|
||||||
|
const { key, ...chipProps } = getItemProps({ index });
|
||||||
|
return <Chip key={key} size="small" label={label} {...chipProps} />;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
placeholder={draft.jobBuckets.length === 0 ? "All job counts" : ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FilterSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VendorFilterOptions({
|
||||||
|
facets,
|
||||||
|
draft,
|
||||||
|
setDraft,
|
||||||
|
}: {
|
||||||
|
facets: VendorFacets | undefined;
|
||||||
|
draft: VendorFilters;
|
||||||
|
setDraft: DraftSetter;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CompanyFilter options={facets?.companies ?? []} draft={draft} setDraft={setDraft} />
|
||||||
|
<TradeFilter options={facets?.trades ?? []} draft={draft} setDraft={setDraft} />
|
||||||
|
<LocationFilter options={facets?.locations ?? []} draft={draft} setDraft={setDraft} />
|
||||||
|
<JobCountFilter options={facets?.jobBuckets ?? []} draft={draft} setDraft={setDraft} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,9 @@
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import BusinessOutlinedIcon from "@mui/icons-material/BusinessOutlined";
|
|
||||||
import BuildOutlinedIcon from "@mui/icons-material/BuildOutlined";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
import { Box, Button, Chip, Drawer, IconButton, Stack, Typography } from "@mui/material";
|
||||||
import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined";
|
import { VendorFilterOptions } from "./vendor-filter-options";
|
||||||
import TagOutlinedIcon from "@mui/icons-material/TagOutlined";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Autocomplete,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Chip,
|
|
||||||
Drawer,
|
|
||||||
IconButton,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/material";
|
|
||||||
import type {
|
|
||||||
VendorFacets,
|
|
||||||
VendorFacetCompany,
|
|
||||||
VendorFacetJobBucket,
|
|
||||||
VendorFacetLocation,
|
|
||||||
} from "@/domain/vendors/types/vendor";
|
|
||||||
import { emptyVendorFilters, type VendorFilters } from "./vendor-filters";
|
import { emptyVendorFilters, type VendorFilters } from "./vendor-filters";
|
||||||
|
import type { VendorFacets } from "@/domain/vendors/types/vendor";
|
||||||
|
|
||||||
interface VendorFiltersDrawerProps {
|
interface VendorFiltersDrawerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -38,32 +15,6 @@ interface VendorFiltersDrawerProps {
|
||||||
|
|
||||||
const DRAWER_WIDTH = 360;
|
const DRAWER_WIDTH = 360;
|
||||||
|
|
||||||
function FilterSection({
|
|
||||||
title,
|
|
||||||
icon,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
icon: ReactNode;
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Accordion disableGutters elevation={0} square sx={{ borderBottom: 1, borderColor: "divider" }}>
|
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`${title}-filters-content`}>
|
|
||||||
<Stack direction="row" spacing={1.5} sx={{ alignItems: "center" }}>
|
|
||||||
<Box sx={{ color: "primary.main", display: "flex" }}>{icon}</Box>
|
|
||||||
<Typography variant="subtitle1" className="font-semibold">
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails id={`${title}-filters-content`} sx={{ pt: 0, pb: 2.5 }}>
|
|
||||||
{children}
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function VendorFiltersDrawer({
|
export function VendorFiltersDrawer({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
@ -74,23 +25,12 @@ export function VendorFiltersDrawer({
|
||||||
const [draft, setDraft] = useState<VendorFilters>(appliedFilters);
|
const [draft, setDraft] = useState<VendorFilters>(appliedFilters);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) setDraft(appliedFilters);
|
||||||
setDraft(appliedFilters);
|
|
||||||
}
|
|
||||||
}, [open, appliedFilters]);
|
}, [open, appliedFilters]);
|
||||||
|
|
||||||
const companyOptions = useMemo(() => facets?.companies ?? [], [facets]);
|
|
||||||
const tradeOptions = useMemo(() => facets?.trades ?? [], [facets]);
|
|
||||||
const locationOptions = useMemo(() => facets?.locations ?? [], [facets]);
|
|
||||||
const jobBucketOptions = useMemo(() => facets?.jobBuckets ?? [], [facets]);
|
|
||||||
|
|
||||||
const activeCount =
|
const activeCount =
|
||||||
draft.companies.length + draft.trades.length + draft.locations.length + draft.jobBuckets.length;
|
draft.companies.length + draft.trades.length + draft.locations.length + draft.jobBuckets.length;
|
||||||
|
|
||||||
const handleClear = () => {
|
|
||||||
setDraft(emptyVendorFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
onApply(draft);
|
onApply(draft);
|
||||||
onClose();
|
onClose();
|
||||||
|
|
@ -133,165 +73,7 @@ export function VendorFiltersDrawer({
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Box className="flex-1 overflow-y-auto" sx={{ px: 2.5 }}>
|
<Box className="flex-1 overflow-y-auto" sx={{ px: 2.5 }}>
|
||||||
<FilterSection title="Company" icon={<BusinessOutlinedIcon />}>
|
<VendorFilterOptions facets={facets} draft={draft} setDraft={setDraft} />
|
||||||
<Stack direction="row" sx={{ alignItems: "center", justifyContent: "space-between" }}>
|
|
||||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
|
||||||
Select vendor companies
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() =>
|
|
||||||
setDraft((current) => ({
|
|
||||||
...current,
|
|
||||||
companies:
|
|
||||||
current.companies.length === companyOptions.length
|
|
||||||
? []
|
|
||||||
: companyOptions.map((company) => company.name),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{draft.companies.length === companyOptions.length && companyOptions.length > 0
|
|
||||||
? "Clear all"
|
|
||||||
: "Select all"}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
size="small"
|
|
||||||
options={companyOptions}
|
|
||||||
getOptionLabel={(option) => (typeof option === "string" ? option : option.name)}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
typeof option === "string" || typeof value === "string"
|
|
||||||
? option === value
|
|
||||||
: option.name === value.name
|
|
||||||
}
|
|
||||||
value={companyOptions.filter((option) => draft.companies.includes(option.name))}
|
|
||||||
onChange={(_event, value) => {
|
|
||||||
const next = (value as (string | VendorFacetCompany)[]).map((entry) =>
|
|
||||||
typeof entry === "string" ? entry : entry.name,
|
|
||||||
);
|
|
||||||
setDraft((current) => ({ ...current, companies: next }));
|
|
||||||
}}
|
|
||||||
renderOption={(props, option, { selected }) => {
|
|
||||||
const { key, ...optionProps } = props;
|
|
||||||
return (
|
|
||||||
<li key={key} {...optionProps}>
|
|
||||||
<Checkbox checked={selected} size="small" sx={{ mr: 1 }} />
|
|
||||||
{option.name}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
renderValue={(value, getItemProps) =>
|
|
||||||
value.map((option, index) => {
|
|
||||||
const label = typeof option === "string" ? option : option.name;
|
|
||||||
const tagProps = getItemProps({ index });
|
|
||||||
const { key, ...chipProps } = tagProps;
|
|
||||||
return <Chip key={key} size="small" label={label} {...chipProps} />;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder={draft.companies.length === 0 ? "All companies" : ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
<FilterSection title="Trade" icon={<BuildOutlinedIcon />}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
size="small"
|
|
||||||
options={tradeOptions}
|
|
||||||
value={draft.trades}
|
|
||||||
onChange={(_event, value) =>
|
|
||||||
setDraft((current) => ({ ...current, trades: value as string[] }))
|
|
||||||
}
|
|
||||||
renderValue={(value, getItemProps) =>
|
|
||||||
value.map((option, index) => {
|
|
||||||
const tagProps = getItemProps({ index });
|
|
||||||
const { key, ...chipProps } = tagProps;
|
|
||||||
return <Chip key={key} size="small" label={option} {...chipProps} />;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder={draft.trades.length === 0 ? "All trades" : ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
<FilterSection title="Location" icon={<LocationOnOutlinedIcon />}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
size="small"
|
|
||||||
options={locationOptions}
|
|
||||||
getOptionLabel={(option) => (typeof option === "string" ? option : option.label)}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
typeof option === "string" || typeof value === "string"
|
|
||||||
? option === value
|
|
||||||
: option.label === value.label
|
|
||||||
}
|
|
||||||
value={locationOptions.filter((option) => draft.locations.includes(option.label))}
|
|
||||||
onChange={(_event, value) => {
|
|
||||||
const next = (value as (string | VendorFacetLocation)[]).map((entry) =>
|
|
||||||
typeof entry === "string" ? entry : entry.label,
|
|
||||||
);
|
|
||||||
setDraft((current) => ({ ...current, locations: next }));
|
|
||||||
}}
|
|
||||||
renderValue={(value, getItemProps) =>
|
|
||||||
value.map((option, index) => {
|
|
||||||
const label = typeof option === "string" ? option : option.label;
|
|
||||||
const tagProps = getItemProps({ index });
|
|
||||||
const { key, ...chipProps } = tagProps;
|
|
||||||
return <Chip key={key} size="small" label={label} {...chipProps} />;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder={draft.locations.length === 0 ? "All locations" : ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
|
|
||||||
<FilterSection title="Total Jobs" icon={<TagOutlinedIcon />}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
size="small"
|
|
||||||
options={jobBucketOptions}
|
|
||||||
getOptionLabel={(option) => (typeof option === "string" ? option : option.label)}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
typeof option === "string" || typeof value === "string"
|
|
||||||
? option === value
|
|
||||||
: option.id === value.id
|
|
||||||
}
|
|
||||||
value={jobBucketOptions.filter((option) => draft.jobBuckets.includes(option.id))}
|
|
||||||
onChange={(_event, value) => {
|
|
||||||
const next = (value as (string | VendorFacetJobBucket)[]).map((entry) =>
|
|
||||||
typeof entry === "string" ? entry : entry.id,
|
|
||||||
);
|
|
||||||
setDraft((current) => ({ ...current, jobBuckets: next }));
|
|
||||||
}}
|
|
||||||
renderValue={(value, getItemProps) =>
|
|
||||||
value.map((option, index) => {
|
|
||||||
const label = typeof option === "string" ? option : option.label;
|
|
||||||
const tagProps = getItemProps({ index });
|
|
||||||
const { key, ...chipProps } = tagProps;
|
|
||||||
return <Chip key={key} size="small" label={label} {...chipProps} />;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
renderInput={(params) => (
|
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
placeholder={draft.jobBuckets.length === 0 ? "All job counts" : ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</FilterSection>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
|
|
@ -303,7 +85,12 @@ export function VendorFiltersDrawer({
|
||||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button variant="outlined" onClick={handleClear} disabled={activeCount === 0} fullWidth>
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setDraft(emptyVendorFilters)}
|
||||||
|
disabled={activeCount === 0}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
Clear all
|
Clear all
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" onClick={handleApply} fullWidth>
|
<Button variant="contained" onClick={handleApply} fullWidth>
|
||||||
|
|
|
||||||
89
src/app/(protected)/vendors/index.tsx
vendored
89
src/app/(protected)/vendors/index.tsx
vendored
|
|
@ -38,6 +38,54 @@ function statusTabLabel(label: string, count: number) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function VendorStatusTabs({
|
||||||
|
isActive,
|
||||||
|
activeCount,
|
||||||
|
inactiveCount,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
isActive: boolean;
|
||||||
|
activeCount: number;
|
||||||
|
inactiveCount: number;
|
||||||
|
onChange: (isActive: boolean) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
value={isActive ? "active" : "inactive"}
|
||||||
|
onChange={(_event, value: "active" | "inactive") => onChange(value === "active")}
|
||||||
|
aria-label="Vendor status"
|
||||||
|
sx={{
|
||||||
|
minHeight: 36,
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
|
backgroundColor: "var(--color-bg-surface)",
|
||||||
|
px: 1.5,
|
||||||
|
"& .MuiTab-root": {
|
||||||
|
minHeight: 36,
|
||||||
|
px: 2,
|
||||||
|
py: 0,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
textTransform: "none",
|
||||||
|
},
|
||||||
|
"& .Mui-selected": { fontWeight: 600 },
|
||||||
|
"& .MuiTabs-indicator": { height: 2 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
value="active"
|
||||||
|
label={statusTabLabel("Active", activeCount)}
|
||||||
|
aria-label={`Active (${activeCount})`}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
value="inactive"
|
||||||
|
label={statusTabLabel("Inactive", inactiveCount)}
|
||||||
|
aria-label={`Inactive (${inactiveCount})`}
|
||||||
|
/>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function describeChips(
|
function describeChips(
|
||||||
filters: VendorFilters,
|
filters: VendorFilters,
|
||||||
jobBucketLabels: Map<string, string>,
|
jobBucketLabels: Map<string, string>,
|
||||||
|
|
@ -100,41 +148,12 @@ export default function VendorsListPage() {
|
||||||
onAddVendor={() => setCreateOpen(true)}
|
onAddVendor={() => setCreateOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs
|
<VendorStatusTabs
|
||||||
value={filters.isActive ? "active" : "inactive"}
|
isActive={filters.isActive}
|
||||||
onChange={(_event, value: "active" | "inactive") =>
|
activeCount={activeCount}
|
||||||
filters.setIsActiveWithReset(value === "active")
|
inactiveCount={inactiveCount}
|
||||||
}
|
onChange={filters.setIsActiveWithReset}
|
||||||
aria-label="Vendor status"
|
/>
|
||||||
sx={{
|
|
||||||
minHeight: 36,
|
|
||||||
flexShrink: 0,
|
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
|
||||||
backgroundColor: "var(--color-bg-surface)",
|
|
||||||
px: 1.5,
|
|
||||||
"& .MuiTab-root": {
|
|
||||||
minHeight: 36,
|
|
||||||
px: 2,
|
|
||||||
py: 0,
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: 500,
|
|
||||||
textTransform: "none",
|
|
||||||
},
|
|
||||||
"& .Mui-selected": { fontWeight: 600 },
|
|
||||||
"& .MuiTabs-indicator": { height: 2 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
value="active"
|
|
||||||
label={statusTabLabel("Active", activeCount)}
|
|
||||||
aria-label={`Active (${activeCount})`}
|
|
||||||
/>
|
|
||||||
<Tab
|
|
||||||
value="inactive"
|
|
||||||
label={statusTabLabel("Inactive", inactiveCount)}
|
|
||||||
aria-label={`Inactive (${inactiveCount})`}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<VendorAppliedFiltersBar
|
<VendorAppliedFiltersBar
|
||||||
resultCount={totalCount}
|
resultCount={totalCount}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue