Source

lib/components/list/list.component.ts

import { Component, OnInit, EventEmitter, Output, Input, HostListener, OnDestroy  } from '@angular/core';
import { InfiniteScrollCustomEvent, RefresherCustomEvent, SpinnerTypes } from '@ionic/angular';
import {
  IonInfiniteScroll,
  IonInfiniteScrollContent,
  IonItem,
  IonLabel,
  IonList,
  IonRefresher,
  IonRefresherContent,
  IonSkeletonText,
  IonText,
  IonThumbnail,
  IonLoading
} from '@ionic/angular/standalone';
import { debounceTime, Subject } from 'rxjs';
import { OperationKeys } from '@decaf-ts/db-decorators';
import { Model, Primitives } from '@decaf-ts/decorator-validation';
import { Condition, Observer, OrderDirection, Paginator } from '@decaf-ts/core';
import {
  BaseCustomEvent,
  Dynamic,
  EventConstants,
  ComponentsTagNames,
  RendererCustomEvent,
  StringOrBoolean,
  KeyValue,
  ListItemCustomEvent
} from '../../engine';
import { ForAngularModule } from '../../for-angular.module';
import { NgxBaseComponent } from '../../engine/NgxBaseComponent';
import {
  stringToBoolean,
  formatDate,
  isValidDate
} from '../../helpers';
import { SearchbarComponent } from '../searchbar/searchbar.component';
import { EmptyStateComponent } from '../empty-state/empty-state.component';
import { ListItemComponent } from '../list-item/list-item.component';
import { ComponentRendererComponent } from '../component-renderer/component-renderer.component';
import { PaginationComponent } from '../pagination/pagination.component';
import { PaginationCustomEvent } from '../pagination/constants';
import { IListEmptyResult, ListComponentsTypes, DecafRepository } from './constants';
import { FunctionLike, IFilterQuery, IFilterQueryItem } from '../../engine';
import { FilterComponent } from '../filter/filter.component';

/**
 * @description A versatile list component that supports various data display modes.
 * @summary This component provides a flexible way to display lists of data with support
 * for infinite scrolling, pagination, searching, and custom item rendering. It can fetch
 * data from various sources including models, functions, or direct data input.
 *
 * The component supports two main display types:
 * 1. Infinite scrolling - Loads more data as the user scrolls
 * 2. Pagination - Displays data in pages with navigation controls
 *
 * Additional features include:
 * - Pull-to-refresh functionality
 * - Search filtering
 * - Empty state customization
 * - Custom item rendering
 * - Event emission for interactions
 *
 * @mermaid
 * sequenceDiagram
 *   participant U as User
 *   participant L as ListComponent
 *   participant D as Data Source
 *   participant E as External Components
 *
 *   U->>L: Initialize component
 *   L->>L: ngOnInit()
 *   L->>D: Request initial data
 *   D-->>L: Return data
 *   L->>L: Process and display data
 *
 *   alt User scrolls (Infinite mode)
 *     U->>L: Scroll to bottom
 *     L->>D: Request more data
 *     D-->>L: Return additional data
 *     L->>L: Append to existing data
 *   else User changes page (Paginated mode)
 *     U->>L: Click page number
 *     L->>L: handlePaginate()
 *     L->>D: Request data for page
 *     D-->>L: Return page data
 *     L->>L: Replace displayed data
 *   end
 *
 *   alt User searches
 *     U->>L: Enter search term
 *     L->>L: handleSearch()
 *     L->>D: Filter data by search term
 *     D-->>L: Return filtered data
 *     L->>L: Update displayed data
 *   end
 *
 *   alt User clicks item
 *     U->>L: Click list item
 *     L->>L: handleClick()
 *     L->>E: Emit clickEvent
 *   end
 *
 * @example
 * <ngx-decaf-list
 *   [source]="dataSource"
 *   [limit]="10"
 *   [type]="'infinite'"
 *   [showSearchbar]="true"
 *   (clickEvent)="handleItemClick($event)"
 *   (refreshEvent)="handleRefresh($event)">
 * </ngx-decaf-list>
 *
 * @extends {NgxBaseComponent}
 * @implements {OnInit}
 */
@Dynamic()
@Component({
  selector: 'ngx-decaf-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss'],
  standalone: true,
  imports: [
    ForAngularModule,
    IonRefresher,
    IonLoading,
    PaginationComponent,
    IonList,
    IonItem,
    IonThumbnail,
    IonSkeletonText,
    IonLabel,
    IonText,
    IonRefresherContent,
    IonInfiniteScroll,
    IonInfiniteScrollContent,
    IonThumbnail,
    IonSkeletonText,
    SearchbarComponent,
    EmptyStateComponent,
    ListItemComponent,
    FilterComponent,
    ComponentRendererComponent
  ]
})
export class ListComponent extends NgxBaseComponent implements OnInit, OnDestroy {

  /**
   * @description The display mode for the list component.
   * @summary Determines how the list data is loaded and displayed. Options include:
   * - INFINITE: Loads more data as the user scrolls (infinite scrolling)
   * - PAGINATED: Displays data in pages with navigation controls
   *
   * @type {ListComponentsTypes}
   * @default ListComponentsTypes.INFINITE
   * @memberOf ListComponent
   */
  @Input()
  type: ListComponentsTypes = ListComponentsTypes.INFINITE;

  /**
   * @description Controls whether the component uses translation services.
   * @summary When set to true, the component will attempt to use translation services
   * for any text content. This allows for internationalization of the list component.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf ListComponent
   */
  @Input()
  override translatable: StringOrBoolean = true;

  /**
   * @description Controls the visibility of the search bar.
   * @summary When set to true, displays a search bar at the top of the list that allows
   * users to filter the list items. The search functionality works by filtering the
   * existing data or by triggering a new data fetch with search parameters.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf ListComponent
   */
  @Input()
  showSearchbar: StringOrBoolean = true;

