チェックを入れると即時棚卸しを実施できるチェックボックスの作成について

何を実現したいのかを書きましょう

資産管理アプリを作成しているが、チェックボックスをインラインに配置したいです。

ユーザーがワンクリックするだけで、ユーザー名とワンクリックした時刻を棚卸し実施者(tanaoroshi_executor)と棚卸し時刻(tanaoroshi_timedate)として、即時インライン編集を介することなくキントーンレコードを上書きするようにしたいです。

さらに、✓が入ったレコードをもう一度ワンクリックすると✓が自動的に消去し、チェックボックスを再度表示させたいです。

発生した問題やエラーメッセージを具体的に書きましょう

以前問い合わせてもらったトピックで記述したアプローチだけど、インライン編集と詳細編集に入って上書きするJSスクリプトに下記のコードが組み込まれていました。

今回は上記の機能を別のJSスクリプトとして取り込んでみたものの、以下の問題が発生しています。
*そもそもチェックボックスが表示されていないことである。

実行したコードをコピー&ペーストしましょう

(function() {
  'use strict';

  /**
   * チェックボックスをクリックしたときに実行される関数
   * @param {boolean} is_checked - チェックが入っているかどうか(true: チェック済み, false: 未チェック)
   * @param {HTMLElement} checked_box - クリックされたチェックボックスの要素
   * @param {number} index - 行のインデックス
   * @param {number} record_id - レコードID
   */
  function clickCheckBox(is_checked, checked_box, index, record_id) {
      const user = kintone.getLoginUser(); // 現在ログインしているユーザーを取得
      const now = new Date(); // 現在の日時を取得

      // 更新するレコードデータを定義
      const body = {
          'app': kintone.getID(), // 現在のアプリIDを取得
          'id': record_id, // 更新するレコードのID
          'record': {
              'tanaoroshi_check': { 'value': is_checked ? ['✔'] : [] },  // チェックボックスの値
              'tanaoroshi_executor': { 'value': is_checked ? user.name : null },  // 実施者の名前(チェックを入れた人)
              'tanaoroshi_timedate': { 'value': is_checked ? formatDate(now) : null }  // 棚卸し日時(現在時刻)
          }
      };

      // キントーンのAPIを使ってレコードを更新
      kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body).then((response) => {
          console.log('Record updated:', response); // 更新成功時にログを出力

          // チェックボックスの表示を更新
          checked_box.style.border = is_checked ? 'none' : '1px solid black'; // チェック済みなら枠線を消す
          checked_box.textContent = is_checked ? '✔' : ''; // チェックマークを表示または削除

          // クリックイベントを更新(トグル動作を実現)
          checked_box.onclick = () => clickCheckBox(!is_checked, checked_box, index, record_id);
      }, (error) => {
          console.error('Error updating record:', error); // 更新失敗時にエラーを表示
      });
  }

  /**
   * 一覧画面が表示されたときに実行される処理
   */
  kintone.events.on('app.record.index.show', function(event) {
      const allRecords = event.records; // 一覧に表示されているすべてのレコードを取得
      const unchecked_inventory_list = []; // チェックが入っていない行のインデックスを保存するリスト

      // レコードごとに処理を実行
      allRecords.forEach((record, index) => {
          if (record.tanaoroshi_check.value.length === 0) { // チェックが入っていない行を判定
              unchecked_inventory_list.push(index); // 未チェックの行をリストに追加
          }
      });

      // チェックボックスを配置するカラムを取得
      const inventory_update_container = document.querySelectorAll(".value-5953336 > div"); // `.value-XXXXX` はフィールドのクラス名

      inventory_update_container.forEach((container, index) => {
          const record_id = Number(allRecords[index].$id.value); // レコードIDを取得

          if (unchecked_inventory_list.includes(index)) {
              // 未チェックの行にチェックボックスを追加
              const new_checkbox = document.createElement('div');
              new_checkbox.style.height = '21px'; // チェックボックスの高さ
              new_checkbox.style.width = '21px'; // チェックボックスの幅
              new_checkbox.style.margin = 'auto'; // センターに配置
              new_checkbox.style.border = '1px solid black'; // 未チェックのときは枠線をつける
              container.append(new_checkbox); // チェックボックスをセル内に追加

              // クリックイベントを設定(チェックを入れる)
              new_checkbox.onclick = () => clickCheckBox(true, new_checkbox, index, record_id);
          } else {
              // すでにチェック済みの行を取得
              const checked_box = container.childNodes[0];
              checked_box.style.height = "21px";
              checked_box.style.width = "21px";
              checked_box.style.margin = "auto";
              checked_box.style.border = "1px solid black";
              checked_box.textContent = '';

              // クリックイベントを設定(チェックを外す)
              checked_box.onclick = () => clickCheckBox(false, checked_box, index, record_id);
          }
      });
  });

  /**
   * 日付を YYYY-MM-DD HH:MM:SS 形式に変換する関数
   * @param {Date} date - JavaScript の Date オブジェクト
   * @returns {string} フォーマット済みの日付
   */
  function formatDate(date) {
      const year = date.getFullYear();
      const month = ('0' + (date.getMonth() + 1)).slice(-2);
      const day = ('0' + date.getDate()).slice(-2);
      const hours = ('0' + date.getHours()).slice(-2);
      const minutes = ('0' + date.getMinutes()).slice(-2);
      const seconds = ('0' + date.getSeconds()).slice(-2);
      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  }

});

