diff --git a/document/小プロ様_検索取得プラグイン開発_基本設計書_20241218時点.xlsx b/document/小プロ様_検索取得プラグイン開発_基本設計書_20241218時点.xlsx index 051c79f..04e0b80 100644 Binary files a/document/小プロ様_検索取得プラグイン開発_基本設計書_20241218時点.xlsx and b/document/小プロ様_検索取得プラグイン開発_基本設計書_20241218時点.xlsx differ diff --git a/document/条件式整理.xlsx b/document/条件式整理.xlsx new file mode 100644 index 0000000..91df648 Binary files /dev/null and b/document/条件式整理.xlsx differ diff --git a/vue-project/my-kintone-plugin/src/js/KintoneIndexEventHandler.ts b/vue-project/my-kintone-plugin/src/js/KintoneIndexEventHandler.ts index 86235f0..53d8b6b 100644 --- a/vue-project/my-kintone-plugin/src/js/KintoneIndexEventHandler.ts +++ b/vue-project/my-kintone-plugin/src/js/KintoneIndexEventHandler.ts @@ -1,49 +1,317 @@ -import type { SavedData } from "@/types/model"; -import type { Button } from "kintone-ui-component"; - +import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, WhereCondition } from "@/types/model"; +import { isType, type OneOf } from "./kintone-rest-api-client"; +declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient; export class KintoneIndexEventHandler { - private config: SavedData; - - constructor(config: SavedData) { - this.config = config; - } - - public init(): void { - this.addButtonToView(); - } - + private config: SavedData; + private currentApp: string; + constructor(config: SavedData, currentApp: string) { + this.config = config; + this.currentApp = currentApp; + } + + public init(): void { + this.addButtonToView(); + } + + // ボタン追加 + private addButtonToView(): void { + const headerSpace = kintone.app.getHeaderMenuSpaceElement(); + if (!headerSpace) { + throw new Error('このページではヘッダー要素が利用できません。'); + }; + // ボタン追加 - private addButtonToView(): void { - const headerSpace = kintone.app.getHeaderMenuSpaceElement(); - if (!headerSpace){ - throw new Error('このページではヘッダー要素が利用できません。'); - }; - - // ボタン追加 - if (document.getElementById('btn-data-fetch')) return; - const kuc = Kucs['1.18.0']; - const button = new kuc.Button({ - text: this.config.buttonName, - type:"submit", - id:'btn-data-fetch', - }); - - // const button = document.createElement('button'); - // button.id = 'btn-data-fetch'; - // button.textContent = this.config.buttonName; - // button.style.margin = '0 8px'; - button.addEventListener('click', () => this.handleButtonClick()); - - headerSpace.appendChild(button); + if (document.getElementById('btn-data-fetch')) return; + const kuc = Kucs['1.18.0']; + const button = new kuc.Button({ + text: this.config.buttonName, + type: "submit", + id: 'btn-data-fetch', + }); + + // const button = document.createElement('button'); + // button.id = 'btn-data-fetch'; + // button.textContent = this.config.buttonName; + // button.style.margin = '0 8px'; + button.addEventListener('click', () => this.handleButtonClick()); + + headerSpace.appendChild(button); + } + + // ボタンクリック + private handleButtonClick = async (): Promise => { + try { + console.log('データ収集開始...'); + await this.execDataFectch(); + } catch (error) { + console.error('Error during data processing:', error); + throw error; } - - // ボタンクリック - private async handleButtonClick(): Promise { - try { - console.log('Button clicked! Starting data processing...'); - alert('データ取得開始'); - } catch (error) { - console.error('Error during data processing:', error); + } + + + /** + * 検索データ取得&作成処理 + */ + private execDataFectch = async (): Promise => { + const mainTable = this.config.joinTables[0]; + const mainRecords = await this.fetchDataFromApp(mainTable); + // フィールド結合 + let mainData = mainRecords.map((record) => { + const rightRecord: Record = {}; + mainTable.fieldsMapping.forEach((mapping => { + rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)] + })); + return rightRecord; + }); + const joinTables = this.config.joinTables.filter((table, index) => index > 0); + for (const table of joinTables) { + const subDatas = await this.fetchDataFromApp(table); + mainData = this.leftJoin(mainData, subDatas, table); + }; + //現在のデータをクリアする + await this.deleteCurrentRecords(); + //データを更新する + await this.saveDataToCurrentApp(mainData); + } + /** + * Appからデータを取得する + * @param joinTable 対象アプリかられレコードを取得する + * @returns + */ + private fetchDataFromApp = async (joinTable: JoinTable): Promise => { + // Filter 条件作成 + const filter = this.getWhereCondition(joinTable.whereConditions); + + const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField)); + if (joinTable.table) { + fetchFields.push(joinTable.table); + } + // KintoneRESTAPI + const client = new KintoneRestAPIClient(); + const records = await client.record.getAllRecords({ + app: joinTable.app, + fields: fetchFields, + condition: filter + }); + //SubTableが含まれる場合、フラットなデータに変換する + return this.convertToFlatDatas(records, joinTable.table); + + } + + private getWhereCondition(whereCondifions: WhereCondition[]): string { + const conds = whereCondifions + .filter((cond) => this.fieldCode(cond.field) !== ''); + const condition = conds.map((cond) => { + const condValue = this.getConditionValue(cond); + return `${this.fieldCode(cond.field)} ${cond.condition} ${condValue}`; + }).join(' and '); + return condition; + } + + private getConditionValue(condi: WhereCondition): string { + const field = condi.field as OneOf; + const data = condi.data; + if (!data) return ""; + if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) { + // For numbers, return as is + return data; + } else if (isType.DATE(field)) { + // If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function + if (/^\d{4}-\d{2}-\d{2}$/.test(data) || data.match(/^\w+\(.*\)$/)) { + return `"${data}"`; } + const date = new Date(data); + return `"${date.toISOString().split('T')[0]}"`; + } else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) { + // If field is DATETIME, format as "yyyy-MM-ddTHH:mm:ssZ" + if (data.match(/^\w+\(.*\)$/)) { + return `"${data}"`; + } + const dateTime = new Date(data); + return `"${dateTime.toISOString()}"`; + } else if ((condi.condition === "in" || condi.condition === "not in" )) { + if( data.includes(",")){ + // Handle "in" and "not in" with comma-separated strings + const items = data.split(",").map(item => `"${item.trim()}"`); + return `(${items.join(",")})`; + } else { + return `"${data}"`; + } + } else { + // Default case for other types (treat as text) + return `"${data}"`; } - } \ No newline at end of file + } + + + /** + * fieldからコードを取得する + * @param field + * @returns + */ + private fieldCode(field: any): string { + if (!field) { + return ""; + } + if (typeof field === 'string' && field) { + return field; + } else if (typeof field === 'object' && 'code' in field) { + return field.code; + } + return ""; + } + + /** + * ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。 + * @param records レコードの配列 + * @returns 変換後のフラットなレコード配列 + */ + private convertToFlatDatas(records: Record[], subTable: string): Record[] { + if (!subTable) { + return records; + } + const flattenedData: Record[] = []; + records.forEach((record) => { + // テーブルフィールドが存在するかを確認 + if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) { + // サブテーブル内の各レコードを処理 + record[subTable].value.forEach((nested: Record) => { + // 親レコードのコピーを作成 + const flatRecord = { ...record }; + + // サブテーブルフィールドを抽出してフラットな構造に追加 + Object.entries(nested.value).forEach(([key, field]) => { + flatRecord[key] = field.value; + }); + + // テーブルフィールドを削除 + delete flatRecord[subTable]; + + // 結果の配列に追加 + flattenedData.push(flatRecord); + }); + } else { + // サブテーブルが空の場合、親レコードをそのまま追加 + const flatRecord = { ...record }; + delete flatRecord[subTable]; + flattenedData.push(flatRecord); + } + }); + + return flattenedData; + } + + /** + * データLeftJoin処理 + * @param mainData + * @param subData + * @param onConditions + * @param fieldsMapping + * @returns + */ + private leftJoin( + mainData: Record[], + subData: Record[], + joinTable: JoinTable + ): Record[] { + + const joinedRecords = mainData.map((mainRecord) => { + const matchedRecord = subData.find((subRecord) => + joinTable.onConditions.every( + (cond) => mainRecord[this.fieldCode(cond.leftField)] === subRecord[this.fieldCode(cond.rightField)] + ) + ); + + // マッチ出来ない場合、LEFTの列のみ返す + if (!matchedRecord) return mainRecord; + + // フィールド結合 + const combinedRecord: Record = { ...mainRecord }; + joinTable.fieldsMapping.forEach((mapping) => { + combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)]; + }); + return combinedRecord; + }); + return joinedRecords; + } + + /** + * 取得先はテーブルの場合の特別対応 + */ + private leftJoinForTable( + mainData: Record[], + subData: Record[], + joinTable: JoinTable + ): any[] { + return mainData.map((mainRecord) => { + const matchedRecord = subData.find((subRecord) => { + const subRows = subRecord[joinTable.table].value as Record[]; + joinTable.onConditions.every( + (cond) => mainRecord[cond.leftField] === subRecord[cond.rightField] + ) + + } + + ); + + // マッチ出来ない場合、LEFTの列のみ返す + if (!matchedRecord) return mainRecord; + + // フィールド結合 + const combinedRecord = { ...mainRecord }; + joinTable.fieldsMapping.forEach((mapping) => { + combinedRecord[mapping.rightField] = matchedRecord[mapping.leftField]; + }); + return combinedRecord; + }); + } + + /** + * 現在アプリのすべてレコードを削除する + */ + private async deleteCurrentRecords(): Promise { + const client = new KintoneRestAPIClient(); + const currentRecords = await client.record.getAllRecords({ + app: this.currentApp, + fields: ["$id"], + }); + const deleteRecords = currentRecords.map(record => { + return { id: record.$id.value as string } + }); + await client.record.deleteAllRecords({ + app: this.currentApp, + records: deleteRecords + }); + client.record.addAllRecords + } + /** + * 結合後のデータを現在のアプリに挿入する + * @param records + */ + private async saveDataToCurrentApp(records: Record[]): Promise { + try { + const client = new KintoneRestAPIClient(); + const result = await client.record.addAllRecords({ + app: this.currentApp, + records: this.convertForUpdate(records) + }); + } catch (error) { + console.error('データ作成時エラーが発生しました:', error); + throw error; + } + } + + /** + * Recordを更新時の形式を変換する + * @param resords + * @returns + */ + private convertForUpdate(resords: Record[]): RecordForParameter[] { + return resords.map((record) => + Object.fromEntries( + Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }]) + ) + ); + } +} \ No newline at end of file diff --git a/vue-project/my-kintone-plugin/src/js/conditions.ts b/vue-project/my-kintone-plugin/src/js/conditions.ts index 0ffd963..319f1d5 100644 --- a/vue-project/my-kintone-plugin/src/js/conditions.ts +++ b/vue-project/my-kintone-plugin/src/js/conditions.ts @@ -5,7 +5,16 @@ import { getFieldObj } from './helper'; // conditionValue = '' | 'eq' | 'ne' // conditionItem = { value: 'eq', label: '=(等しい)', type: 'input', func: (a: string, b: string) => a === b } // = conditionMap[conditionValue] -export type ConditionValue = '' | 'eq' | 'ne' | 'test'; +export type ConditionValue = ""| "=" +| "!=" +| ">=" +| "<=" +| "<" +| ">" +| "like" +| "not like" +| "in" +| "not in"; type ConditionItem = { value: ConditionValue; @@ -16,9 +25,9 @@ type ConditionItem = { export const conditionList: ConditionItem[] = [ { value: '', label: '--------', type: '', func: (a: string, b: string) => true }, - { value: 'eq', label: '=(等しい)', type: 'input', func: (a: string, b: string) => a === b }, - { value: 'ne', label: '≠ (等しくない)', type: 'input', func: (a: string, b: string) => a !== b }, - { value: 'test', label: 'test combobox', type: 'select', func: (a: string, b: string) => a < b }, + { value: '=', label: '=(等しい)', type: 'input', func: (a: string, b: string) => a === b }, + { value: '!=', label: '≠ (等しくない)', type: 'input', func: (a: string, b: string) => a !== b }, + { value: '>=', label: 'test combobox', type: 'select', func: (a: string, b: string) => a < b }, ]; // search from conditionList diff --git a/vue-project/my-kintone-plugin/src/js/desktop.ts b/vue-project/my-kintone-plugin/src/js/desktop.ts index e785768..2cad521 100644 --- a/vue-project/my-kintone-plugin/src/js/desktop.ts +++ b/vue-project/my-kintone-plugin/src/js/desktop.ts @@ -1,11 +1,13 @@ -import type { SavedData } from "@/types/model"; +import type { Field, FieldLayout, SavedData } from "@/types/model"; import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler"; (function (PLUGIN_ID) { kintone.events.on('app.record.index.show', (event) => { const setting = kintone.plugin.app.getConfig(PLUGIN_ID); - const config:SavedData = getConfig(setting); - const handler = new KintoneIndexEventHandler(config); + const config:SavedData = getConfig(setting); + const currentApp = kintone.app.getId()?.toString(); + if(!currentApp) return; + const handler = new KintoneIndexEventHandler(config,currentApp); handler.init(); return event; }); @@ -14,106 +16,252 @@ import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler"; * @param setting * @returns */ - function getConfig(setting:any):SavedData{ - const config:SavedData={ + function getConfig(setting:any):SavedData{ + const config:SavedData={ buttonName:setting.buttonName, joinTables:JSON.parse(setting.joinTables) } return config; } - function getTestSetting(config:SavedData):SavedData{ - const mainTable = { - id:12345, - app:"140", - onConditions:[ - { - leftField:"文字列__1行_", - rightField:"文字列__1行_" - } - ], - fieldsMapping:[{ - leftField:"", - rightField:"" - }, - { - leftField:"", - rightField:"" - } - ], - table:"", - whereConditions:[ - { - field:"", - condition:"", - data:"" - } - ] - }; - const leftJoinTable1 = { - id:12345, - app:"140", - onConditions:[ - { - leftField:"文字列__1行_", - rightField:"文字列__1行_" - } - ], - fieldsMapping:[{ - leftField:"", - rightField:"" - }, - { - leftField:"", - rightField:"" - } - ], - table:"", - whereConditions:[ - { - field:"", - condition:"", - data:"" - } - ] - }; - const leftJoinTable2 = { - id:12345, - app:"140", - onConditions:[ - { - leftField:"文字列__1行_", - rightField:"文字列__1行_" - } - ], - fieldsMapping:[{ - leftField:"", - rightField:"" - }, - { - leftField:"", - rightField:"" - } - ], - table:"", - whereConditions:[ - { - field:"", - condition:"", - data:"" - } - ] - }; - const retConfig :SavedData ={ - buttonName : config.buttonName, - joinTables:[ - mainTable, - leftJoinTable1, - leftJoinTable2 - ] - }; - return retConfig; - } + // function getTestSetting(config:SavedData):SavedData{ + // const retConfig :SavedData ={ + // "buttonName": "集約する", + // "joinTables": [ + // { + // "id": "m6c69ojr-bx9l8n9fm", + // "app": "140", + // "table": "", + // "onConditions": [ + // { + // "leftField": "", + // "rightField": "", + // "id": "m6c69ojr-7pho6voer" + // } + // ], + // "fieldsMapping": [ + // { + // "leftField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行_", + // "label": "社員番号", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "rightField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行_", + // "label": "社員番号", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "id": "m6c69ojr-i75a82qvb" + // }, + // { + // "leftField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__0", + // "label": "氏名", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "rightField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__0", + // "label": "氏名", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "id": "m6c6v8sp-7yw58m6e9" + // } + // ], + // "whereConditions": [ + // { + // "field": "", + // "condition": "", + // "data": "", + // "id": "m6c69ojr-az5k7bvp6" + // } + // ] + // }, + // { + // "id": "m6c6u15u-yq4kzp31c", + // "app": "141", + // "table": "使用者テーブル", + // "onConditions": [ + // { + // "leftField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "社員番号", + // "label": "社員番号", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "rightField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行_", + // "label": "社員番号", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "id": "m6c6ue5e-k7yibisue" + // } + // ], + // "fieldsMapping": [ + // { + // "leftField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "氏名", + // "label": "氏名", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "rightField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__0", + // "label": "氏名", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "id": "m6c6ue5e-bjdpae18q" + // } + // ], + // "whereConditions": [ + // { + // "field": "", + // "condition": "", + // "data": "", + // "id": "m6c6ue5e-1rf2v4l9m" + // } + // ] + // }, + // { + // "id": "m6c6vrso-vr7a7v5h1", + // "app": "143", + // "table": "テーブル", + // "onConditions": [ + // { + // "leftField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__1", + // "label": "資産管理番号", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "rightField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__1", + // "label": "資産管理番号", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "id": "m6c6vx94-02xbd48xt" + // } + // ], + // "fieldsMapping": [ + // { + // "leftField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__2", + // "label": "資産名", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "rightField": { + // "type": "SINGLE_LINE_TEXT", + // "code": "文字列__1行__2", + // "label": "資産名", + // "noLabel": false, + // "required": false, + // "minLength": "", + // "maxLength": "", + // "expression": "", + // "hideExpression": false, + // "unique": false, + // "defaultValue": "" + // }, + // "id": "m6c6vx94-t7d8juf6x" + // } + // ], + // "whereConditions": [ + // { + // "field": "", + // "condition": "", + // "data": "", + // "id": "m6c6vx94-gbxhjgf8m" + // } + // ] + // } + // ] + // }; + // return retConfig; + // } })(kintone.$PLUGIN_ID); diff --git a/vue-project/my-kintone-plugin/src/types/model.d.ts b/vue-project/my-kintone-plugin/src/types/model.d.ts index 62791ac..39e99cf 100644 --- a/vue-project/my-kintone-plugin/src/types/model.d.ts +++ b/vue-project/my-kintone-plugin/src/types/model.d.ts @@ -26,9 +26,9 @@ export interface JoinTable { } // 存储的数据格式 -export interface SavedData { +export interface SavedData { buttonName: string; - joinTables: JoinTable[]; + joinTables: JoinTable[]; } export interface FieldsInfo { @@ -46,3 +46,17 @@ export interface CachedSelectedAppData { loading: boolean, table: JoinTable, } +export type Record = { + [fieldCode: string]: Field; +}; + +export type RecordForParameter = { + [fieldCode: string]: { + value: unknown; + }; +}; + +export type Field={ + type:string; + value:any; +} \ No newline at end of file