  /**
   * @description Direct data input for the list component.
   * @summary Provides a way to directly pass data to the list component instead of
   * fetching it from a source. When both data and source are provided, the component
   * will use the source to fetch data only if the data array is empty.
   *
   * @type {KeyValue[] | undefined}
   * @default undefined
   * @memberOf ListComponent
   */
  @Input()
  data?: KeyValue[] | undefined = undefined;

  /**
   * @description The data source for the list component.
   * @summary Specifies where the list should fetch its data from. This can be either:
   * - A string URL or endpoint identifier
   * - A function that returns data when called
   * The component will call this source when it needs to load or refresh data.
   *
   * @type {string | FunctionLike}
   * @required
   * @memberOf ListComponent
   */
  @Input()
  source!: string | FunctionLike;

  /**
   * @description The starting index for data fetching.
   * @summary Specifies the index from which to start fetching data. This is used
   * for pagination and infinite scrolling to determine which subset of data to load.
   *
   * @type {number}
   * @default 0
   * @memberOf ListComponent
   */
  @Input()
  start: number = 0;

  /**
   * @description The number of items to fetch per page or load operation.
   * @summary Determines how many items are loaded at once during pagination or
   * infinite scrolling. This affects the size of data chunks requested from the source.
   *
   * @type {number}
   * @default 10
   * @memberOf ListComponent
   */
  @Input()
  limit: number = 10;

  /**
   * @description Controls whether more data can be loaded.
   * @summary When set to true, the component will allow loading additional data
   * through infinite scrolling or pagination. When false, the component will not
   * attempt to load more data beyond what is initially displayed.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf ListComponent
   */
  @Input()
  loadMoreData: StringOrBoolean = true

  /**
   * @description The style of dividing lines between list items.
   * @summary Determines how dividing lines appear between list items. Options include:
   * - "inset": Lines are inset from the edges
   * - "full": Lines extend the full width
   * - "none": No dividing lines
   *
   * @type {"inset" | "full" | "none"}
   * @default "full"
   * @memberOf ListComponent
   */
  @Input()
  lines: "inset" | "full" | "none" = "full";

  /**
   * @description Controls whether the list has inset styling.
   * @summary When set to true, the list will have inset styling with rounded corners
   * and margin around the edges. This creates a card-like appearance for the list.
   *
   * @type {StringOrBoolean}
   * @default false
   * @memberOf ListComponent
   */
  @Input()
  inset: StringOrBoolean = false;

  /**
   * @description The threshold for triggering infinite scroll loading.
   * @summary Specifies how close to the bottom of the list the user must scroll
   * before the component triggers loading of additional data. This is expressed
   * as a percentage of the list height.
   *
   * @type {string}
   * @default "15%"
   * @memberOf ListComponent
   */
  @Input()
  scrollThreshold: string = "15%";

  /**
   * @description The position where new items are added during infinite scrolling.
   * @summary Determines whether new items are added to the top or bottom of the list
   * when loading more data through infinite scrolling.
   *
   * @type {"bottom" | "top"}
   * @default "bottom"
   * @memberOf ListComponent
   */
  @Input()
  scrollPosition: "bottom" | "top" = "bottom";

  /**
   * @description Custom text to display during loading operations.
   * @summary Specifies the text shown in the loading indicator when the component
   * is fetching data. If not provided, a default loading message will be used.
   *
   * @type {string | undefined}
   * @memberOf ListComponent
   */
  @Input()
  loadingText?: string;

  /**
   * @description Controls the visibility of the pull-to-refresh feature.
   * @summary When set to true, enables the pull-to-refresh functionality that allows
   * users to refresh the list data by pulling down from the top of the list.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf ListComponent
   */
  @Input()
  showRefresher: StringOrBoolean = true;

  /**
   * @description The type of spinner to display during loading operations.
   * @summary Specifies the visual style of the loading spinner shown during data
   * fetching operations. Uses Ionic's predefined spinner types.
   *
   * @type {SpinnerTypes}
   * @default "circular"
   * @memberOf ListComponent
   */
  @Input()
  loadingSpinner: SpinnerTypes = "circular";

  // /**
  //  * @description Query parameters for data fetching.
  //  * @summary Specifies additional query parameters to use when fetching data from
  //  * the source. This can be provided as a string (JSON) or a direct object.
  //  *
  //  * @type {string | KeyValue | undefined}
  //  * @memberOf ListComponent
  //  */
  // @Input()
  // query?: string | KeyValue;

  /**
   * @description Controls whether the filtering functionality is enabled.
   * @summary When set to true, enables the filter component that allows users to create
   * complex search criteria with multiple field filters, conditions, and values.
   * When false, disables the filter interface entirely.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf ListComponent
   */
  @Input()
  enableFilter: StringOrBoolean = true;

  /**
   * @description Sorting parameters for data fetching.
   * @summary Specifies how the fetched data should be sorted. This can be provided
   * as a string (field name with optional direction) or a direct object.
   *
   * @type {string | KeyValue | undefined}
   * @memberOf ListComponent
   */
  @Input()
  sortDirection: OrderDirection = OrderDirection.DSC;


  /**
   * @description Sorting parameters for data fetching.
   * @summary Specifies how the fetched data should be sorted. This can be provided
   * as a string (field name with optional direction) or a direct object.
   *
   * @type {string | KeyValue | undefined}
   * @memberOf ListComponent
   */
  @Input()
  sortBy!: string;


  /**
   * @description Controls whether sorting functionality is disabled.
   * @summary When set to true, disables the sort controls and prevents users from
   * changing the sort order or field. The list will maintain its default or
   * programmatically set sort configuration without user interaction.
   *
   * @type {StringOrBoolean}
   * @default false
   * @memberOf ListComponent
   */
  @Input()
  disableSort: StringOrBoolean = false;


