<template>
  <div class="q-pa-md q-gutter-y-md">
    <div
      class="flex justify-between items-center bg-white q-pa-md rounded-borders"
    >
      <div class="column full-width">
        <div>
          <div
            class="text-h6 flex items-center q-gutter-y-md text-grey-9 col q-pr-md"
          >
            <q-btn
              padding="xs"
              class="text-grey-7 q-mr-md"
              color="grey-3"
              icon="arrow_back"
              @click="
                returnToParentView('registries', 'cards/personal-accounts')
              "
            />
            <div>
              <div class="text-h5">{{ title }}</div>
              <div class="text-caption">{{ $t('registries.subtitle') }}</div>
            </div>
            <q-space />
            <div class="row items-center q-gutter-x-md q-gutter-y-sm">
              <q-btn
                v-show="isChildrensRestCashback"
                class="q-ml-sm"
                color="positive"
                icon="assignment_returned"
                :label="$t('registries.exportsBtnLabel')"
                @click="goToUnloadingList"
              />
              <q-btn
                v-show="isSocialTreasure"
                class="q-ml-sm"
                icon="assignment_returned"
                :label="$t('registries.exportsBtnLabel')"
                color="positive"
                :disable="!hasOrdersRegistry"
                :title="
                  hasOrdersRegistry
                    ? $t('registries.registryOrder.follow')
                    : $t('registries.registryOrder.none')
                "
                @click="goToOrderList"
              />
              <q-btn
                v-if="isAdmin"
                icon="delete"
                color="primary"
                :label="$t('registries.deleteBtnLabel')"
                @click="deleteRegistryConfirm"
              />
              <q-btn
                v-if="isAdmin"
                icon="create"
                color="primary"
                :label="$t('registries.editBtnLabel')"
                @click="editRegistry"
              />
              <q-btn
                v-if="!isAccreditation && !isChildrensRestCashback"
                icon="create"
                color="primary"
                :label="$t('registries.mappingLabel')"
                @click="setViewEditMappingTable(true)"
              />
              <q-btn
                v-if="isAdmin"
                icon="rule"
                color="primary"
                :label="$t('registries.inspectRecordsBtnLabel')"
                :loading="loading"
                @click="changeInspectRecordsModalVisibility(true)"
              />
              <q-btn
                icon="add"
                color="primary"
                v-if="!isChildrensRestCashback"
                :label="$t('registries.createBtnLabel')"
                @click="createRegistryItem"
              />
              <q-btn
                v-if="registry && isAccreditation && !isChildrensRestCashback"
                icon="attach_file"
                color="primary"
                :label="$t('registries.uploadBtnLabel')"
                @click="importAccreditationRegistryItem"
              />
              <q-btn
                v-if="
                  (!registry || !isAccreditation) && !isChildrensRestCashback
                "
                icon="attach_file"
                color="primary"
                :label="$t('registries.uploadBtnLabel')"
                @click="importRegistryItem"
              />
              <q-btn
                v-if="isSocialTreasure && canGenerateRegistry"
                icon="attach_file"
                :label="$t('registries.unloadingList.orderGenerateBtn')"
                color="primary"
                :disable="!model.selected.length"
                @click="generateAnOrder"
              />
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="bg-white rounded-borders">
      <AccreditationList
        v-if="isAccreditation"
        :model="model"
        :go-to-registry-object="goToRegistryObject"
        :update-table-data="updateTableData"
        :search="search"
        :export-records="exportRecords"
      />
      <CashbackList
        v-if="isChildrensRestCashback"
        :model="model"
        :go-to-registry-object="goToRegistryObject"
        :update-table-data="updateTableData"
        :search="search"
        :export-records="exportRecords"
      />
      <BaseList
        v-if="!isAccreditation && !isChildrensRestCashback"
        :model="model"
        :go-to-registry-object="goToRegistryObject"
        :update-table-data="updateTableData"
        :search="search"
        :export-records="exportRecords"
        :default-columns="defaultColumns"
      />
      <RegistryFormDialog
        :registry-dialog.sync="registryDialog"
        :form-model.sync="formModel"
      />
      <UploadCertDialog
        process-key="StampOrderProcess"
        :openDialog.sync="signPopup"
        :model="dataForSigner"
        :processInfo="currentProcess"
        @signed="onSigned"
      />
      <UploadRegistryItem
        v-if="registry"
        :open.sync="uploadModal"
        :schema="registry.code"
        :title="title"
        @upload="updateTableData"
      />
      <AccreditationUploadRegistryItem
        v-if="registry"
        :open.sync="accreditationUploadModal"
        :schema="registry.code"
        :title="title"
        @upload="updateTableData"
      />
      <CreateRegistryItem
        v-if="registry"
        :open.sync="createModal"
        :registry="registry"
        @create="updateTableData"
      />
      <!-- Модальное окно "Актуализировать состав" -->
      <InspectRecordsModal
        v-if="registry"
        :visible.sync="isInspectRecordsModalVisible"
        @inspect="inspectRecords"
      />
      <MappingTableDialog
        v-if="registry"
        :open.sync="viewModalEditMappingTable"
        :edit.sync="isServicesConstruct"
        :default-values="defaultColumns"
        :registry-table="registry.registryTable"
        :registry="registry"
        @update="updateDefaultColumns"
      />
    </div>
  </div>
