Merge remote-tracking branch 'origin/dev' into codex/pr30-lint-fix

This commit is contained in:
Alexandre Brandizzi 2026-07-24 18:43:30 -03:00
commit 07c8799a3f
3 changed files with 271 additions and 206 deletions

View file

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

View file

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

View file

@ -1,42 +1,11 @@
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 { SlidersHorizontal } from "lucide-react";
import { AdvancedSearchFilterSections } from "./adv-filter-sections";
import { useAdvancedSearchDraft } from "./use-advanced-search-draft";
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 { 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;
@ -61,54 +30,8 @@ export function WorkOrdersAdvancedSearchSheet({
pmTypes,
vendorTechs,
}: WorkOrdersAdvancedSearchSheetProps) {
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 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);
};
const { draft, setDraft, activeCount, toggleIn, setMany, handleClearAll, handleApply } =
useAdvancedSearchDraft({ open, initial, onApply, onClearAll, onOpenChange });
return (
<SideSheet open={open} onClose={() => onOpenChange(false)} aria-label="Advanced Filters">
@ -129,129 +52,16 @@ export function WorkOrdersAdvancedSearchSheet({
<IconCloseButton onClick={() => onOpenChange(false)} />
</div>
<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>
<AdvancedSearchFilterSections
draft={draft}
setDraft={setDraft}
toggleIn={toggleIn}
setMany={setMany}
sites={sites}
users={users}
pmTypes={pmTypes}
vendorTechs={vendorTechs}
/>
<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}>