ラベル Javascript の投稿を表示しています。 すべての投稿を表示
ラベル Javascript の投稿を表示しています。 すべての投稿を表示

2020年5月12日火曜日

kintoneのデータをSQL文で加工する

最近kintone漬け。
といっても、新しい機能を追加していくような楽しい作業ではなく、運用中のアプリの不具合対応(デバッグ)と細かい改良ばかりで、少し疲れてきた。

そんな中で、kintoneのデータの加工・集計をラクにやる方法はないものか? と調べていたら、
kintone でSQLを使う
という記事を発見。
alasqlというJavascript用データベースを使うと、kintoneのデータをSQLで集計できるじゃん。これすごい。
SUM(Group By)で合計値を求めたり、複数のアプリのデータをJoinで組み合わせたりできる。
(逆に言うと、こんな簡単なことがkintone標準機能ではできなかったりするのだが…)

"kintone alasql"でググるとそこそこ記事が出てくるので、もしかしてkintoneガチ勢には当たり前の話?

早速、工数管理アプリに入っている工数明細をプロジェクト単位・個人単位でSUMする画面を作ってみたので、備忘のために記事に残しておく。

元データはこんな感じ。

Figure.1 集計元のkintoneアプリ
設計方針

データは、event.recordsではなくAPIで取得する(event.recordsは最大でも100件しかデータが取れないので、集計目的では使えない)。 APIは生で叩くのではなく、kintone-rest-api-clientを利用。(getAllRecordsWithCursorが最強。みんな使うといいと思う。)

データの表示にはjQuery DataTablesを利用する。DataTablesを使って多機能な一覧画面を作ろう! – cybozu developer network とかが参考になる。

動作環境

アプリ設定画面の「JavaScript / CSSでカスタマイズ」にて以下URLを指定。

https://js.cybozu.com/jquery/3.4.0/jquery.min.js
https://js.cybozu.com/datatables/v1.10.19/js/jquery.dataTables.min.js
https://unpkg.com/@kintone/rest-api-client@latest/umd/KintoneRestAPIClient.min.js
https://js.cybozu.com/datatables/v1.10.19/css/jquery.dataTables.min.css

また、http://alasql.org/ から alasql.min.js をDLし、PC用のJavaScriptファイルにアップロードしておく(alasqlのzipをダウンロードして解凍し、distディレクトリから取り出せば良い)。

もう1点、DataTablesの見栄えをkintoneに近づけるため、DataTablesをkintoneライクにする魔法のコード - Qiitaからkintone-datatables.cssをDLし、PC用のCSSファイルにアップロードしておくとよい(作者の方に感謝)。

サンプルコード HTML

アプリに一覧「プロジェクト別集計」を作成しておく。
レコード一覧の表示形式を「カスタマイズ」にし、以下のHTMLを記述。

サンプルコード js
/*
 * project_summary.js
 * 工数管理アプリのデータをプロジェクト別・個人別に集計
 * 
 * required: jquery.dataTables.min.js
 * https://js.cybozu.com/datatables/v1.10.19/js/jquery.dataTables.min.js
 * 
 * required: kintone-rest-api-client
 * https://unpkg.com/@kintone/rest-api-client@latest/umd/KintoneRestAPIClient.min.js
 * 
 * requeired: alasql.min.js
 * http://alasql.org/
 */