</template>

<script lang="ts">
import { Component } from 'vue-property-decorator';
import get from 'lodash/get';
import { mixins } from 'vue-class-component';
import { saveAs } from 'file-saver';
import { v4 as getUUID } from 'uuid';

import {
  DocumentApi,
  InspectRecordsApi,
  Document,
} from '@openapi/registry-data';
import { Process, RegistryApi, RegistryTable } from '@openapi/registry-service';
import { WorkflowServiceApi, WFSchema } from '@openapi/wf';
import { DocumentConverterApi, LoadAndConvertRequestModelExtensionEnum } from '@openapi/document-converter';

import { notifyError, notifySuccess } from '@utils/notify';
import { pushRoute } from '@utils/routes';

import ReturnToParentView from '@mixins/ReturnToParentView';
import Loadable from '@/mixins/Loadable';

import S3UrlSignerFilesService from '@services/s3urlsigner/files';

import { USER_SCOPES } from '@constants/entities/Scope';
import SearchQuery from '@interfaces/entities/SearchQuery';
import { RegistryWithForm } from '@models/RegistryWithForm';

import UploadRegistryItem from '@components/registries/upload-registry-item.vue';
import CreateRegistryItem from '@components/registries/create-registry-item.vue';
import RegistryFormDialog from '@components/registries/registry-form-dialog.vue';
import AccreditationUploadRegistryItem from '@components/registries/accreditation-upload-registry-item.vue';
import AccreditationList from '@components/registries/custom-list/accreditation-list.vue';
import BaseList from '@components/registries/custom-list/base-list.vue';
import CashbackList from '@components/registries/custom-list/cashback-list.vue';
import UploadCertDialog from '@components/common/base-cert-uploader/dialog.vue';
import InspectRecordsModal from './inspect-records-modal.vue';
import MappingTableDialog from './mapping-table-dialog.vue';
import { RegistryTableColumn } from '@/models/RegistryTableColumn';
import {
  registryPositiveDecisionId as ukranianRefugeePositiveDecisionRegistryId,
  columns as ukranianRefugeeColumns,
} from './constants/ukranianRefugee';

const registryApi = new RegistryApi();
const documentApi = new DocumentApi();
const wfApi = new WorkflowServiceApi();
const documentConverterApi = new DocumentConverterApi();

const EXPORT_PROCESS = 'ExportXmlRegistryData';
const ACCREDITATION_CODE = 'RegistryAccreditation';
const EMERGENCIES_CODE = 'RegistryEmergencies';
const CHILDRENS_REST_CASHBACK_CODE = 'RegistryKompensDetskOtdMIR';
const DEFAULT_EXPORT_EXTENSION = 'xml';

type ExportRecordsOptions = {
  exnension: 'xml' | 'xls' | 'csv';
  isNewBtn: boolean; // @TODO временное решение, которое позволяет понять: используется новая кнопка выгрузки или старая - как откажемся от старой - надо будет свойство удалить!!!
};

interface ProsessOpts {
  taskId: string;
  traceId: string;
  formKey: string;
}

