Redmineで周知情報を管理する

# はじめに

この記事は、Redmine Advent Calender 2023/12/23の記事です　投稿遅くなってすみません（汗

みなさまの組織では、組織全体にニュース等を周知・連絡するときにどのようにされていますか？私の会社では、周知する内容を朝ミーティング等で連絡し、締め切り間際になったら、〇日までですのでお忘れなく、というような念押し確認をするような運用をしています。ただ、この運用では、周知する側の人は何度も声かけているのに、なかなか全体に浸透しなかったり、周知される側の人でその話は確認済みの人は、何度も念押し確認されてしまったりします。また、締め切り過ぎてしまった場合に確認が漏れていた場合はそのまま放置されて知らないままになってしまうか、あとで注意されてしまったりします。

なんか効率がよくないなと思いましたので、周知をRedmineでうまく管理できないかと考えて、すこしRedmineをカスタマイズを検討してみました。

# どんなふうに運用したいか

* 周知する人：Redmineのチケットとして周知情報を登録すればOK。何度も周知内容を繰り返して連絡しないでも済むようにしたい。
* 周知される人：「確認」ボタンをクリックすることで、その周知をその人が確認したことにできる。（１：１のメッセージだと既読つけるようなイメージです）うっかり間違って「確認」をクリックした場合「確認取消」ができる。
→対象者が全員内容を「確認」すれば、周知完了となる。もし、なかなか確認しない人がいた場合、その人に個別に連絡して確認を促すことができるようにしたい。

# カスタマイズ結果
カスタマイズ内容（作成したView Customizeスクリプト）の紹介はこの後のセクションで書くことにし、先にどういうふうにできたかを紹介します。

### 周知事項の登録
周知する人が、周知の内容をチケットに登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138759/c5dadc74-f15c-5a2e-d75c-e73a1c1e2726.png)

### チケット表示画面
「確認：未確認者：17名 」のように、未確認の人が何名か表示されています。「確認」ボタンをクリックすることで、自分がその周知を確認したことになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138759/ba9979ca-509e-fda1-b606-4f6531093c40.png)

### 未確認者が少なくなると
未確認者が５名以下になると、未確認の人の氏名が表示されるようになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138759/4cdfbf92-3a11-7a1b-4148-257afdc96ae6.png)

### 自分が「確認」をクリックすると
いま、Adminユーザで「確認」ボタンをクリックしたので、未確認ユーザからAdminユーザの表示が消え、ボタンが「確認取消」になりました。確認取消をクリックすると、また未確認者に戻ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138759/10577efa-877d-c006-9bc1-d61307fa875c.png)

# カスタマイズ内容
### トラッカー、カスタムフィールドの設定
「周知」トラッカーに「確認」カスタムフィールドを追加します。形式は「ユーザー」にします。このカスタムフィールドで、だれが確認したのかを登録することにします。

### ViewCustomizeスクリプト
作成したViewCustomizeスクリプトは以下のとおりです。