(function() {
  "use strict";
  kintone.events.on(['app.record.index.show'], function(event){
    if(event.viewName !== 'プロジェクト別集計') return;

    // Initialize kintone-rest-api-client
    const client = new KintoneRestAPIClient();

    var myTable;

    var appId = kintone.app.getId();
    var query = kintone.app.getQuery();
    // APIでデータ取得
    client.record.getAllRecordsWithCursor({
      app: appId,
      query: query,
    }).then(function(response){

      // kintoneのrecords(json)を、alasqlで扱える配列に変換
      function convertToRows(records) {
        var rows = records.map(function(record){
          var keys = Object.keys(record);
          var row = {};
          keys.map(function(key){
            switch (key) {
              case '担当者氏名':
                // '担当者氏名'フィールドからname(=氏名)を取り出す
                row['担当者氏名'] = record[key].value[0]['name'];
                break;
              case '作業担当部署':
                // '作業担当部署'フィールドからname(=部署名)を取り出す
                row['作業担当部署'] = record[key].value[0]['name'];
                break;
              default:
                // 他のフィールドはvalueを取り出す。数値の場合はNumberに変換
                row[key] = record[key].type === 'NUMBER' ? Number(record[key].value) : record[key].value;
            }
          });
          return row;
        });
        return rows;
      }
      var recordSet = convertToRows(response);

      // sum([工数]) by alasql
      var sqlResult = alasql(
          "SELECT [プロジェクトコード], [作業担当部署], [担当者氏名], SUM([工数計_人月]) AS [工数計_人月] FROM ? GROUP BY [プロジェクトコード], [作業担当部署], [担当者氏名]", 
        [recordSet]
      );

      // 表に表示するカラム(alasqlの出力結果の項目名)
      var columns = [
        'プロジェクトコード',
        '作業担当部署',
        '担当者氏名',
        '工数計_人月',
      ];

      // 表のタイトルを好きにいじれるよう、別変数で定義。このサンプルではあまり意味がない。
      var titles = {
        プロジェクトコード: 'プロジェクトコード',
        作業担当部署: '作業担当部署',
        担当者氏名: '担当者氏名',
        工数計_人月: '工数計',
      };

      // DataTables初期化
      myTable = $('#myTable').DataTable({
        // DataTableの設定値はお好みで。
        destroy: true,
        pageLength: 100,
        lengthChange: false,
        searching: false,
        info: false,
        data: sqlResult.map(function(record){
          return columns.reduce(function(data, column){
            data[column] = record[column];
            return data;
          }, {
            レコード番号: record['レコード番号']
          });
        }),
        columns: columns.map(function(column){
          return {
            title: this[column],
            data: column
          }
        }, titles)
      });
    });
  });
})();
動作イメージ

サンプルの動作結果はこんな感じ。

Figure.2 カスタマイズ結果


画像にモザイクがかかりまくっていて何がなんだか分からないが、結果はとても良好。パフォーマンスも今のところ申し分ない。

今回はSUMしか使っていないが、そのうちJoinも使ってみたい。

2020年2月3日月曜日

kintoneのデータをスプレッドシートで編集する(Tabulator編)

最近、kintoneでエンジニアの工数管理みたいなことをしている。
工数データをスプレッドシート型のUIでサクサク入力したくて、HandsontablesTabulatorの両方を試してみた。
Handsontablesを使う方法は、cybozu developer network上にHandsontableを使ってkintoneをExcelライクに入力しよう その1という記事が載っていたりするのだけど、Handsontable自体が最近有償化されてしまったのと、いろいろ実装がうまくいかないところがあって、断念。
一方のTabulatorは、そこそこいい感じに動いているので、備忘のためまとめておく。
ちなみに、今回はレコードの追加や複製、削除には対応していない。あくまで既存のレコードの編集のみ実装している(追加や複製はそのうちやるかも)。
要件(やりたいこと)
kintoneの工数管理アプリに、以下のような案件別・担当者別の日々の工数情報が入っているとする。

プロジェクト番号客先名案件名担当者名年月1日工数2日工数31日工数
12345678○×商事株式会社メールシステム構築山田太郎2020/010.520
12345678○×商事株式会社メールシステム構築鈴木花子2020/01030.5
23456789▲△自動車株式会社仮想化基盤構築田中勘太郎2020/012.51.57

