kintone API

予算アプリと実績アプリの集計表をカスタマイズビューに表示する

目次

caution
警告

レコード一括取得時に1万を超えるレコードを取得する可能性がある場合は、運用・適用中のプログラムのご確認並びに修正対応の検討をお願いします。
詳細は、次のページを参照してください。
offsetの制限値を考慮したkintoneのレコード一括取得について

概要

こちらのサンプルは、予算管理アプリと実績管理アプリのデータを使って、予算/実績の差異と達成率を計算し、集計表に表示させるカスタマイズです。

カスタマイズビューという機能を利用し、ひとつの集計表にデータをまとめられるので、複数のアプリを確認したり、手動で集計したりする手間を省くことができます。
カスタマイズの適用方法は、「 適用手順 」を参照してください。

完成形

デモ環境

デモ環境で実際に動作を確認できます。

ログイン情報は、次のページを参照してください。
cybozu developer networkデモ環境

下準備

適用手順

今回のカスタマイズは、「実績管理」アプリが対象です。

カスタマイズビューの設定

アプリストアから「 予算・実績管理 (External link) 」を追加した場合、この設定は不要です。
また、カスタマイズビューを作成するには、kintoneの管理権限が必要です。

  1. 一覧を追加します。

  2. 新規に一覧を作成し「レコード一覧の表示形式」を「カスタマイズ」に設定します。

  3. HTML欄に次のHTMLを入力し保存します。

    1
    2
    
    <table id="view"></table>
    <div id="pager"></div>

カスタマイズビューの設定方法の詳細は、次のページを参照してください。
カスタマイズする場合 | kintoneヘルプ (External link) を参照してください。

また、カスタマイズビューの使い方について、次のチュートリアル記事により詳しい解説がありますので、興味ある方はぜひそちらも確認してください。
カスタマイズビューを作成してみよう

JavaScriptファイル/CSSファイルの追加

カスタマイズに必要なファイルを追加します。

  1. アプリの設定画面から、「JavaScript / CSSによるカスタマイズ」を開きます。
  2. 次の内容を設定します。

サンプルプログラム

