GoogleSite を使ってダッシュボードを作る

# はじめに
先日、業務でGoogle Workspaceの機能について調査する機会がありました。その際、せっかくなので調べたものをまとめるためにGoogleSiteをリンク集的に使用したのですが、これが思いのほか使い心地が良かったというのがことの始まりです。

GoogleSiteを外部に公開するためのサイトづくりに使おうとするとどうしてもWordPressなどと比べてしまい、物足りない点も見受けられますが、個人用にポータルサイト・まとめサイト的な利用をする分には可能性を感じたため、いろいろとカスタマイズを試しました。以下はそこで行ったことの記録となります。

（この記事を書くうえでついでにGoogleSiteの使用事例など調べていたところ、どうやら社内ポータルとしての使用事例がちらほらと見受けられました。これらの後追いとなる部分も多分に含まれます。）


# GoogleWorkspaceのサービスとの連携
GoogleSiteには、スプレッドシートやスライドなど、他Googleサービスのコンテンツを簡単に埋め込むことができる機能があります。本記事ではGAS（GoogleAppsScript）とスプレッドシートで作成できるグラフや、フォームの自動集計を利用したTODOリスト作成機能をサイト上に設置して、ダッシュボードを作成してみることとします。
（HTMLコードやWebURLを埋め込んで直接コンテンツを表示することもできますが、本記事ではGoogleの他サービスとの連携を活用していきます。）

最終的にはこのようなサイトが出来上がります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/639def7e-0ce7-c3da-1ef4-8a2849ed5d5b.png" alt="作ったサイト" width=80%>

## Oura APIを使って睡眠時の情報をサイトに表示
近頃ではPokemon Sleep、スマートウオッチなど手軽に睡眠時のバイタルデータを得る手段がいくらでもありますね。私が使用しているOura RingというスマートリングはこのデータをAPIで取得することができます。今回はこのバイタルデータをグラフ化してサイトに表示させてみます。

### 1. GASでOura APIからバイタルデータを取得する
今回は最新の睡眠の段階とスコアを取得して表示してみます。
GASの処理内容は記事の本旨でないため簡単に流しますが、APIから情報を取得し、後々グラフにしやすいようにスプレッドシートに貼り付けている内容となります。
GASのAPI連携は以下を参考に実装しました。