@Component({
  components: {
    BaseList,
    UploadRegistryItem,
    UploadCertDialog,
    AccreditationUploadRegistryItem,
    CreateRegistryItem,
    RegistryFormDialog,
    AccreditationList,
    CashbackList,
    InspectRecordsModal,
    MappingTableDialog,
  },
})
export default class RegistryList extends mixins(ReturnToParentView, Loadable) {
  title = '';
  registryId = this.$route.params.id;
  searchQuery: SearchQueryWithRefugees;
  registry: RegistryWithForm | null = null;
  uploadModal = false;
  accreditationUploadModal = false;
  createModal = false;
  loading = false;
  schemas: Array<WFSchema> = [];
  signModal = false;
  dataForSigner: Record<string, any> = {};
  model = {
    loading: false,
    pagination: {
      descending: true,
      rowsPerPage: 10,
      sortBy: 'document.resDate',
      rowsNumber: 0,
      page: 1,
    },
    selected: [],
    records: [] as Document[],
  };

  currentItem: Partial<Document> | null = null;

  registryDialog = false;
  formModel: RegistryWithForm | null = null;

  private queryString = '';

  private fulltextString = '';

  private costumExportExnension = 'xls';

  isAccreditation = false;
  isEmergencies = false;
  isChildrensRestCashback = false;
  isInspectRecordsModalVisible = false;
  viewModalEditMappingTable = false;

  defaultColumns: RegistryTableColumn[] = this
    .isUkranianRefugeePositiveDecisionRegistry
    ? ukranianRefugeeColumns
    : [
        {
          id: getUUID(),
          label: this.$t('registries.finishDate') as string,
          name: 'document.resDate',
          selected: true,
        },
        {
          id: getUUID(),
          label: this.$t('registries.department') as string,
          name: 'departmentFullName',
          selected: true,
        },
        {
          id: getUUID(),
          label: this.$t('registries.status') as string,
          name: 'status.internal',
          selected: true,
        },
        {
          id: getUUID(),
          label: this.$t('registries.objectCard.startDate') as string,
          name: 'applicationSystem.startDate',
          selected: true,
        },
        {
          id: getUUID(),
          label: this.$t('registries.objectCard.decisionNumber') as string,
          name: 'document.number',
          selected: true,
        },
      ];

  // COMPUTED ==================================================================================================================

  get isCustomExportProcess(): any {
    return this.schemas.find(
      (schema: any) => schema.key === `Export${this.registry?.code}`
    );
  }

  get isAdmin(): boolean {
    return this.$AC.haveScope(USER_SCOPES.CLOUD_ADMIN);
  }

  get isServicesConstruct(): boolean {
    return this.$AC.haveScope(USER_SCOPES.SERVICES_CONSTRUCT);
  }

  get isUkranianRefugeePositiveDecisionRegistry(): boolean {
    return this.$route.params.id === ukranianRefugeePositiveDecisionRegistryId;
  }

  get hasOrdersRegistry(): boolean {
    return !!this.registry?.registryOrderCode;
  }

  get canGenerateRegistry(): boolean {
    return this.$AC.haveOneOfScope([
      USER_SCOPES.UNLOADING_CREATE,
      USER_SCOPES.UNLOADING_LINK_BTN,
    ]);
  }

  // HOOKS =====================================================================================================================

  created(): void {
    this.model.loading = true;
    this.init().finally(() => {
      this.model.loading = false;
    });
  }

  // METHODS ===================================================================================================================

  async init(): Promise<void> {
    await this.fetchRegistryData();
    await this.updateTableData();
    const resault = await wfApi.getCodes();
    this.schemas = resault?.data?.schemas ?? [];
    await this.$store.dispatch(
      'registryService/referenceBooks/initReferenceBooks',
      [
        {
          force: true,
          id: this.isAccreditation
            ? 'accreditation_it_1_0_1__application_status'
            : 'gpzu_2_application_status',
        },
        {
          force: true,
          id: 'gpzu_5_goal_request',
        },
      ]
    );
  }

  get isSocialTreasure(): boolean {
    return !!this.registry?.isSocialTreasure;
  }