JavaScript

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
/*
 * カスタマイズビューのサンプルプログラム
 * Copyright (c) 2026 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  // 予算管理アプリのアプリID
  const BUDGET_APP_ID = 68;
  // 実績管理アプリID(現在表示しているアプリ)
  const RESULTS_APP_ID = kintone.app.getId();

  // ページング・ソート用の状態管理
  const state = {
    data: [],
    currentPage: 1,
    rowsPerPage: 10,
    sortColumn: null,
    sortOrder: 'asc',
  };

  /**
   * 【メイン処理】予実管理のカスタマイズビューを表示
   * @param {Array} records - 予算レコード配列
   */
  const dispYojitsuCustomizeView = async (records) => {
    try {
      const data = await makeYojitsuData(records);
      state.data = data;
      state.currentPage = 1;
      renderTable();
    } catch (error) {
      console.error('データの読み込みに失敗しました', error);
      const viewElement = document.getElementById('view');
      viewElement.innerHTML = '<p class="error-message">データの読み込みに失敗しました。</p>';
    }
  };

  /**
   * 【データ取得・加工】予算管理アプリを全レコード取得
   * @param {Number} appId - アプリID
   * @param {Number} offset - オフセット
   * @param {Number} limit - 取得件数
   * @param {Array} allRecords - 累積レコード配列
   * @return {Promise<Array>} 全レコード
   */
  const fetchRecords = async (appId, offset = 0, limit = 100, allRecords = []) => {
    try {
      const body = {
        app: appId,
        query: `order by レコード番号 asc limit ${limit} offset ${offset}`,
      };
      const resp = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', body);
      allRecords.push(...resp.records);

      if (resp.records.length === limit) {
        return fetchRecords(appId, offset + limit, limit, allRecords);
      }
      return allRecords;
    } catch (error) {
      console.error('予算管理アプリのレコード取得に失敗しました', error);
      throw error;
    }
  };

  /**
   * 【データ取得・加工】予実管理データのカスタマイズビュー用データの作成
   * @param {Array} records - 予算レコード配列
   * @return {Promise<Array>} 予実データ配列
   */
  const makeYojitsuData = async (records) => {
    const allData = [];

    for (const record of records) {
      // 拠点名の取得
      const locationName = record['拠点'].value;

      // 予算額の取得
      const budgetAmount = parseInt(record['予算'].value, 10);

      try {
        // 実績管理アプリから該当拠点のレコードを取得
        const body = {
          app: RESULTS_APP_ID,
          query: `拠点 = "${locationName}"`,
        };
        const resp = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', body);

        // 実績合計を計算(該当拠点の全レコードの実績合計フィールドを集計)
        const totalResults = resp.records.reduce((sum, obj) => {
          return sum + parseInt(obj['実績合計'].value, 10);
        }, 0);

        // 差異と達成率を計算
        const difference = totalResults - budgetAmount;
        const achievementRate = budgetAmount !== 0 ? (totalResults / budgetAmount) * 100 : 0;

        allData.push({
          segment: locationName,
          budget: budgetAmount,
          results: totalResults,
          difference,
          achievementRate,
        });
      } catch (error) {
        console.error(`実績管理情報が取得できませんでした。`, error);
      }
    }
    return allData;
  };

  /**
   * 【ユーティリティ】テーブルセル作成
   * @param {String} text - セルのテキスト
   * @param {String} className - CSSクラス名
   * @param {String} additionalClass - 追加のCSSクラス名(オプショナル)
   * @return {HTMLElement} td要素
   */
  const createTableCell = (text, className, additionalClass = '') => {
    const td = document.createElement('td');
    td.textContent = text;
    td.className = className;
    additionalClass && td.classList.add(additionalClass);
    return td;
  };

  /**
   * 【UI描画】テーブルヘッダーの作成
   * @param {String} sortColumn - ソート対象カラム
   * @param {String} sortOrder - ソート順
   * @return {HTMLElement} thead要素
   */
  const createTableHeader = (sortColumn, sortOrder) => {
    const thead = document.createElement('thead');
    const headerRow = document.createElement('tr');

    const columns = [
      {key: 'segment', label: '拠点'},
      {key: 'budget', label: '予算'},
      {key: 'results', label: '実績'},
      {key: 'difference', label: '差異'},
      {key: 'achievementRate', label: '達成率'},
    ];

    columns.forEach((col) => {
      const th = document.createElement('th');
      th.textContent = col.label;
      th.className = 'sortable';
      th.dataset.column = col.key;

      if (sortColumn === col.key) {
        const indicator = document.createElement('span');
        indicator.className = 'sort-indicator';
        indicator.textContent = sortOrder === 'asc' ? ' ▲' : ' ▼';
        th.appendChild(indicator);
      }

      th.addEventListener('click', () => handleSort(col.key));
      headerRow.appendChild(th);
    });

    thead.appendChild(headerRow);
    return thead;
  };

  /**
   * 【UI描画】テーブルボディの作成
   * @param {Array} pageData - ページング済みデータ
   * @return {HTMLElement} tbody要素
   */
  const createTableBody = (pageData) => {
    const tbody = document.createElement('tbody');

    pageData.forEach((row) => {
      const tr = document.createElement('tr');

      tr.appendChild(createTableCell(row.segment, 'segment-cell'));
      tr.appendChild(createTableCell(formatCurrency(row.budget), 'number-cell'));
      tr.appendChild(createTableCell(formatCurrency(row.results), 'number-cell'));
      tr.appendChild(
        createTableCell(
          formatCurrency(row.difference),
          'number-cell',
          row.difference < 0 ? 'negative' : '',
        ),
      );
      tr.appendChild(createTableCell(`${row.achievementRate.toFixed(2)}%`, 'rate-cell'));

      tbody.appendChild(tr);
    });

    return tbody;
  };

  /**
   * 【UI描画】
   * テーブルのレンダリング
   */
  const renderTable = () => {
    const {data, currentPage, rowsPerPage, sortColumn, sortOrder} = state;

    // ソート済みデータ
    const sortedData = sortColumn ? sortData(data, sortColumn, sortOrder) : data;

    // ページング
    const startIndex = (currentPage - 1) * rowsPerPage;
    const endIndex = startIndex + rowsPerPage;
    const pageData = sortedData.slice(startIndex, endIndex);

    const tableContainer = document.getElementById('view');
    let table = tableContainer.querySelector('.yojitsu-table');

    // 既存のテーブルがあるか確認
    if (!table) {
      // 初回作成:テーブル全体を作成
      table = document.createElement('table');
      table.className = 'yojitsu-table';

      const thead = createTableHeader(sortColumn, sortOrder);
      table.appendChild(thead);

      const tbody = createTableBody(pageData);
      table.appendChild(tbody);

      tableContainer.appendChild(table);
    } else {
      // 既存テーブルの更新:ヘッダーとボディを更新
      const oldThead = table.querySelector('thead');
      const newThead = createTableHeader(sortColumn, sortOrder);
      table.replaceChild(newThead, oldThead);

      const oldTbody = table.querySelector('tbody');
      const newTbody = createTableBody(pageData);
      table.replaceChild(newTbody, oldTbody);
    }

    renderPager(sortedData.length);
  };

  /**
   * 【UI描画】ページャーのレンダリング
   * @param {Number} totalRows - 総レコード数
   */
  const renderPager = (totalRows) => {
    const {currentPage, rowsPerPage} = state;
    const totalPages = Math.ceil(totalRows / rowsPerPage);

    const pagerContainer = document.getElementById('pager');
    pagerContainer.innerHTML = '';

    const pager = document.createElement('div');
    pager.className = 'pager-controls';

    const rowsPerPageLabel = document.createElement('span');
    rowsPerPageLabel.textContent = '表示件数: ';
    pager.appendChild(rowsPerPageLabel);

    const select = document.createElement('select');
    select.className = 'rows-per-page';
    [1, 10, 20].forEach((num) => {
      const option = document.createElement('option');
      option.value = num;
      option.textContent = num;
      if (num === rowsPerPage) option.selected = true;
      select.appendChild(option);
    });
    select.addEventListener('change', (e) => {
      state.rowsPerPage = parseInt(e.target.value, 10);
      state.currentPage = 1;
      renderTable();
    });
    pager.appendChild(select);

    const pageInfo = document.createElement('span');
    pageInfo.className = 'page-info';
    const startRow = (currentPage - 1) * rowsPerPage + 1;
    const endRow = Math.min(currentPage * rowsPerPage, totalRows);
    pageInfo.textContent = ` | ${startRow}-${endRow} / ${totalRows}件`;
    pager.appendChild(pageInfo);

    const buttonContainer = document.createElement('div');
    buttonContainer.className = 'pagination-buttons';

    const firstBtn = createPagerButton(
      '≪',
      () => {
        state.currentPage = 1;
        renderTable();
      },
      currentPage === 1,
    );
    buttonContainer.appendChild(firstBtn);

    const prevBtn = createPagerButton(
      '‹',
      () => {
        state.currentPage--;
        renderTable();
      },
      currentPage === 1,
    );
    buttonContainer.appendChild(prevBtn);

    const pageNum = document.createElement('span');
    pageNum.className = 'page-number';
    pageNum.textContent = ` ${currentPage} / ${totalPages} `;
    buttonContainer.appendChild(pageNum);

    const nextBtn = createPagerButton(
      '›',
      () => {
        state.currentPage++;
        renderTable();
      },
      currentPage === totalPages,
    );
    buttonContainer.appendChild(nextBtn);

    const lastBtn = createPagerButton(
      '≫',
      () => {
        state.currentPage = totalPages;
        renderTable();
      },
      currentPage === totalPages,
    );
    buttonContainer.appendChild(lastBtn);

    pager.appendChild(buttonContainer);
    pagerContainer.appendChild(pager);
  };

  /**
   * 【UI描画】ページャーボタン作成
   * @param {String} text - ボタンテキスト
   * @param {Function} onClick - クリックハンドラ
   * @param {Boolean} disabled - 無効化フラグ
   * @return {HTMLElement} ボタン要素
   */
  const createPagerButton = (text, onClick, disabled) => {
    const button = document.createElement('button');
    button.textContent = text;
    button.className = 'pager-button';
    button.disabled = disabled;
    button.addEventListener('click', onClick);
    return button;
  };

  /**
   * 【UI描画】ソート処理
   * @param {String} column - ソート対象カラム
   */
  const handleSort = (column) => {
    if (state.sortColumn === column) {
      state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc';
    } else {
      state.sortColumn = column;
      state.sortOrder = 'asc';
    }
    renderTable();
  };

  /**
   * 【ユーティリティ】通貨フォーマット
   * @param {Number} value - 数値
   * @return {String} フォーマット済み通貨文字列
   */
  const formatCurrency = (value) => {
    return new Intl.NumberFormat('ja-JP', {
      style: 'currency',
      currency: 'JPY',
    }).format(value);
  };

  /**
   * 【ユーティリティ】データのソート
   * @param {Array} data - ソート対象データ
   * @param {String} column - ソート対象カラム
   * @param {String} order - ソート順(asc/desc)
   * @return {Array} ソート済みデータ
   */
  const sortData = (data, column, order) => {
    return [...data].sort((a, b) => {
      const aVal = a[column];
      const bVal = b[column];

      if (aVal < bVal) return order === 'asc' ? -1 : 1;
      if (aVal > bVal) return order === 'asc' ? 1 : -1;
      return 0;
    });
  };

  // イベント登録
  kintone.events.on(['app.record.index.show'], async (event) => {
    try {
      const records = await fetchRecords(BUDGET_APP_ID);
      await dispYojitsuCustomizeView(records);
    } catch (error) {
      console.error('エラーが発生しました', error);
    }
    return event;
  });
})();