  /**
   * @description Icon to display when the list is empty.
   * @summary Specifies the icon shown in the empty state when no data is available.
   * This can be any icon name supported by the application's icon system.
   *
   * @type {string | undefined}
   * @default 'ti-database-exclamation'
   * @memberOf ListComponent
   */
  @Input()
  emptyIcon?: string = 'ti-database-exclamation';

  /**
   * @description Configuration for the empty state display.
   * @summary Customizes how the empty state is displayed when no data is available.
   * This includes the title, subtitle, button text, icon, and navigation link.
   *
   * @type {Partial<IListEmptyResult>}
   * @default {
   *   title: 'empty.title',
   *   subtitle: 'empty.subtitle',
   *   showButton: false,
   *   icon: 'alert-circle-outline',
   *   buttonText: 'locale.empty.button',
   *   link: ''
   * }
   * @memberOf ListComponent
   */
  @Input()
  empty: Partial<IListEmptyResult> = {
    title: 'empty.title',
    subtitle: 'empty.subtitle',
    showButton: false,
    icon: 'alert-circle-outline',
    buttonText: 'locale.empty.button',
    link: ''
  }

  /**
   * @description The current page number in paginated mode.
   * @summary Tracks which page is currently being displayed when the component
   * is in paginated mode. This is used for pagination controls and data fetching.
   *
   * @type {number}
   * @default 1
   * @memberOf ListComponent
   */
  page: number = 1;

  /**
   * @description The total number of pages available.
   * @summary Stores the calculated total number of pages based on the data size
   * and limit. This is used for pagination controls and boundary checking.
   *
   * @type {number}
   * @memberOf ListComponent
   */
  pages!: number;

  /**
   * @description Indicates whether a refresh operation is in progress.
   * @summary When true, the component is currently fetching new data. This is used
   * to control loading indicators and prevent duplicate refresh operations from
   * being triggered simultaneously.
   *
   * @type {boolean}
   * @default false
   * @memberOf ListComponent
   */
  refreshing: boolean = false;

  /**
   * @description Array used for rendering skeleton loading placeholders.
   * @summary Contains placeholder items that are displayed during data loading.
   * The length of this array determines how many skeleton items are shown.
   *
   * @type {string[]}
   * @default new Array(2)
   * @memberOf ListComponent
   */
  skeletonData: string[] = new Array(2);

  /**
   * @description The processed list items ready for display.
   * @summary Stores the current set of items being displayed in the list after
   * processing from the raw data source. This may be a subset of the full data
   * when using pagination or infinite scrolling.
   *
   * @type {KeyValue[]}
   * @memberOf ListComponent
   */
  items!: KeyValue[];

  /**
   * @description The current search query value.
   * @summary Stores the text entered in the search bar. This is used to filter
   * the list data or to send as a search parameter when fetching new data.
   *
   * @type {string | undefined}
   * @memberOf ListComponent
   */
  searchValue?: string | IFilterQuery | undefined;

  /**
   * @description A paginator object for handling pagination operations.
   * @summary Provides a paginator object that can be used to retrieve and navigate
   * through data in chunks, reducing memory usage and improving performance.
   *
   * The paginator object is initialized in the `ngOnInit` lifecycle hook and is
   * used to fetch and display data in the pagination component. It is an instance
   * of the `Paginator` class from the `@decaf-ts/core` package, which provides
   * methods for querying and manipulating paginated data.
   *
   * @type {Paginator<Model>}
   * @memberOf PaginationComponent
   */
  paginator!: Paginator<Model> | undefined;

  /**
   * @description The last page number that was displayed.
   * @summary Keeps track of the previously displayed page number, which is useful
   * for handling navigation and search operations in paginated mode.
   *
   * @type {number}
   * @default 1
   * @memberOf ListComponent
   */
  lastPage: number = 1

  /**
   * @description Event emitter for refresh operations.
   * @summary Emits an event when the list data is refreshed, either through pull-to-refresh
   * or programmatic refresh. The event includes the refreshed data and component information.
   *
   * @type {EventEmitter<BaseCustomEvent>}
   * @memberOf ListComponent
   */
  @Output()
  refreshEvent: EventEmitter<BaseCustomEvent> = new EventEmitter<BaseCustomEvent>();

  /**
   * @description Event emitter for item click interactions.
   * @summary Emits an event when a list item is clicked. The event includes the data
   * of the clicked item, allowing parent components to respond to the interaction.
   *
   * @type {EventEmitter<KeyValue>}
   * @memberOf ListComponent
   */
  @Output()
  clickEvent:  EventEmitter<ListItemCustomEvent|RendererCustomEvent> = new EventEmitter<ListItemCustomEvent|RendererCustomEvent>();

  /**
   * @description Subject for debouncing click events.
   * @summary Uses RxJS Subject to collect click events and emit them after a debounce
   * period. This prevents multiple rapid clicks from triggering multiple events.
   *
   * @private
   * @type {Subject<CustomEvent | ListItemCustomEvent | RendererCustomEvent>}
   * @memberOf ListComponent
   */
  private clickItemSubject: Subject<CustomEvent | ListItemCustomEvent | RendererCustomEvent> = new Subject<CustomEvent | ListItemCustomEvent | RendererCustomEvent>();


  /**
   * @description Subject for debouncing repository observation events.
   * @summary RxJS Subject that collects repository change events and emits them after
   * a debounce period. This prevents multiple rapid repository changes from triggering
   * multiple list refresh operations, improving performance and user experience.
   *
   * @private
   * @type {Subject<any>}
   * @memberOf ListComponent
   */
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private observerSubjet: Subject<any> = new Subject<any>();

  /**
   * @description Observer object for repository change notifications.
   * @summary Implements the Observer interface to receive notifications when the
   * underlying data repository changes. This enables automatic list updates when
   * data is created, updated, or deleted through the repository.
   *
   * @private
   * @type {Observer}
   * @memberOf ListComponent
   */
  private observer: Observer = { refresh: async (... args: unknown[]): Promise<void> => this.observeRepository(...args)}