これをスプレッドシートで入力・編集したい。下に合計行もあるとうれしい。
準備①:kintoneの"一覧"を準備
kintoneの「アプリの設定」画面にて、一覧"Tabulator"を作成(※好きな名前でよい)。
レコード一覧の表示形式を「カスタマイズ」にして、以下のようなHTMLを設定しておく。
準備②:Javascriptの準備
kintoneの「アプリの設定」画面の「JavaScript / CSSでカスタマイズ」にて、以下をリンクしておく。
PC用のJavaScriptファイル
https://js.cybozu.com/jquery/3.4.0/jquery.min.js
https://unpkg.com/tabulator-tables@4.5.3/dist/js/tabulator.min.js
PC用のCSSファイル
https://unpkg.com/tabulator-tables@4.5.3/dist/css/tabulator.min.css
これで準備OK。
サンプルコード
以下のコードを「PC用のJavaScriptファイル」にアップロードする。
/**
 * kintone_tabulator.js
 * kintone上でスプレッドシートによるデータ表示・更新を実現
 * Tabulatorにて実装
 * 
 * @author dsp74118
 * @version 1.0 2020.02.03
 *
 */
(function() {

  "use strict";

  // レコード更新用データを生成する関数
  var setParams = function(record) {
    var result = {};
    for (var prop in record) {
      // note: アップデートしないフィールドを除外する処理をここに入れるとよい
      result[prop] = record[prop];
    }
  }

  // 一覧ビュー表示用のイベントハンドラ
  kintone.events.on(['app.record.index.show'], function(event) {
    // 「Tabulator」一覧のみ処理
    if (event.viewName !== "Tabulator") return;
    var records = event.records;

    // 列の定義
    var columns = [
      {title: "プロジェクト番号", field: "プロジェクト番号.value"},
      {title: "担当者氏名", field: "担当者氏名.value"},
      {title: "年月", field: "年月.value"},
      {title: "客先名", field: "客先名.value"},
      {title: "案件名", field: "案件名.value"},
    ];
    for (var i = 1; i <= 31; i++) {
      columns.push({
        title: i + "日工数",
        field: "_" + i + "日工数.value",
        align: "right",
        editor: true,
        bottomCalc: "sum",
        validator: [{type: validateManhours, parameters: {}}]
      });
    }

    // Tabulator初期化
    var tab = new Tabulator('#spreadsheet', {
//      layoutを指定指定すると表示が表示が崩れるためコメントアウト
//      layout: "fitData",
      data: records,
      // 左右矢印キーでセル移動
      keybindings:{
          "navLeft" : "37",
          "navRight" : "39",
      },
      columns: columns,

      // 更新処理
      cellEdited:function(cell) {
        console.log(cell);
        var id = cell._cell.row.data['レコード番号']['value'];

        // データを更新用に加工し配列に格納
        var updateRecords = [];
        updateRecords.push({
          id: id,
          record: setParams(cell._cell.row.data),
        });

        // 更新用Requestを作成
        var requests = [];
        requests.push({
          method: "PUT",
          api: "/k/v1/records.json",
          payload: {
            app: kintone.app.getId(),
            records: updateRecords
          }
        });

        // bulkrequestで一括で更新
        // 失敗した場合はロールバックされる
        var self = this;
        kintone.api('/k/v1/bulkRequest', 'POST', {requests: requests},
          function(resp) {
            console.dir(requests);
            console.dir(resp);
            console.log('Data updated successfully.');
            self.redraw();
          },
          function(resp) {
            console.dir(resp);
            console.log('Data update fail.');
          }
        );
      },
    });
  });
})();
event.recordsをそのままTabulatorに食わせられるので、コードがすごくシンプル! これはうれしい。
kintoneのデータ更新にはbulkrequestを使ったけど、複数セル同時編集はできないはずなので、bulkrequestではなく普通のrecords.jsonで良いかも。

注意点がいくつか。
  • layout: "fitData" で列幅を自動調整させると、横スクロール時に表示がめちゃくちゃ乱れるので、やめた。
  • 列の設定で frozen: true にて左側に特定列を固定すると、これまた横スクロール時に表示がめちゃくちゃ乱れるので、やめた。
このあたり、Tabulator側の問題な気がする(kintoneのCSSとの相性かも?)。まだ追及はできていない。
結果
こんな感じ。Excelと同等の使用感を再現することは無理だけど、矢印キーで縦横無尽に移動しながら、工数をExcel風に入力できるようにはなっている。
※このブログ記事用に別途アプリを作ることが難しい状況なので、実際に職場で作っているアプリのスクショにモザイクかけまくったものを掲載することをご容赦ください。
 ちなみにアプリは開発中のもので、データはテストデータです。悪しからず。

