ガルーンポータル活用Tips #2「社員紹介ポータル」

目次

はじめに

今回は、大企業向けグループウェア「サイボウズGaroon」のポータル活用企画の第二弾として、「社員紹介ポータル」を作成していきたいと思います。

完成図

キャプチャの左下のポートレットが今回作成していくポートレットです。
特定の期間内に入社した人を紹介するポータルとして活用していきます。
kintoneアプリからデータを取得し、ランダムに決められた数の社員を紹介する処理を加えるので、毎回違った社員が表示され多くの社員紹介を目にできます。

基本的には前回の記事の ガルーンポータル活用Tips #1「行き先案内板」 と同じテクニックでkintoneのレコードを取得し、HTMLポートレットに表示していきます。

kintoneアプリ(自己紹介アプリ)の準備

Garoonのポータルからデータを参照させるためのアプリを作成していきます。
kintoneアプリのフィールドは以下のように配置していきます。

フィールド名 フィールドコード フィールドタイプ
氏名 name 文字列(1行)
入社日 hire_date 日付
写真 attachment 添付ファイル
配属部署 department ドロップダウン
座右の銘 motto 文字列(1行)
自己紹介 self_introduction 文字列(複数行)

認証は基本的にログインユーザーの権限を使います。
社員紹介のため問題ないと思いますが、このkintoneアプリのレコードの閲覧権限はすべてのユーザーで閲覧できるように設定しておくことをおすすめします。

リソースの準備

第一弾と同様に、静的ファイル置き場としてGaroonの「ファイル管理」を使います。

画像の準備

  1. 次の画像をローカルに保存します。

  2. 分かりやすくするため、ファイル管理にポートレット用のフォルダーを作成します。
    今回は「新入社員紹介用」とします。
  3. 作成した「新入社員紹介用」フォルダーに手順1の画像をアップロードします。

CSSファイルの準備

次にレイアウト調整用ファイル「main.css」を作成します。

  1. CSSサンプルコード を参考に、「main.css」を作成してください。
    文字コードは「UTF-8」で保存します。
  2. ファイル管理の一覧画面で、 画像の準備 でアップロードした画像のダウンロードアイコンからリンクのアドレスをコピーします。
  3. 手順2でコピーしたリンクを CSSサンプルコード の223行目に記載します。
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/*
 * Garoon Portal sample program
 * Copyright (c) 2016 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */
/*
 *
 * Title: Calomama Graph
 * Last Modified: 2015-05-19
 * Description: Pages Style
 *
 */
/* =========== INDEX LIST ============
  1: RESET
  2: COMMON
  3: LAYOUT
  4: MODULE
====================================== */
/* ===================================
  1: RESET
====================================== */
/*@import "normalize";*/
/* ===================================
  2: COMMON
====================================== */
.template-news h1, .template-news h2, .template-news h3, .template-news h4, .template-news h5, .template-news h6 {
  margin: 0;
  font-weight: normal;
  line-height: 1.5;
}
.template-news p,
.template-news ul,
.template-news ol,
.template-news dl {
  list-style: none;
  margin: 0;
  line-height: 1.4;
  font-size: 11px;
  font-size: 1.1rem;
}
.template-news img {
  line-height: 1;
  vertical-align: top;
}
.template-news table {
  width: 100%;
  border-collapse: collapse;
}
.template-news th,
.template-news td {
  text-align: left;
}