  /**
   * @description List of available indexes for data querying and filtering.
   * @summary Provides a list of index names that can be used to optimize data querying and filtering
   * operations, especially in scenarios with large datasets.
   *
   * Indexes can significantly improve the performance of data retrieval by allowing the database
   * to quickly locate and retrieve relevant data based on indexed fields.
   *
   * @type {string[]}
   * @default []
   * @memberOf ListComponent
   */
  indexes!: string[];

  /**
   * @description Initializes a new instance of the ListComponent.
   * @summary Creates a new ListComponent and sets up the base component with the appropriate
   * component name. This constructor is called when Angular instantiates the component and
   * before any input properties are set. It passes the component name to the parent class
   * constructor to enable proper localization and component identification.
   *
   * The constructor is intentionally minimal, with most initialization logic deferred to
   * the ngOnInit lifecycle hook. This follows Angular best practices by keeping the constructor
   * focused on dependency injection and basic setup, while complex initialization that depends
   * on input properties is handled in ngOnInit.
   *
   * @memberOf ListComponent
   */
  constructor() {
    super("ListComponent");
  }


  /**
   * @description Initializes the component after Angular sets the input properties.
   * @summary Sets up the component by initializing event subscriptions, processing boolean
   * inputs, and loading the initial data. This method prepares the component for user
   * interaction by ensuring all properties are properly initialized and data is loaded.
   *
   * @returns {Promise<void>}
   *
   * @mermaid
   * sequenceDiagram
   *   participant A as Angular Lifecycle
   *   participant L as ListComponent
   *   participant D as Data Source
   *
   *   A->>L: ngOnInit()
   *   L->>L: Set up click event debouncing
   *   L->>L: Process boolean inputs
   *   L->>L: Configure component based on inputs
   *   L->>L: refresh()
   *   L->>D: Request initial data
   *   D-->>L: Return data
   *   L->>L: Process and display data
   *   L->>L: Configure empty state if needed
   *   L->>L: initialize()
   *
   * @memberOf ListComponent
   */
  async ngOnInit(): Promise<void> {
    this.clickItemSubject.pipe(debounceTime(100)).subscribe(event => this.clickEventEmit(event as ListItemCustomEvent | RendererCustomEvent));
    this.observerSubjet.pipe(debounceTime(100)).subscribe(args => this.handleObserveEvent(args[0], args[1], args[2]));
    this.enableFilter = stringToBoolean(this.enableFilter);
    this.limit = Number(this.limit);
    this.start = Number(this.start);
    this.inset = stringToBoolean(this.inset);
    this.showRefresher = stringToBoolean(this.showRefresher);
    this.loadMoreData = stringToBoolean(this.loadMoreData);
    this.showSearchbar = stringToBoolean(this.showSearchbar);
    this.disableSort = stringToBoolean(this.disableSort);
    if(typeof this.item?.['tag'] === 'boolean' && this.item?.['tag'] === true)
      this.item['tag'] = ComponentsTagNames.LIST_ITEM as string;

    await this.refresh();

    if(this.operations.includes(OperationKeys.CREATE) && this.route)
      this.empty.link = `${this.route}/${OperationKeys.CREATE}`;

    this.initialize();

    if(this.model instanceof Model && this._repository)
      this._repository.observe(this.observer);
  }

  /**
   * @description Cleans up resources when the component is destroyed.
   * @summary Performs cleanup operations when the component is being removed from the DOM.
   * This includes clearing references to models and data to prevent memory leaks.
   *
   * @returns {void}
   * @memberOf ListComponent
   */
  ngOnDestroy(): void {
    if(this._repository)
      this._repository.unObserve(this.observer);
    this.data =  this.model = this._repository = this.paginator = undefined;
  }

  /**
   * @description Handles repository observation events with debouncing.
   * @summary Processes repository change notifications and routes them appropriately.
   * For CREATE events with a UID, handles them immediately. For other events,
   * passes them to the debounced observer subject to prevent excessive updates.
   *
   * @param {...unknown[]} args - The repository event arguments including table, event type, and UID
   * @returns {Promise<void>}
   * @memberOf ListComponent
   */
  async observeRepository(...args: unknown[]): Promise<void> {
    const [table, event, uid] = args;
    if(event === OperationKeys.CREATE && !!uid)
      return this.handleObserveEvent(table as string, event, uid as string | number);
    return this.observerSubjet.next(args);
  }

  /**
   * @description Handles specific repository events and updates the list accordingly.
   * @summary Processes repository change events (CREATE, UPDATE, DELETE) and performs
   * the appropriate list operations. This includes adding new items, updating existing
   * ones, or removing deleted items from the list display.
   *
   * @param {string} table - The table/model name that changed
   * @param {OperationKeys} event - The type of operation (CREATE, UPDATE, DELETE)
   * @param {string | number} uid - The unique identifier of the affected item
   * @returns {Promise<void>}
   * @memberOf ListComponent
   */
  async handleObserveEvent(table: string, event: OperationKeys, uid: string | number): Promise<void> {
    if(event === OperationKeys.CREATE) {
      if(uid) {
        await this.handleCreate(uid);
      } else {
        await this.refresh(true);
      }
    } else {
      if(event === OperationKeys.UPDATE)
        await this.handleUpdate(uid);
      if(event === OperationKeys.DELETE)
        this.handleDelete(uid);
      this.refreshEventEmit();
    }
  }


