import '@material/web/icon/icon.js' import '@material/web/button/elevated-button.js' import '@operato/data-grist/ox-grist.js' import '@operato/data-grist/ox-filters-form.js' import '@operato/data-grist/ox-record-creator.js' import { CommonButtonStyles, CommonHeaderStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles' import { PageView } from '@operato/shell' import { css, html } from 'lit' import { customElement, property, query } from 'lit/decorators.js' import { ScopedElementsMixin } from '@open-wc/scoped-elements' import { ColumnConfig, DataGrist, FetchOption } from '@operato/data-grist' import { client } from '@operato/graphql' import { i18next, localize } from '@operato/i18n' import { notify, openPopup } from '@operato/layout' import { OxPopup, OxPrompt } from '@operato/popup' import { isMobileDevice } from '@operato/utils' import { p13n } from '@operato/p13n' import gql from 'graphql-tag' import { KpiValueImporter } from './kpi-value-importer' @customElement('kpi-value-list-page') export class KpiValueListPage extends p13n(localize(i18next)(ScopedElementsMixin(PageView))) { static styles = [ ScrollbarStyles, CommonGristStyles, CommonHeaderStyles, css` :host { display: flex; width: 100%; --grid-record-emphasized-background-color: #8b0000; --grid-record-emphasized-color: #ff6b6b; } ox-grist { overflow-y: auto; flex: 1; } ox-filters-form { flex: 1; } ` ] static get scopedElements() { return { 'kpi-value-importer': KpiValueImporter } } @property({ type: Object }) gristConfig: any @property({ type: String }) mode: 'CARD' | 'GRID' | 'LIST' = isMobileDevice() ? 'CARD' : 'GRID' @query('ox-grist') private grist!: DataGrist get context() { return { title: i18next.t('title.kpi value list'), search: { handler: (search: string) => { this.grist.searchText = search }, value: this.grist.searchText }, filter: { handler: () => { this.grist.toggleHeadroom() } }, help: 'kpi/kpi-value', actions: [ { title: '일괄 편집', action: this._openEditor.bind(this), icon: 'edit', style: 'background: var(--md-sys-color-tertiary-container); color: var(--md-sys-color-on-tertiary-container);' }, { title: i18next.t('button.save'), action: this._updateKpiValue.bind(this), ...CommonButtonStyles.save }, { title: i18next.t('button.delete'), action: this._deleteKpiValue.bind(this), ...CommonButtonStyles.delete } ], exportable: { name: i18next.t('title.kpi value list'), data: this.exportHandler.bind(this) }, importable: { handler: this.importHandler.bind(this) } } } render() { const mode = this.mode || (isMobileDevice() ? 'CARD' : 'GRID') return html`
(this.mode = 'GRID')} ?active=${mode == 'GRID'}>grid_on (this.mode = 'LIST')} ?active=${mode == 'LIST'}>format_list_bulleted (this.mode = 'CARD')} ?active=${mode == 'CARD'}>apps
` } async pageInitialized(lifecycle: any) { this.gristConfig = { list: { fields: [ 'kpi', 'version', 'valueDate', 'value', 'score', 'kpiOrgScope', 'inputType', 'source', 'meta', 'createdAt', 'updatedAt', 'creator', 'updater' ], details: [ 'kpi', 'version', 'valueDate', 'value', 'score', 'kpiOrgScope', 'inputType', 'source', 'meta', 'createdAt', 'updatedAt', 'creator', 'updater' ] }, columns: [ { type: 'gutter', gutterName: 'sequence' }, { type: 'gutter', gutterName: 'row-selector', multiple: true }, // KPI Value 재계산 버튼 추가 { type: 'gutter', gutterName: 'button', icon: 'refresh', title: '재계산', handlers: { click: (columns, data, column, record, rowIndex) => { this._recalculateKpiValue(record) } } }, { type: 'string', name: 'kpiOrgScope', header: '조직', record: { editable: false }, width: 120, renderer: (value, column, record, rowIndex, field) => { return value?.entityName || value?.org || '' } }, { type: 'string', name: 'kpi', header: 'KPI', record: { editable: false, renderer: (v, c, r) => r.kpi?.name }, width: 150 }, { type: 'number', name: 'version', header: '버전', record: { editable: false }, width: 80 }, { type: 'string', name: 'valueDate', header: '실적일', record: { editable: true }, width: 120 }, { type: 'number', name: 'value', header: '실적값', record: { editable: true }, width: 120 }, { type: 'number', name: 'score', header: '성과점수', record: { editable: false }, width: 120 }, { type: 'string', name: 'inputType', header: '입력방식', record: { editable: false }, width: 100 }, { type: 'string', name: 'source', header: '수집출처', record: { editable: false }, width: 120 }, { type: 'object', name: 'meta', header: '메타', record: { editable: false }, width: 120 }, { type: 'datetime', name: 'createdAt', header: '생성일', record: { editable: false }, width: 180 }, { type: 'datetime', name: 'updatedAt', header: '수정일', record: { editable: false }, width: 180 }, { type: 'resource-object', name: 'creator', header: '생성자', record: { editable: false, renderer: (v, c, r) => r.creator?.name }, width: 120 }, { type: 'resource-object', name: 'updater', header: '수정자', record: { editable: false, renderer: (v, c, r) => r.updater?.name }, width: 120 } ], rows: { appendable: false, selectable: { multiple: true } }, sorters: [{ name: 'valueDate' }] } } async pageUpdated(changes: any, lifecycle: any) { if (this.active) { // do something here when this page just became as active } } async fetchHandler({ page = 1, limit = 100, sortings = [], filters = [] }: FetchOption) { const response = await client.query({ query: gql` query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) { responses: kpiValues(filters: $filters, pagination: $pagination, sortings: $sortings) { items { id kpi { id name } version valueDate value score inputType source meta kpiOrgScope { id org } updater { id name } updatedAt creator { id name } createdAt } total } } `, variables: { filters, pagination: { page, limit }, sortings } }) return { total: response.data.responses.total || 0, records: response.data.responses.items || [] } } async _openEditor() { const { KpiValueEditorPage } = await import('./kpi-value-editor-page.js') await openPopup(html` <${KpiValueEditorPage}> `) } async _deleteKpiValue() { if ( await OxPrompt.open({ title: i18next.t('text.are_you_sure'), text: i18next.t('text.sure_to_x', { x: i18next.t('text.delete') }), confirmButton: { text: i18next.t('button.confirm') }, cancelButton: { text: i18next.t('button.cancel') } }) ) { const ids = this.grist.selected.map(record => record.id) if (ids && ids.length > 0) { const response = await client.mutate({ mutation: gql` mutation ($ids: [String!]!) { deleteKpiValues(ids: $ids) } `, variables: { ids } }) if (!response.errors) { this.grist.fetch() notify({ message: i18next.t('text.info_x_successfully', { x: i18next.t('text.delete') }) }) } } } } async _updateKpiValue() { let patches = this.grist.dirtyRecords if (patches && patches.length) { patches = patches.map(patch => { let patchField: any = patch.id ? { id: patch.id } : {} const dirtyFields = patch.__dirtyfields__ for (let key in dirtyFields) { patchField[key] = dirtyFields[key].after } patchField.cuFlag = patch.__dirty__ return patchField }) const response = await client.mutate({ mutation: gql` mutation ($patches: [KpiValuePatch!]!) { updateMultipleKpiValue(patches: $patches) { id kpi { id name } version valueDate value score } } `, variables: { patches } }) if (!response.errors) { this.grist.fetch() } } } async _recalculateKpiValue(kpiValue) { try { const response = await client.mutate({ mutation: gql` mutation ($id: String!) { recalculateKpiValue(id: $id) { id value score valueDate kpiOrgScope { id entityName org } } } `, variables: { id: kpiValue.id } }) if (!response.errors) { notify({ message: 'KPI Value가 성공적으로 재계산되었습니다.' }) this.grist.fetch() } } catch (error) { notify({ message: 'KPI Value 재계산 중 오류가 발생했습니다.' }) } } async creationCallback(kpiValue) { try { const response = await client.query({ query: gql` mutation ($kpiValue: NewKpiValue!) { createKpiValue(kpiValue: $kpiValue) { id } } `, variables: { kpiValue }, context: { hasUpload: true } }) if (!response.errors) { this.grist.fetch() document.dispatchEvent( new CustomEvent('notify', { detail: { message: i18next.t('text.data_created_successfully') } }) ) } return true } catch (ex) { console.error(ex) document.dispatchEvent( new CustomEvent('notify', { detail: { type: 'error', message: i18next.t('text.error') } }) ) return false } } async exportHandler() { const exportTargets = this.grist.selected.length ? this.grist.selected : this.grist.dirtyData.records const targetFieldSet = new Set(['id', 'name', 'description', 'active']) return exportTargets.map(kpiValue => { let tempObj = {} for (const field of targetFieldSet) { tempObj[field] = kpiValue[field] } return tempObj }) } async importHandler(records) { const popup = openPopup( html` { history.back() this.grist.fetch() }} > `, { backdrop: true, size: 'large', title: i18next.t('title.import kpi-value') } ) popup.onclosed = () => { this.grist.fetch() } } }