ここで呼び出している関数が違いますね。

(上記を修正しても動かない場合、まずはerrorなどがでていないかみてみてください!

「いいね!」 1

末尾の });})(); と書かないと
即時関数全体が一切実行されません。

レコード一覧画面のフィールド要素は
APIで取得できるので document.querySelectorAll よりも
kintone.app.getFieldElements で取得した方がいいです。

「いいね!」 2

返信遅くなって大変すみません。

今回チェックボックスを表示させることができました。
ありがとうございます。

私はいったんコードを書き直してみました。

実施したいのはチェックボックスを一覧編集に入る前に(kintone.app.index.show)の段階でクリックすると、レコードが即時に更新できる機能です。現状デバッグを試みたいが、クリックしても棚卸し更新者と棚卸し更新日付が即時更新されなかったし、クリックしてもエラーメッセージが出なくて途方に暮れています。

現在図のように止まっています。

フィールドコードはそれぞれ棚卸しチェックボックス(tanaoroshi_check)、棚卸し更新者(tanaoroshi_executor)、棚卸し日時(tanaoroshi_timedate)となっています。

解決の糸口を示していただけますととても助かります。

(function() {
  'use strict';

  /**
   * チェックボックスをクリックしたときに実行される関数
   * @param {boolean} is_checked - チェックが入っているかどうか(true: チェック済み, false: 未チェック)
   * @param {HTMLElement} checked_box - クリックされたチェックボックスの要素
   * @param {number} index - 行のインデックス
   * @param {number} record_id - レコードID
   */

    /**
   * 日付を YYYY-MM-DD HH:MM:SS 形式に変換する関数
   * @param {Date} date - JavaScript の Date オブジェクト
   * @returns {string} フォーマット済みの日付
   */
    function formatDate(date) {
        return date.toISOString();
    }

    function clickCheckBox(is_checked, checked_box, index, record_id) {
        const user = kintone.getLoginUser();
        const now = new Date();
        const body = {
            app: 721,
            id: record_id,
            record: {}
        };

        if (is_checked) {
            checked_box.style.border = '1px solid black';
            checked_box.style.height = '21px';
            body.record = {
                tanaorosi_check: { value: [] }
            };

            kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body);
            checked_box.oneclick = () => clickCheckBox(false, checked_box, index, record_id);
        } else {
            checked_box.style.height = '21px';
            body.record = {
                tanaoroshi_check: { value: ['✓']},
                tanaoroshi_executor: { value: user.name},
                tanaoroshi_timedate: { value: formatDate(now)}
            };
            
            kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body);
            checked_box.oneclick = () => clickCheckBox(true, checked_box, index, record_id)

        }
    }    


  /**
   * 一覧画面が表示されたときに実行される処理
   */
  kintone.events.on('app.record.index.show', function(event) {
      const allRecords = event.records; // 一覧に表示されているすべてのレコードを取得
      const unchecked_inventory_list = []; // チェックが入っていない行のインデックスを保存するリスト

      // レコードごとに処理を実行
      allRecords.forEach((record, index) => {
          if (record.tanaoroshi_check.value.length === 0) { // チェックが入っていない行を判定
              unchecked_inventory_list.push(index); // 未チェックの行をリストに追加
          }
      });

      // チェックボックスを配置するカラムを取得
      const inventory_update_container = document.querySelectorAll('.value-5953336 > div'); // `.value-XXXXX` はフィールドのクラス名

      inventory_update_container.forEach((container, index) => {
          const record_id = Number(allRecords[index].$id.value); // レコードIDを取得

          if (unchecked_inventory_list.includes(index)) {
              // 未チェックの行にチェックボックスを追加
              const new_checkbox = document.createElement('div');
                new_checkbox.style.height = '21px'; // チェックボックスの高さ
                new_checkbox.style.width = '21px'; // チェックボックスの幅
                new_checkbox.style.margin = 'auto'; // センターに配置
                new_checkbox.style.border = '1px solid black'; // 未チェックのときは枠線をつける
              container.append(new_checkbox); // チェックボックスをセル内に追加

              // クリックイベントを設定(チェックを入れる)
              new_checkbox.onclick = () => clickCheckBox(false, new_checkbox, index, record_id);
          } else {
              // すでにチェック済みの行を取得
              const checked_box = container.childNodes[0];
              checked_box.style.height = "21px";
              checked_box.style.width = "21px";
              checked_box.style.margin = "auto";
              checked_box.style.border = "1px solid black";
              checked_box.textContent = '';

              // クリックイベントを設定(チェックを外す)
              checked_box.onclick = () => clickCheckBox(true, checked_box, index, record_id);
          }
      });
  });

})();

クリックしたときに更新されていないように見えているだけで、実際にはレコードは更新されていると思います。クリックした後に画面をリロードすれば、チェック済みの状態で表示されると思います。

細かな点でいえば、

ここのフィールドコードが tanaorosi_check になっている

ここの checked_box.textContent が空なので未チェックと同じ見た目になっている

ここだけ直せば最低限動くかなと思います。


クリック後にチェックボックス表示を変えたい場合は textContent を変えるなどすればよいと思いますが、あくまでチェックボックスだけなので、更新者や日時を表示させようとするなら画面のリロードが必要になるかなと思います。

kintone.api のコールバック関数で、更新後のレコードの値を
innerText で描画したら、リロード無しでできました。

(function() {
    'use strict';

    function formatDateForDisplay(date) {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        const hours = date.getHours();
        const minutes = String(date.getMinutes()).padStart(2, '0');
        return `${year}-${month}-${day} ${hours}:${minutes}`;
    }

    function clickCheckBox(is_checked, checked_box, index, record_id) {
        const user = kintone.getLoginUser();
        const now = new Date();
        const body = {
            app: kintone.app.getId(),
            id: record_id,
            record: {}
        };

        if (is_checked) {
            // チェックを外したとき
            checked_box.style.border = '1px solid black';
            checked_box.style.height = '21px';
            checked_box.textContent = '';
            body.record = {
                tanaoroshi_check: { value: [] },
                tanaoroshi_executor: { value: '' },
                tanaoroshi_timedate: { value: '' }
            };

            kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body, function() {
                const dateCell = kintone.app.getFieldElements('tanaoroshi_timedate')[index].querySelector('span');
                const executorCell = kintone.app.getFieldElements('tanaoroshi_executor')[index].querySelector('span');
                dateCell.innerText = '';
                executorCell.innerText = '';
            });

            checked_box.onclick = () => clickCheckBox(false, checked_box, index, record_id);
        } else {
            // チェックを入れたとき
            checked_box.style.border = '1px solid black';
            checked_box.style.height = '21px';
            checked_box.textContent = '✓';
            body.record = {
                tanaoroshi_check: { value: ['✓'] },
                tanaoroshi_executor: { value: user.name },
                tanaoroshi_timedate: { value: now.toISOString() }
            };

            kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body, function() {
                const dateCell = kintone.app.getFieldElements('tanaoroshi_timedate')[index].querySelector('span');
                const executorCell = kintone.app.getFieldElements('tanaoroshi_executor')[index].querySelector('span');
                dateCell.innerText = formatDateForDisplay(now);
                executorCell.innerText = user.name;
            });

            checked_box.onclick = () => clickCheckBox(true, checked_box, index, record_id);
        }
    }

    /**
     * 一覧画面が表示されたときに実行される処理
     */
    kintone.events.on('app.record.index.show', function(event) {
        const allRecords = event.records; // 一覧に表示されているすべてのレコードを取得
        const unchecked_inventory_list = []; // チェックが入っていない行のインデックスを保存するリスト

        // レコードごとに処理を実行
        allRecords.forEach((record, index) => {
            if (record.tanaoroshi_check.value.length === 0) { // チェックが入っていない行を判定
                unchecked_inventory_list.push(index); // 未チェックの行をリストに追加
            }
        });

        // チェックボックスを配置するカラムを取得
        const inventory_update_container = document.querySelectorAll('.value-5953336 > div'); // `.value-XXXXX` はフィールドのクラス名
        inventory_update_container.forEach((container, index) => {
            const record_id = Number(allRecords[index].$id.value); // レコードIDを取得

            if (unchecked_inventory_list.includes(index)) {
                // 未チェックの行にチェックボックスを追加
                const new_checkbox = document.createElement('div');
                new_checkbox.style.height = '21px'; // チェックボックスの高さ
                new_checkbox.style.width = '21px'; // チェックボックスの幅
                new_checkbox.style.margin = 'auto'; // センターに配置
                new_checkbox.style.border = '1px solid black'; // 未チェックのときは枠線をつける
                container.append(new_checkbox); // チェックボックスをセル内に追加

                // クリックイベントを設定(チェックを入れる)
                new_checkbox.onclick = () => clickCheckBox(false, new_checkbox, index, record_id);
            } else {
                // すでにチェック済みの行を取得
                const checked_box = container.childNodes[0];
                checked_box.style.height = "21px";
                checked_box.style.width = "21px";
                checked_box.style.margin = "auto";
                checked_box.style.border = "1px solid black";
                checked_box.textContent = '✓';

                // クリックイベントを設定(チェックを外す)
                checked_box.onclick = () => clickCheckBox(true, checked_box, index, record_id);
            }
        });
    });

})();