  /**
   * @description Function for tracking items in the list.
   * @summary Provides a tracking function for the `*ngFor` directive in the component template.
   * This function is used to identify and control the rendering of items in the list,
   * preventing duplicate or unnecessary rendering.
   *
   * The `trackItemFn` function takes two parameters: `index` (the index of the item in the list)
   * and `item` (the actual item from the list). It returns the tracking key, which in this case
   * is the union of the `uid` of the item with the model name.
   *
   * @param {number} index - The index of the item in the list.

   * @param {KeyValue | string | number} item - The actual item from the list.
   * @returns {string | number} The tracking key for the item.
   * @memberOf ListComponent
   */
  override trackItemFn(index: number, item: KeyValue | string | number): string | number {
    return `${ (item as KeyValue)?.['uid'] || (item as KeyValue)?.[this.pk]}-${index}`;
  }


  /**
   * Handles the create event from the repository.
   *
   * @param {string | number} uid - The ID of the item to create.
   * @returns {Promise<void>} A promise that resolves when the item is created and added to the list.
   */
  async handleCreate(uid: string | number): Promise<void> {
    const result = await this._repository?.read(uid);
    const item = this.mapResults([result as KeyValue])[0];
    this.items = this.data = [item, ...this.items || []];
  }


  /**
   * @description Handles the update event from the repository.
   * @summary Updates the list item with the specified ID based on the new data.
   *
   * @param {string | number} uid - The ID of the item to update
   * @returns {Promise<void>}
   * @private
   * @memberOf ListComponent
   */
  async handleUpdate(uid: string | number): Promise<void> {
    const item: KeyValue = this.itemMapper(await this._repository?.read(uid) || {}, this.mapper);
    this.data = [];
    for(const key in this.items as KeyValue[]) {
        const child = this.items[key] as KeyValue;
        if(child['uid'] === item['uid']) {
          this.items[key] = Object.assign({}, child, item);
          break;
        }
    }
    setTimeout(() => {
      this.data = [ ...this.items];
    }, 0);
  }

  /**
   * @description Removes an item from the list by ID.
   * @summary Filters out an item with the specified ID from the data array and
   * refreshes the list display. This is typically used after a delete operation.
   *
   * @param {string} uid - The ID of the item to delete
   * @param {string} pk - The primary key field name
   * @returns {Promise<void>}
   *
   * @memberOf ListComponent
   */
  handleDelete(uid: string | number, pk?: string): void  {
    if(!pk)
      pk = this.pk;
    this.items = this.data?.filter((item: KeyValue) => item['uid'] !== uid) || [];
  }


  /**
   * @description Handles click events from list items.
   * @summary Listens for global ListItemClickEvent events and passes them to the
   * debounced click subject. This allows the component to respond to clicks on
   * list items regardless of where they originate from.
   *
   * @param {ListItemCustomEvent | RendererCustomEvent} event - The click event
   * @returns {void}
   *
   * @memberOf ListComponent
   */
  @HostListener('window:ListItemClickEvent', ['$event'])
  handleClick(event: ListItemCustomEvent | RendererCustomEvent): void {
    this.clickItemSubject.next(event);
  }

  /**
   * @description Handles search events from the search bar.
   * @summary Processes search queries from the search bar component, updating the
   * displayed data based on the search term. The behavior differs between infinite
   * and paginated modes to provide the best user experience for each mode.
   *
   * @param {string | undefined} value - The search term or undefined to clear search
   * @returns {Promise<void>}
   *
   * @mermaid
   * flowchart TD
   *   A[Search Event] --> B{Type is Infinite?}
   *   B -->|Yes| C[Disable loadMoreData]
   *   B -->|No| D[Enable loadMoreData]
   *   C --> E{Search value undefined?}
   *   E -->|Yes| F[Enable loadMoreData]
   *   E -->|No| G[Store search value]
   *   D --> G
   *   F --> H[Reset page to 1]
   *   G --> I[Refresh data]
   *   H --> I
   *
   * @memberOf ListComponent
   */
  @HostListener('window:searchbarEvent', ['$event'])
  async handleSearch(value: string | IFilterQuery | undefined): Promise<void> {
    if(this.type === ListComponentsTypes.INFINITE) {
      this.loadMoreData = false;
      if(value === undefined) {
        this.loadMoreData = true;
        this.page = 1;
      }
      this.searchValue = value;
      await this.refresh(true);
    } else {
      this.loadMoreData = true;
      this.searchValue = value;
      if(value === undefined)
        this.page = this.lastPage;
      await this.refresh(true);
    }
  }


  /**
   * @description Handles filter events from the filter component.
   * @summary Processes filter queries from the filter component and applies them
   * to the list data. This method acts as a bridge between the filter component
   * and the search functionality, converting filter queries into search operations.
   *
   * @param {IFilterQuery | undefined} value - The filter query object or undefined to clear filters
   * @returns {Promise<void>}
   * @memberOf ListComponent
   */
  async handleFilter(value: IFilterQuery | undefined): Promise<void> {
    await this.handleSearch(value);
  }

  /**
   * @description Clears the current search and resets the list.
   * @summary Convenience method that clears the search by calling handleSearch
   * with undefined. This resets the list to show all data without filtering.
   *
   * @returns {Promise<void>}
   * @memberOf ListComponent
   */
  async clearSearch(): Promise<void> {
    await this.handleSearch(undefined);
  }

  /**
   * @description Emits a refresh event with the current data.
   * @summary Creates and emits a refresh event containing the current list data.
   * This notifies parent components that the list data has been refreshed.
   *
   * @param {KeyValue[]} [data] - Optional data to include in the event
   * @returns {void}
   *
   * @memberOf ListComponent
   */
  refreshEventEmit(data?: KeyValue[]): void {
    if(!data)
      data = this.items;
    this.skeletonData = new Array(data?.length || 2);
    this.refreshEvent.emit({
      name: EventConstants.REFRESH,
      data: data || [],
      component: this.componentName
    });
  }