CSS

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/*
 * カスタマイズビューのサンプルスタイル
 * Copyright (c) 2026 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

/* コンテナ */
#view {
  width: 100%;
  max-width: 1100px;
  margin: 20px auto;
  overflow-x: auto;
}

/* テーブル */
.yojitsu-table {
  width: 100%;
  border-collapse: collapse;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  font-size: 14px;
}

/* 共通スタイル */
.yojitsu-table th,
.yojitsu-table td {
  padding: 12px 8px;
}

.yojitsu-table th:last-child,
.yojitsu-table td:last-child {
  border-right: none;
}

/* ヘッダー */
.yojitsu-table thead {
  background-color: #4a90e2;
  color: #fff;
}

.yojitsu-table th {
  text-align: center;
  font-weight: 600;
  border-right: 1px solid rgba(255, 255, 255, 0.3);
  cursor: pointer;
  user-select: none;
  transition: background-color 0.2s;
}

.yojitsu-table th:hover {
  background-color: #357abd;
}

.yojitsu-table th.sortable {
  position: relative;
}

.sort-indicator {
  margin-left: 4px;
  font-size: 12px;
}

/* ボディ */
.yojitsu-table tbody tr {
  border-bottom: 1px solid #e0e0e0;
  transition: background-color 0.2s;
}

