import '@material/web/button/elevated-button.js' import '@material/web/button/filled-button.js' import '@material/web/button/text-button.js' import '@material/web/icon/icon.js' import '@operato/data-grist/ox-grist.js' import deepEquals from 'lodash-es/isEqual' import gql from 'graphql-tag' import { LitElement, css, html } from 'lit' import { customElement, property, state, query } from 'lit/decorators.js' import { DataGrist } from '@operato/data-grist/ox-grist.js' import { i18next, localize } from '@operato/i18n' import { client } from '@operato/graphql' import { notify } from '@operato/layout' import { FetchOption } from '@operato/data-grist' import { CommonHeaderStyles } from '@operato/styles' interface KpiGrade { name: string minValue: number maxValue: number score?: number color?: string description?: string } type KpiGrades = KpiGrade[] @customElement('kpi-grade-editor') export class KpiGradeEditor extends localize(i18next)(LitElement) { static styles = [ CommonHeaderStyles, css` :host { display: flex; flex-direction: column; background-color: var(--md-sys-color-surface); } ox-grist { flex: 1; } ` ] @property({ type: Object }) kpi: any = { grades: [] } @state() grades: KpiGrades = this.kpi?.grades || [] @state() gristConfig: any = null @query('ox-grist') grist!: DataGrist async firstUpdated() { if (this.kpi?.grades) { this.grades = [...this.kpi.grades] } this.gristConfig = { list: { fields: ['name', 'minValue', 'maxValue', 'score', 'color', 'description'] }, columns: [ { type: 'gutter', gutterName: 'row-selector', multiple: true, fixed: true }, { type: 'gutter', gutterName: 'sequence', fixed: true }, { type: 'gutter', gutterName: 'button', fixed: true, icon: 'add', handlers: { click: 'record-copy' } }, { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_upward', handlers: { click: 'move-up' } }, { type: 'gutter', gutterName: 'button', fixed: true, icon: 'arrow_downward', handlers: { click: 'move-down' } }, { type: 'string', name: 'name', header: '등급명', record: { editable: true }, width: 100 }, { type: 'number', name: 'minValue', header: '최소값', record: { editable: true }, width: 100 }, { type: 'number', name: 'maxValue', header: '최대값', record: { editable: true }, width: 100 }, { type: 'number', name: 'score', header: '점수', record: { editable: true }, width: 80 }, { type: 'color', name: 'color', header: '색상', record: { editable: true }, width: 100 }, { type: 'string', name: 'description', header: '설명', record: { editable: true }, width: 200 } ], rows: { selectable: { multiple: true } }, pagination: { infinite: true } } } render() { return html` ` } async fetchHandler({ page, limit, sorters = [] }: FetchOption) { return { total: this.grades.length, records: this.grades } } async _updateGrades() { this.grades = this.grist.dirtyData.records .map(patch => { const { name, minValue, maxValue, score, color, description } = patch return { name, minValue, maxValue, score, color, description } }) .sort((a, b) => a.minValue - b.minValue) as any if (!this._validateGrades()) { return } if (!deepEquals(this.kpi?.grades, this.grades)) { try { const response = await client.mutate({ mutation: gql` mutation ($id: String!, $patch: KpiPatch!) { updateKpi(id: $id, patch: $patch) { id name grades } } `, variables: { id: this.kpi.id, patch: { grades: this.grades } } }) this.grades = response.data.updateKpi.grades this.grist.fetch() } catch (error) { notify({ message: '등급 저장 중 오류가 발생했습니다.' }) } } } _validateGrades(): boolean { if (this.grades.length === 0) { notify({ message: '최소 1개 이상의 등급을 설정해야 합니다.' }) return false } const names = this.grades.map(g => g.name) const uniqueNames = new Set(names) if (names.length !== uniqueNames.size) { notify({ message: '등급명이 중복되었습니다.' }) return false } for (let i = 0; i < this.grades.length; i++) { const grade = this.grades[i] if (grade.minValue >= grade.maxValue) { notify({ message: `등급 "${grade.name}"의 최소값이 최대값보다 크거나 같습니다.` }) return false } if (i > 0) { const prevGrade = this.grades[i - 1] if (prevGrade.maxValue !== grade.minValue) { notify({ message: `등급 "${prevGrade.name}"과 "${grade.name}" 사이에 간격이 있습니다.` }) return false } } } return true } async _deleteGrades() { this.grist.deleteSelectedRecords(true) } }