{"version":3,"file":"filter-model.cjs","names":[],"sources":["../../../src/components/FilterBar/filter-model.ts"],"sourcesContent":["// The filter model behind `FilterBar`: what a field can be compared with, how a\n// filter reads in words, and how a filter set survives a page reload.\n//\n// Flat AND, deliberately. Nested `(a OR b) AND c` groups are a different component\n// with a different UI (a tree with per-node operators) and a different serialization\n// — trying to be both produces a builder that is clumsy for the 95% case, which is\n// \"status is paid, created after March, text contains nota\".\n\n/** Comparison a filter can make. */\nexport type FilterOperator =\n    | \"eq\"\n    | \"ne\"\n    | \"contains\"\n    | \"gt\"\n    | \"gte\"\n    | \"lt\"\n    | \"lte\"\n    | \"between\"\n    | \"in\"\n    | \"empty\"\n    | \"notEmpty\";\n\n/** Kind of value a field holds, which decides its operators and its input. */\nexport type FilterFieldType = \"text\" | \"number\" | \"date\" | \"select\" | \"boolean\";\n\n/** One filterable field. */\nexport interface FilterField {\n    /** Key sent to the backend. */\n    name: string;\n    /** What the user sees. */\n    label: string;\n    type: FilterFieldType;\n    /** Choices for `select`. */\n    options?: ReadonlyArray<{ value: string; label: string }>;\n    /** Restrict or reorder the operators offered. Defaults per type. */\n    operators?: readonly FilterOperator[];\n    /** Placeholder for the value input. */\n    placeholder?: string;\n}\n\n/** One applied filter. */\nexport interface Filter {\n    field: string;\n    operator: FilterOperator;\n    /** `between` carries a pair; `in` carries a list; `empty`/`notEmpty` carry nothing. */\n    value?: string | readonly string[];\n}\n\n/** Operators offered per field type, in the order they appear. */\nconst OPERATORS_BY_TYPE: Record<FilterFieldType, FilterOperator[]> = {\n    text: [\"contains\", \"eq\", \"ne\", \"empty\", \"notEmpty\"],\n    number: [\"eq\", \"ne\", \"gt\", \"gte\", \"lt\", \"lte\", \"between\"],\n    date: [\"eq\", \"gt\", \"gte\", \"lt\", \"lte\", \"between\"],\n    select: [\"eq\", \"ne\", \"in\", \"empty\", \"notEmpty\"],\n    boolean: [\"eq\"],\n};\n\n/** Operators that take no value at all. */\nconst VALUELESS: ReadonlySet<FilterOperator> = new Set([\"empty\", \"notEmpty\"]);\n\n/** Operators that take more than one value. */\nconst MULTI: ReadonlySet<FilterOperator> = new Set([\"between\", \"in\"]);\n\n/** Human labels for the operators, per locale. */\nconst OPERATOR_LABELS: Record<\"pt-BR\" | \"en\", Record<FilterOperator, string>> = {\n    \"pt-BR\": {\n        eq: \"é\",\n        ne: \"não é\",\n        contains: \"contém\",\n        gt: \"maior que\",\n        gte: \"maior ou igual a\",\n        lt: \"menor que\",\n        lte: \"menor ou igual a\",\n        between: \"entre\",\n        in: \"é um de\",\n        empty: \"está vazio\",\n        notEmpty: \"não está vazio\",\n    },\n    en: {\n        eq: \"is\",\n        ne: \"is not\",\n        contains: \"contains\",\n        gt: \"greater than\",\n        gte: \"at least\",\n        lt: \"less than\",\n        lte: \"at most\",\n        between: \"between\",\n        in: \"is any of\",\n        empty: \"is empty\",\n        notEmpty: \"is not empty\",\n    },\n};\n\n/** The operators a field offers. */\nexport function operatorsFor(field: FilterField): FilterOperator[] {\n    return [...(field.operators ?? OPERATORS_BY_TYPE[field.type])];\n}\n\n/** Label of an operator. */\nexport function operatorLabel(operator: FilterOperator, locale: \"pt-BR\" | \"en\" = \"pt-BR\"): string {\n    return OPERATOR_LABELS[locale][operator] ?? operator;\n}\n\n/** True when the operator needs no value input. */\nexport function isValueless(operator: FilterOperator): boolean {\n    return VALUELESS.has(operator);\n}\n\n/** True when the operator takes several values. */\nexport function isMulti(operator: FilterOperator): boolean {\n    return MULTI.has(operator);\n}\n\n/**\n * Is this filter complete enough to apply?\n *\n * An incomplete filter is not an error to shout about — it is a half-filled form.\n * The component uses this to keep \"Add\" disabled, which says the same thing without\n * a message nobody asked for.\n */\nexport function isComplete(filter: Filter): boolean {\n    if (isValueless(filter.operator)) return true;\n    const { value } = filter;\n    if (value === undefined) return false;\n    if (Array.isArray(value)) {\n        if (filter.operator === \"between\") return value.length === 2 && value.every(Boolean);\n        return value.length > 0 && value.every(Boolean);\n    }\n    return String(value).trim() !== \"\";\n}\n\n/** Human text for one value, resolving a `select` option to its label. */\nfunction valueLabel(field: FilterField | undefined, raw: string): string {\n    const option = field?.options?.find((candidate) => candidate.value === raw);\n    return option ? option.label : raw;\n}\n\n/**\n * One filter, in words: `\"Status é Pago\"`.\n *\n * Used for the chip and for the screen-reader announcement, from the same source —\n * a chip that reads `status=eq:paid` to a sighted user and something else to a\n * screen reader would be two different truths.\n */\nexport function describeFilter(\n    filter: Filter,\n    fields: readonly FilterField[],\n    locale: \"pt-BR\" | \"en\" = \"pt-BR\",\n): string {\n    const field = fields.find((candidate) => candidate.name === filter.field);\n    const name = field?.label ?? filter.field;\n    const operator = operatorLabel(filter.operator, locale);\n    if (isValueless(filter.operator)) return `${name} ${operator}`;\n\n    const values = Array.isArray(filter.value)\n        ? filter.value.map((raw) => valueLabel(field, raw))\n        : [valueLabel(field, String(filter.value ?? \"\"))];\n\n    if (filter.operator === \"between\" && values.length === 2) {\n        const joiner = locale === \"en\" ? \"and\" : \"e\";\n        return `${name} ${operator} ${values[0]} ${joiner} ${values[1]}`;\n    }\n    return `${name} ${operator} ${values.join(\", \")}`;\n}\n\n/** Default operator for a field — the first one it offers. */\nexport function defaultOperator(field: FilterField): FilterOperator {\n    return operatorsFor(field)[0] ?? \"eq\";\n}\n\n/**\n * Serialize filters into URL search params.\n *\n * One param per filter, `field=operator:value`, with `|` between the values of a\n * multi-value operator. A filter set that cannot survive a reload is a filter set\n * people re-enter every time they open a link somebody sent them, so this is part\n * of the model rather than something each app reinvents.\n *\n * Repeated fields are kept: `status=eq:paid&status=eq:sent` is two filters, and\n * collapsing them would silently drop one.\n *\n * @param filters - Applied filters.\n * @returns Params ready to merge into a location.\n */\nexport function filtersToSearchParams(filters: readonly Filter[]): URLSearchParams {\n    const params = new URLSearchParams();\n    for (const filter of filters) {\n        if (!isComplete(filter)) continue;\n        const value = Array.isArray(filter.value) ? filter.value.join(\"|\") : (filter.value ?? \"\");\n        params.append(\n            filter.field,\n            isValueless(filter.operator) ? filter.operator : `${filter.operator}:${value}`,\n        );\n    }\n    return params;\n}\n\n/**\n * Read filters back from URL search params.\n *\n * Anything that does not parse is dropped rather than guessed at: a hand-edited URL\n * is the normal way this input arrives, and rendering a filter the app cannot\n * evaluate would show a list that does not match what the chips claim.\n *\n * @param params - Params from the location.\n * @param fields - Known fields; a param naming anything else is ignored.\n * @returns The filters that parsed.\n */\nexport function filtersFromSearchParams(\n    params: URLSearchParams,\n    fields: readonly FilterField[],\n): Filter[] {\n    const known = new Map(fields.map((field) => [field.name, field]));\n    const filters: Filter[] = [];\n\n    for (const [name, raw] of params.entries()) {\n        const field = known.get(name);\n        if (!field) continue;\n\n        const separator = raw.indexOf(\":\");\n        const operator = (separator < 0 ? raw : raw.slice(0, separator)) as FilterOperator;\n        if (!operatorsFor(field).includes(operator)) continue;\n\n        if (isValueless(operator)) {\n            filters.push({ field: name, operator });\n            continue;\n        }\n        if (separator < 0) continue;\n\n        const rest = raw.slice(separator + 1);\n        const value = isMulti(operator) ? rest.split(\"|\") : rest;\n        const filter: Filter = { field: name, operator, value };\n        if (isComplete(filter)) filters.push(filter);\n    }\n\n    return filters;\n}\n\n/** Labels the bar needs, per locale. */\ninterface FilterStrings {\n    add: string;\n    apply: string;\n    cancel: string;\n    clear: string;\n    field: string;\n    operator: string;\n    value: string;\n    from: string;\n    to: string;\n    yes: string;\n    no: string;\n    remove: (description: string) => string;\n    active: (count: number) => string;\n    empty: string;\n}\n\nconst PT_BR: FilterStrings = {\n    add: \"Adicionar filtro\",\n    apply: \"Aplicar\",\n    cancel: \"Cancelar\",\n    clear: \"Limpar filtros\",\n    field: \"Campo\",\n    operator: \"Condição\",\n    value: \"Valor\",\n    from: \"De\",\n    to: \"Até\",\n    yes: \"Sim\",\n    no: \"Não\",\n    remove: (description) => `Remover filtro: ${description}`,\n    active: (count) => (count === 1 ? \"1 filtro ativo\" : `${count} filtros ativos`),\n    empty: \"Nenhum filtro\",\n};\n\nconst EN: FilterStrings = {\n    add: \"Add filter\",\n    apply: \"Apply\",\n    cancel: \"Cancel\",\n    clear: \"Clear filters\",\n    field: \"Field\",\n    operator: \"Condition\",\n    value: \"Value\",\n    from: \"From\",\n    to: \"To\",\n    yes: \"Yes\",\n    no: \"No\",\n    remove: (description) => `Remove filter: ${description}`,\n    active: (count) => (count === 1 ? \"1 active filter\" : `${count} active filters`),\n    empty: \"No filters\",\n};\n\n/** Locale strings for the bar. */\nexport function filterStrings(locale: \"pt-BR\" | \"en\"): FilterStrings {\n    return locale === \"en\" ? EN : PT_BR;\n}\n"],"mappings":"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"}