mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 20:42:05 +00:00
Merge remote-tracking branch 'origin/dev' into codex/pr43-dev-integration
This commit is contained in:
commit
1c451e07b4
3 changed files with 271 additions and 206 deletions
|
|
@ -0,0 +1,187 @@
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import {
|
||||||
|
CalendarDays,
|
||||||
|
CheckCircle2,
|
||||||
|
FileText,
|
||||||
|
Flame,
|
||||||
|
MapPin,
|
||||||
|
Tag,
|
||||||
|
Truck,
|
||||||
|
UserX,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { AdvDocPill, AdvFilterPill, AdvStatusPill } from "./adv-filter-pill";
|
||||||
|
import { AdvMultiSelect } from "./adv-multi-select";
|
||||||
|
import { AdvSection } from "./adv-section";
|
||||||
|
import { AdvTypeGrid } from "./adv-type-grid";
|
||||||
|
import { DateField } from "@/components/ui/date-input";
|
||||||
|
import { SelectAllAction } from "@/components/ui/select-all-action";
|
||||||
|
import {
|
||||||
|
ALL_WIZARD_STATUSES,
|
||||||
|
ASSIGNEE_FILTER_UNASSIGNED,
|
||||||
|
COMPLETION_DOC_OPTIONS,
|
||||||
|
DATE_RANGE_OPTIONS,
|
||||||
|
DOC_FILTER_LABELS,
|
||||||
|
type AdvancedFilterSetKey,
|
||||||
|
type WorkOrderAdvancedFilters,
|
||||||
|
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
|
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||||
|
import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
|
import { isDefaultDateRange } from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
|
|
||||||
|
type AdvancedSearchFilterSectionsProps = {
|
||||||
|
draft: WorkOrderAdvancedFilters;
|
||||||
|
setDraft: Dispatch<SetStateAction<WorkOrderAdvancedFilters>>;
|
||||||
|
toggleIn: (key: AdvancedFilterSetKey, value: string) => void;
|
||||||
|
setMany: (key: AdvancedFilterSetKey, values: string[], select: boolean) => void;
|
||||||
|
sites: { id: string | number; name: string }[];
|
||||||
|
users: WorkOrderUser[];
|
||||||
|
pmTypes: string[];
|
||||||
|
vendorTechs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AdvancedSearchFilterSections({
|
||||||
|
draft,
|
||||||
|
setDraft,
|
||||||
|
toggleIn,
|
||||||
|
setMany,
|
||||||
|
sites,
|
||||||
|
users,
|
||||||
|
pmTypes,
|
||||||
|
vendorTechs,
|
||||||
|
}: AdvancedSearchFilterSectionsProps) {
|
||||||
|
const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => (
|
||||||
|
<div className="mb-2 flex justify-end">
|
||||||
|
<SelectAllAction
|
||||||
|
anySelected={selectedSize > 0}
|
||||||
|
onClick={() => setMany(key, all, selectedSize === 0)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto px-4">
|
||||||
|
<AdvSection
|
||||||
|
icon={CalendarDays}
|
||||||
|
label="Date range"
|
||||||
|
count={isDefaultDateRange(draft.dateRange) ? 0 : 1}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{DATE_RANGE_OPTIONS.map((option) => (
|
||||||
|
<AdvFilterPill
|
||||||
|
key={option.key}
|
||||||
|
label={option.label}
|
||||||
|
selected={draft.dateRange === option.key}
|
||||||
|
onClick={() => setDraft((prev) => ({ ...prev, dateRange: option.key }))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{draft.dateRange === "custom" && (
|
||||||
|
<div className="mt-2.5 flex items-center gap-2">
|
||||||
|
<DateField
|
||||||
|
label="From"
|
||||||
|
value={draft.customFrom}
|
||||||
|
onChange={(value) => setDraft((prev) => ({ ...prev, customFrom: value }))}
|
||||||
|
/>
|
||||||
|
<DateField
|
||||||
|
label="To"
|
||||||
|
value={draft.customTo}
|
||||||
|
onChange={(value) => setDraft((prev) => ({ ...prev, customTo: value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={MapPin} label="Site" count={draft.sites.size}>
|
||||||
|
<AdvMultiSelect
|
||||||
|
options={sites.map((site) => ({
|
||||||
|
value: String(site.id),
|
||||||
|
label: site.name,
|
||||||
|
}))}
|
||||||
|
selected={draft.sites}
|
||||||
|
onToggle={(value) => toggleIn("sites", value)}
|
||||||
|
onBulkToggle={(values, select) => setMany("sites", values, select)}
|
||||||
|
placeholder="Search site…"
|
||||||
|
/>
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={Tag} label="Type of WO" count={draft.types.size}>
|
||||||
|
<AdvTypeGrid
|
||||||
|
selected={draft.types}
|
||||||
|
onToggle={(type) => toggleIn("types", type)}
|
||||||
|
onSelectAll={(types, select) => setMany("types", types, select)}
|
||||||
|
/>
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={Users} label="Assigned to" count={draft.dispatchers.size}>
|
||||||
|
<AdvMultiSelect
|
||||||
|
options={[
|
||||||
|
{ value: ASSIGNEE_FILTER_UNASSIGNED, label: "Unassigned" },
|
||||||
|
...users.map((user) => ({ value: String(user.id), label: user.name })),
|
||||||
|
]}
|
||||||
|
selected={draft.dispatchers}
|
||||||
|
onToggle={(value) => toggleIn("dispatchers", value)}
|
||||||
|
onBulkToggle={(values, select) => setMany("dispatchers", values, select)}
|
||||||
|
placeholder="Search assigned to…"
|
||||||
|
renderLeft={(id) =>
|
||||||
|
id === ASSIGNEE_FILTER_UNASSIGNED ? (
|
||||||
|
<UserX size={14} className="shrink-0 text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<span className="size-2 shrink-0 rounded-full bg-muted-foreground" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={CheckCircle2} label="Status" count={draft.statuses.size}>
|
||||||
|
{selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)}
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{ALL_WIZARD_STATUSES.map((status) => (
|
||||||
|
<AdvStatusPill
|
||||||
|
key={status}
|
||||||
|
label={status}
|
||||||
|
dotColor={WIZARD_STATUS_DOTS[status]}
|
||||||
|
selected={draft.statuses.has(status)}
|
||||||
|
selectedBg="var(--color-primary-tint)"
|
||||||
|
onClick={() => toggleIn("statuses", status)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={Flame} label="PM type" count={draft.pmTypes.size}>
|
||||||
|
<AdvMultiSelect
|
||||||
|
options={pmTypes.map((pmType) => ({ value: pmType, label: pmType }))}
|
||||||
|
selected={draft.pmTypes}
|
||||||
|
onToggle={(value) => toggleIn("pmTypes", value)}
|
||||||
|
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
|
||||||
|
placeholder="Search pm type…"
|
||||||
|
/>
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={Truck} label="Vendor / tech" count={draft.vendorTechs.size}>
|
||||||
|
<AdvMultiSelect
|
||||||
|
options={vendorTechs.map((name) => ({ value: name, label: name }))}
|
||||||
|
selected={draft.vendorTechs}
|
||||||
|
onToggle={(value) => toggleIn("vendorTechs", value)}
|
||||||
|
onBulkToggle={(values, select) => setMany("vendorTechs", values, select)}
|
||||||
|
placeholder="Search vendor / tech…"
|
||||||
|
/>
|
||||||
|
</AdvSection>
|
||||||
|
|
||||||
|
<AdvSection icon={FileText} label="Completion doc" count={draft.docs.size}>
|
||||||
|
{selectAllRow("docs", [...COMPLETION_DOC_OPTIONS], draft.docs.size)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{COMPLETION_DOC_OPTIONS.map((doc) => (
|
||||||
|
<AdvDocPill
|
||||||
|
key={doc}
|
||||||
|
label={DOC_FILTER_LABELS[doc]}
|
||||||
|
selected={draft.docs.has(doc)}
|
||||||
|
onClick={() => toggleIn("docs", doc)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AdvSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
EMPTY_ADVANCED_FILTERS,
|
||||||
|
type AdvancedFilterSetKey,
|
||||||
|
type WorkOrderAdvancedFilters,
|
||||||
|
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
|
import {
|
||||||
|
cloneAdvancedFilters,
|
||||||
|
countAdvancedFilters,
|
||||||
|
} from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
|
|
||||||
|
type UseAdvancedSearchDraftOptions = {
|
||||||
|
open: boolean;
|
||||||
|
initial: WorkOrderAdvancedFilters;
|
||||||
|
onApply: (filters: WorkOrderAdvancedFilters) => void;
|
||||||
|
onClearAll: () => void;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useAdvancedSearchDraft({
|
||||||
|
open,
|
||||||
|
initial,
|
||||||
|
onApply,
|
||||||
|
onClearAll,
|
||||||
|
onOpenChange,
|
||||||
|
}: UseAdvancedSearchDraftOptions) {
|
||||||
|
const [draft, setDraft] = useState<WorkOrderAdvancedFilters>(initial);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setDraft(cloneAdvancedFilters(initial));
|
||||||
|
}
|
||||||
|
}, [open, initial]);
|
||||||
|
|
||||||
|
const activeCount = countAdvancedFilters(draft);
|
||||||
|
|
||||||
|
const toggleIn = (key: AdvancedFilterSetKey, value: string) => {
|
||||||
|
setDraft((prev) => {
|
||||||
|
const next = new Set(prev[key] as Set<string>);
|
||||||
|
if (next.has(value)) {
|
||||||
|
next.delete(value);
|
||||||
|
} else {
|
||||||
|
next.add(value);
|
||||||
|
}
|
||||||
|
return { ...prev, [key]: next };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMany = (key: AdvancedFilterSetKey, values: string[], select: boolean) => {
|
||||||
|
setDraft((prev) => {
|
||||||
|
const next = new Set(prev[key] as Set<string>);
|
||||||
|
values.forEach((value) => (select ? next.add(value) : next.delete(value)));
|
||||||
|
return { ...prev, [key]: next };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAll = () => {
|
||||||
|
setDraft(cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS));
|
||||||
|
onClearAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApply = () => {
|
||||||
|
onApply(cloneAdvancedFilters(draft));
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { draft, setDraft, activeCount, toggleIn, setMany, handleClearAll, handleApply };
|
||||||
|
}
|
||||||
|
|
@ -1,42 +1,11 @@
|
||||||
import { useEffect, useState } from "react";
|
import { SlidersHorizontal } from "lucide-react";
|
||||||
import {
|
import { AdvancedSearchFilterSections } from "./adv-filter-sections";
|
||||||
CalendarDays,
|
import { useAdvancedSearchDraft } from "./use-advanced-search-draft";
|
||||||
CheckCircle2,
|
|
||||||
FileText,
|
|
||||||
Flame,
|
|
||||||
MapPin,
|
|
||||||
SlidersHorizontal,
|
|
||||||
Tag,
|
|
||||||
Truck,
|
|
||||||
UserX,
|
|
||||||
Users,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { AdvDocPill, AdvFilterPill, AdvStatusPill } from "./adv-filter-pill";
|
|
||||||
import { AdvMultiSelect } from "./adv-multi-select";
|
|
||||||
import { AdvSection } from "./adv-section";
|
|
||||||
import { AdvTypeGrid } from "./adv-type-grid";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DateField } from "@/components/ui/date-input";
|
|
||||||
import { IconCloseButton } from "@/components/ui/icon-close-button";
|
import { IconCloseButton } from "@/components/ui/icon-close-button";
|
||||||
import { SelectAllAction } from "@/components/ui/select-all-action";
|
|
||||||
import { SideSheet } from "@/components/ui/side-sheet";
|
import { SideSheet } from "@/components/ui/side-sheet";
|
||||||
import {
|
import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
ALL_WIZARD_STATUSES,
|
|
||||||
ASSIGNEE_FILTER_UNASSIGNED,
|
|
||||||
COMPLETION_DOC_OPTIONS,
|
|
||||||
DATE_RANGE_OPTIONS,
|
|
||||||
DOC_FILTER_LABELS,
|
|
||||||
EMPTY_ADVANCED_FILTERS,
|
|
||||||
type AdvancedFilterSetKey,
|
|
||||||
type WorkOrderAdvancedFilters,
|
|
||||||
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
|
||||||
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
||||||
import { WIZARD_STATUS_DOTS } from "@/domain/work-orders/types/work-order-wizard";
|
|
||||||
import {
|
|
||||||
cloneAdvancedFilters,
|
|
||||||
countAdvancedFilters,
|
|
||||||
isDefaultDateRange,
|
|
||||||
} from "@/domain/work-orders/utils/advanced-filters-utils";
|
|
||||||
|
|
||||||
type WorkOrdersAdvancedSearchSheetProps = {
|
type WorkOrdersAdvancedSearchSheetProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -61,54 +30,8 @@ export function WorkOrdersAdvancedSearchSheet({
|
||||||
pmTypes,
|
pmTypes,
|
||||||
vendorTechs,
|
vendorTechs,
|
||||||
}: WorkOrdersAdvancedSearchSheetProps) {
|
}: WorkOrdersAdvancedSearchSheetProps) {
|
||||||
const [draft, setDraft] = useState<WorkOrderAdvancedFilters>(initial);
|
const { draft, setDraft, activeCount, toggleIn, setMany, handleClearAll, handleApply } =
|
||||||
|
useAdvancedSearchDraft({ open, initial, onApply, onClearAll, onOpenChange });
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
setDraft(cloneAdvancedFilters(initial));
|
|
||||||
}
|
|
||||||
}, [open, initial]);
|
|
||||||
|
|
||||||
const activeCount = countAdvancedFilters(draft);
|
|
||||||
|
|
||||||
const toggleIn = (key: AdvancedFilterSetKey, value: string) => {
|
|
||||||
setDraft((prev) => {
|
|
||||||
const next = new Set(prev[key] as Set<string>);
|
|
||||||
if (next.has(value)) {
|
|
||||||
next.delete(value);
|
|
||||||
} else {
|
|
||||||
next.add(value);
|
|
||||||
}
|
|
||||||
return { ...prev, [key]: next };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setMany = (key: AdvancedFilterSetKey, values: string[], select: boolean) => {
|
|
||||||
setDraft((prev) => {
|
|
||||||
const next = new Set(prev[key] as Set<string>);
|
|
||||||
values.forEach((value) => (select ? next.add(value) : next.delete(value)));
|
|
||||||
return { ...prev, [key]: next };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => (
|
|
||||||
<div className="mb-2 flex justify-end">
|
|
||||||
<SelectAllAction
|
|
||||||
anySelected={selectedSize > 0}
|
|
||||||
onClick={() => setMany(key, all, selectedSize === 0)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClearAll = () => {
|
|
||||||
setDraft(cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS));
|
|
||||||
onClearAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleApply = () => {
|
|
||||||
onApply(cloneAdvancedFilters(draft));
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SideSheet open={open} onClose={() => onOpenChange(false)} aria-label="Advanced Filters">
|
<SideSheet open={open} onClose={() => onOpenChange(false)} aria-label="Advanced Filters">
|
||||||
|
|
@ -129,129 +52,16 @@ export function WorkOrdersAdvancedSearchSheet({
|
||||||
<IconCloseButton onClick={() => onOpenChange(false)} />
|
<IconCloseButton onClick={() => onOpenChange(false)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-4">
|
<AdvancedSearchFilterSections
|
||||||
<AdvSection
|
draft={draft}
|
||||||
icon={CalendarDays}
|
setDraft={setDraft}
|
||||||
label="Date range"
|
toggleIn={toggleIn}
|
||||||
count={isDefaultDateRange(draft.dateRange) ? 0 : 1}
|
setMany={setMany}
|
||||||
>
|
sites={sites}
|
||||||
<div className="flex flex-wrap gap-1.5">
|
users={users}
|
||||||
{DATE_RANGE_OPTIONS.map((option) => (
|
pmTypes={pmTypes}
|
||||||
<AdvFilterPill
|
vendorTechs={vendorTechs}
|
||||||
key={option.key}
|
/>
|
||||||
label={option.label}
|
|
||||||
selected={draft.dateRange === option.key}
|
|
||||||
onClick={() => setDraft((prev) => ({ ...prev, dateRange: option.key }))}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{draft.dateRange === "custom" && (
|
|
||||||
<div className="mt-2.5 flex items-center gap-2">
|
|
||||||
<DateField
|
|
||||||
label="From"
|
|
||||||
value={draft.customFrom}
|
|
||||||
onChange={(value) => setDraft((prev) => ({ ...prev, customFrom: value }))}
|
|
||||||
/>
|
|
||||||
<DateField
|
|
||||||
label="To"
|
|
||||||
value={draft.customTo}
|
|
||||||
onChange={(value) => setDraft((prev) => ({ ...prev, customTo: value }))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={MapPin} label="Site" count={draft.sites.size}>
|
|
||||||
<AdvMultiSelect
|
|
||||||
options={sites.map((site) => ({
|
|
||||||
value: String(site.id),
|
|
||||||
label: site.name,
|
|
||||||
}))}
|
|
||||||
selected={draft.sites}
|
|
||||||
onToggle={(value) => toggleIn("sites", value)}
|
|
||||||
onBulkToggle={(values, select) => setMany("sites", values, select)}
|
|
||||||
placeholder="Search site…"
|
|
||||||
/>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={Tag} label="Type of WO" count={draft.types.size}>
|
|
||||||
<AdvTypeGrid
|
|
||||||
selected={draft.types}
|
|
||||||
onToggle={(type) => toggleIn("types", type)}
|
|
||||||
onSelectAll={(types, select) => setMany("types", types, select)}
|
|
||||||
/>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={Users} label="Assigned to" count={draft.dispatchers.size}>
|
|
||||||
<AdvMultiSelect
|
|
||||||
options={[
|
|
||||||
{ value: ASSIGNEE_FILTER_UNASSIGNED, label: "Unassigned" },
|
|
||||||
...users.map((user) => ({ value: String(user.id), label: user.name })),
|
|
||||||
]}
|
|
||||||
selected={draft.dispatchers}
|
|
||||||
onToggle={(value) => toggleIn("dispatchers", value)}
|
|
||||||
onBulkToggle={(values, select) => setMany("dispatchers", values, select)}
|
|
||||||
placeholder="Search assigned to…"
|
|
||||||
renderLeft={(id) =>
|
|
||||||
id === ASSIGNEE_FILTER_UNASSIGNED ? (
|
|
||||||
<UserX size={14} className="shrink-0 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
<span className="size-2 shrink-0 rounded-full bg-muted-foreground" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={CheckCircle2} label="Status" count={draft.statuses.size}>
|
|
||||||
{selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)}
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{ALL_WIZARD_STATUSES.map((status) => (
|
|
||||||
<AdvStatusPill
|
|
||||||
key={status}
|
|
||||||
label={status}
|
|
||||||
dotColor={WIZARD_STATUS_DOTS[status]}
|
|
||||||
selected={draft.statuses.has(status)}
|
|
||||||
selectedBg="var(--color-primary-tint)"
|
|
||||||
onClick={() => toggleIn("statuses", status)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={Flame} label="PM type" count={draft.pmTypes.size}>
|
|
||||||
<AdvMultiSelect
|
|
||||||
options={pmTypes.map((pmType) => ({ value: pmType, label: pmType }))}
|
|
||||||
selected={draft.pmTypes}
|
|
||||||
onToggle={(value) => toggleIn("pmTypes", value)}
|
|
||||||
onBulkToggle={(values, select) => setMany("pmTypes", values, select)}
|
|
||||||
placeholder="Search pm type…"
|
|
||||||
/>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={Truck} label="Vendor / tech" count={draft.vendorTechs.size}>
|
|
||||||
<AdvMultiSelect
|
|
||||||
options={vendorTechs.map((name) => ({ value: name, label: name }))}
|
|
||||||
selected={draft.vendorTechs}
|
|
||||||
onToggle={(value) => toggleIn("vendorTechs", value)}
|
|
||||||
onBulkToggle={(values, select) => setMany("vendorTechs", values, select)}
|
|
||||||
placeholder="Search vendor / tech…"
|
|
||||||
/>
|
|
||||||
</AdvSection>
|
|
||||||
|
|
||||||
<AdvSection icon={FileText} label="Completion doc" count={draft.docs.size}>
|
|
||||||
{selectAllRow("docs", [...COMPLETION_DOC_OPTIONS], draft.docs.size)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{COMPLETION_DOC_OPTIONS.map((doc) => (
|
|
||||||
<AdvDocPill
|
|
||||||
key={doc}
|
|
||||||
label={DOC_FILTER_LABELS[doc]}
|
|
||||||
selected={draft.docs.has(doc)}
|
|
||||||
onClick={() => toggleIn("docs", doc)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</AdvSection>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2 border-t border-border px-4 py-3">
|
<div className="flex shrink-0 items-center gap-2 border-t border-border px-4 py-3">
|
||||||
<Button variant="ghost" className="text-xs" onClick={handleClearAll}>
|
<Button variant="ghost" className="text-xs" onClick={handleClearAll}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue