1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【kintone】プロセス管理のステータスを一括更新

1
Last updated at Posted at 2026-02-13

サンプルアプリのおすすめ機能体験パックに含まれる休暇申請アプリを例に
一覧に表示中のレコードのステータスを一括更新できるようにした。


一覧のメニューの右側にボタンを配置。

タ.png

ボタンを押すと、確認ダイアログが出る。

タイ.png

ステータス更新中はスピナーを表示。

タイト.png

更新が終わると、ステータスの遷移結果、実行されたアクション、更新件数が表示される。

タイトル.png

ダイアログを閉じると、リロードされ、ステータス更新が画面に反映される。

タイトルな.png


PC用のJavaScript / CSSファイル は下図のようにする。

fdsb.png

sample.js は、休暇申請アプリのプロセス管理の設定に合わせて以下のように書く。

grsし.png

sample.js
(() => {
  'use strict';

  // kintone REST API Client のインスタンスを生成
  const client = new KintoneRestAPIClient();
  
  // 更新対象となるステータス
  const UPDATE_STATUSES = ['下書き', '承認者確認中', '差し戻し'];
  
  // ステータスに対応するアクションのマッピング(複数のアクションを持つ承認者確認中は書かない)
  const ACTION_MAP = {
    '下書き': '申請する',
    '差し戻し': '再申請する'
  };
  
  // アクション実行後の遷移先ステータスのマッピング
  const NEXT_STATUS_MAP = {
    '申請する': '承認者確認中',
    '承認する': '完了',
    '差し戻す': '差し戻し',
    '再申請する': '承認者確認中'
  };

  // 一覧画面が表示されたときの処理
  kintone.events.on('app.record.index.show', async (event) => {
    // 一覧のメニューの右側の要素を取得
    const headerSpace = kintone.app.getHeaderMenuSpaceElement();
    
    // 既にボタンが追加されている場合は重複追加しない
    if (headerSpace.querySelector('.bulk-status-update-btn')) return event;

    // kintone UI Component を使用してボタンを作成
    const button = new Kuc.Button({
      text: 'ステータス一括更新',
      type: 'submit',
      className: 'bulk-status-update-btn'
    });
    
    // 取得した要素に作成したボタンを追加
    headerSpace.appendChild(button);

    // ボタンクリック時の処理
    button.addEventListener('click', async () => {
      try {
        // ボタンを無効化して二重クリックを防止
        button.disabled = true;
        button.text = '処理中...';

        // 現在のアプリIDを取得
        const appId = kintone.app.getId();
        
        // 現在の一覧に適用されているクエリを取得し、limit と offset を除去
        const query = kintone.app.getQuery()
          ?.replace(/limit\s+\d+/gi, '')
          .replace(/offset\s+\d+/gi, '')
          .trim();

        // クエリに一致する全レコードを取得
        const records = await client.record.getAllRecordsWithCursor({
          app: appId,
          ...(query && { query })
        });

        // レコードが1件も取得できなかった場合は処理終了
        if (records.length === 0) {
          await Swal.fire({
            icon: 'info',
            title: 'レコードなし',
            html: '現在表示中の一覧にレコードがありません'
          });
          return;
        }

        // ログインユーザーのログイン名を取得
        const loginUserCode = kintone.getLoginUser().code;
        
        // 作業者にログインユーザー以外が設定されているレコードの有無を示すフラグ
        let excludedByAssignee = false;

        // 取得したレコードから更新可能なレコードを絞り込む
        const validRecords = records.filter(r => {
          // 更新対象なステータス(下書き、承認者確認中、差し戻し)でないレコードは除外
          if (!UPDATE_STATUSES.includes(r.ステータス.value)) return false;
          
          // 作業者が空の場合は誰でも更新可能なのでtrueを返す
          if (!r.作業者?.value?.length) return true;
          
          // 作業者が設定されている場合は、ログインユーザーが作業者に含まれているかチェック
          const isAssigned = r.作業者.value.map(u => u.code).includes(loginUserCode);
          if (!isAssigned) excludedByAssignee = true;
          return isAssigned;
        });

        // ステータスごとのレコード件数を集計するためのオブジェクト
        const statusCounts = {};
        
        // 更新可能なレコードをループして、ステータス別に件数をカウント
        validRecords.forEach(r => {
          const status = r.ステータス.value;
          statusCounts[status] = (statusCounts[status] || 0) + 1;
        });

        // 全ステータスの合計件数を計算
        const totalCount = Object.values(statusCounts).reduce((a, b) => a + b, 0);
        
        // 更新可能なレコードが1件もない場合は処理終了
        if (totalCount === 0) {
          // 作業者チェックで除外されたレコードがある場合とない場合でメッセージを変える
          const msg = excludedByAssignee > 0
            ? '他のユーザーが作業者に指定されているので<br>更新できません'
            : 'ステータスが完了のレコードしかありません';
          await Swal.fire({ icon: 'info', title: '更新対象なし', html: msg });
          return;
        }

        // 更新するステータスの選択肢を作成
        const statusOptions = UPDATE_STATUSES
          .filter(s => statusCounts[s] > 0) // 件数が0のステータスは除外
          .map(s => ({ value: s, text: `${s} (${statusCounts[s]}件)` }));

        // 選択されたステータスを格納する変数
        let selectedStatus;
        
        // 選択肢が複数ある場合はユーザーに選ばせる
        if (statusOptions.length > 1) {
          // ラジオボタンのHTMLを生成
          const optionsHtml = statusOptions.map((opt, i) =>
            `<div style="margin: 10px 0;">
              <input type="radio" id="s${i}" name="status" value="${opt.value}" ${i === 0 ? 'checked' : ''}>
              <label for="s${i}" style="margin-left: 8px; cursor: pointer;">${opt.text}</label>
            </div>`
          ).join('');

          // ステータス選択ダイアログを表示
          const result = await Swal.fire({
            title: 'ステータス選択',
            html: `<div style="text-align: left;"><p style="margin-bottom: 15px;">更新するステータスを選択してください</p>${optionsHtml}</div>`,
            icon: 'question',
            showCancelButton: true,
            confirmButtonText: '次へ',
            cancelButtonText: 'キャンセル',
            confirmButtonColor: '#3085d6',
            cancelButtonColor: '#6c757d',
            preConfirm: () => document.querySelector('input[name="status"]:checked')?.value
          });

          // キャンセルされた場合は処理中止
          if (!result.isConfirmed) return;
          
          // 選択されたステータスを変数に保存
          selectedStatus = result.value;
        // 選択肢が1つしかない場合は自動的にそれを選ぶ
        } else {
          selectedStatus = statusOptions[0].value;
        }

        // アクションを取得
        let actionName = ACTION_MAP[selectedStatus];
        
        // 承認者確認中を更新する場合は「承認する」か「差し戻す」をユーザーに選ばせる
        if (selectedStatus === '承認者確認中') {
          const result = await Swal.fire({
            title: 'アクション選択',
            html: `<p>承認者確認中のレコード${statusCounts.承認者確認中}件に対する<br>アクションを選択してください</p>`,
            icon: 'question',
            showCancelButton: true,
            showDenyButton: true,
            confirmButtonText: '承認する',
            denyButtonText: '差し戻す',
            cancelButtonText: 'キャンセル',
            confirmButtonColor: '#3085d6',
            denyButtonColor: '#d33',
            cancelButtonColor: '#6c757d'
          });

          if (result.isConfirmed) actionName = '承認する';
          else if (result.isDenied) actionName = '差し戻す';
          else return;
        }

        // 選択されたステータスのレコードを抽出
        const targetRecords = validRecords.filter(r => r.ステータス.value === selectedStatus);

        // 更新前の最終確認ダイアログを表示
        const confirmResult = await Swal.fire({
          title: '確認',
          html: `<p>${selectedStatus}のレコード${targetRecords.length}件を更新します</p>
                 <p>アクション: ${actionName}</p><p>よろしいですか?</p>`,
          icon: 'warning',
          showCancelButton: true,
          confirmButtonText: '実行',
          cancelButtonText: 'キャンセル',
          confirmButtonColor: '#3085d6',
          cancelButtonColor: '#6c757d'
        });

        // キャンセルされた場合は処理中止
        if (!confirmResult.isConfirmed) return;

        // 処理中ダイアログを表示
        Swal.fire({
          title: '処理中...',
          html: '<div class="swal2-loader" style="display: block; margin: 20px auto;"></div><p style="margin-top: 20px;">ステータスを更新しています...</p>',
          allowOutsideClick: false,
          showConfirmButton: false
        });

        // 更新対象レコードの配列を作成
        const recordsToUpdate = targetRecords.map(r => ({
          id: r.$id.value,
          action: actionName,
          ...(actionName === '差し戻す' && { assignee: r.作成者.value.code }) // 差し戻す場合は作業者に作成者を指定
        }));

        // ステータス一括更新APIを実行
        await client.record.updateRecordsStatus({ app: appId, records: recordsToUpdate });

        // アクション実行後の遷移先ステータスを取得
        const afterStatus = NEXT_STATUS_MAP[actionName];
        
        // 処理完了ダイアログを表示(更新前後のステータスと実行されたアクション、更新件数を表示)
        await Swal.fire({
          icon: 'success',
          title: '処理完了',
          html: `<div style="margin-bottom: 20px;">
                   <p style="font-size: 16px; margin-bottom: 10px;">
                     <strong>${selectedStatus}</strong> → <strong style="color: #3085d6;">${afterStatus}</strong>
                   </p>
                   <p style="color: #666; font-size: 14px;">アクション: ${actionName}</p>
                 </div>
                 <hr style="margin: 20px 0; border: none; border-top: 1px solid #ddd;">
                 <p>成功: <strong style="color: #28a745;">${recordsToUpdate.length}件</strong></p>`,
          confirmButtonText: 'OK'
        });

        // リロードして最新の状態を画面に反映
        location.reload();

      // エラーが発生した場合はコンソールとダイアログにログを出力
      } catch (error) {
        console.error('エラー:', error);

        await Swal.fire({
          icon: 'error',
          title: 'エラー',
          text: error.message
        });
      // 処理完了後にボタンを元の状態に戻す
      } finally {
        button.disabled = false;
        button.text = 'ステータス一括更新';
      }
    });

    return event;
  });

})();
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?