  fetchRegistryData(): Promise<RegistryWithForm | void> {
    return registryApi
      .getRegistry(this.registryId, 'form,registryTable')
      .then(({ data }) => {
        const context = data as RegistryWithForm;

        if (context?.name && context?.code) {
          this.title = context.name;
          this.registry = context;
          this.isAccreditation = context.code === ACCREDITATION_CODE;
          this.isEmergencies = context.code === EMERGENCIES_CODE;
          this.isChildrensRestCashback =
            context.code === CHILDRENS_REST_CASHBACK_CODE;

          if (this.isEmergencies || this.isChildrensRestCashback) {
            this.model.pagination.sortBy = 'document.resDate';
          }

          if (this.isAccreditation) {
            this.model.pagination.sortBy = 'document.resRegNumber';
          }

          if (context?.registryTable && context.registryTable?.attrList) {
            this.defaultColumns = context.registryTable
              .attrList as RegistryTableColumn[];
          }
        }
      });
  }

  updateTableData({ pagination } = this.model): Promise<void> {
    const query = [];

    if (typeof this.searchQuery !== 'undefined') {
      this.queryString = this.searchQuery?.query || '';
      this.fulltextString = this.searchQuery?.fulltext || '';
    }

    if (this.queryString) {
      query.push(`${this.queryString}`);
    }

    this.model.pagination.descending = pagination.descending;
    this.model.pagination.rowsPerPage = pagination.rowsPerPage;
    this.model.pagination.sortBy = pagination.sortBy;
    this.model.pagination.page = pagination.page;

    if (!this.$AC.haveOneOfScope([USER_SCOPES.CLOUD_ADMIN, USER_SCOPES.ALL_ORG_ADMIN])) {
      const currentUser: any = this.$store.getters['oidcClient/currentUser'];
      query.push(`departmentId=="${currentUser.domain_id}"`);
    }

    return documentApi
      .pagingDocuments(
        this.registry?.code || '',
        pagination.page,
        pagination.rowsPerPage,
        query.join(' and '),
        this.fulltextString,
        `${pagination.descending ? '-' : '+'}${pagination.sortBy}`
      )
      .then(({ data }) => {
        if (data.totalRows) {
          this.model.pagination.rowsNumber = data.totalRows;
        }
        this.model.records = data.records || [];
      })
      .catch(() => {
        console.warn(`failed count ${this.registry?.code}`);
      });
  }

  search(query: SearchQuery): void {
    this.searchQuery = query;
    this.updateTableData();
  }

  goToRegistryObject(id: string, opts?: any): void {
    pushRoute(
      this.$router,
      {
        name: 'cards',
        params: {
          id,
          path: `registry-object`,
        },
        query: {
          active: 'data',
          reg: this.registry?.code,
          regid: this.registryId,
          components: this.$store.getters[
            'cardComponents/registryObject/components'
          ],
        },
      },
      opts?.targetBlank
    );
  }

  goToRegistryList(): void {
    this.$router.push({
      path: `/registries`,
    });
  }

  goToOrderList(): void {
    console.log('registry-payment', this.registry?.registryOrderCode);
    if (!this.registry?.registryOrderCode) {
      notifyError('Не возможно открыть реестр распоряжений');
      return;
    }
    this.$router.push({
      name: 'registry-order',
      params: {
        id: this.registry?.registryOrderCode,
      },
    });
  }

  goToUnloadingList(): void {
    this.$router.push({
      name: 'registry-unloading',
      params: {
        id: this.registryId,
      }
    });
  }

  importRegistryItem(): void {
    this.uploadModal = true;
  }

  importAccreditationRegistryItem(): void {
    this.accreditationUploadModal = true;
  }

  createRegistryItem(): void {
    this.createModal = true;
  }

  exportRecords(options?: ExportRecordsOptions): void {
    let selectedIds: string[] = [];

    if (this.model.selected.length) {
      selectedIds = this.model.selected.map((el: RegistryWithForm) => el.id);
    }
    const { exnension, isNewBtn } = options || {};
    if (exnension) {
      this.costumExportExnension = exnension;
    }

    this.exportRecordsByIds(selectedIds, isNewBtn);
  }

  editRegistry(): void {
    registryApi.getRegistry(this.registryId, 'form').then(({ data }) => {
      this.formModel = data;
      this.registryDialog = true;
    });
  }

  deleteRegistryConfirm(): void {
    this.$q
      .dialog({
        title: this.$t('registries.objectCard.delete') as string,
        message: this.$t('registries.deleteInfo') as string,
        cancel: true,
      })
      .onOk(() => this.deleteRegistry());
  }

