diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-pill.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-pill.tsx new file mode 100644 index 00000000..5b7c3394 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-filter-pill.tsx @@ -0,0 +1,78 @@ +import { DotTogglePill } from "@/components/ui/dot-toggle-pill"; + +import { TogglePill } from "@/components/ui/toggle-pill"; + +import { ToggleSegment } from "@/components/ui/toggle-segment"; + +type AdvFilterPillProps = { + label: string; + + selected: boolean; + + onClick: () => void; + + className?: string; +}; + +export function AdvFilterPill({ label, selected, onClick, className }: AdvFilterPillProps) { + return ( + + {label} + + ); +} + +type AdvStatusPillProps = { + label: string; + + dotColor: string; + + selected: boolean; + + selectedBg?: string; + + selectedText?: string; + + onClick: () => void; +}; + +export function AdvStatusPill({ + label, + + dotColor, + + selected, + + selectedBg, + + selectedText, + + onClick, +}: AdvStatusPillProps) { + return ( + + ); +} + +type AdvDocPillProps = { + label: string; + + selected: boolean; + + onClick: () => void; +}; + +export function AdvDocPill({ label, selected, onClick }: AdvDocPillProps) { + return ( + + {label} + + ); +} diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-multi-select.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-multi-select.tsx new file mode 100644 index 00000000..f9f877aa --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-multi-select.tsx @@ -0,0 +1,100 @@ +import { useState, type ReactNode } from "react"; + +import { CheckboxListItem } from "@/components/ui/checkbox-list-item"; + +import { SearchInput } from "@/components/ui/search-input"; + +import { SelectAllAction } from "@/components/ui/select-all-action"; + +type AdvMultiSelectOption = { + value: string; + + label: string; +}; + +type AdvMultiSelectProps = { + options: AdvMultiSelectOption[]; + + selected: Set; + + onToggle: (value: string) => void; + + onBulkToggle?: (values: string[], select: boolean) => void; + + placeholder: string; + + renderLeft?: (value: string) => ReactNode; +}; + +export function AdvMultiSelect({ + options, + + selected, + + onToggle, + + onBulkToggle, + + placeholder, + + renderLeft, +}: AdvMultiSelectProps) { + const [query, setQuery] = useState(""); + + const list = query + ? options.filter((option) => option.label.toLowerCase().includes(query.toLowerCase())) + : options; + + const anySelected = selected.size > 0; + + return ( +
+ + +
+ + {selected.size} of {options.length} selected + + + + anySelected + ? onBulkToggle?.( + options.map((option) => option.value), + + false, + ) + : onBulkToggle?.( + list.map((option) => option.value), + + true, + ) + } + /> +
+ +
+ {list.map((option) => ( + onToggle(option.value)} + /> + ))} + + {list.length === 0 && ( +
No matches
+ )} +
+
+ ); +} diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-section.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-section.tsx new file mode 100644 index 00000000..f7195508 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-section.tsx @@ -0,0 +1,19 @@ +import type { ElementType, ReactNode } from "react"; + +import { CollapsibleSection } from "@/components/ui/collapsible-section"; + +type AdvSectionProps = { + icon?: ElementType; + + label: string; + + count?: number; + + defaultOpen?: boolean; + + children: ReactNode; +}; + +export function AdvSection(props: AdvSectionProps) { + return ; +} diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/adv-type-grid.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/adv-type-grid.tsx new file mode 100644 index 00000000..b1762731 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/advanced-search/adv-type-grid.tsx @@ -0,0 +1,43 @@ +import { SelectAllAction } from "@/components/ui/select-all-action"; + +import { ToggleCard } from "@/components/ui/toggle-card"; + +import { + WO_TYPE_DESCRIPTIONS, + WO_TYPES, +} from "@/domain/work-orders/types/work-order-advanced-filters"; + +import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; + +type AdvTypeGridProps = { + selected: Set; + + onToggle: (type: WOType) => void; + + onSelectAll: (types: WOType[], select: boolean) => void; +}; + +export function AdvTypeGrid({ selected, onToggle, onSelectAll }: AdvTypeGridProps) { + return ( + <> +
+ 0} + onClick={() => onSelectAll([...WO_TYPES], selected.size === 0)} + /> +
+ +
+ {WO_TYPES.map((type) => ( + onToggle(type)} + /> + ))} +
+ + ); +} diff --git a/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx b/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx new file mode 100644 index 00000000..06a3b283 --- /dev/null +++ b/src/app/(protected)/workorders/_components/list/advanced-search/work-orders-advanced-search-sheet.tsx @@ -0,0 +1,273 @@ +import { useEffect, useState } from "react"; +import { + CalendarDays, + 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 { DateField } from "@/components/ui/date-input"; +import { IconCloseButton } from "@/components/ui/icon-close-button"; +import { SelectAllAction } from "@/components/ui/select-all-action"; +import { SideSheet } from "@/components/ui/side-sheet"; +import { + 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 { 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 = { + open: boolean; + onOpenChange: (open: boolean) => void; + initial: WorkOrderAdvancedFilters; + onApply: (filters: WorkOrderAdvancedFilters) => void; + onClearAll: () => void; + sites: { id: string | number; name: string }[]; + users: WorkOrderUser[]; + pmTypes: string[]; + vendorTechs: string[]; +}; + +export function WorkOrdersAdvancedSearchSheet({ + open, + onOpenChange, + initial, + onApply, + onClearAll, + sites, + users, + pmTypes, + vendorTechs, +}: WorkOrdersAdvancedSearchSheetProps) { + const [draft, setDraft] = useState(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); + 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); + values.forEach((value) => (select ? next.add(value) : next.delete(value))); + return { ...prev, [key]: next }; + }); + }; + + const selectAllRow = (key: AdvancedFilterSetKey, all: string[], selectedSize: number) => ( +
+ 0} + onClick={() => setMany(key, all, selectedSize === 0)} + /> +
+ ); + + const handleClearAll = () => { + setDraft(cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS)); + onClearAll(); + }; + + const handleApply = () => { + onApply(cloneAdvancedFilters(draft)); + onOpenChange(false); + }; + + return ( + onOpenChange(false)} aria-label="Advanced Filters"> + Advanced Filters + +
+
+ + + + Advanced Filters + + + {activeCount > 0 && ( + · {activeCount} active + )} +
+ onOpenChange(false)} /> +
+ +
+ +
+ {DATE_RANGE_OPTIONS.map((option) => ( + setDraft((prev) => ({ ...prev, dateRange: option.key }))} + /> + ))} +
+ {draft.dateRange === "custom" && ( +
+ setDraft((prev) => ({ ...prev, customFrom: value }))} + /> + setDraft((prev) => ({ ...prev, customTo: value }))} + /> +
+ )} +
+ + + ({ + 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…" + /> + + + + toggleIn("types", type)} + onSelectAll={(types, select) => setMany("types", types, select)} + /> + + + + ({ 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 ? ( + + ) : ( + + ) + } + /> + + + + {selectAllRow("statuses", [...ALL_WIZARD_STATUSES], draft.statuses.size)} +
+ {ALL_WIZARD_STATUSES.map((status) => ( + toggleIn("statuses", status)} + /> + ))} +
+
+ + + ({ value: pmType, label: pmType }))} + selected={draft.pmTypes} + onToggle={(value) => toggleIn("pmTypes", value)} + onBulkToggle={(values, select) => setMany("pmTypes", values, select)} + placeholder="Search pm type…" + /> + + + + ({ value: name, label: name }))} + selected={draft.vendorTechs} + onToggle={(value) => toggleIn("vendorTechs", value)} + onBulkToggle={(values, select) => setMany("vendorTechs", values, select)} + placeholder="Search vendor / tech…" + /> + + + + {selectAllRow("docs", [...COMPLETION_DOC_OPTIONS], draft.docs.size)} +
+ {COMPLETION_DOC_OPTIONS.map((doc) => ( + toggleIn("docs", doc)} + /> + ))} +
+
+
+ +
+ +
+ {activeCount > 0 && ( + + {activeCount} filter set + + )} +
+ +
+
+ ); +}