2019年10月29日火曜日

kintone APIで取り出したJSONデータの重複排除をする

最近、私の所属組織ではkintoneで案件(プロジェクト)管理をしている。
JavaScriptでごりごりカスタマイズしていたりするのだけど、そんな中で備忘録を残しておいたほうがよさそうなネタがあったので投稿。

要件(やりたいこと)
kintoneのプロジェクト一覧アプリに、以下のような情報が入っているとする。

プロジェクト番号客先名案件名担当者名
12345678○×商事株式会社メールシステム構築山田太郎
12345678○×商事株式会社メールシステム構築鈴木花子
23456789▲△自動車株式会社仮想化基盤構築田中勘太郎
※kintoneのアプリは正規化ができないので、1つの案件を複数名で担当する場合、上記のようにプロジェクト番号、客先名、案件名は重複して登録される。

このうち、プロジェクト番号、客先名、案件名の3つをkintone APIで取り出し、重複排除したい(=一意なデータとして取り出したい)というのが今回の要件。

まず、上記のプロジェクト一覧アプリをkintone APIで取り出すと、recordsに以下のようなJSONデータ(オブジェクト値の配列)が入ってくる。
[
 {
  プロジェクト番号: {type: "SINGLE_LINE_TEXT", value: "12345678"},
  客先名: {type: "SINGLE_LINE_TEXT", value: "○×商事株式会社"},
  案件名: {type: "SINGLE_LINE_TEXT", value: "メールシステム構築"}
 },
 {
  プロジェクト番号: {type: "SINGLE_LINE_TEXT", value: "12345678"},
  客先名: {type: "SINGLE_LINE_TEXT", value: "○×商事株式会社"},
  案件名: {type: "SINGLE_LINE_TEXT", value: "メールシステム構築"}
 },
 {
  プロジェクト番号: {type: "SINGLE_LINE_TEXT", value: "23456789"},
  客先名: {type: "SINGLE_LINE_TEXT", value: "▲△自動車株式会社"},
  案件名: {type: "SINGLE_LINE_TEXT", value: "仮想化基盤構築"}
 },
 {
  …
 }
]
これの重複排除をするにはどうすればよいか?
オブジェクト値の配列なので、一工夫必要である(Javascriptにおいてはオブジェクト値を単純に比較演算子やindexOfで評価できないため)。

結論
StackOverflowに頼ってしまいました。
How to remove duplicates from multidimensional array?
このQAは多次元配列のdedupeに関してだが、オブジェクト値の配列にも転用可能。

以下、コード。
StackOverflowのコードをまんまパクらせていただいてるけど、本記事は自分への備忘なのでご勘弁。
オブジェクト値をstringifyし、それをインデックスにした別配列(itemsFound)を使って重複チェックする。

なお、kintoneからのデータ取得には、APIの生使用ではなくkintoneUtilityを利用。
(function() {
  "use strict";
  kintone.events.on(['app.record.index.show'], function(event){
    kintoneUtility.rest.getAllRecordsByQuery({
      app: xx, //プロジェクト一覧アプリのID
      fields: [
        'プロジェクト番号',
        '客先名',
        '案件名',
      ],
    }).then(function(response){
      // 重複排除を行う関数
      function multiDimensionalUnique(arr) {
        var uniques = [];
        var itemsFound = {};
        for(var i = 0, l = arr.length; i < l; i++) {
            var stringified = JSON.stringify(arr[i]);
            if(itemsFound[stringified]) { continue; }
            uniques.push(arr[i]);
            itemsFound[stringified] = true;
        }
        return uniques;
      }

      // ここから本処理
      // kintoneUtilityで取得したデータを重複排除する
      var projects = multiDimensionalUnique(response.records);
      console.dir(projects);

      // 後続処理(略)

    });
    return event;
  });
})();