/* ===================================
  4: MODULE
====================================== */
.template-news {
  background: #f2f2f2;
}
.template-news-footer {
  background: #666;
  padding: 30px;
  color: #fff;
}
.template-news-footer p {
  font-size: 9px;
  font-size: 0.9rem;
}
.template-news-footer .footer-nav-wrap {
  display: table;
  width: 100%;
}
.template-news-footer .footer-nav-wrap .nav-box {
  width: 33.33333333%;
  display: table-cell;
  box-sizing: border-box;
  padding: 20px 20px 0 0;
}
.template-news-footer .footer-nav-wrap .nav-box li {
  margin-bottom: 7px;
}
.template-news-footer .footer-nav-wrap .nav-box a {
  display: inline-block;
  padding-left: 15px;
  color: #fff;
  font-size: 9px;
  font-size: 0.9rem;
}
.template-news-footer .footer-nav-wrap .nav-box a:before {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 4px 0 4px 6px;
  border-color: transparent transparent transparent #ffffff;
  margin-right: 5px;
  margin-left: -15px;
}
.template-news-footer .footer-nav-wrap .nav-box a:hover {
  color: #ffff66;
}
.template-news-contents {
  padding: 30px;
  background: #f2f2f2;
}
.template-news-box {
  margin-bottom: 20px;
  padding: 20px 30px;
  background: #fff;
}
.template-news-box h2 {
  margin: 0 0 15px;
  padding: 0;
  font-weight: bold;
  font-size: 18px;
  font-size: 1.8rem;
}
.template-news-box h2 .icon {
  display: inline-block;
  margin-right: 5px;
  vertical-align: text-top;
}
.template-news-box.box-red {
  border-top: 3px solid #ff6666;
}
.template-news-box.box-yellow {
  border-top: 3px solid #ebb218;
}
.template-news-box.box-green {
  border-top: 3px solid #91cd5c;
}
.template-news-box.box-purple {
  border-top: 3px solid #c684ca;
}
.template-news-box.box-blue {
  border-top: 3px solid #61aee4;
}
.template-news-box.box-turquoise {
  border-top: 3px solid #25d3c4;
}
.template-news-box .box-content {
  margin-bottom: 20px;
  /*
        .truncate_more {
          @include font-size(10);
          line-height: 1.5;
        }
  */
}
.template-news-box .box-content:after {
  content: '';
  display: block;
  clear: both;
}
.template-news-box .box-content h3 {
  margin-bottom: 12px;
  font-size: 13px;
  font-size: 1.3rem;
  font-weight: bold;
}
.template-news-box .box-content p {
  font-size: 10px;
  font-size: 1rem;
  line-height: 1.5;
}
.template-news-box.box-layout-left .box-content > img {
  float: left;
  margin: 0 20px 10px 0;
}
.template-news-box.box-layout-right .box-content > img {
  float: right;
  margin: 0 0 10px 20px;
}
.template-news .btn a {
  display: block;
  border: 1px solid #e0e0e0;
  padding: 15px;
  color: #0a62ad;
  text-decoration: none;
}
.template-news .btn a:hover {
  background: #fbfbfb;
  color: #cc3300;
}
.template-news .btn.btn-more {
  padding-top: 10px;
  clear: both;
}
.template-news .btn.btn-more a {
  text-align: center;
}
.template-news .btn.btn-more a:after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 10px 6px 0 6px;
  border-color: #cccccc transparent transparent transparent;
  margin-left: 5px;
}
.template-news .btn.btn-more-close a:after {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0 6px 10px 6px;
  border-color: transparent transparent #cccccc transparent;
  margin-left: 5px;
}
.template-news .btn.btn-link {
  clear: both;
}
.template-news .btn.btn-link a {
  text-align: right;
}
.template-news .btn.btn-link a:after {
  content: url(../img/icn_link.png);
  content: url("/g/cabinet/download.csp/-/icn_link.png?fid=XX&ticket=&hid=XX&.png");
  width: 17px;
  height: 15px;
  display: inline-block;
  margin-left: 5px;
}

JavaScriptファイルの準備

  1. JavaScriptサンプルコード を参考に、「garoon-kin-integration.js」を作成してください。
    文字コードは「UTF-8」で保存します。

  2. 次の項目を書き換えます。

    • 46行目APP_IDを作成した「自己紹介アプリ」のアプリ番号に書き換えます。
    • 47行目DATE_NUMで、何日前以降に入社した人を抽出するかを設定します。

ポイント

  • 関数generateRandomxにて乱数を生成し、毎回ランダムに表示する順番をかえています。
  • 関数textTrimerにて自己紹介が100文字以上の場合はトリミングして末尾に「…」をつけています。
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
/*
* Garoon Portal sample program
* Copyright (c) 2016 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
*/

