作業の記録をとりたくて・・・GAS

#はじめに
上手にスケジュールを立てるにはまず**自分がどの作業にどれくらい時間を使っているのか**を把握するところから！と言うことで、ちまちま作業記録をGoogleカレンダーに付けていたのですが、開始時間を忘れてしまったり、そもそもつけるのを忘れてしまったり…。そして何よりめんどくさい…。
ということで、GASでボタンを押すだけで作業記録が取れるようにタイムカード的なものを作ってみました。

#完成品
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/6be94666-19e3-9543-0a8b-7c1f40a50f79.gif)

1. 必要項目を入力してstartボタンを押下し、作業開始
2. もくもくと作業する
3. 作業が終了したらstopボタンを押下し、作業終了

するとこんな感じでgoogleカレンダーに登録されます。楽ちん！
<img width="400" alt="グループ 1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/6fa34082-c7f7-a91f-54f9-7ad81114aca7.png">

#使うもの
使用するのは下記の3つだけです。
1. Googleスプレッドシート
2. Googleカレンダー
3. Google Apps Script（GAS）

#実装
それでは早速実装していきましょう！

##まずは下準備
###1. スプレッドシートを新規作成し、記録をつけたいカレンダーの情報をまとめておく
<img width="400" alt="グループ 2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/28fe81d0-bea9-37f5-d12b-6583a08c0454.png">
GASを使って特定のGoogleカレンダーに予定（今回の場合は記録）を登録するためにはカレンダーIDが必要になります。
私は予定によってカレンダーをかなり細かく分けており、記録をつける際もカレンダーを分けたかったので、新しいスプレッドシートを作成して記録をつけたいカレンダーの名前とIDをまとめておきました。
ここで名前に設定したものがカレンダーのプルダウンで表示されます。
<img width="300" alt="スクリーンショット 2021-12-14 20.21.10" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/1cd3162b-0b50-4630-be5f-652e63267c09.png">
ちなみにカレンダーIDはGoogleカレンダーを開いて、使いたいカレンダー横の3点リーダーをクリックして、
<img width="300" alt="グループ 9" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/b3684015-f815-c2b9-bba9-ae1429e4372f.png">
設定と共有を開き、
<img width="300" alt="グループ 10" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/3b435da8-162c-8ad4-0095-3a75f82a2aae.png">
カレンダーの統合から取得できます。
<img width="500" alt="グループ 3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/204a141c-4acb-fbe1-bcc6-83987315e247.png">

###2. 1.で作成したスプレッドシートからスクリプトエディタを開いて、必要なファイルを作成する
1.で作成したスプレッドシートのメニュー内、**拡張機能>Apps Script**からエディタを開きます。
<img width="300" alt="グループ 4" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/e1b33d4b-e3f3-bbdd-8551-600147cbf3b8.png">
<img width="500" alt="スクリーンショット 2021-12-14 20.53.08" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/57d4b9dd-76e6-1639-3ab2-f6f0c2aaf808.png">
コード.gsは最初から用意されていますが、それ以外に必要なファイルは**ファイル**横の＋ボタンから任意で追加作成します。
<img width="300" alt="グループ 5" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/f87c3858-990c-dfb4-5665-582afdd9fc33.png">
下準備はこれで完了です！次は作成したファイルにコードを書いていきます。

##コードを書く

###GAS
スプレッドシートやカレンダーの操作等、Googleのサーバーにアクセスする処理は.gsファイルに記述していきます。

```Javascript:コード.gs
// 公開したWebアプリにアクセスがあった場合に実行
function doGet() {
  // 表示させるhtmlを指定
  const temp = HtmlService.createTemplateFromFile("index");
  // スプレッドシートからカレンダーの情報を取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getActiveSheet();
  const calData = sheet.getRange(2, 1, sheet.getLastRow()-1, 2).getValues();

  temp.calData = calData;

  return temp
    .evaluate()
    .setTitle('タイムカード')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// カレンダーに記録を登録
function addCal(calId,calTtl,start,end,calDesc){
  const calendar = CalendarApp.getCalendarById(calId);
  const title = calTtl;
  const startTime = new Date(start);
  const endTime = new Date(end);
  const option = {
    description: calDesc
  }
  
  calendar.createEvent(title, startTime, endTime, option);
}
```

`doGet(e)` 関数は特定のイベントが発生したときに自動で実行されるシンプルトリガーの一種で、ユーザーがWebアプリにアクセスした際に実行されます。

