import { NgTemplateOutlet } from '@angular/common';
import * as i0 from '@angular/core';
import { input, booleanAttribute, model, signal, computed, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
import * as i1 from '@angular/material/icon';
import { MatIconModule } from '@angular/material/icon';
import { SdDate } from '@sdcorejs/angular/forms/date';
import { SdDatetime } from '@sdcorejs/angular/forms/datetime';
import { SdInput } from '@sdcorejs/angular/forms/input';
import { SdInputNumber } from '@sdcorejs/angular/forms/input-number';
import { SdSelect } from '@sdcorejs/angular/forms/select';
import { SdItemDefDefDirective } from '@sdcorejs/angular/forms/directives';
import { SdOperator } from '@sdcorejs/angular/components/operator';
import { FilterUtilities } from '@sdcorejs/utils/fns';

/** Type guard — narrows a `QbNode` to a `QbGroup`. */
function isQbGroup(node) {
    return node.kind === 'group';
}
// ---------------------------------------------------------------------------
// Operator vocabulary per field type. Mirrors query-bar's
// SD_QUERY_OPERATORS_BY_TYPE so the two components stay consistent, but is kept
// local so query-builder has no dependency on the query-bar package.
// ---------------------------------------------------------------------------
/** Allowed operators per field type — used when `field.operators` is not set. */
const QB_OPERATORS_BY_TYPE = {
    string: ['CONTAIN', 'NOT_CONTAIN', 'EQUAL', 'NOT_EQUAL', 'START_WITH', 'END_WITH', 'NULL', 'NOT_NULL'],
    number: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'GREATER_OR_EQUAL', 'LESS_THAN', 'LESS_OR_EQUAL', 'BETWEEN', 'NULL', 'NOT_NULL'],
    boolean: ['EQUAL', 'NOT_EQUAL'],
    date: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'LESS_THAN', 'BETWEEN', 'NULL', 'NOT_NULL'],
    datetime: ['EQUAL', 'NOT_EQUAL', 'GREATER_THAN', 'LESS_THAN', 'BETWEEN', 'NULL', 'NOT_NULL'],
    values: ['IN', 'NOT_IN', 'EQUAL', 'NOT_EQUAL', 'NULL', 'NOT_NULL'],
};
/** Default operator when a rule is first created for a field. */
const QB_DEFAULT_OPERATOR_BY_TYPE = {
    string: 'CONTAIN',
    number: 'EQUAL',
    boolean: 'EQUAL',
    date: 'BETWEEN',
    datetime: 'BETWEEN',
    values: 'IN',
};
/** Operators with no value payload — the value editor is hidden for these. */
const QB_NO_DATA_OPERATORS = ['NULL', 'NOT_NULL'];
/** Operators whose value is an array — the value editor offers multi-select. */
const QB_MULTI_OPERATORS = ['IN', 'NOT_IN'];
/** Operators allowed for `field` — explicit override wins, else the per-type set. */
function qbAllowedOperators(field) {
    if (!field)
        return [];
    if (field.operators?.length)
        return field.operators;
    return QB_OPERATORS_BY_TYPE[field.type] ?? [];
}
/** Starting operator for a new rule on `field` — falls back to the per-type default. */
function qbDefaultOperator(field) {
    if (!field)
        return undefined;
    const allowed = qbAllowedOperators(field);
    if (field.defaultOperator && allowed.includes(field.defaultOperator))
        return field.defaultOperator;
    const byType = QB_DEFAULT_OPERATOR_BY_TYPE[field.type];
    return allowed.includes(byType) ? byType : allowed[0];
}
/** True when `op` is NULL / NOT_NULL (no value editor). */
function qbIsNoDataOperator(op) {
    return !!op && QB_NO_DATA_OPERATORS.includes(op);
}
/** True when `op` is IN / NOT_IN (array value). */
function qbIsMultiOperator(op) {
    return !!op && QB_MULTI_OPERATORS.includes(op);
}
/** True when the operator accepts one right-hand operand, including a field reference. */
function qbSupportsFieldCompareOperator(op) {
    return !!op && !qbIsNoDataOperator(op) && !qbIsMultiOperator(op) && op !== 'BETWEEN';
}
/**
 * Icon fallback per field type when `SdQueryBuilderField.icon` is not set.
 * Mirrors query-bar's `SD_QUERY_TYPE_ICON` so the two components read consistently,
 * but kept local so query-builder has no dependency on the query-bar package.
 */
const QB_TYPE_ICON = {
    string: 'text_fields',
    number: 'tag',
    boolean: 'toggle_on',
    date: 'event',
    datetime: 'schedule',
    values: 'list',
};
/**
 * Icon shown before a field in the field picker / selected trigger.
 * Priority: `field.icon` → `QB_TYPE_ICON[field.type]` → `'tune'` (default, like query-bar).
 */
function qbFieldIcon(field) {
    if (!field)
        return 'tune';
    return field.icon ?? QB_TYPE_ICON[field.type] ?? 'tune';
}
// ---------------------------------------------------------------------------
// Node factories — monotonic counter for stable, render-safe ids (no Math.random
// / Date.now so SSR + tests stay deterministic).
// ---------------------------------------------------------------------------
let qbSeq = 0;
/** Next stable node id with the given prefix. */
function qbId(prefix = 'qb') {
    return `${prefix}-${++qbSeq}`;
}
/** Build a fresh rule node (optionally pre-filled). */
function qbNewRule(field, operator, value, valueSource, compareField) {
    return { id: qbId('r'), kind: 'rule', field, operator, value, valueSource, compareField };
}
/** Build a fresh group node. */
function qbNewGroup(logic = 'AND', children = []) {
    return { id: qbId('g'), kind: 'group', logic, children };
}
/** Sentinel held as a rule value (and emitted as `data`) for the "today" date mode. */
const QB_TODAY = 'TODAY';
/** Type guard — narrows a rule value to a utils `DateRelative` (offset spec). */
const qbIsRelativeDate = FilterUtilities.isDateRelative;
/** Type guard — narrows a rule value to the `'TODAY'` sentinel ("now"/today mode). */
function qbIsToday(v) {
    return v === QB_TODAY;
}
/** Starting relative value when a rule first switches to "relative" mode. */
function qbDefaultRelative() {
    return { amount: 1, direction: 'previous', unit: 'day' };
}
/** Stable option list for the date-mode select (module ref — never reallocated). */
const QB_DATE_MODES = [
    { value: 'absolute', display: 'Ngày cụ thể', icon: 'event' },
    { value: 'now', display: 'Hôm nay', icon: 'today' },
    { value: 'relative', display: 'Tương đối', icon: 'history' },
];
/** Stable option list for choosing between a literal value and another field. */
const QB_VALUE_SOURCE_OPTIONS = [
    { value: 'literal', display: 'Nhập giá trị', icon: 'edit_note' },
    { value: 'field', display: 'Chọn trường', icon: 'view_column' },
];
/** Stable combined direction×unit option list (token `'unit:direction'`). */
const QB_RELATIVE_UNIT_OPTIONS = [
    { value: 'day:previous', display: 'ngày trước' },
    { value: 'day:next', display: 'ngày tới' },
    { value: 'week:previous', display: 'tuần trước' },
    { value: 'week:next', display: 'tuần tới' },
    { value: 'month:previous', display: 'tháng trước' },
    { value: 'month:next', display: 'tháng tới' },
];
/** Shared empty option array — stable ref for fallbacks (avoids per-call allocation). */
const QB_EMPTY_OPTIONS = [];