  /**
   * @description Emits a click event for a list item.
   * @summary Processes and emits a click event when a list item is clicked.
   * This extracts the relevant data from the event and passes it to parent components.
   *
   * @private
   * @param {ListItemCustomEvent | RendererCustomEvent} event - The click event
   * @returns {void}
   *
   * @memberOf ListComponent
   */
  private clickEventEmit(event: ListItemCustomEvent | RendererCustomEvent): void {
    this.clickEvent.emit(event);
  }

  /**
   * @description Refreshes the list data from the configured source.
   * @summary This method handles both initial data loading and subsequent refresh operations,
   * including pull-to-refresh and infinite scrolling. It manages the data fetching process,
   * updates the component's state, and handles pagination or infinite scrolling logic based
   * on the component's configuration.
   *
   * The method performs the following steps:
   * 1. Sets the refreshing flag to indicate a data fetch is in progress
   * 2. Calculates the appropriate start and limit values based on pagination settings
   * 3. Fetches data from the appropriate source (model or request)
   * 4. Updates the component's data and emits a refresh event
   * 5. Handles pagination or infinite scrolling state updates
   * 6. Completes any provided event (like InfiniteScrollCustomEvent)
   *
   * @param {InfiniteScrollCustomEvent | RefresherCustomEvent | boolean} event - The event that triggered the refresh,
   * or a boolean flag indicating if this is a forced refresh
   * @returns {Promise<void>} A promise that resolves when the refresh operation is complete
   *
   * @mermaid
   * sequenceDiagram
   *   participant L as ListComponent
   *   participant D as Data Source
   *   participant E as Event System
   *
   *   L->>L: refresh(event)
   *   L->>L: Set refreshing flag
   *   L->>L: Calculate start and limit
   *   alt Using model
   *     L->>D: getFromModel(force, start, limit)
   *     D-->>L: Return data
   *   else Using request
   *     L->>D: getFromRequest(force, start, limit)
   *     D-->>L: Return data
   *   end
   *   L->>E: refreshEventEmit()
   *   alt Infinite scrolling mode
   *     L->>L: Check if reached last page
   *     alt Last page reached
   *       L->>L: Complete scroll event
   *       L->>L: Disable loadMoreData
   *     else More pages available
   *       L->>L: Increment page number
   *       L->>L: Complete scroll event after delay
   *     end
   *   else Paginated mode
   *     L->>L: Clear refreshing flag after delay
   *   end
   *
   * @memberOf ListComponent
   */
  @HostListener('window:BackButtonNavigationEndEvent', ['$event'])
  async refresh(event: InfiniteScrollCustomEvent | RefresherCustomEvent | boolean = false): Promise<void> {
    //  if(typeof force !== 'boolean' && force.type === EventConstants.BACK_BUTTON_NAVIGATION) {
    //    const {refresh} = (force as CustomEvent).detail;
    //    if(!refresh)
    //      return false;
    //  }

    this.refreshing = true;
    const start: number = this.page > 1 ? (this.page - 1) * this.limit : this.start;
    const limit: number = (this.page * (this.limit > 12 ? 12 : this.limit));

    this.data = !this.model ?
      await this.getFromRequest(!!event, start, limit)
      : await this.getFromModel(!!event) as KeyValue[];

    this.refreshEventEmit();

    if(this.type === ListComponentsTypes.INFINITE) {
      if(this.page === this.pages) {
        if((event as InfiniteScrollCustomEvent)?.target)
          (event as InfiniteScrollCustomEvent).target.complete();
        this.loadMoreData = false;
      } else {
        this.page += 1;
        this.refreshing = false;
        setTimeout(() => {
            if((event as InfiniteScrollCustomEvent)?.target && (event as CustomEvent)?.type !== EventConstants.BACK_BUTTON_NAVIGATION)
              (event as InfiniteScrollCustomEvent).target.complete();
        }, 200);
      }
    } else {
      setTimeout(() => {
        this.refreshing = false;
      }, 200)
    }
  }