.yojitsu-table tbody tr:hover {
  background-color: #f5f5f5;
}

.yojitsu-table tbody tr:last-child {
  border-bottom: none;
}

.yojitsu-table td {
  border-right: 1px solid #e0e0e0;
}

/* セルのスタイル */
.segment-cell {
  text-align: center;
  font-weight: 500;
}

.number-cell {
  text-align: right;
}

.number-cell.negative {
  color: #ff0000;
  font-weight: 600;
}

.rate-cell {
  text-align: center;
  font-weight: 500;
}

/* ページャー */
#pager {
  margin: 20px auto;
  max-width: 1100px;
  padding: 0 10px;
}

.pager-controls {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 15px 20px;
  background-color: #f9f9f9;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 14px;
}

.rows-per-page {
  padding: 6px 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background-color: #fff;
  cursor: pointer;
  outline: none;
  transition: border-color 0.2s;
}

.rows-per-page:hover {
  border-color: #4a90e2;
}

.rows-per-page:focus {
  border-color: #4a90e2;
  box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}

.page-info {
  color: #666;
  margin-left: 10px;
}

.pagination-buttons {
  display: flex;
  gap: 5px;
  align-items: center;
}

.pager-button {
  padding: 6px 12px;
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
  outline: none;
  min-width: 36px;
}

.pager-button:hover:not(:disabled) {
  background-color: #4a90e2;
  color: #fff;
  border-color: #4a90e2;
}

.pager-button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  background-color: #f5f5f5;
}

.page-number {
  padding: 0 10px;
  font-weight: 600;
  color: #333;
}

/* エラーメッセージ */
.error-message {
  color: #d32f2f;
  padding: 20px;
  text-align: center;
  background-color: #ffebee;
  border: 1px solid #ef5350;
  border-radius: 4px;
  margin: 20px;
}

/* レスポンシブ対応 */
@media screen and (max-width: 768px) {
  .yojitsu-table {
    font-size: 12px;
  }

  .yojitsu-table th,
  .yojitsu-table td {
    padding: 8px 4px;
  }

  .pager-controls {
    flex-direction: column;
    align-items: stretch;
  }

  .pagination-buttons {
    justify-content: center;
  }
}

使用したAPI

  1. アプリのIDを取得する
  2. kintone REST APIリクエストを送信する
  3. APIのURLを取得する
  4. 複数のレコードを取得する
  5. イベントハンドラーを登録する
  6. レコード一覧画面を表示した後のイベント
information

このTipsは、2026年2月版kintoneで動作を確認しています。