(() => {
  'use strict';
  // calculate N month age.
  const calcOneM = (n) =>{
    const dt = new Date();
    dt.setDate(dt.getDate() - parseInt(n, 10));
    const year = dt.getFullYear();
    const month = (dt.getMonth() < 10) ? '0' + (dt.getMonth() + 1) : dt.getMonth() + 1;
    const day = dt.getDate();
    return (year + '-' + month + '-' + day);
  };
  // generate randum numbers.
  const generateRandomx = (count) => {
    const generated = [];
    let generatedCount = generated.length;
    for (let i = 0; i < count; i++) {
      let candidate = Math.floor(Math.random() * count);
      for (let j = 0; j < generatedCount; j++) {
        if (candidate === generated[j]) {
          candidate = Math.floor(Math.random() * count);
          j = -1;
        }
      }
      generated[i] = candidate;
      generatedCount++;
    }
    return generated;
  };
  // 100 characters or more, the shortening.
  const textTrimer = (tx, limit) => {
    const afterTxt = '...';
    const textTrim = tx.substr(0, limit);
    if (limit < tx.length) {
      return textTrim + afterTxt;
    }
    return tx;
  };
  const APP_ID = 15;
  const DATE_NUM = 60;
  fetch(`/k/v1/records.json?app=${APP_ID}&hire_date>"${calcOneM(DATE_NUM)}"`, {
    method: 'GET',
    headers: {
      'X-Requested-With': 'XMLHttpRequest'
    }
  }).then(response => {
    return response.json();
  }).then((resp) => {
    const records = resp.records;
    const fileKeyArray = [];
    let contentsEle;
    let innerDiv;
    let count = 0;
    const randomArray = generateRandomx(records.length);
    const forNum = (records.length < 5) ? records.length : 5;
    if (records.length === 0) {
      const div = document.createElement('div');
      div.innerHTML = `<b>${calcOneM(DATE_NUM)} 以降に入社した方はいません</b>`;
      const newsContents = document.querySelector('.template-news-contents');
      newsContents.insertBefore(div, newsContents.firstChild);
      const p = document.createElement('p');
      p.textContent = 'Not Found';
      document.querySelector('.template-news-box').appendChild(p);
      return;
    }
    for (let i = 0; i < forNum; i++) {
      contentsEle = document.getElementById(`contents-${i}`);
      const h2 = document.createElement('h2');
      h2.textContent = `${records[randomArray[i]].name.value} (${records[randomArray[i]].department.value})`;
      contentsEle.appendChild(h2);
      innerDiv = document.createElement('div');
      innerDiv.className = 'box-content';
      const img = document.createElement('img');
      img.id = `capture-${i}`;
      img.width = 120;
      img.height = 120;
      innerDiv.appendChild(img);
      const h3 = document.createElement('h3');
      h3.textContent = `座右の銘 : ${records[randomArray[i]].motto.value}`;
      innerDiv.appendChild(h3);
      const p1 = document.createElement('p');
      p1.textContent = textTrimer(records[randomArray[i]].self_introduction.value, 100);
      innerDiv.appendChild(p1);
      const p2 = document.createElement('p');
      p2.className = 'btn btn-link';
      const a = document.createElement('a');
      a.href = `/k/${APP_ID}/show#record=${records[randomArray[i]].$id.value}`;
      a.textContent = '自己紹介詳細へ';
      p2.appendChild(a);
      innerDiv.appendChild(p2);
      contentsEle.appendChild(innerDiv);
      fileKeyArray.push(records[randomArray[i]].attachment.value[0].fileKey);
    }
    if (forNum < 5) {
      for (let j = forNum; j < 5; j++) {
        const contents = document.getElementById(`contents-${j}`);
        const p = document.createElement('p');
        p.textContent = 'Not Found';
        contents.appendChild(p);
      }
    }
    // generate file objects.
    const loopFetchFile = (key) => {
      return fetch(`/k/v1/file.json?fileKey=${key}`, {
        method: 'GET',
        headers: {
          'X-Requested-With': 'XMLHttpRequest'
        }
      }).then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.blob();
      }).then(blob => {
        const url = window.URL || window.webkitURL;
        const imageObj = url.createObjectURL(blob);
        document.getElementById(`capture-${count}`).src = imageObj;
        if (count < fileKeyArray.length - 1) {
          count++;
          return loopFetchFile(fileKeyArray[count]);
        }
      });
    };
    loopFetchFile(fileKeyArray[count]);
  }); // end of done
})();

Garoonポートレットの準備

ポートレットのHTML

今回使うポートレットを作成していきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!--
* Garoon Portal sample program
* Copyright (c) 2016 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
-->
<div class="template-news">
  <div class="template-news-contents">
    <div id="contents-0" class="template-news-box box-yellow box-layout-left">
    </div>
    <div id="contents-1" class="template-news-box box-green box-layout-left">
    </div>
    <div id="contents-2" class="template-news-box box-purple box-layout-left">
    </div>
    <div id="contents-3" class="template-news-box box-blue box-layout-left">
    </div>
    <div id="contents-4" class="template-news-box box-turquoise box-layout-left">
    </div>
  </div>
</div>

JavaScriptファイルとCSSファイルのアップロード

JavaScript/CSSによるカスタマイズから「garoon-kin-integration.js」と「main.css」をアップロードします。

ポータルに配置

ポートレットを作成したらポータルに配置しましょう。

動作確認

kintoneにいくつかのレコードを登録します。

JavaScriptで50日前以降に入社した社員を、Garoonポートレットで絞り込んで表示するように指定します。(実行日:2016/07/15)
Garoonを開いて設置したポータルを確認します。

おわりに

無事に社員紹介ポートレットができました!

今回は、Garoonポートレットをkintoneと連携させて動的に社員紹介を掲示するポートレットを作成しました。
応用すれば、他にもいろいろなパターンで動的なポートレットを作成できると思うのでぜひチャレンジしてみてください!

ガルーンポータル活用Tips

更新履歴

  • 2020/02/19
    jQueryの追加手順およびjQuery.noConflict(true)を使うようにコードを修正
  • 2024/04/24
    • jQueryを使わないようにコードを修正
    • JavaScriptとCSSファイルをファイル管理にアップロードする方法から、ポータルの「JavaScript / CSSによるカスタマイズ」を使用する方法に変更
information

このTipsは、2024年4月版Garoonとkintoneで動作を確認しています。