```javascript:Redmine_notice.js
// パスのパターン  /issues/[0-9]+$
// プロジェクトのパターン  使用するプロジェクトに合わせる
// 種別      JavaScript

$(function() {
    // 管理対象のユーザクラスのインスタンスを作成
  var Users = new UsersClass(':input#issue_custom_field_values_7');
  // カスタムフィールドのID
  var customFieldId = '7';

  // selected が false であるユーザデータ数
    var unselectedUsersCount = Users.count(user => !user.selected);
  // selected が false であるユーザデータをリスト
  var unselectedUsersList = Users.list(user => !user.selected);
    // 自分自身のIDを取得
  var myId = ViewCustomize.context.user.id;
  // 選択されているユーザのidの配列を取得
  var selectedUserIds = Users.getIds(user => user.selected);
  // 自分が確認済みかどうか
   if (!Users.isSelected(ViewCustomize.context.user.id)){  // 確認済みでない場合
    var confirmButton = '<input type="button" id="confirmButton" value="確認">';
    // 選択されたユーザIDに自分自身のIDを追加
      selectedUserIds.push(ViewCustomize.context.user.id);
  } else {						// 確認済みの場合
    var confirmButton = '<input type="button" id="confirmButton" value="確認取消">';
    // 選択されたユーザIDから自分自身のIDを削除
    selectedUserIds = selectedUserIds.filter(id => id !== String(myId));
  }

  // 既存のinputタグの後に新しいinputタグを追加
  var unselectedUserText = unselectedUsersList.map(user => user.text).join(',');
    $("div.cf_7 div.value").after(confirmButton);  // 確認／確認取消ボタンを表示
  if (unselectedUsersCount < 5){	// 未確認が5名以下の場合
        $("div.cf_7 div.value").after( "未確認者：" + unselectedUserText + "<br>" );	// 未確認者の氏名を表示
  } else {
        $("div.cf_7 div.value").after( "未確認者：" + unselectedUsersCount + "名<br>" );  // 未確認者の人数を表示
  }
  $("div.cf_7 div.value").hide();    // もともと表示されている確認済みのユーザ情報を隠す

    // ボタンクリック時の処理
    $(":input#confirmButton").click(function(){
    // RedmineのREST APIエンドポイントを取得
    var apiUrl = location.pathname + ".json";


    // Redmine APIに送信するデータ
    var data = {
      issue: {
        custom_fields: [
          {
            id: customFieldId,
            value: selectedUserIds
          }
        ]
      }
    };

    // Redmine APIを呼び出す
    $.ajax({
      url: apiUrl,
      type: 'PUT',
      contentType: 'application/json',
      data: JSON.stringify(data),
      headers: {
        'X-Redmine-API-Key': ViewCustomize.context.user.apiKey
      },
      success: function(response) {
        console.log('Custom field updated successfully:', response);
	location.reload();
      },
      error: function(error) {
        console.error('Error updating custom field:', error);
      }
    });
  });
});


// UsersClass クラスの定義
class UsersClass {
  constructor(selector) {
    this.userData = {};
    this.createUserHash(selector);
  }

  createUserHash(selector) {
    // 指定されたセレクタからvalue、text、selectedを取得し、ハッシュ(userData)に保存
    $(selector + ' option').each((index, element) => {
      var value = $(element).val();
      var text = $(element).text();
      var selected = $(element).prop('selected');

      // オプションのデータをハッシュ(userData)に保存
      this.userData[value] = {
        text: text,
        selected: selected
      };
    });
  }

  // 条件に基づいて人数をカウントするメソッド
  count(condition) {
    return Object.values(this.userData).filter(user => condition(user)).length;
  }

  // 条件に基づいてユーザデータのハッシュをリストするメソッド
  list(condition) {
    return Object.values(this.userData).filter(user => condition(user));
  }

  // 指定されたvalueのユーザがselectedであればtrueを返すメソッド
  isSelected(value) {
    if (this.userData[value]) {
      return this.userData[value].selected;
    }
    return false; // valueが存在しない場合はfalseを返す
  }

  // 条件に基づいてユーザのidの配列を取得するメソッド
  getIds(condition) {
    return Object.keys(this.userData).filter(value => condition(this.userData[value]));
  }
}
```

### 工夫ポイント
* UserClassクラスにユーザの情報をもたせて、必要なアクセスをメソッドとして作成
* 自分が未確認の場合は「確認」ボタン、確認済みの場合は「確認取消」ボタンを表示
* ボタンをクリックすると、REST APIで「確認」カスタムフィールドの内容を更新する
* 自身のユーザIDやapiKeyなどの情報はViewCustomize.contextより取得している

# 今後の発展について

* 周知する対象者を限定できるようにする。これには、周知対象者を別カスタムフィールドで設定するようにし、その対象者が全員確認したかどうかを表示するようにすればよい
* 未確認者に対して、個別に催促をできるようにする。これには、未確認者に対してTeams等のWebHookにより通知する、などが考えられる。
* 全員の確認が終わったら、自動的に周知チケットを終了にする。未確認者が0になったときにステータスを終了にすることは可能だが、最後の人が「確認取消」できなくなる。当面は、周知した人が目視確認のうえ、手動でステータスを更新するのがよいかもしれない。

# おわりに

ViewCustomizeスクリプトは本当にいろいろなカスタマイズができますね。onozatyさん、すばらしいプラグインをご提供いただきありがとうございます！