  /**
 * @description Handles pagination events from the pagination component.
 * @summary Processes pagination events by updating the current page number and
 * refreshing the list data to display the selected page. This method is called
 * when a user interacts with the pagination controls to navigate between pages.
 *
 * @param {PaginationCustomEvent} event - The pagination event containing page information
 * @returns {void}
 *
 * @memberOf ListComponent
 */
handlePaginate(event: PaginationCustomEvent): void {
  const { page} = event.data;
  this.page = page;
  this.refresh(true);
}

/**
 * @description Handles pull-to-refresh events from the refresher component.
 * @summary Processes refresh events triggered by the user pulling down on the list
 * or by programmatic refresh requests. This method refreshes the list data and
 * completes the refresher animation when the data is loaded.
 *
 * @param {InfiniteScrollCustomEvent | CustomEvent} [event] - The refresh event
 * @returns {Promise<void>} A promise that resolves when the refresh operation is complete
 *
 * @memberOf ListComponent
 */
async handleRefresh(event?: InfiniteScrollCustomEvent | CustomEvent): Promise<void> {
  await this.refresh(event as InfiniteScrollCustomEvent || true);
  if(event instanceof CustomEvent)
    setTimeout(() => {
      // Any calls to load data go here
      (event.target as HTMLIonRefresherElement).complete();
    }, 400);
}

/**
 * @description Filters data based on a search string.
 * @summary Processes the current data array to find items that match the provided
 * search string. This uses the arrayQueryByString utility to perform the filtering
 * across all properties of the items.
 *
 * @param {KeyValue[]} results - The array of items to search through
 * @param {string} search - The search string to filter by
 * @returns {KeyValue[]} A promise that resolves to the filtered array of items
 *
 * @memberOf ListComponent
 */
  parseSearchResults(results: KeyValue[], search: string): KeyValue[] {
    return results.filter((item: KeyValue) =>
      Object.values(item).some(value =>
          value.toString().toLowerCase().includes((search as string)?.toLowerCase())
        )
    );
  }

/**
 * @description Fetches data from a request source.
 * @summary Retrieves data from the configured source function or URL, processes it,
 * and updates the component's data state. This method handles both initial data loading
 * and subsequent refresh operations when using an external data source rather than a model.
 *
 * @param {boolean} force - Whether to force a refresh even if data already exists
 * @param {number} start - The starting index for pagination
 * @param {number} limit - The maximum number of items to retrieve
 * @returns {Promise<KeyValue[]>} A promise that resolves to the fetched data
 *
 * @memberOf ListComponent
 */
async getFromRequest(force: boolean = false, start: number, limit: number): Promise<KeyValue[]> {
  let request: KeyValue[] = [];
  if(!this.data?.length || force || (this.searchValue as string)?.length || !!(this.searchValue as IFilterQuery)) {
    // (self.data as ListItem[]) = [];
    if(!(this.searchValue as string)?.length && !(this.searchValue as IFilterQuery)) {
      if(!this.source && !this.data?.length) {
        this.logger.info('No data and source passed to infinite list');
        return [];
      }

      if(this.source instanceof Function)
        request = await this.source();

      if(!Array.isArray(request))
        request = request?.['response']?.['data'] || request?.['results'] || [];
      this.data = [... await this.parseResult(request)];
      if(this.data?.length)
        this.items = this.type === ListComponentsTypes.INFINITE ?
          (this.items || []).concat([...this.data.slice(start, limit)]) : [...request.slice(start, limit) as KeyValue[]];
    } else {
      this.data = this.parseSearchResults(this.data as [], this.searchValue as string);
      this.items = this.data;
    }
  }

  if(this.loadMoreData && this.type === ListComponentsTypes.PAGINATED)
    this.getMoreData(this.data?.length || 0);
  return this.data || [] as KeyValue[];
}

/**
 * @description Fetches data from a model source.
 * @summary Retrieves data from the configured model using its pagination or find methods,
 * processes it, and updates the component's data state. This method handles both initial
 * data loading and subsequent refresh operations when using a model as the data source.
 *
 * @param {boolean} force - Whether to force a refresh even if data already exists
 * @param {number} start - The starting index for pagination
 * @param {number} limit - The maximum number of items to retrieve
 * @returns {Promise<KeyValue[]>} A promise that resolves to the fetched data
 *
 * @memberOf ListComponent
 */
async getFromModel(force: boolean = false): Promise<KeyValue[]> {
  let data = [ ... this.data || []];
  let request: KeyValue[] = [];

  // getting model repository
  if(!this._repository)
    this._repository = this.repository;
  const repo = this._repository as DecafRepository<Model>;
  if(!this.data?.length || force || (this.searchValue as string)?.length || !!(this.searchValue as IFilterQuery)) {
    try {
     if(!(this.searchValue as string)?.length && !(this.searchValue as IFilterQuery)) {
        (this.data as KeyValue[]) = [];
        // const rawQuery = this.parseQuery(self.model as Repository<Model>, start, limit);
        // request = this.parseResult(await (this.model as any)?.paginate(start, limit));
          if(!this.paginator) {
            this.paginator = await repo
              .select()
              .orderBy([this.pk as keyof Model, this.sortDirection])
              .paginate(this.limit);
          }
          request = await this.parseResult(this.paginator);
      } else {

        if(!this.indexes)
          this.indexes = (Object.values(this.mapper) || [this.pk]);

        const condition = this.parseConditions(this.searchValue as string | number | IFilterQuery);
        request = await this.parseResult(await repo.query(condition, (this.sortBy || this.pk) as keyof Model, this.sortDirection));
        data = [];
      }
      data = this.type === ListComponentsTypes.INFINITE ? [... (data).concat(request)] : [...request];
    } catch(error: unknown) {
      this.logger.error((error as Error)?.message || `Unable to find ${this.model} on registry. Return empty array from component`);
    }
  }

  if(data?.length) {
    if(this.searchValue) {
      this.items = [...data];
      if(this.items?.length <= this.limit)
        this.loadMoreData = false;
    } else {
      this.items = [...data];
    }
  }
  if(this.type === ListComponentsTypes.PAGINATED && this.paginator)
      this.getMoreData(this.paginator.total);
  return data || [] as KeyValue[];
}

/**
 * @description Converts search values or filter queries into database conditions.
 * @summary Transforms search input or complex filter queries into Condition objects
 * that can be used for database querying. Handles both simple string/number searches
 * across indexed fields and complex filter queries with multiple criteria.
 *
 * For simple searches (string/number):
 * - Creates conditions that search across all indexed fields
 * - Uses equality for numeric values and regex for string values
 * - Combines conditions with OR logic to search multiple fields
 *
 * For complex filter queries:
 * - Processes each filter item with its specific condition type
 * - Supports Equal, Not Equal, Contains, Not Contains, Greater Than, Less Than
 * - Updates sort configuration based on the filter query
 * - Combines multiple filter conditions with OR logic
 *
 * @param {string | number | IFilterQuery} value - The search value or filter query object
 * @returns {Condition<Model>} A Condition object for database querying
 * @memberOf ListComponent
 */
parseConditions(value: string | number | IFilterQuery): Condition<Model> {
  let _condition: Condition<Model>;
  if(typeof value === Primitives.STRING || typeof value === Primitives.NUMBER) {
    _condition = Condition.attribute<Model>(this.pk as keyof Model).eq(!isNaN(value as number) ? Number(value) : value);
    for (const index of this.indexes) {
        if(index === this.pk)
          continue;
        let orCondition;
        if(!isNaN(value as number)) {
          orCondition = Condition.attribute<Model>(index as keyof Model).eq(Number(value));
        } else {
          orCondition = Condition.attribute<Model>(index as keyof Model).regexp(value as string);
        }
        _condition = _condition.or(orCondition);
    }
  } else {
    const {query, sort} = value as IFilterQuery;
    _condition = Condition.attribute<Model>(this.pk as keyof Model).dif('null');

    if(query?.length)
      _condition = undefined as unknown as Condition<Model>;

    (query || []).forEach((item: IFilterQueryItem) => {
      const {value, condition, index} = item;
      let val = value as string | number;
      if(index === this.pk || !isNaN(val as number))
        val = Number(val);
      let orCondition;
      switch (condition) {
        case "Equal":
          orCondition = Condition.attribute<Model>(index as keyof Model).eq(val);
          break;
        case "Not Equal":
          orCondition = Condition.attribute<Model>(index as keyof Model).dif(val);
          break;
        case "Not Contains":
          orCondition = !Condition.attribute<Model>(index as keyof Model).regexp(new RegExp(`^(?!.*${val}).*$`));
          break;
        case "Contains":
          orCondition = Condition.attribute<Model>(index as keyof Model).regexp(val as string);
          break;
        case "Greater Than":
          orCondition = Condition.attribute<Model>(index as keyof Model).gte(val);
          break;
        case "Less Than":
          orCondition = Condition.attribute<Model>(index as keyof Model).lte(val);
          break;
      }
      _condition = (!_condition ?
        orCondition : _condition.and(orCondition as unknown as Condition<Model>)) as Condition<Model>;
    });

    this.sortBy = sort?.value as keyof Model || this.pk;
    this.sortDirection = sort?.direction || this.sortDirection;
  }
  return _condition as Condition<Model>;

}

/**
 * @description Processes query results into a standardized format.
 * @summary Handles different result formats from various data sources, extracting
 * pagination information when available and applying any configured data mapping.
 * This ensures consistent data structure regardless of the source.
 *
 * @protected
 * @param {KeyValue[] | Paginator} result - The raw query result
 * @returns {KeyValue[]} The processed array of items
 *
 * @memberOf ListComponent
 */
protected async parseResult(result: KeyValue[] | Paginator<Model>): Promise<KeyValue[]> {
  if(!Array.isArray(result) && ('page' in result && 'total' in result)) {
    const paginator = result as Paginator<Model>;
    result = await paginator.page(this.page);
    // TODO: Chage for result.total;
    this.getMoreData(paginator.total);
  } else {
    this.getMoreData((result as KeyValue[])?.length || 0);
  }
  return (Object.keys(this.mapper || {}).length) ?
    this.mapResults(result) : result;
}

/**
 * @description Updates pagination state based on data length.
 * @summary Calculates whether more data is available and how many pages exist
 * based on the total number of items and the configured limit per page.
 * This information is used to control pagination UI and infinite scrolling behavior.
 *
 * @param {number} length - The total number of items available
 * @returns {void}
 *
 * @memberOf ListComponent
 */
getMoreData(length: number): void {
  if(this.type === ListComponentsTypes.INFINITE) {
    if(this.paginator)
      length = length * this.limit;
    if(length <= this.limit) {
      this.loadMoreData = false;
    } else {
      this.pages = Math.floor(length / this.limit);
      if((this.pages * this.limit) < length)
        this.pages += 1;
      if(this.pages === 1)
        this.loadMoreData = false;
    }
  } else {
    this.pages = length;
    if(this.pages === 1)
      this.loadMoreData = false;
  }
}

/**
 * @description Maps a single item using the configured mapper.
 * @summary Transforms a data item according to the mapping configuration,
 * extracting nested properties and formatting values as needed. This allows
 * the component to display data in a format different from how it's stored.
 *
 * @protected
 * @param {KeyValue} item - The item to map
 * @param {KeyValue} mapper - The mapping configuration
 * @param {KeyValue} [props] - Additional properties to include
 * @returns {KeyValue} The mapped item
 *
 * @memberOf ListComponent
 */
protected itemMapper(item: KeyValue, mapper: KeyValue, props?: KeyValue): KeyValue {
  return Object.entries(mapper).reduce((accum: KeyValue, [key, value]) => {
    const arrayValue = value.split('.');
    if (!value) {
      accum[key] = value;
    } else {
      if (arrayValue.length === 1) {
        value = item?.[value] || value;
        if(isValidDate(value))
          value = `${formatDate(value)}`;
        accum[key] = value;
      } else {
        let val;

        for (const _value of arrayValue)
          val = !val
            ? item[_value]
            : (typeof val === 'string' ? JSON.parse(val) : val)[_value];


        if (isValidDate(new Date(val)))
          val = `${formatDate(val)}`;

        accum[key] = val === null || val === undefined ? value : val;
      }
    }
    return Object.assign({}, props || {}, accum);
  }, {});
}

/**
 * @description Maps all result items using the configured mapper.
 * @summary Applies the itemMapper to each item in the result set, adding
 * common properties like operations and route information. This transforms
 * the raw data into the format expected by the list item components.
 *
 * @param {KeyValue[]} data - The array of items to map
 * @returns {KeyValue[]} The array of mapped items
 *
 * @memberOf ListComponent
 */
  mapResults(data: KeyValue[]): KeyValue[] {
    if(!data || !data.length)
      return [];
    // passing uid as prop to mapper
    this.mapper = {... this.mapper, ... {uid: this.pk}};
    const props = Object.assign({
      operations: this.operations,
      route: this.route,
      ...  Object.keys(this.item).reduce((acc: KeyValue, key: string) => {
        acc[key] = this.item[key];
        return acc;
      }, {}),
      // ... (!this.item.render ? {} :  Object.keys(this.item).reduce((acc: KeyValue, key: string) => {
      //   acc[key] = this.item[key as keyof IListItemProp];
      //   return acc;
      // }, {}))
    });
    return data.reduce((accum: KeyValue[], curr) => {
        accum.push({... this.itemMapper(curr, this.mapper as KeyValue, props), ... {pk: this.pk}});
        return accum;
    }, []);
  }
}