JqueryでRedmineの「チケットページ」をもっと便利に(年ごと、月ごと、四半期ごとの集計機能の開発)

## JqueryでRedmineの「チケットページ」をもっと便利に(年ごと、月ごと、四半期ごとの集計機能の開発)

[Redmine Advent Calendar 2023](https://adventar.org/calendars/8974)の16日目の記事として作成しました。


* * *
### 既存のRedmineで不便なこと、改善したい点

Redmineのチケットページでは、強力なフィルタリングとグルーピング機能が提供されていて、その機能を活用することで、必要な情報を必要な出力結果として取得することができます。
グルーピング機能は同じグループごとに、そのグループに属しているチケットの件数と、そのグループのタイトル部分で開閉ができるので、不要なグループは閉じることによって、必要なチケットだけを表示でき、可読性の向上に寄与しています。
下図の例のように、「トラッカー」でグルーピングをして、グルーピングのタイトルであるトラッカー名ごとに表示、非表示をタイトルをクリックすることで可能になります。

<img width="100%" alt="チケットページのフィルタリングとグルーピング機能" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/d388074b-149f-2713-7e34-b7f0fafc2a69.png">
＜チケットページのフィルタリングとグルーピング機能＞

非常に強力なフィルタリングとグルーピング機能ですが、日付形式のフィールドでグルーピングをすると、年・月・日の内、日付でグルーピングがされてしまいます。もちろん日付でグルーピングをしたいというニーズもあるのでしょうが、私の場合は、年ごと、もしくは月ごとでグルーピングをしたいのですが、Redmineのデフォルトのグルーピング機能ではそれを実現する方法がありません。下図の場合、同じ日に属しているチケットが1件づつしか無いため、グルーピングをしている意味をあまり感じることができません。

<img width="100%" alt="日付ごとでグルーピングがされる" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/5721bf0f-f132-da2e-ac83-aadcdb37492d.png">
＜日付ごとでグルーピングがされる＞

これを年ごと、月ごとでグルーピングをしたい場合、別途リスト形式のカスタムフィールドを作り、そこに年なり、月を設定して、日付形式のグルーピングではなく、リスト形式でのグルーピングをさせる方法が考えられますが、手間がかかるので現実的ではないと思います。

この問題を解決するために今回、「チケットページ」に、JavaScript（Jquery）だけで、更新日付フィールドを対象に年ごと、年プラス月ごと、年プラス四半期ごとにグルーピングできる機能を作ってRedmineの「チケットページ」をもっと便利に、プチ改造をしてみます。

下図が完成イメージです。サンプル画像のように改造します。
１）年ごと、年プラス月ごと、年プラス四半期ごとにチケットの件数を表示
２）年ごと、年プラス月ごと、年プラス四半期ごとにグルーピングして開閉機能を追加

<img width="100%" alt="年ごとのグルーピングの完成イメージ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/a7c4c262-9129-d482-e330-4d786af0c0ad.png">
＜年ごとのグルーピングの完成イメージ＞

　
<img width="100%" alt="年プラス月ごとのグルーピングの完成イメージ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/6c285053-9443-dece-b8f1-720a752ab74e.png">
＜年プラス月ごとのグルーピングの完成イメージ＞

　
<img width="100%" alt="年プラス四半期ごとのグルーピングの完成イメージ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/2ba917d7-14e9-7842-1316-926f1da705b6.png">
＜年プラス四半期ごとのグルーピングの完成イメージ＞



該当のページはRedmineの公式ページである「[https://redmine.org](https://redmine.org)」で実行しています。サーバーサイドの技術を使う場合、サーバーの編集権限がないので、このような改造はできませんが、JavaScript（Jquery）はブラウザサイドの技術なので、Redmine本体には全く手を加えずに、自分が使っているブラウザでのみ独自の機能を追加できるという点で、非常に手軽に、そして、自由な改造ができるというメリットがあります。

                  
* * *         
### 実現した方法

#### 下記のプロセスでRedmineの「チケットページ」をもっと便利に改造します。

１）グルーピングの対象フィールドである「更新日付」の日付情報を取得して変数上でグルーピングの種別を取得します。
２）「１）」で取得した種別のグルーピング名をタイトルとして追加して、グルーピングさせます。
３）「２）」で取得したグルーピング名に所属するチケットの件数をグルーピング名の要素にテキストとして追加します。

#### 具体的な方法を説明します。

#### 1.1.　チケット一覧ページの表の列数を取得

下図のプログラムソースでチケット一覧ページの表の列数を取得して変数に入れます。
この値はグルーピングをするときに追加するタイトル行のセルを結合させて一行にさせる必要があり、colspanの値として活用します。チケット一覧ページの表の列数は動的に変動するために現在の値を取得する必要があります。

```JavaScript

  myColspan = $("#content table thead th").length;

```
#### 1.2.　グルーピングの対象フィールドの値を取得

「更新日」のフィールドのクラス名は「.updated_on」であり、「.updated_on」クラスを対象にeachでループを回します。「.updated_on」クラスのテキストを取得をすると「2020-11-11 00:27」のような値を取得できるので、少し強引ですが、先頭から4文字は年を表す文字列、先頭から6文字から7文字の文字は月を表す文字列として、決め打ちで月と年を変数に取得します。四半期の区分は「月」の値でグルーピングをして変数化させます。

下記でgroupTitleNameが３回定義されていますが、実際には一つだけ定義をすれば良いので、使用しない定義はコメントアウトしてあります。
groupTitleNameで定義した文字列がグルーピングされた時のタイトルの文言になります。

```JavaScript

$(".updated_on").each(function (index) {
  // 「updated_on」クラスを持つ要素のテキストを取得
  var text = $(this).text();

  // テキストから年と月を抽出
  var year = parseInt(text.substring(0, 4), 10);
  var month = parseInt(text.substring(5, 7), 10);

  var quarter;

  if (month >= 1 && month <= 3) {
    quarter = "第1";
  } else if (month >= 4 && month <= 6) {
    quarter = "第2";
  } else if (month >= 7 && month <= 9) {
    quarter = "第3";
  } else if (month >= 10 && month <= 12) {
    quarter = "第4";
  }
  
  //グループタイトルに年プラス月を使用
  var groupTitleName = year + "年" + month + "月";
  //グループタイトルに年プラス四半期を使用
  //var groupTitleName = year + "年" + quarter + "四半期";　　＜＝＝コメントアウト
  //グループタイトルに年を使用
  //var groupTitleName = year + "年";　　　＜＝＝コメントアウト

})  
    
 ```
        
#### 1.3.　件数集計用にクラスを追加

該当のグループに属する要素の数を集計するためにgroupTitleNameで取得した文字列をクラス名として追加します。

<img width="100%" alt="groupTitleNameで取得した文字列をクラス名として追加" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/2dc16212-835a-cad2-a8b9-934f0b8cd6b1.png">
＜groupTitleNameで取得した文字列をクラス名として追加＞


```JavaScript

    // 親要素に新しいクラスを追加
  $(this).parent().addClass(groupTitleName);
  
```

               
#### 2.1.　グループ名が変更されたら該当のグループ名をタイトルとして挿入

上記の「$(".updated_on").each」を実行する前に、グループ名が重複しているのか、初めて出現したのかを切り分けるために、prevgroupTitleNameという変数を先に定義して、nullを入れておきます。

上記の$(this).parent().addClass(groupTitleName);の後に、グループのタイトルを入れるために下記の処理を追加します。
prevgroupTitleNameは３つの場合分けをします。
１）prevgroupTitleName === nullの場合は、groupTitleNameをprevgroupTitleNameに代入
２）prevgroupTitleName === groupTitleNameの場合は、ひとつ前の行と現在の行のグループ名が同じであるため、何の処理も追加しません。
３）それ以外の場合は、ひとつ前の行と現在の行のグループ名が異なるため、チケット一覧ページの表に新しいグループ名を持つ行を追加します。具体的には「newTr.insertBefore($(this).parent());」で追加をしています。追加するグルーピング名のタイトルの行は１列にしたいので、attr("colspan", myColspan)でセルの結合をさせます。


```JavaScript

    //上記の$(".updated_on").each(function (index) {　より上で下記の変数を定義

    var prevgroupTitleName = null;

    //上記の$(".updated_on").each(function (index) {　の中で、「var groupTitleName = year + "年" + month + "月";」の次の行に下記のプログラムを記述


  // prevgroupTitleNameがnullの場合
  if (prevgroupTitleName === null) {
    // 前回の値を更新
    prevgroupTitleName = groupTitleName;
    // groupTitleNameが前回と同じ場合
  } else if (prevgroupTitleName === groupTitleName) {
    // groupTitleNameが前回と異なる場合
  } else {
    // 前回の値を更新
    prevgroupTitleName = groupTitleName;

    // 新しいtr要素を作成
    var newTr = $("<tr>").addClass("group open");
    // 新しいtd要素を作成してtrに追加
    var newTd = $("<td>").attr("colspan", myColspan).appendTo(newTr);
    // span要素を作成してtdに追加
    var newSpan = $("<span>")
      .addClass("expander icon icon-expended")
      .html("&nbsp;")
      .on("click", function () {
        toggleRowGroup(this);
      })
      .appendTo(newTd);
    var newSpan = $("<span>")
      .addClass(groupTitleName)
      .text(groupTitleName)
      .appendTo(newTd);
    // a要素を作成してtdに追加
    var newA = $("<a>")
      .attr("href", "#")
      .on("click", function () {
        toggleAllRowGroups(this);
        return false;
      })
      .addClass("toggle-all")
      .text("Collapse all/Expand all")
      .appendTo(newTd);

    // 新しいtr要素をDOMに挿入
    newTr.insertBefore($(this).parent());


```

#### 3.1.　グルーピング名に所属するチケットの件数を追加

グルーピング名のタイトルの行には上記で「group」というクラス名を付与しているので、groupクラスでeachでループを回して、groupクラスに属している２番目のspanのクラス名を取得します。
このクラス名はグループ名のタイトルとそのグループに属しているチケットの各行で共通して使用されています。(「1.3.　件数集計用にクラスを追加」でそのための処理をしています。)そのため、このクラスの要素の数を数えて、そこから１を引いた値が、該当のグループに属しているチケット数になります。
その値をグルーピング名のタイトルの行にテキストとして追加します。
                  
```JavaScript

$(".group").each(function (index) {
  myClass = $(this).find("span").eq(1).attr("class");
  $(this)
    .find("span")
    .eq(1)
    .text(
      $(this).find("span").eq(1).text() +
        "(" +
        ($("." + myClass).length - 1) +
        "件)"
    );
});

```

#### 4.Jqueryのスクリプトを実行する方法

上記のJqueryのスクリプトを実行する方法は色々とありますが、自分が管理しているRedmineであれば[View Customize](https://github.com/onozaty/redmine-view-customize/blob/master/README.ja.md)がインストールされていれば、一番簡単にJqueryを適用できます。今回はRedmineの公式ページである「[https://redmine.org](https://redmine.org)」で実行しているため、[View Customize](https://github.com/onozaty/redmine-view-customize/blob/master/README.ja.md)は使用することができず、ブラウザの開発者ツールで実行しました。そのため、永続性はなく、あくまでも動作確認をする用途での実行になります。


<img width="100%" alt="ブラウザの開発者ツールで実行" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633321/79fdce74-196d-3176-32ae-7b28987861f2.png">
＜ブラウザの開発者ツールで実行＞

* * *
### 感想


* Redmineのチケットで見積書、請求書、納品書の発行日、売上日や売上高の管理をしていますが、Redmineのグルーピング機能ではYMDの「D」日付でグルーピングされてしまい、実際には月や年や四半期ごとに件数を把握したいというニーズを満たすことができませんでした。
* 今回作成したJqueryでは簡単に、月や年や四半期ごとに件数を把握したり、グループごとに折り畳みができるので、必要な情報を素早く確認することができて、便利になります。
* Redmineの[公式ページ](https://redmine.org)のように、サーバーの編集権限がなくても、JavaScript（Jquery）で機能を開発すれば、Redmine本体には全く手を加えずに、自分が使っているブラウザでのみ独自の機能を追加でき、非破壊的に開発をすることができるので、開発したプログラムにバグがあったとしても他人に迷惑をかけることがないので、非常に手軽に、そして、自由な改造ができるというメリットがあります。

* * *
#### この記事の作成者の紹介
* * *
山崎進
* Redmine、Jquery、JavaScript,Rails、Ruby、SQL、VBA、RPAの開発を行なっています。
* 自動化、業務の効率化に高い関心があります。
* 下記の媒体で情報を発信しています。Redmineのプラグインの開発などに対応が可能ですので、お気軽にご連絡ください。

　* Qiita：https://qiita.com/ankosoft
　* Twitter：https://twitter.com/yamasaki24
　* Redmine Advent Calendarで記事投稿
　* redmine.tokyoで講演多数
　* Redmine Japan Vol.1 前夜祭、Redmine Japan Vol.3で講演
　* https://ankosoft.co.jp/blog/
　* https://technology.ankosoft.co.jp/

* * *
### 関連記事

 *　[JqueryでRedmineのメニューにアイコンを入れる方法](https://qiita.com/yam_asaki/items/32097598cb0a653dd916)

 *　[JqueryでRedmineのメニューを閉じたり開いたりする方法](https://qiita.com/yam_asaki/items/5ac4a1127beeed53059c)

　*　[JqueryでRedmineの「活動ページ」をもっと便利に(タイトルを開閉したり、曜日を入れたり)](https://qiita.com/yam_asaki/items/718513b52b30b30f65bc)

　*　[JqueryでRedmineの「活動ページ」をもっと便利に②(フィルタリング機能と移動機能)](https://qiita.com/yam_asaki/items/2428610fd89bc73ce0ab)

　*　[JqueryでRedmineの「wikiページ」をもっと便利に(フィルタリング機能と移動機能)](https://qiita.com/yam_asaki/items/a6a32c45dbbc72a28836)

　*　[JqueryでRedmineの「チケットページ」をもっと便利に(年ごと、月ごと、四半期ごとの集計機能の開発)](https://qiita.com/yam_asaki/items/176d07588793b3e66b5d)

　*　[RedmineのパスワードやログインIDを忘れた時の復旧方法](https://qiita.com/yam_asaki/items/6881c92b30dc85f589ad)

　*　[「rails console」を使ってRedmine上のスケジュールを一括変更する方法](https://qiita.com/yam_asaki/items/1d41dc8653773f3522e6)