  async deleteRegistry(): Promise<void> {
    await documentApi
      .countDocuments(this.registry?.code || '')
      .then(({ data }) => {
        console.log(
          `records: ${data} linked: ${this.registry?.linkedRegistries?.length}`
        );
      })
      .catch(() => console.log('0 linked records'));
    await registryApi.deleteRegistry([this.registryId]);
    this.goToRegistryList();
  }

  exportProccesHandler(): string {
    // TODO: временное решение, пока в api workflow не будет добавлен метод поиска бп таск PGS-5824

    if (this.isCustomExportProcess?.key) {
      return this.isCustomExportProcess.key;
    }

    return EXPORT_PROCESS;
  }

  async exportRecordsByIds(ids: string[], isNewBtn = false): Promise<void> {
    const query = [];

    if (typeof this.searchQuery !== 'undefined') {
      this.queryString = this.searchQuery?.query || '';
      this.fulltextString = this.searchQuery?.fulltext || '';
    }

    if (this.registryId === ukranianRefugeePositiveDecisionRegistryId) {
      this.queryString = this.searchQuery?.queryConverter || '';
      this.fulltextString = this.searchQuery?.fulltextConverter || '';
    }
    if (this.queryString) {
      query.push(`${this.queryString}`);
    }

    if (isNewBtn) {
      if (!this.$AC.haveOneOfScope([USER_SCOPES.CLOUD_ADMIN, USER_SCOPES.ALL_ORG_ADMIN])) {
        const currentUser: any = this.$store.getters['oidcClient/currentUser'];
        // На данный момент converter ругается на любые кавычки с ошибкой 400 
        query.push(`departmentId==${currentUser.domain_id}`);
      }

      const columns = this.defaultColumns
        .filter((column) => column.selected)
        .map((column) => ({ label: column.label, name: column.name }));

      try {
        const extension: LoadAndConvertRequestModelExtensionEnum =
          LoadAndConvertRequestModelExtensionEnum[this.costumExportExnension as keyof typeof LoadAndConvertRequestModelExtensionEnum];
        const { data } = await documentConverterApi.loadAndConvertDocuments({
          extension,
          columns: [
            { label: this.$t('registries.applicationNumber') as string, name: 'orderId' },
            ...columns
          ],
          ids,
          registryCode: this.registry?.code as string,
          query: query.join(' and '),
          fulltext: this.fulltextString,
        }, { responseType: 'blob' });

        saveAs(data, `${this.title}.${this.costumExportExnension}`);
      } catch (e) {
        notifyError('Ошибка экспорта!');
      }

      return;
    }

    if (!this.$AC.haveOneOfScope([USER_SCOPES.CLOUD_ADMIN, USER_SCOPES.ALL_ORG_ADMIN])) {
      const currentUser: any = this.$store.getters['oidcClient/currentUser'];
      query.push(`departmentId==${currentUser.domain_id}`);
    }
    return wfApi
      .start({
        processKey: this.exportProccesHandler(),
        userId: this.$store.getters['oidcClient/currentUser'].sub,
        data: {
          ids,
          registryCode: this.registry?.code,
          query: query.join(' and '),
          // .replace(' or ', '+or+')
          // .replace(' and ', '+and+'),
          fulltext: this.fulltextString,
        },
      })
      .then(({ data: { traceId } }) => {
        const handler = (e: any) => {
          const data = JSON.parse(get(e, 'data', '{}'));
          if (
            [EXPORT_PROCESS, 'ExportRegistryFileDownload'].includes(
              data.className
            ) &&
            data.traceId === traceId
          ) {
            console.log('[RegistryList] export push', data);
            if (data.body.fileName) {
              const { strings = [] } = data.body.fileName;
              this.costumExportExnension =
                strings.length > 0
                  ? strings[strings.length - 1].replace('.', '')
                  : 'xls';
            }
            if (data.body.url) {
              S3UrlSignerFilesService.fetchFileFromS3ByUrl({
                url: data.body.url,
              }).then(({ file }) => {
                const extension = this.isCustomExportProcess
                  ? this.costumExportExnension
                  : DEFAULT_EXPORT_EXTENSION;
                saveAs(file, `${this.title}.${extension}`);
                if (this.isChildrensRestCashback) {
                  notifySuccess(
                    'xls файл успешно выгружен',
                    'check_circle_outline'
                  );
                }
              });
            } else {
              notifyError(`Ошибка экспорта: ${data.body.description || ''}`);
            }

            // @ts-ignore
            this.$websockets.removeListener(handler);
          }
        };

        // @ts-ignore
        this.$websockets.addListener(handler);
      });
  }

