import { Component, EventEmitter, HostBinding, Input, Output, TemplateRef } from '@angular/core'; import { FormGroup, FormBuilder, FormControl, ValidatorFn } from '@angular/forms'; import { SelectionModel } from '@angular/cdk/collections'; import { DialogService } from '@app/core/services/dialog.service'; import { HttpService } from '@app/core/services/http.service'; import { I18nService } from '@app/core/services/i18n.service'; import { ToastService } from '@app/core/services/toast.service'; import { TableColumnDefinition } from '@app/shared/components/table-column-pattern/table-column-pattern.component'; import { CrudEditComponent } from './edit/edit.component'; @Component({ selector: 'crud-template', templateUrl: './crud.component.html', styleUrls: ['./crud.component.scss'] }) export class CrudComponent { constructor( private _formBuilder: FormBuilder, private _dialogService: DialogService, private _httpService: HttpService, private _i18nService: I18nService, private _toastService: ToastService ) { } @HostBinding('class') private _: string = 'crud-template'; private _columns: TableColumnDefinition; private _fixed: boolean = false; private _pageable: boolean = true; private _selectable: 'single' | 'multiple' | false = 'multiple'; private _editable: boolean = true; private _addable: boolean = true; private _deletable: boolean = true; private _rowClass: (row: any) => string; private _toolbarTemplate: TemplateRef; private _toolbarButtonsTemplate: TemplateRef; private _toolbarIconsTemplate: TemplateRef; private _toolbarSelectionTemplate: TemplateRef; private _rowOperationTemplate: TemplateRef; private _rowExpandingTemplate: TemplateRef; private _editor: { id: string, type: string, label: string, order?: number, template?: TemplateRef, range?: any[], convertor?: (val: any) => any, validator: ValidatorFn[], required?: boolean, default?: any }[]; private _pk: string = 'id'; private _api: string; private _query: string = ''; private _filter: { enabled?: boolean, query?: string, form: FormGroup, controls?: { id: string, type: string, label: string, order?: number, template?: TemplateRef, range?: any[], convertor?: (val: any) => string, default?: any }[] } = { form: this._formBuilder.group({}) }; private _pageIndex: number = 0; private _pageSize: number = 30; private _sort: string = ''; private _selection = new SelectionModel(true, []); private _rows: any[] = []; private _total: number = 0; private _loading: boolean = true; private _convertor: (e: any) => Promise<{ items: any[], total?: number }>; private _error: (e: any) => boolean; @Output('refresh') public refreshEvent: EventEmitter = new EventEmitter(); @Input() public set defination(value: { id: string, header?: string, template?: TemplateRef, style?: any, sortable?: boolean, sticky?: 'start' | 'end', display?: boolean, filter?: { field: 'text' | 'number' | 'select' | 'multi-select' | 'date' | 'date-range', order?: number, template?: TemplateRef, range?: any[], convertor?: (val: any) => string, default?: any }, editor?: { field: 'text' | 'number' | 'select' | 'multi-select' | 'date', order?: number, template?: TemplateRef, range?: any[], convertor?: (val: any) => any, validator?: ValidatorFn[], required?: boolean, default?: any } }[]) { const columns = value ? value.filter((item: any) => item.display !== null) .map((item: any) => { return { id: item.id, header: item.header, template: item.template, style: item.style, sortable: item.sortable, sticky: item.sticky, display: item.display }; }) : []; if (this._selectable) { columns.unshift({ id: '@selection', header: null, template: null, style: null, sortable: false, sticky: 'start', display: true }); } if (this._editable || this.rowOperationTemplate) { columns.push({ id: '@operation', header: null, template: null, style: null, sortable: false, sticky: 'end', display: true }); } this._columns = new TableColumnDefinition(columns); this._editor = []; this._filter.enabled = false; this._filter.query = this._query; this._filter.form.controls = {}; this._filter.controls = []; value && value.forEach(item => { if (item.editor) { this._editor.push({ id: item.id, type: item.editor.field, label: item.header, order: item.editor.order, template: item.editor.template, range: item.editor.range, convertor: item.editor.convertor, validator: item.editor.validator, required: item.editor.required, default: item.editor.default ?? null }); } if (item.filter) { this._filter.enabled = true; if (item.filter.field == 'date-range') { const controlFrom = new FormControl(); const controlTo = new FormControl(); if (item.filter.default) { controlFrom.setValue(item.filter.default[0]); controlFrom.setValue(item.filter.default[1]); } this._filter.form.addControl(`${item.id}From`, controlFrom); this._filter.form.addControl(`${item.id}To`, controlTo); } else { const control = new FormControl(); if (item.filter.default) { control.setValue(item.filter.default); } this._filter.form.addControl(item.id, control); } this._filter.controls.push({ id: item.id, type: item.filter.field, label: item.header, order: item.filter.order, template: item.filter.template, range: item.filter.range, convertor: item.filter.convertor, default: item.filter.default ?? null }); } }); } @Input() public set fixed(value: boolean) { this._fixed = value; } @Input() public set pageable(value: boolean) { this._pageable = value; } @Input() public set selectable(value: 'single' | 'multiple' | false) { this._selectable = value; if (value) { if (this._columns?.length && !this._columns.find(item => item.id == '@selection')) { this._columns.unshift({ id: '@selection', sticky: 'start' }); } this._selection['_multiple'] = value != 'single'; } else { if (this._columns?.length && this._columns[0].id == '@selection') { this._columns.shift(); } } } @Input() public set editable(value: boolean) { this._editable = value; if (value) { if (this._columns?.length && !this._columns.find(item => item.id == '@operation')) { this._columns.push({ id: '@operation', sticky: 'end' }); } } else { if (this._columns?.length && this._columns[this._columns.length - 1].id == '@operation') { this._columns.pop(); } } } @Input() public set addable(value: boolean) { this._addable = value; } @Input() public set deletable(value: boolean) { this._deletable = value; } @Input('row-class') public set rowClass(value: (row: any) => string) { this._rowClass = value; } @Input('toolbar-template') public set toolbarTemplate(value: TemplateRef) { this._toolbarTemplate = value; } @Input('toolbar-buttons-template') public set toolbarButtonsTemplate(value: TemplateRef) { this._toolbarButtonsTemplate = value; } @Input('toolbar-icons-template') public set toolbarIconsTemplate(value: TemplateRef) { this._toolbarIconsTemplate = value; } @Input('toolbar-selection-template') public set toolbarSelectionTemplate(value: TemplateRef) { this._toolbarSelectionTemplate = value; } @Input('row-operation-template') public set rowOperationTemplate(value: TemplateRef) { this._rowOperationTemplate = value; } @Input('row-expanding-template') public set rowExpandingTemplate(value: TemplateRef) { this._rowExpandingTemplate = value; } @Input() public set pk(value: string) { this._pk = value; } @Input() public set api(value: string) { if (this._api = value) { this.search('init'); } } @Input() public set query(value: string) { this._filter.query = this._query = value ? `${value}&` : ''; } @Input() public set convertor(value: (e: any) => Promise<{ items: any[], total?: number }>) { this._convertor = value; } @Input() public set error(value: (e: any) => boolean) { this._error = value; } public set loading(value: boolean) { this._loading = value; } public get columns() { return this._columns; } public get fixed() { return this._fixed; } public get pageable() { return this._pageable; } public get selectable() { return this._selectable; } public get editable() { return this._editable; } public get addable() { return this._addable; } public get deletable() { return this._deletable; } public get rowClass() { return this._rowClass; } public get toolbarTemplate() { return this._toolbarTemplate; } public get toolbarButtonsTemplate() { return this._toolbarButtonsTemplate; } public get toolbarIconsTemplate() { return this._toolbarIconsTemplate; } public get toolbarSelectionTemplate() { return this._toolbarSelectionTemplate; } public get rowOperationTemplate() { return this._rowOperationTemplate; } public get rowExpandingTemplate() { return this._rowExpandingTemplate; } public get filter() { return this._filter; } public get pageIndex() { return this._pageIndex; } public get pageSize() { return this._pageSize; } public get selection() { return this._selection; } public get rows() { return this._rows; } public get total() { return this._total; } public get loading() { return this._loading; } public async refresh(e?: any): Promise { let action: string = null; if (e) { if (e.pageIndex !== undefined) { action = 'page'; this._pageIndex = e.pageIndex; this._pageSize = e.pageSize; } else if (e.active !== undefined) { action = 'sort'; this._sort = e.direction ? e.direction == 'desc' ? `~${e.active}` : e.active : ''; } else { action = e.action; } } //king kun bug let res: any = null; if (this._pageable) { let lastPage: number = -1; while (this._pageIndex > lastPage) { lastPage > -1 && (this._pageIndex = lastPage); res = await this._httpService.get(`${this._api}?${this._filter.query}sort=${this._sort}&offset=${this._pageSize * this._pageIndex}&limit=${this._pageSize}`); lastPage = res ? Math.max(0, Math.ceil(res.total / this._pageSize) - 1) : Number.MAX_VALUE; } } else { res = await this._httpService.get(`${this._api}?${this._filter.query}sort=${this._sort}`); } if (res) { if (this._convertor) { res = await this._convertor(res); } this._loading = false; this._total = res.total; this._rows = res.items; this._selection.clear(); } this.refreshEvent.emit(action); } public search(action?: string) { let query: string = this._query; this._filter.controls.forEach(item => { if (item.convertor) { query += item.convertor(this._filter.form.controls[item.id].value); } else { switch (item.type) { case 'multi-select': if (this._filter.form.controls[item.id].value instanceof Array) { query += this._filter.form.controls[item.id].value.map((id: any) => `${item.id}=${id}&`).join('') || `${item.id}=0&`; } break; case 'date': if (this._filter.form.controls[item.id].value?._isAMomentObject) { query += `${item.id}=${this._filter.form.controls[item.id].value.format('yyyy-MM-DD')}&`; } break; case 'date-range': if (this._filter.form.controls[`${item.id}From`].value?._isAMomentObject) { query += `${item.id}From=${this._filter.form.controls[`${item.id}From`].value.format('yyyy-MM-DD')}&`; } if (this._filter.form.controls[`${item.id}To`].value?._isAMomentObject) { query += `${item.id}To=${this._filter.form.controls[`${item.id}To`].value.format('yyyy-MM-DD')}&`; } break; default: if (this._filter.form.controls[item.id].value?.toString().length) { query += `${item.id}=${this._filter.form.controls[item.id].value}&`; } break; } } }); this._filter.query = query; this._pageIndex = 0; this.refresh({ action: action ?? 'search' }); } public reset() { this._filter.query = this._query; this._filter.controls.forEach(item => { switch (item.type) { case 'date-range': this._filter.form.controls[`${item.id}From`].reset(item.default && item.default[0]); this._filter.form.controls[`${item.id}To`].reset(item.default && item.default[1]); break; default: this._filter.form.controls[item.id].reset(item.default); break; } }); this.search('reset'); } public async edit(item?: any): Promise { if (await this._dialogService.open(CrudEditComponent, { autoFocus: !item, data: { data: item, fields: this._editor, pk: this._pk, api: this._api, error: this._error } })) { this.refresh({ action: item ? 'edit' : 'add' }); } } public async delete(): Promise { if (await this._dialogService.confirm(this._i18nService.translate('shared.notification.confirm'))) { const res: boolean = await this._httpService.post(`${this._api}/batch`, { method: 'delete', data: this._selection.selected.map(i => i[this._pk]) }).catch(e => { switch(e.status) { case 409: this._toastService.show(this._i18nService.translate(`routes.basic.role.error.conflict.${e.error}`)); break; case 410: this._toastService.show(this._i18nService.translate(`routes.basic.role.error.gone.${e.error}`)); break; case 422: this._toastService.show(this._i18nService.translate('shared.notification.fail')); break; default: break; } }) !== undefined; if (res) { this._toastService.show(this._i18nService.translate('shared.notification.success')); this.refresh({ action: 'delete' }); } } } }