diff --git a/src/components/features/query/popovers/filter-rows.tsx b/src/components/features/query/popovers/filter-rows.tsx index d782e84..646ade9 100644 --- a/src/components/features/query/popovers/filter-rows.tsx +++ b/src/components/features/query/popovers/filter-rows.tsx @@ -1,5 +1,6 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Combobox } from "@/components/ui/combobox"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { @@ -11,34 +12,38 @@ import { } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { FilterRowsSelection } from "@/lib/firebase/types"; +import { CATEGORICAL_COLUMNS } from "@/services/query"; import { Check, Pencil, Plus, X } from "lucide-react"; import { useState } from "react"; +const BOOLEAN_OPTIONS = ["true", "false"]; + /** * Set of possible operators for filtering rows based on data type. */ const CONDITION_OPTIONS: Record = { string: [ - { value: "matches", label: "matches" }, - { value: "does_not_match", label: "does not match" }, - { value: "equals", label: "equals" }, - { value: "not_equals", label: "is not equal to" }, + { value: "matches", label: "contains" }, + { value: "does_not_match", label: "does not contain" }, + { value: "equals", label: "exactly equals" }, + { value: "not_equals", label: "is not exactly equal to" }, ], number: [ - { value: "equals", label: "equals" }, - { value: "not_equals", label: "is not equal to" }, + { value: "equals", label: "exactly equals" }, + { value: "not_equals", label: "is not exactly equal to" }, { value: "greater_than", label: "greater than" }, { value: "less_than", label: "less than" }, ], boolean: [ - { value: "equals", label: "equals" }, - { value: "not_equals", label: "is not equal to" }, + { value: "equals", label: "exactly equals" }, + { value: "not_equals", label: "is not exactly equal to" }, ], }; interface FilterRowsProps { columns: string[]; columnTypes: Record; + columnValueOptions: Record; filterSelections: FilterRowsSelection[]; onAddFilter: (filter: FilterRowsSelection) => void; onRemoveFilter: (filterId: string) => void; @@ -53,6 +58,7 @@ interface FilterRowsProps { export function FilterRows({ columns, columnTypes, + columnValueOptions, filterSelections, onAddFilter, onRemoveFilter, @@ -283,13 +289,38 @@ export function FilterRows({ ))} - setNewFilterValue(e.target.value)} - disabled={!newFilterCondition} - /> + {(() => { + const isBoolean = type === "boolean"; + const isCategorical = CATEGORICAL_COLUMNS.has(newFilterColumn); + const options = isBoolean + ? BOOLEAN_OPTIONS + : isCategorical + ? (columnValueOptions[newFilterColumn] ?? []) + : []; + + if (isBoolean || isCategorical) { + return ( + + ); + } + + return ( + setNewFilterValue(e.target.value)} + disabled={!newFilterCondition} + /> + ); + })()} + ))} + + + )} + + ); +} diff --git a/src/providers/query-provider.tsx b/src/providers/query-provider.tsx index 91ac870..47a245e 100644 --- a/src/providers/query-provider.tsx +++ b/src/providers/query-provider.tsx @@ -11,6 +11,7 @@ import type { import type { FlattenedApplicant } from "@/services/query"; import { calculateApplicantPoints, + extractColumnValues, flattenApplicantData, subscribeToApplicants, } from "@/services/query"; @@ -138,6 +139,8 @@ interface QueryContextType { // Final data after processing tableData: FlattenedApplicant[]; + columnValueOptions: Record; + // Actions onColumnToggle: (column: string) => void; onGroupByChange: (selection: GroupBySelection | undefined) => void; @@ -182,6 +185,8 @@ export function QueryProvider({ children }: QueryProviderProps) { const [filterSelections, setFilterSelections] = useState([]); const [sorting, setSorting] = useState([]); + const columnValueOptions = useMemo(() => extractColumnValues(applicants), [applicants]); + const tableData = useMemo(() => { let filtered = applicants; if (filterSelections.length > 0) { @@ -288,6 +293,7 @@ export function QueryProvider({ children }: QueryProviderProps) { filterSelections, sorting, tableData, + columnValueOptions, onColumnToggle: handleColumnToggle, onGroupByChange: setGroupBySelection, onFilterAdd: handleFilterAdd, diff --git a/src/services/query.ts b/src/services/query.ts index c6eea5f..9786230 100644 --- a/src/services/query.ts +++ b/src/services/query.ts @@ -331,6 +331,64 @@ export interface FlattenedApplicant { [key: string]: string | number | boolean | Date | null | Record | undefined; // extra keys for group-by results } +export const CATEGORICAL_COLUMNS: ReadonlySet = new Set([ + "applicationStatus", + "educationLevel", + "major", + "role", + "dietaryRestriction", + "culturalBackground", + "school", + "gender", + "countryOfResidence", + "academicYear", + "canadianStatus", + "disability", + "haveTransExperience", + "indigenousIdentification", + "jobPosition", + "travellingToHackathon", + "engagementSource", + "ageByHackathon", + "graduation", +]); + +export const MULTI_VALUE_COLUMNS: ReadonlySet = new Set([ + "major", + "gender", + "dietaryRestriction", + "culturalBackground", + "role", + "engagementSource", +]); + +export const extractColumnValues = ( + applicants: FlattenedApplicant[], +): Record => { + const result: Record = {}; + for (const column of CATEGORICAL_COLUMNS) { + const values = new Set(); + const isMultiValue = MULTI_VALUE_COLUMNS.has(column); + for (const applicant of applicants) { + const raw = applicant[column]; + if (raw === null || raw === undefined) continue; + if (isMultiValue) { + for (const token of String(raw).split(",")) { + const trimmed = token.trim(); + if (trimmed) values.add(trimmed); + } + } else { + const trimmed = String(raw).trim(); + if (trimmed) values.add(trimmed); + } + } + result[column] = [...values].sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ); + } + return result; +}; + /** * Calculates all hackers' points from day-of events asynchronously *