Slack(ワークフロービルダー、SlackAPI)とGoogle App Script (GAS)などを使って有給申請書が作成されるシステムをローコードで作成しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348060/90f79d94-2f9d-b3f8-c61e-76e7c4aa6df3.png)


## 作ったもの
##### slackのワークフロービルダーから有給取得日を送信すると、slackに有給申請書のファイルが添付され、同時にGoogleカレンダーに登録される
#### 仕様やルール
- 有給申請書のテンプレート上で、[申請者の名前、日付、所属部署]が入力された状態でファイルを作成し、slack上にメンション付きで通知する
- 有給取得予定日はGoogleカレンダーに登録される(土日祝不可、営業日3日前以内の場合は申請不可などのバリデーション、日付をまたぐ場合等)
- スプレッドシートで作成された申請書をエクセルに変換してGoogleDriveへ保存する
- slackワークフロービルダーから複数人が同時に送信しても正常に動作する


## 使用ツール等と各々がやること
- Slackワークフロービルダー
    - 有給取得日のフォーム作成(日付、slackのuserId、フォーム送信日)
    - フォームから送信した値が追加されるスプレッドシートを選択
- SlackAPI
    - Botからファイル、メッセージの送信
    - SlackAPIのPermissionでファイルのアップロード(file:write)とチャットのメッセージ(chat:write)を許可する
    - OAuthからBotの「xoxb-xxxx-xxxx」トークンを取得しておく(GASからslackへ送信する際に使用)
- Google App Script (以下GAS)
    - 使用する他ツールの操作(slackへの送信やスプレッドシートの操作、エクセル変換、driveへの保存、googleカレンダーの登録など)
    - ワークフロービルダーで選択したスプレッドシートから、拡張機能→App Scriptを作成
    - トリガーの作成 (slackワークフロービルダーからスプレッドシートに追加されたらGASを起動できるようにする)
- スプレッドシート
    - Slackワークフロービルダーからの入力値を受け取るシート作成
    - 申請書(今回は有給申請書)のテンプレートシート作成
    - スプレッドシートのID,テンプレートシートのgIDを取得しておく(スプレッドシートの書き換え、エクセル変換時に使用)
- Google Drive
    - 申請書のファイルを格納するフォルダ作成
    - フォルダID 取得しておく(GASからdriveへ保存する際に使用)
- Googleカレンダー
    - カレンダーAPI(日付バリデーション、祝日一覧の作成で使用)

## GASのコードを部分的に説明
#### ロックする
slackワークフロービルダーから同時、または連続で送信された時にロックをかけていないと後からの値に更新され２重になって値が帰ってきてしまいます。
なのでロック(今回はスプレッドシート自体にロックをかけました(getDocumentLock))をかけて期待通りに動作させます。
```js
function myFunction() {
 const lock = LockService.getDocumentLock();
 // ロックを取得する
 if (lock.tryLock(10 * 1000)) {

   //~処理中~   

   // ロック開放
   lock.releaseLock();
 }
}
```


#### 日付バリデーション
 (myFunction関数内)
```js
// 日付バリデーションでfalseなら失敗のメッセージをslackに送信して終了
const validationResult = validationDate(result['startDate'], result['endDate']);
if(validationResult['result'] === false) {
  failMessageSlack(result['mention'], validationResult['message']);
  return;
}
```

- 日付型チェック
```js
Object.prototype.toString.call(startDate) !== "[object Date]"
```

- 何営業日かのチェック(例: 3日前以内であれば不可)
「祝日一覧!$A$1:$A)」の箇所は祝日一覧のシートを作成しておき、参照します。
参考例: https://note.com/taraco123/n/n515eebc7beba
```js
//営業日前チェック
const diffRange = sheet.getRange('F'+lastRow); //申請日付の取得
diffRange.setFormula(`=NETWORKDAYS(E${lastRow}, B${lastRow}, 祝日一覧!$A$1:$A) - 1`);
const diff = diffRange.getValue();
if (diff < 3) {
    return {
        'result':false,
        'message':'３営業日前までが可能です。'
    }
}
```

#### スプレッドシートのエクセル変換
```js
const options = {
    method: 'get',
    headers: {"Authorization": "Bearer " + ScriptApp.getOAuthToken()},
};
//スプレッドシートの内容をエクセル変換
//SpreadSheetId=スプレッドシートのID, TemplateSheettGId=スプレッドシート内のexportしたいシートのID
const fetchUrl = "https://docs.google.com/spreadsheets/d/" + SpreadSheetId + "/export?exportFormat=xlsx&gid=SID".replace("SID",TemplateSheettGId);
const xlsxFile = UrlFetchApp.fetch(fetchUrl,options).getBlob().setName(変換後のファイル名);
```

#### GoogleDriveで作成したフォルダ以下へファイルを保存
```js
// Googleドライブにアップロードする
function gdriveUpload(folderID, uploadFile){
  const folder = DriveApp.getFolderById(folderID);
  const drive_file = folder.createFile(uploadFile);

  const  fileId = drive_file.getId();
  return fileId; //後にDriveからファイル取得するため、ファイルIDを返す
}
```

#### GoogleDriveからファイルを取得する
一つ前で取得したファイルIDを使ってファイルを取得する
```js
const file = DriveApp.getFileById(driveFileId);
```

#### 取得したファイルをslackAPIで作成したBotで送信
```js
function uploadFileSlack(file, mention){
  const payload = {
    'token' : SlackAppToken, //Botのトークン
    'channels' : ChannelId, //送信したいチャンネルのID
    'file': file.getBlob(),
    'filename': file.getName(),
    'initial_comment': `${mention}`,//slackワークフロービルダーから送信されたメンションを使用
    'title': file.getName(),
  };

  const options = {
    'payload' : payload
  };

  UrlFetchApp.fetch('https://slack.com/api/files.upload', options);
}
```

#### Googleカレンダーへの登録
1日の場合 => createAllDayEvent(title,startDate)
数日にまたがる場合 => createAllDayEvent(title,startDate, endDate)
```js
function registCalenderEvent(startDate, endDate, name) {
  const myCalendar = CalendarApp.getCalendarById(CalendarId);
  const color = CalendarApp.EventColor.YELLOW;
  const title = `【有給】${name}`;

  //日付をまたぐ予定の場合
  if (startDate < endDate) {
    endDate.setDate(endDate.getDate() + 1);
    const event = myCalendar.createAllDayEvent(title,startDate, endDate);
    event.setColor(color);
    return;
  }

  const event = myCalendar.createAllDayEvent(title,startDate);
  event.setColor(color);
}
```

#### テンプレートのスプレッドシートを更新後、反映させる
スプレッドシートはデータの蓄積用、申請書を作成するための更新用の２つあり、下記は更新用のために使う
```js
SpreadsheetApp.flush();
```

#### GASで使用するプロパティの設定(slackのトークンやスプレッドシートIDなど)
例:
```js
const SlackAppToken = PropertiesService.getScriptProperties().getProperty('slack_TOKEN');
```

#### 最後に
初めてGASを使用したが、作成できるまでが早いので楽しかった。
また、slackワークフロービルダーもとても便利で他にも色々連携できそうだと思った。
間違いの箇所やこうやるといいよ的なのがあったらご教示してもらえると幸いでございます。