>[Google Apps Scriptを利用してWeb APIを取得する方法を試してみた](https://dev.classmethod.jp/articles/google-apps-script-request/)
>[GASでJSON形式データを読み込み、処理可能なオブジェクトや配列に変換する方法](https://auto-worker.com/blog/?p=3760)

また、このメイン関数については4時間おきの定期実行を設定しています。

**ソースコード**
```js:graph.gs(GAS)
const mySpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheetSleep_phase_5_min = mySpreadsheet.getSheetByName("sleep_phase_5_min");
const sheetDaily_sleep = mySpreadsheet.getSheetByName("daily_sleep");

// Oura API情報
const BaseURL = "https://api.ouraring.com/v2/usercollection/";
const requestHeaders = {
  "Authorization": "APIアクセストークンを入力"
}
const requestOptions = {
  "method" : "get",
  "headers" : requestHeaders
}

function main(){
  outputDaily_sleep();
  outputSleep();
}

// Daily_sleepから必要なデータを取得
function outputDaily_sleep() {
  
  const requestURL = BaseURL + "daily_sleep";
  const response = UrlFetchApp.fetch(requestURL, requestOptions);

  var responseCode = response.getResponseCode();
  var responseText = response.getContentText();
  Logger.log(responseCode);

  const json = JSON.parse(responseText);

  const score = json["data"][json["data"].length - 1]["score"];
  var date = new Date(Utilities.parseDate(json["data"][json["data"].length - 1]["day"], 'Asia/Tokyo', "yyyy-MM-dd"));
  
  const oldDate = sheetDaily_sleep.getRange(2, 1).getValue();
  const oldScore = sheetDaily_sleep.getRange(2, 2).getValue();

  if(date.getTime() != oldDate.getTime() || score != oldScore){
    sheetDaily_sleep.clear();
    sheetDaily_sleep.getRange(1, 1).setValue(oldDate);
    sheetDaily_sleep.getRange(1, 2).setValue(oldScore);
    sheetDaily_sleep.getRange(2, 1).setValue(date);
    sheetDaily_sleep.getRange(2, 2).setValue(score);
  }
}

// sleepから必要なデータを取得
function outputSleep() {

  const requestURL = BaseURL + "sleep";
  const response = UrlFetchApp.fetch(requestURL, requestOptions);

  const responseCode = response.getResponseCode();
  const responseText = response.getContentText();
  Logger.log(responseCode);

  const json = JSON.parse(responseText);

// json["data"][json["data"].length - 1]で最新のものだけ取得、シートの内容をクリア後に最新のsleep_phase_5_minを反映させる
  const sleepPhase = json["data"][json["data"].length - 1]["sleep_phase_5_min"].split("");
  const bedtime_start = Utilities.parseDate(json["data"][json["data"].length - 1]["bedtime_start"], 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ss'+09:00'");
  const bedtime_end = Utilities.parseDate(json["data"][json["data"].length - 1]["bedtime_end"], 'Asia/Tokyo', "yyyy-MM-dd'T'HH:mm:ss'+09:00'");
  var date = new Date(bedtime_start);
  var endDate = new Date(bedtime_end)

  sheetSleep_phase_5_min.clear();
  for(let i=1;i<sleepPhase.length+1;i++){
    if(i == sleepPhase.length){
      sheetSleep_phase_5_min.getRange(i, 1).setValue(endDate)
    }else if(i % 12 == 1){
      sheetSleep_phase_5_min.getRange(i, 1).setValue(date);
    }
    date.setMinutes(date.getMinutes() + 5);
    sheetSleep_phase_5_min.getRange(i, 2).setValue(sleepPhase[i-1]);
  }
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/9cde656f-63ee-218b-393d-e9e17d9c802f.png" alt="4時間おきにmainを実行" width=40%>


### 2. スプレッドシートにグラフを挿入
このコードが正常に動くと以下のようにスプレッドシートにデータが出力されるため、このデータを使ってグラフを作成します。
▼睡眠スコア
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/71f55b12-43ca-525b-7990-3f53386ec630.png" alt="睡眠スコア" width=50%>

▼睡眠段階
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/ea52039c-2832-0eeb-49a6-569175a79d40.png" alt="睡眠段階" width=50%>

今回は睡眠スコアには「スコアカードグラフ」を、睡眠段階には「階段面グラフ」を使用します。

▼睡眠スコア
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/3933e238-d8c8-7432-fc53-8b9417e19c22.png" alt="スコアカード" width=50%>

▼睡眠段階
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/78ea181b-60a5-9c55-f539-b1588e3c0c06.png" alt="階段面グラフ" width=70%>


一気にPokemon Sleepなどでよく見る"あのグラフ"っぽくなります。

### 3. サイトにグラフを埋め込む
ここまできたらサイトにグラフを埋め込みます。
サイトの編集画面を開き、挿入>グラフから先ほどのスプレッドシートを選択
→スプレッドシート内に存在するグラフが表示されるため、これらを選択し、いい感じに配置すれば完了です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/0160e713-a20d-d687-01b8-d420d707edce.png" alt="挿入>グラフ" width=60%>


# GoogleFormでTODOリスト作成
GASでグラフを作成して表示することで情報の表示はできるようになりました。他のAPIなど片っ端からデータを取得してよりサイトを豪華にすることも考えられますが、現状では普通にOura専用アプリを使えば良いとなってしまいます。

せっかくなのでもう一つ、GoogleSiteはフォームの記入ページを直接埋め込み、回答を送信することができます。これを使ってTODOリスト的なものを作成します。


### 1. GoogleFormを用意する
新規にフォームを作成し質問を作成します。今回はTODOにあたる「すること」と、後々フォームで削除したいTODOを指定できるよう、「削除する行」の質問を作りました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/3ffff0ed-dc9d-4f92-4d0f-6e7ea69bcbe5.png" alt="フォーム作成" width=60%>

フォームができたら解答>スプレッドシートにリンクを選び、解答が自動的に反映されるスプレッドシートを作成します。
ついでに1つ2つ、試しに解答してみて解答が保存されることを確認しました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/754336b9-595f-48ea-7a59-ebaf4a167a37.png" alt="スプレッドシートにリンク" width=60%>


### 2. スプレッドシートにグラフを挿入
今回はTODOリストなので表グラフを使用します。
グラフの範囲は「すること」の行のみを指定することで、空白セルがリストに入り込まないようにしています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/740b8f45-1657-e1ce-6561-c4403422d12d.png" alt="表グラフ" width=70%>


### 3. GASでTODOリストの削除機能を作る
このままだとリストから項目の削除ができないため、この処理をGASで実装します。
解答送信時に「削除する行」が入力されていた時に対応する行を削除しつつ、「すること」が未入力の場合にはその行も削除して余計な行が残らないようにしています。
フォームとGASの連携は以下を参考に実装しました。

>[GASでGoogleフォームの値を取得する（トリガー編）](https://walking-elephant.blogspot.com/2021/01/gas.namedvalues.html)

この関数はフォームの送信をトリガーとして動いています。

**ソースコード**
```js:form.gs(GAS)
const mySpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheetFormAnswer = mySpreadsheet.getSheetByName("フォームの回答 1");

function deleteRow(e) {

  console.log(e.namedValues);

  //削除する行に数字が入っているときのみ行削除する
  if(e.namedValues["削除する行を入力"][0].match(/^[0-9]+$/g)){
    sheetFormAnswer.deleteRow(parseInt(e.namedValues["削除する行を入力"][0]) + 1);
  }

  //することが空欄の場合、その行を削除する 
  var lastRow = sheetFormAnswer.getLastRow();
  for(let i=2;i<lastRow + 1;i++){
    if(sheetFormAnswer.getRange(i,2).getValue() == ""){
      sheetFormAnswer.deleteRow(i);
      i--;
      lastRow--;
    }
  }
}

```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/af7d61a6-2909-3b57-86fc-6bbd4017bd52.png" alt="フォーム送信時に実行" width=50%>


### 4. サイトにフォームとリストを埋め込む
サイトにフォームとリストを埋め込みます。
サイトの編集画面を開き、挿入>フォームから作成したフォームを選択
同様に、挿入>グラフからフォームと連携しているスプレッドシートを選び、その中のリストを選択
→これらをいい感じに配置すればTODOリストっぽく使えるフォームの完成です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/639def7e-0ce7-c3da-1ef4-8a2849ed5d5b.png" alt="作ったサイト" width=80%>

### 5. 公開する
GoogleSiteの手順に沿ってサイトを公開します。
公開範囲を自分だけに設定しておくのを忘れないようにします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3243404/7bab3cfc-17f1-49d0-f5f8-759e46b44c9c.png" alt="公開範囲を制限" width=50%>

# おわりに
GoogleSite、単なるリンク集として使用する分にも十分便利に使えますが、やはり他のGoogleサービスとの連携が簡単で扱いやすく、GASさえ書いてしまえばできることは広がるのかなといった印象を感じました。
本記事のような個人用ページであったり、プレゼン用の資料をサイトで作成するような用途であれば輝く場面も多いと思います。