返信遅くなり申し訳ありません。

せっかくみんなさんの助力もあるのに結局いろいろと自力で作動できるコードをくみ上げた。

結局DOM操作からのアプローチでしたが、うまくいきました。

(function() {
    'use strict';
  
    /**
     * チェックボックスをクリックしたときに実行される関数
     * @param {boolean} is_checked - チェックが入っているかどうか(true: チェック済み, false: 未チェック)
     * @param {HTMLElement} checked_box - クリックされたチェックボックスの要素
     * @param {number} index - 行のインデックス
     * @param {number} record_id - レコードID
     */
  
      /**
     * 日付を YYYY-MM-DD HH:MM:SS 形式に変換する関数
     * @param {Date} date - JavaScript の Date オブジェクト
     * @returns {string} フォーマット済みの日付
     */
      function formatDate(date) {
          return date.toISOString();
      }
  
      function getJSTFormattedDate() {
          const date = new Date(new Date().toLocaleString('en-US', { timeZone: 'Asia/Tokyo' }));
          const yyyy = date.getFullYear();
          const mm = String(date.getMonth() + 1).padStart(2, '0');
          const dd = String(date.getDate()).padStart(2, '0');
          const hh = String(date.getHours()).padStart(2, '0');
          const min = String(date.getMinutes()).padStart(2, '0');
          return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
      }

      function showTemporaryMessage(text) {
        const messageDiv = document.createElement('div');
        messageDiv.textContent = text;
        messageDiv.style.position = 'fixed';
        messageDiv.style.top = '20px';
        messageDiv.style.left = '50%';
        messageDiv.style.transform = 'translateX(-50%)';
        messageDiv.style.backgroundColor = '#f0f9ff';
        messageDiv.style.border = '1px solid #88c3ff';
        messageDiv.style.padding = '10px 20px';
        messageDiv.style.borderRadius = '6px';
        messageDiv.style.boxShadow = '0 2px 6px rgba(0,0,0,0.2)';
        messageDiv.style.color = '#0078D4';
        messageDiv.style.fontWeight = 'bold';
        messageDiv.style.zIndex = '9999';
    
        document.body.appendChild(messageDiv);
    
        setTimeout(() => {
            messageDiv.remove();
        }, 10000); // 2.5秒後に自動で消える
      }
    
  
      function clickCheckBox(is_checked, checked_box, index, record_id) {
          const user = kintone.getLoginUser();
          const now = new Date();
          const body = {
              app: 721,
              id: record_id,
              record: {}
          };
  
          if (is_checked) {
              checked_box.style.border = '1px solid black';
              checked_box.style.height = '21px';
              body.record = {
                  tanaoroshi_check: { 
                    value: []
                  }
              };
  
              kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body)
              .then(()=>{
                  console.log('Unchecked successfully');
                  checked_box.textContent = '';
                  checked_box.style.opacity = '1';
                  checked_box.style.border = '1px solid black';
                  checked_box.style.cursor = 'pointer';
                  checked_box.onclick = () => clickCheckBox(false, checked_box, index, record_id);
              })
              .catch((err) => {
                  console.error('Error unchecking:',err);
              }
              
              );
              
          } else {
              checked_box.style.height = '21px';
              body.record = {
                  tanaoroshi_check: { value: ['✓']},
                  tanaoroshi_executor: { value: user.name},
                  tanaoroshi_timedate: { value: formatDate(now)}
              };
              
              kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', body)
              .then(() => {
                  console.log('Checked successfully');
                  checked_box.onclick = () => clickCheckBox(true, checked_box, index, record_id);
                  
                  checked_box.textContent = '✓';
                  checked_box.style.opacity = '1';
                  checked_box.style.color = 'green'; // ← 文字色指定(黒でもOK)
                  checked_box.style.textAlign = 'center';
                  checked_box.style.fontSize = '16px';
                  checked_box.style.fontWeight = 'bold';
                  checked_box.style.backgroundColor = 'transparent'; // 背景は透明に
                  checked_box.style.border = 'none'; // ボーダーは無しでも良い
                  checked_box.style.cursor = 'default';
  
                  // 🔽 棚卸実施者・日時を画面上に反映する(フィールドID: 5953341, 5953342)
                  document.querySelectorAll('.value-5953341 div span')[index].textContent = user.name;
  
                  const jst = getJSTFormattedDate();
                  document.querySelectorAll('.value-5953342 div span')[index].textContent = jst;

                  const recordNum = record_id;
                  showTemporaryMessage(`レコード番号:${recordNum} の棚卸し状態が更新されました。`); 
  
  
                })
                /*.catch((err) => {
                  console.error('Error checking:', err);
                }) 👈デバッグ用に*/
          }
      }    
  
  
    /**
     * 一覧画面が表示されたときに実行される処理
     */
    kintone.events.on('app.record.index.show', function(event) {
        const allRecords = event.records; // 一覧に表示されているすべてのレコードを取得
        const unchecked_inventory_list = []; // チェックが入っていない行のインデックスを保存するリスト
  
        // レコードごとに処理を実行
        allRecords.forEach((record, index) => {
            if (record.tanaoroshi_check.value.length === 0) { // チェックが入っていない行を判定
                unchecked_inventory_list.push(index); // 未チェックの行をリストに追加
            }
        });
  
        // チェックボックスを配置するカラムを取得
        const inventory_update_container = document.querySelectorAll('.value-5953336 > div'); // `.value-XXXXX` はフィールドのクラス名
  
        inventory_update_container.forEach((container, index) => {
            const record_id = Number(allRecords[index].$id.value); // レコードIDを取得
  
            if (unchecked_inventory_list.includes(index)) {
                // 未チェックの行にチェックボックスを追加
                const new_checkbox = document.createElement('div');
                  new_checkbox.style.height = '21px'; // チェックボックスの高さ
                  new_checkbox.style.width = '21px'; // チェックボックスの幅
                  new_checkbox.style.margin = 'auto'; // センターに配置
                  new_checkbox.style.border = '1px solid black'; // 未チェックのときは枠線をつける
                container.append(new_checkbox); // チェックボックスをセル内に追加
  
                // クリックイベントを設定(チェックを入れる)
                new_checkbox.onclick = () => clickCheckBox(false, new_checkbox, index, record_id);
            } else {
                // すでにチェック済みの行を取得
                const checked_box = container.childNodes[0];
                checked_box.style.height = "21px";
                checked_box.style.width = "21px";
                checked_box.style.margin = "auto";
                checked_box.style.border = "none"; // ✅ 枠なし
                checked_box.textContent = '✓';     // ✅ 初期✓
                checked_box.style.color = 'green';
                checked_box.style.fontWeight = 'bold';
                checked_box.style.fontSize = '16px';
                checked_box.style.textAlign = 'center';
                checked_box.style.cursor = 'default';
  
                // クリックイベントを設定(チェックを外す)
                checked_box.onclick = () => clickCheckBox(true, checked_box, index, record_id);
            }
        });
    });
    
    kintone.events.on('app.record.index.edit.submit.success', function(event) {
      console.log("✔ 一覧編集 保存成功");
      setTimeout(() => {
        location.reload(); // ← 少し待ってから実行
      }, 250); // 0.5秒後(必要なら1000msくらいにしてもOK)
      return event;
    });

    kintone.events.on('app.record.index.edit.show', function () {
        function attachCancelEvents() {
          const cancelButton = document.querySelector('.recordlist-cancel-button-gaia');
          const cancelIcon = document.querySelector('.recordlist-cancel-icon-gaia');
      
          if (cancelButton && !cancelButton.dataset.listenerAttached) {
            cancelButton.dataset.listenerAttached = 'true';
            cancelButton.addEventListener('click', () => {
              console.log("❌ キャンセルボタン押下 → リロードします");
              setTimeout(() => {location.reload()}, 250);
            });
          }
      
          if (cancelIcon && !cancelIcon.dataset.listenerAttached) {
            cancelIcon.dataset.listenerAttached = 'true';
            cancelIcon.addEventListener('click', () => {
              console.log("❌ キャンセルアイコン押下 → リロードします");
              setTimeout(() => {location.reload()}, 250);
            });
          }
        }
      
        // ① 初回に即チェック
        attachCancelEvents();
      
        // ② 後から表示された場合も補足するために監視継続
        const observer = new MutationObserver(() => {
          attachCancelEvents();
        });
      
        observer.observe(document.body, {
          childList: true,
          subtree: true
        });
    });
 

このトピックはベストアンサーに選ばれた返信から 3 日が経過したので自動的にクローズされました。新たに返信することはできません。