// SQL symbol for each single-value comparison operator.
const SYMBOL = {
    EQUAL: '=',
    NOT_EQUAL: '!=',
    GREATER_THAN: '>',
    LESS_THAN: '<',
    GREATER_OR_EQUAL: '>=',
    LESS_OR_EQUAL: '<=',
};
const LIKE_OPERATORS = ['CONTAIN', 'NOT_CONTAIN', 'START_WITH', 'NOT_START_WITH', 'END_WITH', 'NOT_END_WITH'];
/** A `Filter` is a logical group when its operator is AND / OR and it carries a `data[]`. */
function isAndOr$1(filter) {
    return !!filter && (filter.operator === 'AND' || filter.operator === 'OR') && Array.isArray(filter.data);
}
function isEmptyValue(v) {
    return v === null || v === undefined || v === '';
}
// ---------------------------------------------------------------------------
// Internal tree → public Filter
// ---------------------------------------------------------------------------
function ruleToFilter(rule) {
    if (!rule.field || !rule.operator)
        return null;
    const operator = rule.operator;
    if (QB_NO_DATA_OPERATORS.includes(operator)) {
        return { field: rule.field, operator };
    }
    if (rule.valueSource === 'field') {
        if (!qbSupportsFieldCompareOperator(operator) || isEmptyValue(rule.compareField))
            return null;
        return { field: rule.field, operator, dataType: 'field', data: rule.compareField };
    }
    if (operator === 'BETWEEN') {
        const v = rule.value;
        if (!v || typeof v !== 'object')
            return null;
        if (isEmptyValue(v.from) || isEmptyValue(v.to))
            return null;
        return { field: rule.field, operator: 'BETWEEN', data: { from: v.from, to: v.to } };
    }
    if (QB_MULTI_OPERATORS.includes(operator)) {
        const arr = Array.isArray(rule.value) ? rule.value : isEmptyValue(rule.value) ? [] : [rule.value];
        if (!arr.length)
            return null;
        return { field: rule.field, operator, data: arr };
    }
    if (qbIsToday(rule.value)) {
        return { field: rule.field, operator, dataType: 'date-today', data: QB_TODAY };
    }
    if (qbIsRelativeDate(rule.value)) {
        const v = rule.value;
        // emit only when fully specified, else drop the incomplete rule
        if (v.unit && v.direction && typeof v.amount === 'number' && v.amount >= 1) {
            return {
                field: rule.field,
                operator,
                dataType: 'date-relative',
                data: { amount: v.amount, direction: v.direction, unit: v.unit },
            };
        }
        return null;
    }
    // A non-relative plain object is an incomplete/stray shape (e.g. a partial offset) — drop it.
    if (rule.value !== null && typeof rule.value === 'object' && !Array.isArray(rule.value))
        return null;
    if (isEmptyValue(rule.value))
        return null;
    return { field: rule.field, operator, data: rule.value };
}
function nodeToFilter(node) {
    if (isQbGroup(node)) {
        const data = node.children.map(nodeToFilter).filter((f) => f != null);
        if (!data.length)
            return null;
        return { operator: node.logic, data };
    }
    return ruleToFilter(node);
}
/**
 * Map the internal builder tree to the public `Filter` (the root is always a
 * `FilterAndOr`). Rules missing field / operator / value — and empty nested
 * groups — are dropped so the result is always a valid `Filter`.
 *
 * @param group - the editable tree's root group.
 * @returns the canonical nested `Filter`, or `null` when no complete rule survives.
 */
function treeToFilter(group) {
    return nodeToFilter(group);
}
// ---------------------------------------------------------------------------
// Public Filter → internal tree (for seeding [value])
// ---------------------------------------------------------------------------
function ruleFromFilter(filter) {
    const operator = filter.operator;
    if (QB_NO_DATA_OPERATORS.includes(operator)) {
        return qbNewRule(filter.field, operator);
    }
    if (filter.dataType === 'field') {
        return qbNewRule(filter.field, operator, undefined, 'field', filter.data);
    }
    // Relative dates: read the utils `dataType` discriminator back into the internal value.
    if (filter.dataType === 'date-today') {
        return qbNewRule(filter.field, operator, QB_TODAY);
    }
    if (filter.dataType === 'date-relative') {
        return qbNewRule(filter.field, operator, filter.data);
    }
    // Back-compat: legacy `{ rel: 'now' | 'offset' }` payloads (pre-dataType migration)
    // still seed the editor correctly.
    const legacy = readLegacyRelative(filter.data);
    if (legacy !== undefined) {
        return qbNewRule(filter.field, operator, legacy);
    }
    return qbNewRule(filter.field, operator, filter.data);
}
/** Map a pre-migration `{ rel }` payload to the new internal value, or `undefined` if not legacy. */
function readLegacyRelative(data) {
    if (!data || typeof data !== 'object')
        return undefined;
    if (data.rel === 'now')
        return QB_TODAY;
    if (data.rel === 'offset' && data.unit && data.direction && typeof data.amount === 'number') {
        return { amount: data.amount, direction: data.direction, unit: data.unit };
    }
    return undefined;
}
function nodeFromFilter(filter) {
    return isAndOr$1(filter) ? groupFromFilter(filter) : ruleFromFilter(filter);
}
function groupFromFilter(filter) {
    return qbNewGroup(filter.operator, filter.data.map(nodeFromFilter));
}
/**
 * Inverse of {@link treeToFilter} — rebuild the editable tree from a `Filter` so a
 * consumer can seed `[value]`. A non-group (bare) filter is wrapped in an AND root;
 * `null` yields an empty AND root.
 *
 * @param filter - a `Filter` (group or leaf), or `null` / `undefined` for an empty tree.
 * @returns a fresh root `QbGroup` mirroring the filter's structure.
 */
