GASで作る社内ツール (キャンセル・報告編)

# はじめに
[Apps Script Advent Calendar 2019 - Qiita](https://qiita.com/advent-calendar/2019/gas) の1日目、兼、[前回 (申請編)](https://qiita.com/nyanko-box/items/1cccba459142a4493270) からの続きです。申請を出した予定のキャンセルと報告機能を実装しました。
[Google Apps Scriptをウェブアプリケーションとして公開する手順](https://www.virment.com/how-to-deploy-google-apps-script-as-webapp/) についてはこちらを参考にいたしました。

# ダミーデータ準備
本記事用のダミー予定2件の内容です。
![2019-11-26_17h39_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/a9442e83-c543-0cca-ff93-ffaf945a8b15.png)

# 予定キャンセル/報告機能の実装
トップページから「予定キャンセル/報告」タブ (下図赤丸) をクリックすると、出張管理データベースから未報告の予定を取得し、カード形式で一覧が表示されます。予定1個に対してボタンをそれぞれ配置したかったのでカード形式にしています。リスト形式で右側にボタンを配置するようなレイアウトも考えたのですが、うまいこと実装できずにこの形式に落ち着きました。
![2019-11-26_17h35_31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/bec6f31a-1cf3-b66a-0b42-7188574b16d0.png)

下記が該当箇所のHTMLとJavaScriptのコードです。

```html
<form>
  <br>
  <div class="list-group">
    <div class="alert alert-info" role="alert">
      <h4>申請済みの予定から、実行する操作を選択してください</h4>
      申請取り下げ: 申請していた予定を削除します<br>
      報告: 報告書を作成します。 クリック後ページ下部へ情報を入力してください
    </div>
    <div id="prefixArea" class="form-row">
    <!--- ここにカードが挿入される --->
    </div>
  </div>
</form>
```

```javascript
function cancel_makeButtonList(arg_pData) {
  var plan_element;
  var str_html;
  var tgtArea_element;
  
  for(var i=0; i<arg_pData.length; i++) {
    plan_element = document.createElement("div");
    plan_element.setAttribute('className', 'form-row');

    // カードの生成:
    // アクション対象の予定を決定するために、
    // 出張管理データベースの行番号をクリック時実行関数に引数に渡す
    str_html  = '<div class="card" id="prefixedPlan[' + i + ']">';
    str_html += '<div class="card-header">'+ arg_pData[i] +'</div>';
    str_html += '<div class="card-body">';
    // 取り下げボタン: クリックでデータベースから該当行を削除する関数を呼び出す
    str_html += '<button type="button" class="btn btn-outline-danger" onClick="cancel_delSelectedPlan(' + i + ')">申請取り下げ</button>';
    // 報告ボタン: クリックでページ下部の報告フォームに情報をセットする
    str_html += '<button type="button" class="btn btn-outline-primary" onClick="report_getPlanInfo(' + i + ')">報告</button>'
    str_html += '</div></div>';

    plan_element.innerHTML = str_html;
    
    // カードを所定の箇所に追加
    tgtArea_element = document.getElementById("prefixArea");
    tgtArea_element.appendChild(plan_element);
  }
}

function cancel_getPrefixedPlans() {
  if(IS_MAKE_LIST == false) {  
    IS_MAKE_LIST = true;  // タブをクリックするたびにカードが生成されることを防ぐ
    google.script.run.withSuccessHandler(function(prefixedPlanData) {
      cancel_makeButtonList(prefixedPlanData); // GASから受信した予定一覧をカードを生成する関数へ渡す
    }).getPrefixedPlans(); // スプレッドシートから未報告の予定を取得して、【管理番号】日付 目的@場所 という文字列で返すGAS側の処理
  }
  else{
    return;
  }
}

```

## 申請取り下げ
上記カードから申請取り下げボタンを押したときに行なう処理です。データベースから対象の行がクリアされます。
![2019-11-26_18h08_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/481975a2-f7a5-39e6-fd96-f36d3b3045e7.png)
![2019-11-26_18h08_57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/a2a8344c-1b15-a5e6-41af-3adc956f4dac.png)
図のように1個目 (12/20) の予定がクリアされ、L列にキャンセルフラグがセットされました。
※ 申請ごとの管理番号を行数から計算している関係で、行そのものは残してキャンセルフラグを立てることにしています。
この処理のJavascriptコードがこちら。

```javascript
 function cancel_delSelectedPlan(idx) {
   // JS側の引数が0ベースであるのに対してデータベースの行番号は2ベース (基本的には1ベースで1行目は見出し)
   var tgt = idx+2;

   google.script.run.deleteTargetRow(tgt); // スプレッドシートの tgt行 を削除するGAS側の関数
   
   alert("選択した予定を削除しました");
   IS_MAKE_LIST = false;
   // window.location.reload(); // 再読み込みはGAS仕様で制限: 白くなって終わる
   window.open(<URL>, '_top'); // @Hiroskey 様よりコメントいただきました
 }
```
~~[こちら](https://teratail.com/questions/146003) でも議論されていましたが、`window.location.reload()`をコールしても白くなってそのままです。`href()`に変えても駄目でした。このあたりに知見のある方は教えていただけると嬉しいです。~~

[2020/10/16: 追記]
@Hiroskey 様よりコメントいただきましたが、
`window.open(<URL>, '_top');`
でTOP画面に遷移するようにすれば所望の挙動をしてくれます。

## 報告
上記カードから報告ボタンを押したときの処理です。申請時の情報 (期間、行き先、目的、管理番号) をデータベースから取得し、入力フォームにセットします。
![2019-11-26_18h19_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/f25a1c34-714b-74ae-4289-c2776a553026.png)
あとは報告内容を詳細に記述して提出ボタンを押すだけです。
提出ボタンを押すとGoogleドライブ上に出張フォルダが作成され、その中に報告書ファイルが生成されます。
出張内容によっては守秘義務契約が絡むこともあるということで、閲覧制限チェックボックスを配置しました。チェックを入れて提出すると、生成されるフォルダのアクセス権が自分と会社の偉い人のみになります。
![2019-11-26_18h26_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/7bca0d51-6a6c-9c5b-bad6-41156dc2f53c.png)
提出処理が完了すると、別タブでGoogleドライブの報告書フォルダが開きます。写真を撮っていたり資料を貰っている場合は、ここにアップロードすれば部内共有がスムーズにできます。
最初はファイルアップローダーを実装してBlob処理する関数を勉強していましたが、Googleドライブを開いちゃえば楽だし高機能だしってことで即オミットしました。
![2019-11-26_18h30_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131278/75a5649c-c067-e62a-d4af-a0a8ce26bc02.png)

本処理のJavascriptコードがこちらです。

```javascript
function send_reportInfo() {

  // 報告対象の行番号を取得
  const report_row_num = document.getElementById("report_row_num").value; // データベース内の行 (報告列にフラグを立てるために使用)
  
  // 報告内容を取得
  const report_txt = document.getElementById("rep_txtarea").value;
  
  // ボタンの無効化とスピナー表示
  $('#btn_send_report').empty(); // 一度全て削除
  $('#btn_send_report').append('<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>送信中…');
  $('#btn_send_report').prop('disabled', false);
  // 一度要素を空にしてから追加してますけど置換でも良さそうですね replaceWith()
  
  // 同行者
  const name_comp = document.getElementById("report_accompanies").value;
  
  // 閲覧制限フラグ
  const is_folderLimit = document.getElementById("rep_check_limitation").checked; 
  
  // 自動入力される予定情報を取得
  const report_period = document.getElementById("report_period").value; // 期間
  const report_site = document.getElementById("report_site").value; // 目的地
  const report_purpose = document.getElementById("report_purpose").value; // 目的
  const report_mng_num = document.getElementById("report_mng_num").value; // 管理番号 (報告書のタイトルに使用する)
  
  // 報告書に入力する情報をリストにしてGAS側に渡す
  const report_info = [report_mng_num, name_comp, report_period, report_site, report_purpose, report_txt, is_folderLimit];
    
  google.script.run.withSuccessHandler(function(planFolderInfo) {      
      /* データベース内の対象予定に報告フラグを立て，報告書フォルダのURLを入力する */
      google.script.run.set_reportFlag(planFolderInfo[0], report_row_num);
      
      /* 別タブで報告書フォルダを開く */
      alert("報告書の作成が完了しました。\nGoogleドライブを開きます。");
      window.open(planFolderInfo[0]);
      // window.location.reload();
      window.open(<URL>, '_top'); // @Hiroskey 様よりコメントいただきました
  }).make_reportSheet(report_info);
}
```

# 終わりに
以上で弊社の出張管理システムの実装ができました。12月中は部員による試用期間とし、来年1月から運用を開始します。従来の申請方法が特殊も特殊で自動管理が難しかったので、今回GAS・JavaScript・HTMLを勉強して実装しました。
このシステムを用いた申請・報告1回につき100円でもいいから給与を増やしてほしい・・・私嬉しい・・・・
通常の (?) Webアプリを作ったことが無いので実感がわかないですが、GASによるWebアプリでは制限もあるようです。そこさえクリティカルでなければ社内ツールの開発なども簡単にできそうです。本記事がそういった実装を行なう方の参考になれば幸いです。

# 参考
1. [Google Apps Scriptをウェブアプリケーションとして公開する手順](https://www.virment.com/how-to-deploy-google-apps-script-as-webapp/)
2. [GASで表示したWEBアプリページを再読み込みしたい](https://teratail.com/questions/146003)
3. [HTML Service: Communicate with Server Functions](https://developers.google.com/apps-script/guides/html/communication)
4. [HTML要素の追加方法いろいろ　挿入位置で分類](https://qiita.com/butakoma/items/89fa687ab90ff28d57ef)
5. [JQueryの目的の場所に要素を追加](https://qiita.com/hitonari45/items/74e625c1feee9a43279e)
6. [Bootstrap - List group](https://getbootstrap.com/docs/4.0/components/list-group/)
7. [Bootstrap - card](https://getbootstrap.com/docs/4.0/components/card/)