※以下は上記ソースコードからの抜粋です

```Javascript:コード.gs
// スプレッドシートからカレンダーの情報を取得
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getActiveSheet();
const calData = sheet.getRange(2, 1, sheet.getLastRow()-1, 2).getValues();

temp.calData = calData;
```
この関数内でスプレッドシートからカレンダーの情報を取得し、テンプレートのプロパティに設定しておきます。
こうすることでHTMLに記述した`<? ... ?>`のような**スクリプトレット**を使用して参照することができます。

スクリプトレットについてはこちらをご参照ください。

https://developers.google.com/apps-script/guides/html/templates?hl=en

###HTML
```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- cssを記述したHTMLファイルの読み込み　-->
    <?!= HtmlService.createHtmlOutputFromFile('reset').getContent(); ?>
    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
  </head>
  <body>
    <div class="wrap">
      <h1 id="heading" class="heading">作業開始する？</h1>
      <div id="time" class="time"></div>
      <select required id="cal">
        <option value="" hidden>カレンダー</option>
        <!-- スプレッドシートの情報からカレンダーを追加　-->
        <? for(let i = 0; i < calData.length; i++){?>
          <option value="<?= calData[i][1] ?>"><?= calData[i][0] ?></option>
        <?}?>
      </select>
      <input type="text" id="ttl" placeholder="タイトル">
      <textarea id="desc" placeholder="説明"></textarea>
      <div class="btn">
        <button type="button" id="startBtn" class="btn__item">start</button>
        <button type="button" id="stopBtn" class="btn__item">stop</button>
      </div>
    </div>
    <!-- jsを記述したHTMLファイルの読み込み　-->
    <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
  </body>
</html>
```
※以下は上記ソースコードからの抜粋です

```html:index.html
<!-- cssを記述したHTMLファイルの読み込み　-->
<?!= HtmlService.createHtmlOutputFromFile('reset').getContent(); ?>
<?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
```
プロジェクト内のcssやjsを記述したHTMLファイルもスクリプトレットを使用して読み込みます。

###Javascript
```html:js.html
<script>
  const startBtn = document.getElementById('startBtn');
  const stopBtn = document.getElementById('stopBtn');
  const heading = document.getElementById('heading');
  const time = document.getElementById('time');
  const cal = document.getElementById('cal');
  const ttl = document.getElementById('ttl');
  const desc = document.getElementById('desc');
  startBtn.disabled = true;
  stopBtn.disabled = true;

  // 他の作業を開始していない状態でカレンダーの選択とタイトルの入力がされたらスタートボタンのdisabledをfalseに変更
  // 説明の入力は任意
  document.addEventListener('change', () => {
    if(!cal.value || !ttl.value || !stopBtn.disabled) return;
    startBtn.disabled = false;
  })

  // スタートボタンを押下した際の挙動
  startBtn.addEventListener('click', () => {
    const startTime = new Date().toLocaleString();
    time.textContent = startTime;
    heading.textContent = '作業開始！';

    startBtn.disabled = true;
    stopBtn.disabled = false;
  });

  // ストップボタンを押下した際の挙動
  stopBtn.addEventListener('click', () => {
    const calId = cal.value;
    const calTtl = ttl.value;
    const startTime = time.textContent;
    const endTime = new Date().toLocaleString();
    const calDesc = desc.value;

    // コード.gsに定義したaddCal()を呼び出して入力された内容を渡す
    google.script.run.addCal(calId,calTtl,startTime,endTime,calDesc);
    reset();
  });

  // 初期化
  const reset = () => {
    startBtn.disabled = true;
    stopBtn.disabled = true;
    heading.textContent = '作業開始する？';
    time.textContent = '';
    cal.selectedIndex = 0;
    ttl.value = '';
    desc.value = '';
  }
</script>
```

※以下は上記ソースコードからの抜粋です

```javascript:js.html
// ストップボタンを押下した際の挙動
stopBtn.addEventListener('click', () => {
  const calId = cal.value;
  const calTtl = ttl.value;
  const startTime = time.textContent;
  const endTime = new Date().toLocaleString();
  const calDesc = desc.value;

  // コード.gsに定義したaddCal()を呼び出して入力された内容を渡す
  google.script.run.addCal(calId,calTtl,startTime,endTime,calDesc);
  reset();
});
```
`google.script.run`はクライアント側からサーバー側の関数を呼び出すAPIです。
`stopBtn`を押下したらカレンダーに記録をつけたいので、このAPIを使用してコード.gsに定義した`addCal()`を呼び出します。