function filterToTree(filter) {
    if (!filter)
        return qbNewGroup('AND', []);
    if (isAndOr$1(filter))
        return groupFromFilter(filter);
    return qbNewGroup('AND', [ruleFromFilter(filter)]);
}
// ---------------------------------------------------------------------------
// Public Filter → SQL-ish highlight tokens (view mode)
// ---------------------------------------------------------------------------
function escapeStr(s) {
    return s.replace(/'/g, "''");
}
const REL_UNIT_VI = { hour: 'giờ', day: 'ngày', week: 'tuần', month: 'tháng' };
/** Render a relative-date offset as readable Vietnamese for the view string. */
function formatRelative(v) {
    const unit = REL_UNIT_VI[v.unit] ?? REL_UNIT_VI.day;
    const dir = v.direction === 'next' ? 'tới' : 'trước';
    return `${v.amount} ${unit} ${dir}`;
}
/** Format one scalar value for the view string — quoted/labelled per field type. */
function formatScalar(field, raw) {
    if (qbIsToday(raw))
        return 'hôm nay';
    if (qbIsRelativeDate(raw))
        return formatRelative(raw);
    if (raw === null || raw === undefined)
        return '';
    if (field?.type === 'boolean') {
        return raw ? field.trueLabel ?? 'Có' : field.falseLabel ?? 'Không';
    }
    if (field?.type === 'values') {
        const opt = field.values?.find(o => o.value === raw);
        return `'${escapeStr(opt ? opt.display : String(raw))}'`;
    }
    if (field?.type === 'number' || typeof raw === 'number' || typeof raw === 'boolean') {
        return String(raw);
    }
    // string / date / datetime / unknown → quoted
    return `'${escapeStr(String(raw))}'`;
}
function likePattern(operator, raw) {
    const esc = escapeStr(raw);
    if (operator === 'CONTAIN' || operator === 'NOT_CONTAIN')
        return `'%${esc}%'`;
    if (operator === 'START_WITH' || operator === 'NOT_START_WITH')
        return `'${esc}%'`;
    return `'%${esc}'`; // END_WITH / NOT_END_WITH
}
function operatorToken(operator) {
    if (LIKE_OPERATORS.includes(operator))
        return operator.startsWith('NOT_') ? 'not like' : 'like';
    return SYMBOL[operator] ?? operator;
}
function ruleTokens(filter, fields) {
    const field = fields.find(f => f.key === filter.field);
    const out = [{ kind: 'field', text: field?.label ?? String(filter.field) }];
    const sp = () => out.push({ kind: 'plain', text: ' ' });
    const operator = filter.operator;
    if (QB_NO_DATA_OPERATORS.includes(operator)) {
        sp();
        out.push({ kind: 'op', text: operator === 'NULL' ? 'is null' : 'is not null' });
        return out;
    }
    if (operator === 'BETWEEN') {
        sp();
        out.push({ kind: 'op', text: 'between' });
        sp();
        out.push({ kind: 'value', text: formatScalar(field, filter.data?.from) });
        out.push({ kind: 'plain', text: ' and ' });
        out.push({ kind: 'value', text: formatScalar(field, filter.data?.to) });
        return out;
    }
    if (QB_MULTI_OPERATORS.includes(operator)) {
        sp();
        out.push({ kind: 'op', text: operator === 'IN' ? 'in' : 'not in' });
        sp();
        const arr = Array.isArray(filter.data) ? filter.data : [];
        out.push({ kind: 'value', text: `(${arr.map((v) => formatScalar(field, v)).join(', ')})` });
        return out;
    }
    if (filter.dataType === 'field') {
        const compareField = fields.find(f => f.key === filter.data);
        sp();
        out.push({ kind: 'op', text: operatorToken(operator) });
        sp();
        out.push({ kind: 'field', text: compareField?.label ?? String(filter.data) });
        return out;
    }
    if (LIKE_OPERATORS.includes(operator)) {
        sp();
        out.push({ kind: 'op', text: operatorToken(operator) });
        sp();
        out.push({ kind: 'value', text: likePattern(operator, String(filter.data ?? '')) });
        return out;
    }
    sp();
    out.push({ kind: 'op', text: operatorToken(operator) });
    sp();
    out.push({ kind: 'value', text: formatScalar(field, filter.data) });
    return out;
}
function tokensFor(filter, fields) {
    if (isAndOr$1(filter)) {
        const out = [];
        filter.data.forEach((child, i) => {
            if (i > 0)
                out.push({ kind: 'logic', text: ` ${filter.operator.toLowerCase()} ` });
            const childTokens = tokensFor(child, fields);
            // Wrap a nested multi-child group in parentheses; a single-child group needs none.
            if (isAndOr$1(child) && child.data.length > 1) {
                out.push({ kind: 'paren', text: '(' }, ...childTokens, { kind: 'paren', text: ')' });
            }
            else {
                out.push(...childTokens);
            }
        });
        return out;
    }
    return ruleTokens(filter, fields);
}
/**
 * Render a `Filter` to a SQL-ish token stream for view mode. Field tokens use the
 * matching field's `label`; operators map to SQL syntax (`= != like between is null …`);
 * `and` / `or` are lowercase; nested multi-child groups are wrapped in parentheses.
 * Returns a token stream (not a string) so the template can wrap each piece in a
 * highlight `<span>` keyed by `QbToken.kind`.
 *
 * @param filter - the `Filter` to render, or `null` / `undefined`.
 * @param fields - field metadata, used to resolve each field key to its display label + value formatting.
 * @returns the ordered tokens, or `[]` when `filter` is empty.
 */
function filterToTokens(filter, fields) {
    if (!filter)
        return [];
    return tokensFor(filter, fields);
}

/* eslint-disable @typescript-eslint/no-explicit-any */
/** A logical `Filter` (AND / OR group) carries a `data[]` array. */
function isAndOr(filter) {
    return !!filter && (filter.operator === 'AND' || filter.operator === 'OR') && Array.isArray(filter.data);
}
const QB_EMPTY_FIELDS = [];
/**
 * Visual filter / rule builder. Compose nested AND / OR groups of
 * `field operator value` conditions; emits the canonical `Filter` (root is a
 * `FilterAndOr` tree) via `[(value)]`, plus a flat `[(filters)]` mirror of the
 * root group's direct children. `mode="view"` renders a read-only SQL-ish raw
 * string with highlighted operators + values.
 */
class SdQueryBuilder {
    // -------------------------------------------------------------------------
    // Inputs
    // -------------------------------------------------------------------------
    /** Main option object. Prefer this API for new usage; individual inputs stay as a migration bridge. */
    option = input(undefined, ...(ngDevMode ? [{ debugName: "option" }] : []));
    /** Filterable fields — each declares a `type` that drives the operator set + value editor. */
    fields = input([], ...(ngDevMode ? [{ debugName: "fields" }] : []));
    /** `'edit'` (default) = interactive tree · `'view'` = read-only raw-query string. */
    mode = input('edit', ...(ngDevMode ? [{ debugName: "mode" }] : []));
    /** `value-only` = literal operands only · `value-or-field` = each rule may compare against another field. */
    comparisonMode = input('value-only', ...(ngDevMode ? [{ debugName: "comparisonMode" }] : []));
    /** Disable all editing controls. */
    disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
    /** Prefix for `data-autoid` on inner controls (E2E selectors). */
    autoId = input(...(ngDevMode ? [undefined, { debugName: "autoId" }] : []));
    // -------------------------------------------------------------------------
    // Two-way models (output contract)
    // -------------------------------------------------------------------------
    /** Canonical output — the root `FilterAndOr` tree, or `null` when empty. `[(value)]`. */
    value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
    /** Convenience mirror — the root group's direct children. `[(filters)]`. */
    filters = model([], ...(ngDevMode ? [{ debugName: "filters" }] : []));
    /** Root group's logical connector. `[(rootLogic)]`. */
    rootLogic = model('AND', ...(ngDevMode ? [{ debugName: "rootLogic" }] : []));
    // -------------------------------------------------------------------------
    // Internal source of truth — the editable tree (carries UI state).
    // -------------------------------------------------------------------------
    #tree = signal(qbNewGroup('AND'), ...(ngDevMode ? [{ debugName: "#tree" }] : []));
    tree = this.#tree.asReadonly();
    /** Last filter WE emitted — used to recognise (and ignore) our own echo in the inbound effect. */
    #lastEmitted = undefined;
    // -------------------------------------------------------------------------
    // Derived state
    // -------------------------------------------------------------------------
    /** `field.key` → field, for fast lookup from a rule. */
    resolvedFields = computed(() => this.option()?.fields ?? this.fields(), ...(ngDevMode ? [{ debugName: "resolvedFields" }] : []));
    resolvedMode = computed(() => this.option()?.mode ?? this.mode(), ...(ngDevMode ? [{ debugName: "resolvedMode" }] : []));
    resolvedComparisonMode = computed(() => this.option()?.comparisonMode ?? this.comparisonMode(), ...(ngDevMode ? [{ debugName: "resolvedComparisonMode" }] : []));
    resolvedDisabled = computed(() => this.option()?.disabled ?? this.disabled(), ...(ngDevMode ? [{ debugName: "resolvedDisabled" }] : []));
    resolvedAutoId = computed(() => this.option()?.autoId ?? this.autoId(), ...(ngDevMode ? [{ debugName: "resolvedAutoId" }] : []));
    fieldByKey = computed(() => {
        const map = {};
        for (const f of this.resolvedFields())
            map[f.key] = f;
        return map;
    }, ...(ngDevMode ? [{ debugName: "fieldByKey" }] : []));
    /** Boolean fields → their stable `[true,false]` option list. Memoized so the template
     *  binding hands `sd-select` the SAME array ref each CD (else its `toObservable(items)`
     *  → `markForCheck()` loops → OOM). Recomputes only when `fields()` changes. */
    #booleanOptionsByKey = computed(() => {
        const map = new Map();
        for (const f of this.resolvedFields()) {
            if (f.type === 'boolean') {
                map.set(f.key, [
                    { value: true, display: f.trueLabel ?? 'Có' },
                    { value: false, display: f.falseLabel ?? 'Không' },
                ]);
            }
        }
        return map;
    }, ...(ngDevMode ? [{ debugName: "#booleanOptionsByKey" }] : []));
    /** Stable option list for the date-mode select. */
    dateModes = QB_DATE_MODES;
    /** Stable option list for the literal-vs-field operand select. */
    valueSourceOptions = QB_VALUE_SOURCE_OPTIONS;
    /** Stable combined unit×direction option list for the relative select. */
    relativeUnitOptions = QB_RELATIVE_UNIT_OPTIONS;
    #compareFieldsByKey = computed(() => {
        const fields = this.resolvedFields();
        const map = new Map();
        for (const left of fields) {
            if (left.allowFieldCompare === false) {
                map.set(left.key, QB_EMPTY_FIELDS);
                continue;
            }
            map.set(left.key, fields.filter(right => {
                if (right.key === left.key)
                    return false;
                if (right.allowFieldCompare === false)
                    return false;
                if (right.type !== left.type)
                    return false;
                if ((left.compareGroup ?? right.compareGroup) !== undefined && left.compareGroup !== right.compareGroup)
                    return false;
                return true;
            }));
        }
        return map;
    }, ...(ngDevMode ? [{ debugName: "#compareFieldsByKey" }] : []));
    /** View-mode token stream — recomputed from the emitted value. */
    tokens = computed(() => filterToTokens(this.option()?.value ?? this.value(), this.resolvedFields()), ...(ngDevMode ? [{ debugName: "tokens" }] : []));
    isView = computed(() => this.resolvedMode() === 'view', ...(ngDevMode ? [{ debugName: "isView" }] : []));
    /** Expose the type-guard to the template. */
    isGroup = isQbGroup;
    /** Resolve the leading icon for a field (own `icon` → per-type → `'tune'`) — used by the field picker. */
    fieldIcon = qbFieldIcon;
    constructor() {
        // Inbound seeding: an EXTERNAL write to value / filters rebuilds the tree.
        // Our own emits are recognised via #lastEmitted and skipped (no echo loop).
        effect(() => {
            const option = this.option();
            const v = option?.value ?? this.value();
            const fl = option?.filters ?? this.filters();
            const incoming = this.#coalesce(v, fl, option?.rootLogic);
            if (this.#filterEq(incoming, this.#lastEmitted))
                return;
            this.#tree.set(filterToTree(incoming));
            this.#commit();
        });
    }
    #coalesce(v, fl, logic) {
        if (v)
            return v;
        if (fl && fl.length)
            return { operator: logic ?? untracked(() => this.rootLogic()), data: fl };
        return null;
    }
    #filterEq(a, b) {
        return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
    }
    // -------------------------------------------------------------------------
    // Tree mutation → emit
    // -------------------------------------------------------------------------
    /** New root ref so the template (which reads `tree()`) re-renders. */
    #bumpTree() {
        this.#tree.set({ ...this.#tree() });
    }
    /** Recompute the output `Filter` from the tree and push it to value / filters / rootLogic. */
    #commit() {
        const filter = treeToFilter(this.#tree());
        this.#lastEmitted = filter;
        this.value.set(filter);
        const filters = isAndOr(filter) ? filter.data : filter ? [filter] : [];
        this.filters.set(filters);
        const logic = this.#tree().logic;
        if (this.rootLogic() !== logic)
            this.rootLogic.set(logic);
        this.option()?.onValueChange?.(filter);
        this.option()?.onFiltersChange?.(filters);
        this.option()?.onRootLogicChange?.(logic);
    }
    /** Mutate-in-place + re-render + emit. */
    #apply() {
        this.#bumpTree();
        this.#commit();
    }
    // -------------------------------------------------------------------------
    // Group / rule structural mutations
    // -------------------------------------------------------------------------
    /** Switch a group's AND/OR connector and re-emit. No-op when disabled or unchanged. */
    setGroupLogic(group, logic) {
        if (this.resolvedDisabled() || group.logic === logic)
            return;
        group.logic = logic;
        this.#apply();
    }
    /** Open this group's "+" menu (closing any other open one). UI-only — does not emit. */
    toggleDropdown(group, event) {
        if (this.resolvedDisabled())
            return;
        event.stopPropagation();
        const willOpen = !group.open;
        this.#closeAllDropdowns(this.#tree());
        group.open = willOpen;
        this.#bumpTree(); // UI-only — no emit
    }
    closeAllDropdowns() {
        if (!this.#hasOpenDropdown(this.#tree()))
            return; // avoid a re-render on every container click
        this.#closeAllDropdowns(this.#tree());
        this.#bumpTree();
    }
    #hasOpenDropdown(node) {
        if (!isQbGroup(node))
            return false;
        return node.open === true || node.children.some(c => this.#hasOpenDropdown(c));
    }
    #closeAllDropdowns(node) {
        if (isQbGroup(node)) {
            node.open = false;
            node.children.forEach(c => this.#closeAllDropdowns(c));
        }
    }
    /** Append a blank rule to `group`. */
    addRule(group) {
        if (this.resolvedDisabled())
            return;
        group.children.push(qbNewRule());
        group.open = false;
        this.#apply();
    }
    /** Append a nested sub-group (seeded with one blank rule) to `group`. */
    addGroup(group) {
        if (this.resolvedDisabled())
            return;
        group.children.push(qbNewGroup('AND', [qbNewRule()]));
        group.open = false;
        this.#apply();
    }
    /** Remove the child at `index` from `parent` (a rule or a whole sub-group). */
    removeNode(parent, index) {
        if (this.resolvedDisabled())
            return;
        parent.children.splice(index, 1);
        this.#apply();
    }
    // -------------------------------------------------------------------------
    // Rule field / operator / value mutations
    // -------------------------------------------------------------------------
    /**
     * Point `rule` at a new field. Resets operator to the field's default and the value
     * to that operator's empty shape — the old operator/value rarely fit the new type.
     *
     * @param rule - the rule being edited.
     * @param key - the chosen field key (from the field picker). Swap-only: a null / undefined
     *   key is a no-op — the field can be changed but never cleared (issue #2).
     */
    setField(rule, key) {
        if (this.resolvedDisabled())
            return;
        if (key == null)
            return; // swap-only: never clear the field (issue #2)
        rule.field = key;
        const field = this.fieldByKey()[rule.field];
        const op = qbDefaultOperator(field);
        rule.operator = op;
        rule.value = this.#defaultValueFor(op);
        this.#useLiteralSource(rule);
        this.#apply();
    }
    /** Change `rule`'s operator and reshape its value to fit the new operator (array / range / none). */
    setOperator(rule, op) {
        if (this.resolvedDisabled())
            return;
        rule.operator = op;
        if (rule.valueSource === 'field' && this.canCompareWithField(rule)) {
            rule.value = null;
            if (!this.#isCompareFieldAllowed(rule, rule.compareField))
                rule.compareField = undefined;
        }
        else {
            rule.value = this.#reshapeValue(op, rule.value);
            this.#useLiteralSource(rule);
        }
        this.#apply();
    }
    /** Set a single-value rule's value (coercing numeric fields to `number`). */
    setScalar(rule, raw) {
        if (this.resolvedDisabled())
            return;
        this.#useLiteralSource(rule);
        rule.value = this.#coerce(rule, raw);
        this.#apply();
    }
    /** Set the lower bound of a BETWEEN rule's `{ from, to }` value. */
    setBetweenFrom(rule, raw) {
        if (this.resolvedDisabled())
            return;
        this.#useLiteralSource(rule);
        rule.value = { ...(rule.value ?? {}), from: this.#coerce(rule, raw) };
        this.#apply();
    }
    /** Set the upper bound of a BETWEEN rule's `{ from, to }` value. */
    setBetweenTo(rule, raw) {
        if (this.resolvedDisabled())
            return;
        this.#useLiteralSource(rule);
        rule.value = { ...(rule.value ?? {}), to: this.#coerce(rule, raw) };
        this.#apply();
    }
    setValueSource(rule, source) {
        if (this.resolvedDisabled())
            return;
        if (source === 'field' && this.resolvedComparisonMode() === 'value-or-field' && this.canCompareWithField(rule)) {
            rule.valueSource = 'field';
            rule.value = null;
            if (!this.#isCompareFieldAllowed(rule, rule.compareField))
                rule.compareField = undefined;
        }
        else {
            this.#useLiteralSource(rule);
            rule.value = this.#defaultValueFor(rule.operator);
        }
        this.#apply();
    }
    setCompareField(rule, key) {
        if (this.resolvedDisabled())
            return;
        if (!this.#isCompareFieldAllowed(rule, key))
            return;
        rule.valueSource = 'field';
        rule.compareField = key;
        rule.value = null;
        this.#apply();
    }
    /** Coerce numeric fields to `number` so the emitted Filter carries a real number. */
    #coerce(rule, raw) {
        const field = this.fieldByKey()[rule.field];
        if (field?.type === 'number' && raw !== null && raw !== undefined && raw !== '') {
            const n = Number(raw);
            return Number.isNaN(n) ? raw : n;
        }
        return raw;
    }
    #defaultValueFor(op) {
        if (qbIsMultiOperator(op))
            return [];
        if (op === 'BETWEEN')
            return { from: null, to: null };
        return null;
    }
    #reshapeValue(op, current) {
        if (qbIsNoDataOperator(op))
            return null;
        if (qbIsMultiOperator(op))
            return Array.isArray(current) ? current : current == null ? [] : [current];
        if (op === 'BETWEEN') {
            // a relative spec / 'TODAY' can't be a BETWEEN endpoint — reset to a fresh range
            return current && typeof current === 'object' && !Array.isArray(current) && !qbIsRelativeDate(current)
                ? current
                : { from: null, to: null };
        }
        // single value — keep a relative spec or the 'TODAY' sentinel; else drop array/range remnants
        if (qbIsToday(current) || qbIsRelativeDate(current))
            return current;
        return Array.isArray(current) || (current && typeof current === 'object') ? null : current;
    }
    #useLiteralSource(rule) {
        rule.valueSource = 'literal';
        rule.compareField = undefined;
    }
    #isCompareFieldAllowed(rule, key) {
        return !!key && this.compareFields(rule).some(f => f.key === key);
    }
    // -------------------------------------------------------------------------
    // Template helpers
    // -------------------------------------------------------------------------
    /** The field metadata for a rule's current `field` key (or undefined if unset). */
    fieldOf(rule) {
        return rule.field ? this.fieldByKey()[rule.field] : undefined;
    }
    /** Operators allowed for a rule, derived from its field's type / override. */
    allowedOperators(rule) {
        return qbAllowedOperators(this.fieldOf(rule));
    }
    /** Whether to render the operator picker — hidden when only one operator is allowed. */
    showOperatorSelector(rule) {
        return this.allowedOperators(rule).length > 1;
    }
    compareFields(rule) {
        const field = this.fieldOf(rule);
        if (!field)
            return QB_EMPTY_FIELDS;
        return this.#compareFieldsByKey().get(field.key) ?? QB_EMPTY_FIELDS;
    }
    canCompareWithField(rule) {
        return qbSupportsFieldCompareOperator(rule.operator) && this.compareFields(rule).length > 0;
    }
    showValueSourceSelector(rule) {
        return this.canCompareWithField(rule) && (this.resolvedComparisonMode() === 'value-or-field' || rule.valueSource === 'field');
    }
    valueSourceOf(rule) {
        return rule.valueSource === 'field' && this.canCompareWithField(rule) ? 'field' : 'literal';
    }
    isFieldSource(rule) {
        return this.valueSourceOf(rule) === 'field';
    }
    /** True for NULL / NOT_NULL — the template then renders no value editor. */
    isNoData(op) {
        return qbIsNoDataOperator(op);
    }
    /** True for IN / NOT_IN — the template then renders a multi-select value editor. */
    isMulti(op) {
        return qbIsMultiOperator(op);
    }
    /** `[true/false]` option list for a boolean field's value select (stable reference). */
    booleanItems(field) {
        return this.#booleanOptionsByKey().get(field.key) ?? QB_EMPTY_OPTIONS;
    }
    /** Current date-value mode of a rule, derived from its value (no separate state). */
    dateMode(rule) {
        const v = rule.value;
        if (qbIsToday(v))
            return 'now';
        if (qbIsRelativeDate(v))
            return 'relative';
        return 'absolute';
    }
    /** Switch a date/datetime rule's value mode, reseeding the value for the new mode. */
    setDateMode(rule, mode) {
        if (this.resolvedDisabled())
            return;
        this.#useLiteralSource(rule);
        if (mode === 'now')
            rule.value = QB_TODAY;
        else if (mode === 'relative')
            rule.value = qbDefaultRelative();
        else
            rule.value = null; // absolute — pick a concrete date
        this.#apply();
    }
    /** Read the offset amount of a relative rule (default 1). */
    relativeAmount(rule) {
        const v = rule.value;
        return qbIsRelativeDate(v) ? v.amount ?? 1 : 1;
    }
    /** Set the offset amount (clamped to an integer >= 1). */
    setRelativeAmount(rule, raw) {
        if (this.resolvedDisabled())
            return;
        this.#useLiteralSource(rule);
        const n = Math.floor(Number(raw));
        const amount = Number.isNaN(n) || n < 1 ? 1 : n;
        const cur = qbIsRelativeDate(rule.value) ? rule.value : qbDefaultRelative();
        rule.value = { amount, direction: cur.direction, unit: cur.unit };
        this.#apply();
    }
    /** Read the `'unit:direction'` token of a relative rule (default `'day:previous'`). */
    relativeUnitDirValue(rule) {
        const v = rule.value;
        if (qbIsRelativeDate(v))
            return `${v.unit}:${v.direction}`;
        return 'day:previous';
    }
    /** Set the offset unit + direction from a `'unit:direction'` token. */
    setRelativeUnitDir(rule, token) {
        if (this.resolvedDisabled())
            return;
        this.#useLiteralSource(rule);
        const [unit, direction] = (token ?? 'day:previous').split(':');
        const cur = qbIsRelativeDate(rule.value) ? rule.value : qbDefaultRelative();
        rule.value = { amount: cur.amount, direction, unit };
        this.#apply();
    }
    /** Build a `data-autoid` for an inner control under this builder's `autoId` prefix. */
    autoIdFor(suffix) {
        return `${this.resolvedAutoId() ?? 'qb'}-${suffix}`;
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdQueryBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
    static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.24", type: SdQueryBuilder, isStandalone: true, selector: "sd-query-builder", inputs: { option: { classPropertyName: "option", publicName: "option", isSignal: true, isRequired: false, transformFunction: null }, fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, comparisonMode: { classPropertyName: "comparisonMode", publicName: "comparisonMode", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, filters: { classPropertyName: "filters", publicName: "filters", isSignal: true, isRequired: false, transformFunction: null }, rootLogic: { classPropertyName: "rootLogic", publicName: "rootLogic", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", filters: "filtersChange", rootLogic: "rootLogicChange" }, ngImport: i0, template: "@if (isView()) {\n  <div class=\"qb-view\" [attr.data-autoid]=\"autoIdFor('view')\" aria-readonly=\"true\">\n    @if (tokens().length) {\n      @for (t of tokens(); track $index) {\n        <span class=\"qb-tok qb-tok-{{ t.kind }}\">{{ t.text }}</span>\n      }\n    } @else {\n      <span class=\"qb-view-empty\">\u2014</span>\n    }\n  </div>\n} @else {\n  <div class=\"qb-container\" [class.is-disabled]=\"resolvedDisabled()\" (click)=\"closeAllDropdowns()\">\n    <ng-container [ngTemplateOutlet]=\"groupTpl\" [ngTemplateOutletContext]=\"{ $implicit: tree(), isRoot: true }\"></ng-container>\n  </div>\n}\n\n<!-- Recursive group node -->\n<ng-template #groupTpl let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n  <div class=\"qb-group\" [class.is-root]=\"isRoot\">\n    <div class=\"qb-header\" [class.is-open]=\"group.open\">\n      <div class=\"qb-logic-switch\">\n        <button type=\"button\" [class.active]=\"group.logic === 'AND'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n        <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'OR')\">OR</button>\n      </div>\n\n      <div class=\"qb-actions\">\n        <button type=\"button\" class=\"qb-btn qb-btn-add\" title=\"Th\u00EAm\" aria-label=\"Th\u00EAm \u0111i\u1EC1u ki\u1EC7n ho\u1EB7c nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"toggleDropdown(group, $event)\">+</button>\n        @if (group.open) {\n          <div class=\"qb-dropdown\" (click)=\"$event.stopPropagation()\">\n            <button type=\"button\" (click)=\"addRule(group)\">+ \u0110i\u1EC1u ki\u1EC7n</button>\n            <button type=\"button\" (click)=\"addGroup(group)\">+ Nh\u00F3m</button>\n          </div>\n        }\n        @if (!isRoot) {\n          <button type=\"button\" class=\"qb-btn qb-btn-remove\" title=\"Xo\u00E1 nh\u00F3m\" aria-label=\"Xo\u00E1 nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n        }\n      </div>\n    </div>\n\n    <div class=\"qb-body\">\n      @for (node of group.children; track node.id; let i = $index) {\n        <div class=\"qb-treenode\" [class.is-group]=\"isGroup(node)\" [class.is-rule]=\"!isGroup(node)\">\n          @if (isGroup(node)) {\n            <ng-container\n              [ngTemplateOutlet]=\"groupTpl\"\n              [ngTemplateOutletContext]=\"{ $implicit: node, isRoot: false, parent: group, index: i }\"\n            ></ng-container>\n          } @else {\n            <ng-container [ngTemplateOutlet]=\"ruleTpl\" [ngTemplateOutletContext]=\"{ $implicit: node, parent: group, index: i }\"></ng-container>\n          }\n        </div>\n      } @empty {\n        <div class=\"qb-empty\">Ch\u01B0a c\u00F3 \u0111i\u1EC1u ki\u1EC7n</div>\n      }\n    </div>\n  </div>\n</ng-template>\n\n<!-- Single rule node: field \u2192 operator \u2192 value -->\n<ng-template #ruleTpl let-rule let-parent=\"parent\" let-index=\"index\">\n  <div class=\"qb-rule\">\n    <sd-select\n      class=\"qb-field\"\n      [items]=\"resolvedFields()\"\n      valueField=\"key\"\n      displayField=\"label\"\n      placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n      size=\"sm\"\n      [model]=\"rule.field\"\n      (modelChange)=\"setField(rule, $event)\"\n      [disabled]=\"resolvedDisabled()\"\n      [clearable]=\"false\"\n      hideInlineError\n      [autoId]=\"autoIdFor('field-' + index)\"\n    >\n      <!-- Option trong dropdown: icon th\u1EC3 hi\u1EC7n type + label. -->\n      <ng-template sdItemDef let-item=\"item\">\n        <span class=\"qb-opt\">\n          <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n          <span class=\"qb-opt-label\">{{ item.label }}</span>\n        </span>\n      </ng-template>\n      <!-- Gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn \u1EDF trigger: icon m\u1EB7c \u0111\u1ECBnh theo type khi field ch\u01B0a c\u00F3 icon ri\u00EAng. -->\n      <ng-template #sdSelected let-item=\"item\">\n        <span class=\"qb-opt\">\n          <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n          <span class=\"qb-opt-label\">{{ item?.label }}</span>\n        </span>\n      </ng-template>\n    </sd-select>\n\n    @if (rule.field) {\n      <div class=\"qb-divider\"></div>\n\n      @if (showOperatorSelector(rule)) {\n        <sd-operator\n          class=\"qb-op\"\n          [operators]=\"allowedOperators(rule)\"\n          [model]=\"rule.operator\"\n          (modelChange)=\"setOperator(rule, $event)\"\n          [disabled]=\"resolvedDisabled()\"\n          [autoId]=\"autoIdFor('op-' + index)\"\n        ></sd-operator>\n      }\n\n      @if (rule.operator && !isNoData(rule.operator)) {\n        <div class=\"qb-divider\"></div>\n\n        @if (showValueSourceSelector(rule)) {\n          <sd-select\n            class=\"qb-value-source\"\n            [items]=\"valueSourceOptions\"\n            valueField=\"value\"\n            displayField=\"display\"\n            size=\"sm\"\n            [model]=\"valueSourceOf(rule)\"\n            (modelChange)=\"setValueSource(rule, $event)\"\n            [disabled]=\"resolvedDisabled()\"\n            [clearable]=\"false\"\n            hideInlineError\n            [autoId]=\"autoIdFor('valuesource-' + index)\"\n          >\n            <ng-template sdItemDef let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item.display }}</span>\n              </span>\n            </ng-template>\n            <ng-template #sdSelected let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item?.icon }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item?.display }}</span>\n              </span>\n            </ng-template>\n          </sd-select>\n          <div class=\"qb-divider\"></div>\n        }\n\n        @if (isFieldSource(rule)) {\n          <sd-select\n            class=\"qb-val qb-compare-field\"\n            [items]=\"compareFields(rule)\"\n            valueField=\"key\"\n            displayField=\"label\"\n            placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n            size=\"sm\"\n            [model]=\"rule.compareField\"\n            (modelChange)=\"setCompareField(rule, $event)\"\n            [disabled]=\"resolvedDisabled()\"\n            [clearable]=\"false\"\n            hideInlineError\n            [autoId]=\"autoIdFor('compare-field-' + index)\"\n          >\n            <ng-template sdItemDef let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item.label }}</span>\n              </span>\n            </ng-template>\n            <ng-template #sdSelected let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item?.label }}</span>\n              </span>\n            </ng-template>\n          </sd-select>\n        } @else if (rule.operator === 'BETWEEN') {\n          <div class=\"qb-between\">\n            @switch (fieldOf(rule)?.type) {\n              @case ('number') {\n                <sd-input-number class=\"qb-val\" placeholder=\"T\u1EEB\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input-number>\n                <span class=\"qb-dash\">\u2014</span>\n                <sd-input-number class=\"qb-val\" placeholder=\"\u0110\u1EBFn\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input-number>\n              }\n              @case ('date') {\n                <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n                <span class=\"qb-dash\">\u2014</span>\n                <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-date>\n              }\n              @case ('datetime') {\n                <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-datetime>\n                <span class=\"qb-dash\">\u2014</span>\n                <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-datetime>\n              }\n            }\n          </div>\n        } @else {\n          @switch (fieldOf(rule)?.type) {\n            @case ('string') {\n              <sd-input class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n            }\n            @case ('number') {\n              <sd-input-number class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input-number>\n            }\n            @case ('boolean') {\n              <sd-select class=\"qb-val\" [items]=\"booleanItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n            }\n            @case ('date') {\n              @let _dateMode = dateMode(rule);\n              <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dateMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n                <ng-template sdItemDef let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item.display }}</span>\n                  </span>\n                </ng-template>\n                <ng-template #sdSelected let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item?.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item?.display }}</span>\n                  </span>\n                </ng-template>\n              </sd-select>\n              @switch (_dateMode) {\n                @case ('absolute') {\n                  <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n                }\n                @case ('relative') {\n                  <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n                  <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n                }\n              }\n            }\n            @case ('datetime') {\n              @let _dtMode = dateMode(rule);\n              <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dtMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n                <ng-template sdItemDef let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item.display }}</span>\n                  </span>\n                </ng-template>\n                <ng-template #sdSelected let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item?.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item?.display }}</span>\n                  </span>\n                </ng-template>\n              </sd-select>\n              @switch (_dtMode) {\n                @case ('absolute') {\n                  <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n                }\n                @case ('relative') {\n                  <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n                  <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n                }\n              }\n            }\n            @case ('values') {\n              <sd-select class=\"qb-val\" [items]=\"fieldOf(rule)!.values || []\" valueField=\"value\" displayField=\"display\" [multiple]=\"isMulti(rule.operator)\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n            }\n          }\n        }\n      }\n    }\n\n    <button type=\"button\" class=\"qb-btn qb-btn-remove-rule\" title=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" aria-label=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n  </div>\n</ng-template>\n", styles: [":host{display:block;font-size:14px;color:var(--sd-text, #333)}.qb-view{display:block;width:100%;min-height:38px;padding:8px 12px;border:1px solid #d0d5dd;border-radius:6px;background:var(--sd-disabled-bg, #f6f7f9);color:var(--sd-text, #333);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.7;cursor:default;-webkit-user-select:text;user-select:text;white-space:pre-wrap;word-break:break-word}.qb-view-empty{color:#98a2b3}.qb-tok{white-space:pre}.qb-tok.qb-tok-field{color:var(--sd-text, #1d2939);font-weight:600}.qb-tok.qb-tok-op{color:var(--sd-primary, #2a66f4);font-weight:600}.qb-tok.qb-tok-value{color:var(--sd-success, #1a7f37)}.qb-tok.qb-tok-logic{color:#98a2b3;font-weight:700;text-transform:uppercase}.qb-tok.qb-tok-paren{color:#667085}.qb-container{padding:16px;background:#fff;border:1px solid #e0e0e0;border-radius:6px}.qb-container.is-disabled{opacity:.7;pointer-events:none}.qb-header{display:flex;align-items:center;gap:8px;margin-bottom:12px;position:relative;z-index:1}.qb-header.is-open{z-index:1000}.qb-logic-switch{display:inline-flex;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:30px}.qb-logic-switch button{border:none;background:transparent;padding:0 14px;font-weight:600;font-size:12px;cursor:pointer;color:#667085;height:100%}.qb-logic-switch button.active{background:var(--sd-primary, #2a66f4);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #eaf1ff);color:var(--sd-primary-dark, #1c4ad9)}.qb-logic-switch button[disabled]{cursor:default}.qb-actions{position:relative;display:flex;align-items:center;gap:4px}.qb-btn{background:transparent;border:none;cursor:pointer;font-size:16px;color:#667085;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px}.qb-btn:hover:not([disabled]){background:var(--sd-primary-light, #eaf1ff);color:var(--sd-primary, #2a66f4)}.qb-btn[disabled]{cursor:default;color:#cbd2dc}.qb-btn-remove:hover:not([disabled]),.qb-btn-remove-rule:hover:not([disabled]){background:#fee4e2;color:#d92d20}.qb-btn-remove-rule{margin-left:auto}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #1018281f;border-radius:6px;min-width:150px;z-index:100;overflow:hidden}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:9px 14px;cursor:pointer;font-size:13px;color:#344054}.qb-dropdown button:hover{background:var(--sd-primary-light, #eaf1ff);color:var(--sd-primary, #2a66f4)}.qb-body{padding-left:24px}.qb-treenode{position:relative;padding-left:22px;margin-bottom:12px}.qb-treenode:last-child{margin-bottom:0}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;border-left:1px dashed #d0d5dd}.qb-treenode:after{content:\"\";position:absolute;left:0;top:50%;width:18px;border-top:1px dashed #d0d5dd}.qb-treenode:last-child:before{bottom:auto;height:calc(50% + 12px)}.qb-empty{color:#98a2b3;font-style:italic;font-size:13px;padding:4px 0}.qb-rule{display:flex;align-items:center;gap:8px;background:#fafbfc;border:1px solid #e0e0e0;border-radius:6px;padding:6px 10px}.qb-rule:hover{border-color:#c2c8d0}.qb-field{min-width:170px}.qb-val{min-width:140px}.qb-value-source{min-width:132px}.qb-compare-field{min-width:170px}.qb-date-val{min-width:0;width:132px;flex:0 0 auto}::ng-deep .qb-opt{display:flex;align-items:center;gap:6px;min-width:0}::ng-deep .qb-opt .qb-opt-icon.mat-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;margin:0;font-size:18px;width:18px;height:18px;line-height:1;color:#667085}::ng-deep .qb-opt .qb-opt-label{min-width:0}.qb-between{display:flex;align-items:center;gap:6px}.qb-dash{color:#98a2b3}.qb-divider{width:1px;align-self:stretch;background:#e0e0e0}.qb-treenode.is-group>.qb-group{border:1px solid #e0e0e0;border-left:2px solid var(--sd-primary, #2a66f4);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:160px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: SdOperator, selector: "sd-operator", inputs: ["model", "operators", "disabled", "autoId"], outputs: ["modelChange"] }, { kind: "component", type: SdSelect, selector: "sd-select", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "valueField", "displayField", "disabledField", "cacheChecksum", "limit", "hyperlink", "minWidthPanel", "hideInlineError", "required", "disabled", "viewed", "multiple", "clearable", "validator", "inlineError", "appearance", "floatLabel", "items", "model"], outputs: ["modelChange", "sdChange", "sdSelection"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "hideInlineError", "blurOnEnter", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdInputNumber, selector: "sd-input-number", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "blurOnEnter", "required", "readonly", "disabled", "viewed", "type", "precision", "format", "min", "max", "validator", "inlineError", "hyperlink", "appearance", "floatLabel", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdDate, selector: "sd-date", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "component", type: SdDatetime, selector: "sd-datetime", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "showSeconds", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "directive", type: SdItemDefDefDirective, selector: "[sdItemDef]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.24", ngImport: i0, type: SdQueryBuilder, decorators: [{
            type: Component,
            args: [{ selector: 'sd-query-builder', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgTemplateOutlet, MatIconModule, SdOperator, SdSelect, SdInput, SdInputNumber, SdDate, SdDatetime, SdItemDefDefDirective], template: "@if (isView()) {\n  <div class=\"qb-view\" [attr.data-autoid]=\"autoIdFor('view')\" aria-readonly=\"true\">\n    @if (tokens().length) {\n      @for (t of tokens(); track $index) {\n        <span class=\"qb-tok qb-tok-{{ t.kind }}\">{{ t.text }}</span>\n      }\n    } @else {\n      <span class=\"qb-view-empty\">\u2014</span>\n    }\n  </div>\n} @else {\n  <div class=\"qb-container\" [class.is-disabled]=\"resolvedDisabled()\" (click)=\"closeAllDropdowns()\">\n    <ng-container [ngTemplateOutlet]=\"groupTpl\" [ngTemplateOutletContext]=\"{ $implicit: tree(), isRoot: true }\"></ng-container>\n  </div>\n}\n\n<!-- Recursive group node -->\n<ng-template #groupTpl let-group let-isRoot=\"isRoot\" let-parent=\"parent\" let-index=\"index\">\n  <div class=\"qb-group\" [class.is-root]=\"isRoot\">\n    <div class=\"qb-header\" [class.is-open]=\"group.open\">\n      <div class=\"qb-logic-switch\">\n        <button type=\"button\" [class.active]=\"group.logic === 'AND'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'AND')\">AND</button>\n        <button type=\"button\" [class.active]=\"group.logic === 'OR'\" [disabled]=\"resolvedDisabled()\" (click)=\"setGroupLogic(group, 'OR')\">OR</button>\n      </div>\n\n      <div class=\"qb-actions\">\n        <button type=\"button\" class=\"qb-btn qb-btn-add\" title=\"Th\u00EAm\" aria-label=\"Th\u00EAm \u0111i\u1EC1u ki\u1EC7n ho\u1EB7c nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"toggleDropdown(group, $event)\">+</button>\n        @if (group.open) {\n          <div class=\"qb-dropdown\" (click)=\"$event.stopPropagation()\">\n            <button type=\"button\" (click)=\"addRule(group)\">+ \u0110i\u1EC1u ki\u1EC7n</button>\n            <button type=\"button\" (click)=\"addGroup(group)\">+ Nh\u00F3m</button>\n          </div>\n        }\n        @if (!isRoot) {\n          <button type=\"button\" class=\"qb-btn qb-btn-remove\" title=\"Xo\u00E1 nh\u00F3m\" aria-label=\"Xo\u00E1 nh\u00F3m\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n        }\n      </div>\n    </div>\n\n    <div class=\"qb-body\">\n      @for (node of group.children; track node.id; let i = $index) {\n        <div class=\"qb-treenode\" [class.is-group]=\"isGroup(node)\" [class.is-rule]=\"!isGroup(node)\">\n          @if (isGroup(node)) {\n            <ng-container\n              [ngTemplateOutlet]=\"groupTpl\"\n              [ngTemplateOutletContext]=\"{ $implicit: node, isRoot: false, parent: group, index: i }\"\n            ></ng-container>\n          } @else {\n            <ng-container [ngTemplateOutlet]=\"ruleTpl\" [ngTemplateOutletContext]=\"{ $implicit: node, parent: group, index: i }\"></ng-container>\n          }\n        </div>\n      } @empty {\n        <div class=\"qb-empty\">Ch\u01B0a c\u00F3 \u0111i\u1EC1u ki\u1EC7n</div>\n      }\n    </div>\n  </div>\n</ng-template>\n\n<!-- Single rule node: field \u2192 operator \u2192 value -->\n<ng-template #ruleTpl let-rule let-parent=\"parent\" let-index=\"index\">\n  <div class=\"qb-rule\">\n    <sd-select\n      class=\"qb-field\"\n      [items]=\"resolvedFields()\"\n      valueField=\"key\"\n      displayField=\"label\"\n      placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n      size=\"sm\"\n      [model]=\"rule.field\"\n      (modelChange)=\"setField(rule, $event)\"\n      [disabled]=\"resolvedDisabled()\"\n      [clearable]=\"false\"\n      hideInlineError\n      [autoId]=\"autoIdFor('field-' + index)\"\n    >\n      <!-- Option trong dropdown: icon th\u1EC3 hi\u1EC7n type + label. -->\n      <ng-template sdItemDef let-item=\"item\">\n        <span class=\"qb-opt\">\n          <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n          <span class=\"qb-opt-label\">{{ item.label }}</span>\n        </span>\n      </ng-template>\n      <!-- Gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn \u1EDF trigger: icon m\u1EB7c \u0111\u1ECBnh theo type khi field ch\u01B0a c\u00F3 icon ri\u00EAng. -->\n      <ng-template #sdSelected let-item=\"item\">\n        <span class=\"qb-opt\">\n          <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n          <span class=\"qb-opt-label\">{{ item?.label }}</span>\n        </span>\n      </ng-template>\n    </sd-select>\n\n    @if (rule.field) {\n      <div class=\"qb-divider\"></div>\n\n      @if (showOperatorSelector(rule)) {\n        <sd-operator\n          class=\"qb-op\"\n          [operators]=\"allowedOperators(rule)\"\n          [model]=\"rule.operator\"\n          (modelChange)=\"setOperator(rule, $event)\"\n          [disabled]=\"resolvedDisabled()\"\n          [autoId]=\"autoIdFor('op-' + index)\"\n        ></sd-operator>\n      }\n\n      @if (rule.operator && !isNoData(rule.operator)) {\n        <div class=\"qb-divider\"></div>\n\n        @if (showValueSourceSelector(rule)) {\n          <sd-select\n            class=\"qb-value-source\"\n            [items]=\"valueSourceOptions\"\n            valueField=\"value\"\n            displayField=\"display\"\n            size=\"sm\"\n            [model]=\"valueSourceOf(rule)\"\n            (modelChange)=\"setValueSource(rule, $event)\"\n            [disabled]=\"resolvedDisabled()\"\n            [clearable]=\"false\"\n            hideInlineError\n            [autoId]=\"autoIdFor('valuesource-' + index)\"\n          >\n            <ng-template sdItemDef let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item.display }}</span>\n              </span>\n            </ng-template>\n            <ng-template #sdSelected let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item?.icon }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item?.display }}</span>\n              </span>\n            </ng-template>\n          </sd-select>\n          <div class=\"qb-divider\"></div>\n        }\n\n        @if (isFieldSource(rule)) {\n          <sd-select\n            class=\"qb-val qb-compare-field\"\n            [items]=\"compareFields(rule)\"\n            valueField=\"key\"\n            displayField=\"label\"\n            placeholder=\"Ch\u1ECDn tr\u01B0\u1EDDng\"\n            size=\"sm\"\n            [model]=\"rule.compareField\"\n            (modelChange)=\"setCompareField(rule, $event)\"\n            [disabled]=\"resolvedDisabled()\"\n            [clearable]=\"false\"\n            hideInlineError\n            [autoId]=\"autoIdFor('compare-field-' + index)\"\n          >\n            <ng-template sdItemDef let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item.label }}</span>\n              </span>\n            </ng-template>\n            <ng-template #sdSelected let-item=\"item\">\n              <span class=\"qb-opt\">\n                <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ fieldIcon(item) }}</mat-icon>\n                <span class=\"qb-opt-label\">{{ item?.label }}</span>\n              </span>\n            </ng-template>\n          </sd-select>\n        } @else if (rule.operator === 'BETWEEN') {\n          <div class=\"qb-between\">\n            @switch (fieldOf(rule)?.type) {\n              @case ('number') {\n                <sd-input-number class=\"qb-val\" placeholder=\"T\u1EEB\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-input-number>\n                <span class=\"qb-dash\">\u2014</span>\n                <sd-input-number class=\"qb-val\" placeholder=\"\u0110\u1EBFn\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-input-number>\n              }\n              @case ('date') {\n                <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-date>\n                <span class=\"qb-dash\">\u2014</span>\n                <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-date>\n              }\n              @case ('datetime') {\n                <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.from\" (modelChange)=\"setBetweenFrom(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('from-' + index)\"></sd-datetime>\n                <span class=\"qb-dash\">\u2014</span>\n                <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value?.to\" (modelChange)=\"setBetweenTo(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('to-' + index)\"></sd-datetime>\n              }\n            }\n          </div>\n        } @else {\n          @switch (fieldOf(rule)?.type) {\n            @case ('string') {\n              <sd-input class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input>\n            }\n            @case ('number') {\n              <sd-input-number class=\"qb-val\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [min]=\"fieldOf(rule)?.min\" [max]=\"fieldOf(rule)?.max\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-input-number>\n            }\n            @case ('boolean') {\n              <sd-select class=\"qb-val\" [items]=\"booleanItems(fieldOf(rule)!)\" valueField=\"value\" displayField=\"display\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n            }\n            @case ('date') {\n              @let _dateMode = dateMode(rule);\n              <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dateMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n                <ng-template sdItemDef let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item.display }}</span>\n                  </span>\n                </ng-template>\n                <ng-template #sdSelected let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item?.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item?.display }}</span>\n                  </span>\n                </ng-template>\n              </sd-select>\n              @switch (_dateMode) {\n                @case ('absolute') {\n                  <sd-date class=\"qb-val qb-date-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-date>\n                }\n                @case ('relative') {\n                  <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n                  <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n                }\n              }\n            }\n            @case ('datetime') {\n              @let _dtMode = dateMode(rule);\n              <sd-select class=\"qb-date-mode\" [items]=\"dateModes\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"_dtMode\" (modelChange)=\"setDateMode(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('datemode-' + index)\">\n                <ng-template sdItemDef let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item.display }}</span>\n                  </span>\n                </ng-template>\n                <ng-template #sdSelected let-item=\"item\">\n                  <span class=\"qb-opt\">\n                    <mat-icon class=\"qb-opt-icon\" fontSet=\"material-icons-outlined\">{{ item?.icon }}</mat-icon>\n                    <span class=\"qb-opt-label\">{{ item?.display }}</span>\n                  </span>\n                </ng-template>\n              </sd-select>\n              @switch (_dtMode) {\n                @case ('absolute') {\n                  <sd-datetime class=\"qb-val\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-datetime>\n                }\n                @case ('relative') {\n                  <sd-input-number class=\"qb-rel-amount\" type=\"positive\" [precision]=\"0\" size=\"sm\" [model]=\"relativeAmount(rule)\" (modelChange)=\"setRelativeAmount(rule, $event)\" [disabled]=\"resolvedDisabled()\" hideInlineError [autoId]=\"autoIdFor('rel-amount-' + index)\"></sd-input-number>\n                  <sd-select class=\"qb-rel-unit\" [items]=\"relativeUnitOptions\" valueField=\"value\" displayField=\"display\" size=\"sm\" [model]=\"relativeUnitDirValue(rule)\" (modelChange)=\"setRelativeUnitDir(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('rel-unit-' + index)\"></sd-select>\n                }\n              }\n            }\n            @case ('values') {\n              <sd-select class=\"qb-val\" [items]=\"fieldOf(rule)!.values || []\" valueField=\"value\" displayField=\"display\" [multiple]=\"isMulti(rule.operator)\" placeholder=\"Gi\u00E1 tr\u1ECB\" size=\"sm\" [model]=\"rule.value\" (modelChange)=\"setScalar(rule, $event)\" [disabled]=\"resolvedDisabled()\" [clearable]=\"false\" hideInlineError [autoId]=\"autoIdFor('val-' + index)\"></sd-select>\n            }\n          }\n        }\n      }\n    }\n\n    <button type=\"button\" class=\"qb-btn qb-btn-remove-rule\" title=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" aria-label=\"Xo\u00E1 \u0111i\u1EC1u ki\u1EC7n\" [disabled]=\"resolvedDisabled()\" (click)=\"removeNode(parent, index)\">\u2715</button>\n  </div>\n</ng-template>\n", styles: [":host{display:block;font-size:14px;color:var(--sd-text, #333)}.qb-view{display:block;width:100%;min-height:38px;padding:8px 12px;border:1px solid #d0d5dd;border-radius:6px;background:var(--sd-disabled-bg, #f6f7f9);color:var(--sd-text, #333);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:13px;line-height:1.7;cursor:default;-webkit-user-select:text;user-select:text;white-space:pre-wrap;word-break:break-word}.qb-view-empty{color:#98a2b3}.qb-tok{white-space:pre}.qb-tok.qb-tok-field{color:var(--sd-text, #1d2939);font-weight:600}.qb-tok.qb-tok-op{color:var(--sd-primary, #2a66f4);font-weight:600}.qb-tok.qb-tok-value{color:var(--sd-success, #1a7f37)}.qb-tok.qb-tok-logic{color:#98a2b3;font-weight:700;text-transform:uppercase}.qb-tok.qb-tok-paren{color:#667085}.qb-container{padding:16px;background:#fff;border:1px solid #e0e0e0;border-radius:6px}.qb-container.is-disabled{opacity:.7;pointer-events:none}.qb-header{display:flex;align-items:center;gap:8px;margin-bottom:12px;position:relative;z-index:1}.qb-header.is-open{z-index:1000}.qb-logic-switch{display:inline-flex;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:30px}.qb-logic-switch button{border:none;background:transparent;padding:0 14px;font-weight:600;font-size:12px;cursor:pointer;color:#667085;height:100%}.qb-logic-switch button.active{background:var(--sd-primary, #2a66f4);color:#fff}.qb-logic-switch button:hover:not(.active):not([disabled]){background:var(--sd-primary-light, #eaf1ff);color:var(--sd-primary-dark, #1c4ad9)}.qb-logic-switch button[disabled]{cursor:default}.qb-actions{position:relative;display:flex;align-items:center;gap:4px}.qb-btn{background:transparent;border:none;cursor:pointer;font-size:16px;color:#667085;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border-radius:4px}.qb-btn:hover:not([disabled]){background:var(--sd-primary-light, #eaf1ff);color:var(--sd-primary, #2a66f4)}.qb-btn[disabled]{cursor:default;color:#cbd2dc}.qb-btn-remove:hover:not([disabled]),.qb-btn-remove-rule:hover:not([disabled]){background:#fee4e2;color:#d92d20}.qb-btn-remove-rule{margin-left:auto}.qb-dropdown{position:absolute;top:100%;left:0;margin-top:4px;background:#fff;border:1px solid #e0e0e0;box-shadow:0 4px 12px #1018281f;border-radius:6px;min-width:150px;z-index:100;overflow:hidden}.qb-dropdown button{display:block;width:100%;text-align:left;background:none;border:none;padding:9px 14px;cursor:pointer;font-size:13px;color:#344054}.qb-dropdown button:hover{background:var(--sd-primary-light, #eaf1ff);color:var(--sd-primary, #2a66f4)}.qb-body{padding-left:24px}.qb-treenode{position:relative;padding-left:22px;margin-bottom:12px}.qb-treenode:last-child{margin-bottom:0}.qb-treenode:before{content:\"\";position:absolute;left:0;top:-12px;bottom:0;border-left:1px dashed #d0d5dd}.qb-treenode:after{content:\"\";position:absolute;left:0;top:50%;width:18px;border-top:1px dashed #d0d5dd}.qb-treenode:last-child:before{bottom:auto;height:calc(50% + 12px)}.qb-empty{color:#98a2b3;font-style:italic;font-size:13px;padding:4px 0}.qb-rule{display:flex;align-items:center;gap:8px;background:#fafbfc;border:1px solid #e0e0e0;border-radius:6px;padding:6px 10px}.qb-rule:hover{border-color:#c2c8d0}.qb-field{min-width:170px}.qb-val{min-width:140px}.qb-value-source{min-width:132px}.qb-compare-field{min-width:170px}.qb-date-val{min-width:0;width:132px;flex:0 0 auto}::ng-deep .qb-opt{display:flex;align-items:center;gap:6px;min-width:0}::ng-deep .qb-opt .qb-opt-icon.mat-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;margin:0;font-size:18px;width:18px;height:18px;line-height:1;color:#667085}::ng-deep .qb-opt .qb-opt-label{min-width:0}.qb-between{display:flex;align-items:center;gap:6px}.qb-dash{color:#98a2b3}.qb-divider{width:1px;align-self:stretch;background:#e0e0e0}.qb-treenode.is-group>.qb-group{border:1px solid #e0e0e0;border-left:2px solid var(--sd-primary, #2a66f4);border-radius:6px;padding:12px;background:#fff}.qb-date-mode{min-width:160px}.qb-rel-amount{width:72px}.qb-rel-unit{min-width:110px}\n"] }]
        }], ctorParameters: () => [], propDecorators: { option: [{ type: i0.Input, args: [{ isSignal: true, alias: "option", required: false }] }], fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], comparisonMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "comparisonMode", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoId: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoId", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], filters: [{ type: i0.Input, args: [{ isSignal: true, alias: "filters", required: false }] }, { type: i0.Output, args: ["filtersChange"] }], rootLogic: [{ type: i0.Input, args: [{ isSignal: true, alias: "rootLogic", required: false }] }, { type: i0.Output, args: ["rootLogicChange"] }] } });

/**
 * Generated bundle index. Do not edit.
 */

export { QB_DATE_MODES, QB_DEFAULT_OPERATOR_BY_TYPE, QB_EMPTY_OPTIONS, QB_MULTI_OPERATORS, QB_NO_DATA_OPERATORS, QB_OPERATORS_BY_TYPE, QB_RELATIVE_UNIT_OPTIONS, QB_TODAY, QB_TYPE_ICON, QB_VALUE_SOURCE_OPTIONS, SdQueryBuilder, filterToTokens, filterToTree, isQbGroup, qbAllowedOperators, qbDefaultOperator, qbDefaultRelative, qbFieldIcon, qbId, qbIsMultiOperator, qbIsNoDataOperator, qbIsRelativeDate, qbIsToday, qbNewGroup, qbNewRule, qbSupportsFieldCompareOperator, treeToFilter };
//# sourceMappingURL=sdcorejs-angular-components-query-builder.mjs.map
