クリスマスの夜でも大丈夫！コラボフローのユーザー更新をGASで「自動化」する方法！


:::note info
本記事で出来ること　▷ コラボフローの組織ユーザーの自動登録
この記事に関するすべてはコラボフローのサービスサポート対象外となります。
:::
今年のクリスマスは日曜日✨
25日は家族とゆっくりすごすぞ！…と思っていたのに。

「25日にワークフローのメンバー追加しておいてほしいから、出勤よろしくね！」



・
・
・
**なんだってー！！**
年に何度もない休日出勤を、よりにもよって**🎄クリスマス🎄**に依頼されてしまった…！

### クリスマスは絶対に出社したくない！！
なにかかんがえろ、家族との時間を死守するためにー！！！
25日に登録すればいいのでしょ？それであれば　**自動化** しちゃえばいいじゃない！


というわけで、作っちゃいました！
**「コラボフロー、自動ユーザー登録カスタマイズ！」**

:::note info
コラボフローとは（PR）
情シス以外のメンバーでも経路、フォームを作れる！知識があれば高度なこともできちゃう超おすすめワークフローサービスです。▶　[ワークフローならコラボフロー](https://www.collabo-style.co.jp/)
:::

## 必要なもの
* Google Apps Script（GAS）＆Googleスプレッドシート
* コラボフロー　管理者権限
* クリスマス出社をしない強い気持ち

## 完成イメージ
Googleスプレッドで予約をしたら、予約時間にユーザー追加される！

![コラボフロー連携.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/8bc3d12b-95e3-3a74-16d7-a28fb7d44ec9.gif)
（初めてキャプチャ動画とってみた！なんか緊張したｗ）

## 設定方法
### ◆コラボフローの設定
* APIキーを発行する　▷ [REST API クイックスタート](https://collaboflow.zendesk.com/hc/ja/articles/360000262636)

### ◆Google Apps Script（GAS）＆Googleスプレッドシート
* スプレッドシートにシートを2つ作成する
　　・ユーザー一覧 （項目＆更新情報は右記参照　▷ [組織情報をCSVで一括処理する](https://collaboflow.zendesk.com/hc/ja/articles/360000889015)）
　　・登録時間設定　
* 以下のソースコードの固有情報（★の箇所：APIエンドポイント、ユーザーID、パスワード、APIキー）を書き換え、Apps scriptに登録する。
　　<details><summary><b>ソースコードを表示（折りたたみ）</b></summary><div>

```Google Apps Script
// コラボフローREST APIリファレンス　http://docs.collaboflow.com/api-docs/
// APIアクセスに必要な定数リスト
// 固有情報 ★の４か所箇所　を変更してご利用ください。

const URL = "https://cloud.collaboflow.com/hogehogesanta/api/index.cfm/v1/users"; //★ユーザー取得用エンドポイントURL
const URL_BULK = "https://cloud.collaboflow.com/hogehogesanta/api/index.cfm/v1/bulk/users"; //★ユーザー一括更新用エンドポイントURL
const API_KEY = "hogehogesantanoapikey";　//★APIキー
const USER = "Christmasyasumitai"; //★操作するユーザー名
const BASE64_ENCODED_KEY = Utilities.base64Encode(USER + "/apikey:" + API_KEY, Utilities.Charset.UTF_8);

//スプレッドシート情報
const ss = SpreadsheetApp.getActiveSpreadsheet();
const user_list_sheet = ss.getSheetByName("ユーザー一覧");
const trigger_sheet = ss.getSheetByName("登録時間設定");
const sheet_header = user_list_sheet.getRange(1,1,1,user_list_sheet.getLastColumn()).getValues()[0];

/** 
 * コラボフローから最新のユーザー一覧を取得する関数
 * @param {object} requestHeaders RESTAPIアクセスに必要なリクエストヘッダー
 * @param {object} requestOptions UrlFetchApp関数に渡すオプション
 */

const getUserList = () => { 
  const requestHeaders = {
      'X-Collaboflow-Authorization' : "Basic " + BASE64_ENCODED_KEY,
  }
  const requestOptions = {
    'method' : 'GET',
    'headers' : requestHeaders
  }
  try {
    const response = UrlFetchApp.fetch(URL + "?limit=100", requestOptions);
    setListData(JSON.parse(response));
    Browser.msgBox("ユーザー情報の取得が完了しました。");
  }
  catch (e) {
    Browser.msgBox("エラーが発生しました。",e.message,Browser.Buttons.OK);
  }
}

/** 
 * getUserList関数で取得したデータをスプレッドシートに展開する関数
 * @param {array}} user_list APIで取得したユーザーデータの配列
 * @param {array} users スプレッドシート展開に使用する親配列
 *  @param {array} user 取得した一人分のユーザー情報を格納する子配列
 */// 
 
const setListData = (response) => {
  const user_list = response.records;
  const users = [];
  user_list.forEach(function(value){
    const user = [];
    sheet_header.forEach(function(attr,index){
      if(Array.isArray(value[attr])){
        user[index] = value[attr].join();
      }else{
        user[index] = value[attr] ?? " ";
      }
    });
    users.push(user);
  });
  user_list_sheet.getRange(2,1,users.length,sheet_header.length).setValues(users);
}

/** 
 * スプレッドシートで編集した内容から登録用データを作成する関数
 * @param {array}} users シートに記載された全ユーザー情報が格納されている二次元配列
 * @param {object}} creatRequestPayload関数の返り値を格納する親オブジェクト
 */

const saveUser = () =>{
  const users = user_list_sheet.getRange(1,1,user_list_sheet.getLastRow(),user_list_sheet.getLastColumn()).getValues();
  users.shift();//ヘッダーを削除
  // console.log(users);
  const postRecords = {
    "records": []
    }
  users.forEach((user) =>{
    postRecords.records.push(creatRequestPayload(user));
  });
  console.log(postRecords);
  responseCode = bulkUpdateReqest(postRecords);

}
/** 
 * saveUser関数で生成したデータをコラボフローに登録する関数
 * @param {object} requestHeaders RESTAPIアクセスに必要なリクエストヘッダー
 * @param {object} requestOptions UrlFetchApp関数に渡すオプション
 */

const bulkUpdateReqest = (requestPayload) =>{
  const requestHeaders = {
      'Content-Type': "application/json",
      'X-Collaboflow-Authorization' : "Basic " + BASE64_ENCODED_KEY,
  }
  const requestOptions = {
    'method' : 'POST',
    'payload' : JSON.stringify(requestPayload),
    'headers' : requestHeaders,
  }
  try {
    const response = UrlFetchApp.fetch(URL_BULK, requestOptions);
    Browser.msgBox("正常に登録されました");
    Logger.log(response);
  }
  catch (e) {
    Browser.msgBox("エラーが発生しました。",e.message,Browser.Buttons.OK);
  }
}


/** 
 * スプレッドシートのヘッダーに一致するデータを取得する関数
 */

const getUserAttr = (name) => {
  for(i = 0; i < sheet_header.length; i++){
    if(sheet_header[i] == name){
      return i;
    }
  };
}

/** 
 * コラボフローAPIの仕様に適した形でユーザー情報のJSONデータを生成する関数
 * @param {object} requestPayload 一人分のユーザー情報を格納するオブジェクト
 * @return {object} 生成されたオブジェクト
 */

const creatRequestPayload = (user) =>{
  const requestPayload = {
    "userid": user[getUserAttr("userid")],
    "name": user[getUserAttr("name")],
    "name_reading": user[getUserAttr("name_reading")],
    "employee_code": user[getUserAttr("employee_code")],
    "phone": user[getUserAttr("phone")],
    "phone_mobile": user[getUserAttr("phone_mobile")],
    "fax": user[getUserAttr("fax")],
    "email": user[getUserAttr("email")],
    "email_mobile": user[getUserAttr("email_mobile")],
    "password_change_required": user[getUserAttr("password_change_required")],
    "lockout": user[getUserAttr("lockout")],
    "order": user[getUserAttr("order")],
    "admin": user[getUserAttr("admin")],
    "extra1": user[getUserAttr("extra1")],
    "extra2": user[getUserAttr("extra2")],
    "extra3": user[getUserAttr("extra3")],
    "extra4": user[getUserAttr("extra4")],
    "extra5": user[getUserAttr("extra5")],
    "password": user[getUserAttr("password")]
  }
  // "groups"と"titles"属性は空白のまま登録しようとするとエラーになるため、存在する場合のみ属性を追加する
  if(user[getUserAttr("groups")] !== ""){
    requestPayload["groups"] = user[getUserAttr("groups")].split(",");
  }
  if(user[getUserAttr("titles")] !== ""){
    requestPayload["titles"] = user[getUserAttr("titles")].split(",");
  }
  // パスワードが空の場合は属性自体を削除する
  if(requestPayload["password"] == ""){
    delete requestPayload["password"];
  }
  return requestPayload;
};

/** 
 * triggerシートから日時を取得してトリガーを設定する関数
 */

const setTrigger = () => {
  const trigger_list= trigger_sheet.getRange(1,1,trigger_sheet.getLastRow(),trigger_sheet.getLastColumn()).getValues();
  trigger_list.shift();//ヘッダーを削除
  trigger_list.forEach((values) =>{
    console.log(values[0]);
    if(values[1] !== "予約確定"){
      //トリガーの時間を設定
      const triggerDay = new Date(values[0]);
      console.log(triggerDay);
      ScriptApp.newTrigger("saveUser")
        .timeBased()
        .at(triggerDay)
        .create();
        values[1] = "予約確定";
    }
   })
  const test = trigger_sheet.getRange(2,1,trigger_sheet.getLastRow()-1,trigger_sheet.getLastColumn()).setValues(trigger_list);
  Browser.msgBox("正常に登録されました");
}

/** 
 * スプレッドシートに独自メニューボタンを追加する関数
 * @param {array}} 追加するボタンの内容を保持する配列
 */

const onOpen = () => {
  const  menuEntries = [
    {name: "最新のユーザー情報を取得する", functionName: "getUserList"},
    {name: "シートの内容で更新する", functionName: "saveUser"},
    {name: "登録予約を設定する", functionName: "setTrigger"}
  ];
  ss.addMenu("コラボフロー", menuEntries);
}　


```

</div></details>


## 使い方
### 1\. ユーザー情報を取得＆修正・追加
1\. メニュー【コラボフロー＞最新のユーザー情報を取得】　をクリック
![メニューの画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/8f355062-f3a6-d343-aef8-27be7c880548.png)

2\. ユーザー一覧シートにデータが取得される。
![データ取得.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/b3adee43-43e0-260f-d6b2-aa73da76b031.png)

3\. 取得されたデータを修正、追加する。
![データ登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/0b5f011b-ac75-316f-de87-873820ec9084.png)

### 2\.予約日時の設定をする

1\. 登録時間設定シートに　登録日時を記入する
2022/12/25 10:00
のように入力しましょう！
![登録時間設定sheet](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/d63ff6ef-de4f-95d9-cbcb-62c196f28967.png)


2\. メニュー【コラボフロー＞登録予約を設定する】　をクリック　
![メニューの画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/de6bc5ea-bdfe-4a91-5144-7eedd531af92.png)
3\. 予約確定になったことを確認して完了！
![予約確定](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/50155db5-1bc8-b475-c33b-a91c26777fd9.png)

### 3\.ワクワクしながら更新日時をまつ！
更新日時になったら、コラボフローが更新！

![コラボフロー登録済み画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3031474/6d0e2208-5b02-3471-c120-f2b15b0c89ce.png)


## GASソースコードについての注意点
・パスワードは空欄の場合更新されない、入力があれば更新されるように記載しています。
・更新ルールはコラボフローの[組織情報をCSVで一括処理する](https://collaboflow.zendesk.com/hc/ja/articles/360000889015)）に準拠します、必須項目が未入力の場合更新されません。
・おまけでシートの内容で更新するボタンも用意しました。即時反映することも可能です。

:::note alert
ソースコード等、**利用者本人の責任において**自由にご利用ください！
ちゃんと作ったつもりですがソースコードの保証はできかねます！
パスワードの絡む更新のため、必ず本運用前に必ずテスト環境などで試してから利用してくださいね。
:::




**これでクリスマスも安心してお休みできますね♪**
うまく使えば、期初の組織管理も行えちゃいますね！


さぁ、明日はクリスマスイブイブ！
@collabo-mori さんの記事も楽しみです🎄