###css

```html:css.html
<style>
  input,
  select,
  textarea {
    padding: 5px 10px;
    margin-top: 5px;
    width: 300px;
  }

  select:invalid {
    color: #757575;
  }
  
  ::placeholder {
    color: #757575;
  }

  button {
    border: none;
  }

  button:disabled {
    opacity: .5;
  }

  .wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    margin-top: 50px;
  }

  .ttl {
    font-size: 20px;
  }

  .time {
    min-height: 85px;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .btn {
    display: flex;
    margin-top: 20px;
    width: 300px;
  }

  .btn__item {
    flex: 1;
    padding: 12px;
    text-align: center;
    font-size: 14px;
    color: #fff;
    background: #798ebf;
    border-radius: 10px;
    cursor: pointer;
  }

  .btn__item + .btn__item {
    margin-left: 12px;
  }
</style>
```
reset.htmlはリセットcssなので省略します。私は[A Modern CSS Reset](https://github.com/hankchizljaw/modern-css-reset/blob/master/dist/reset.css)を使用しました！

##いざ、デプロイ!

エディタ右上の**デプロイ>新しいデプロイ**を開き、種類は**ウェブアプリ**を選択します。
それから、説明文、実行ユーザーとアクセスユーザーを設定してデプロイします。
<img width="300" alt="グループ 6" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/611f5515-5eb9-7a2b-b96a-0251b2b346f5.png">
<img width="400" alt="グループ 7" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/39125633-139c-e4f1-fc67-0b66e6300c37.png">
アクセス許可を求められるので、アカウントを選択して許可してあげます。
<img width="400" alt="グループ 11" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/9fdaeb52-750e-fcf9-a147-913ea38109af.png">
<img width="300" alt="グループ 12" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/2b592ddb-3e46-8c5a-e1a2-b9fa6ab6e785.png">
赤枠のURLにアクセスして問題なくページが表示されていれば完成です！やったー！
<img width="400" alt="グループ 8" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/5ae79337-1585-cbe3-2cbc-d50410ef493f.png">

##トラブルシューディング
あれ？登録できたけど時間がずれてるぞ？と思ったら…
<img width="400" alt="グループ 8" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/d33134f6-b565-052f-3384-3d56f5a02b6a.png">
タイムゾーンがずれている可能性があるので、一旦スクリプトエディタに戻って歯車マークの**プロジェクトの設定**を開きます。
<img width="400" alt="グループ 8" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/6e15e629-3c34-2de9-97c4-81a4d7ab95cc.png">
<img width="400" alt="グループ 8" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2319704/5da49cc4-beee-5ba8-d30b-2dd0a15cdcc1.png">
**「appsscript.json」マニフェスト ファイルをエディタで表示する**にチェックを入れてエディタに戻ると、**appsscript.json**が追加されているので開きます。

```json:appsscript.json
{
  "timeZone": "America/New_York",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "MYSELF"
  }
}
```
`timeZone`が`America/New_York`になっていますね。日本在住の方は下記のように`Asia/Tokyo`、それ以外の方はお住まいの地域のタイムゾーンに変更してあげましょう。

```json:appsscript.json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "MYSELF"
  }
}
```
保存して再度デプロイしてみてください！

#おわりに
サーバーサイドの知識がなくても簡単にサクサクっとWebアプリを公開できるし、Googleのアプリも操作できるし、GASって便利なやつですね。
他にもいろいろなことができそうなので、もう少し深堀して勉強してみたいです。
私のスケジューリングが上手になることを祈りつつ…**完！**

#参考
[GASでWebページを表示するdoGet関数のスクリプトについて丁寧に解説](https://tonari-it.com/gas-web-doget-html-output/)
[HTML Service: Templated HTML](https://developers.google.com/apps-script/guides/html/templates)
[GASでWebアプリを作ろう①  |  LogicalCreation](https://sugiyamatatsuya.com/gas%e3%81%a7web%e3%82%a2%e3%83%97%e3%83%aa%e3%82%92%e4%bd%9c%e3%82%8d%e3%81%86/)
[GASでウェブアプリケーション公開方法と実行・アクセスユーザー設定を解説 | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=3565)