  updateDefaultColumns(defaultColumns: RegistryTableColumn[]): void {
    this.defaultColumns = defaultColumns;
  }

  /**
   * Показать модальное окно актуализации состава записей реестра
   */
  changeInspectRecordsModalVisibility(visible = false): void {
    this.isInspectRecordsModalVisible = visible;
  }

  /**
   * Актуализировать состав записей реестра
   */
  async inspectRecords({
    from = '',
    to = '',
  }: {
    from: string;
    to: string;
  }): Promise<void> {
    this.changeInspectRecordsModalVisibility(false);

    /* Если реестра нет, то и смысла нет */
    if (!this.registry?.code) {
      notifyError(this.$t('registries.inspectRecordsFail').toString());

      return;
    }

    const inspectRecordsApi = await new InspectRecordsApi();
    const inspectRequest = inspectRecordsApi
      .inspectRegistryRecords(this.registry?.code, from, to)
      .then(() => {
        this.model.loading = true;
        this.init().finally(() => {
          notifySuccess(this.$t('registries.inspectRecordsSuccess').toString());
          this.model.loading = false;
        });
      })
      .catch(() => {
        notifyError(this.$t('registries.inspectRecordsFail').toString());
      });

    this.loadingUntilPromiseResolved(inspectRequest);
  }

  setViewEditMappingTable(viewModal = false): void {
    this.viewModalEditMappingTable = viewModal;
  }

  currentProcess: ProsessOpts | null = null;
  signPopup = false;

  async generateAnOrder(): Promise<void> {
    if (this.model.selected.length > 10) {
      return notifyError('Генерация распоряжения под выборку решений предполагает не более 10 решений. Если необходимо сгенерировать по большему числу решений, то просим снять выделение с выбранных решений.')
    }
    this.loading = true;
    this.$q.loading.show();
    try {
      this.$store
        .dispatch('wf/processes/dispatchProcess', {
          processKey: 'BaseOrderCreate',
          data: {
            registryCode: this.registry?.code,
            registryOrderCode: this.registry?.registryOrderCode,
            userId: this.$store.getters['oidcClient/currentUser'].sub,
            ids: this.model.selected,
          },
        })
        .then(({ traceId }) => {
          const handler = async (e: any) => {
            const data = JSON.parse(get(e, 'data', '{}'));
            if (data.traceId === traceId) {
              this.loading = false;
              const type = get(data, 'body.type', null);
              const outData = get(data, 'body.userTask.outData', null);
              const taskId = get(data, 'body.userTask.taskId', '');
              const formKey = get(data, 'body.userTask.formKey', '');
              if (type === 'error') {
                this.$q.loading.hide();
              }
              if (outData && outData.success) {
                this.$q.loading.hide();
                this.$websockets.removeListener(handler);

                this.addSign(
                  {
                    registryOrderCode: this.registry?.registryOrderCode,
                    exportId: outData.exportId,
                    documents: outData.filesForSign,
                  },
                  {
                    traceId,
                    taskId,
                    formKey,
                  }
                );
              }
            }
          };
          this.$websockets.addListener(handler);
        });
    } catch (e) {
      this.$q.loading.hide();
      if (e instanceof Error) notifyError(e.message);
    }
    this.loading = false;
  }

  addSign(data: Record<string, any>, process?: ProsessOpts): void {
    this.dataForSigner = data;
    this.currentProcess = process || null;
    this.signPopup = true;
  }

  async onSigned({
    exportId,
    signedFiles,
    signed,
    state,
    certificate,
  }: any): Promise<void> {
    try {
      await new DocumentApi().updateDocument('AppealOrder', [
        {
          id: exportId,
          // @ts-ignore TODO openapi
          signedFiles,
          signed,
          state,
          certificate,
        },
      ]);
    } catch (error) {
      if (error instanceof Error) notifyError(error.message);
    }
    this.updateTableData();
  }
}

interface WebSockets {
  instance: WebSocket | null;
}
interface SearchQueryWithRefugees extends SearchQuery {
  queryConverter?: string;
  fulltextConverter?: string;
}
</script>
