申請承認フローをGoogle Formsで完結！～紙申請削減第一歩～

# :pencil2:紙削減の第一歩
こんにちは。
小売業会社で人事を担当しているMiyuです！
先日は「～いますか？」電話を削減を目標にLINEで在社確認できるようにしました。
今回は**紙申請書を減らそう**と思い、1つの申請書をデジタル化しました。

## :page_facing_up:紙申請が多すぎる
私の主な人事業務内容は下記の通りです。

1\. 従業員の残業確認
2\. 勤怠実績の修正
3\. 監査項目の確認と対策書の収集
4\. 従業員データ変更・修正登録（異動など）
5\. 年次有給取得の確認
6\. 年に２回申請可能な制度の管理

この中で紙対応を行っている業務は

1\. 従業員の残業確認
2\. 勤怠実績の修正
3\. 監査項目の確認と対策書の収集
6\. 年に２回申請可能な制度の管理

の4つです！めっちゃ多いのです。
(1か月に200枚以上の紙が私のメールボックスや机にあふれています）

ということで今回は従業員の残業確認で使用する**残業申請書**をデジタル化しました！

### :paperclip:残業申請書とは
今回デジタル化をする残業申請書は残業する従業員（申請者）が本日の残業予定時間・発生した理由を申請書に記載し上司へ報告。その後上司（またはその日の承認者）が確認し承認、または差し戻しをします。承認後残業時間を把握するために所属長へ報告を兼ねて提出、1か月内で28時間残業された方については人事より提出要請が入るため人事へ申請書を提出する。下記の仕組みになっています。
![申請者.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/a3103da8-52fe-2cfe-66ac-9e3187bf2cda.png)

今の残業申請書（文言はダミー用に修正済）
![スクリーンショット 2024-08-30 230146.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/ba7887a7-6868-db3a-959d-4046bdb1e91a.png)

主に私、人事が注意して確認している点は
+ 残業時間は何時間か？
+ 発生理由は何なのか？（事前に防げるものではなかったのか）
+ この件は上司に報告されており、承認されているのか？
+ 残業する日に申請し、承認されているのか？（事後申請は申請・承認もできないので基本NGとしています）

この4つの項目をクリアできそうな紙を使わない申請・承認フローを考えてみました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/a294e85a-a26a-848f-8512-8dfb2faaf069.png)

今回の目的は紙を使わない申請・承認フローです。一番変更したい部分は承認者上司から人事（私宛）に提出する部分となります。通常人事から店舗・各部署へ「○○さんの残業が先月28時間超えているので申請書を超えた分から提出をしてください」と呼びかけます。その後総務の方が書類をかき集め会社で使用している書類送達便（郵便のように宛名を記載して送付するとその宛先に届く社内システムです）で送付をしていただきます。この部分をGoogle Formsで最初から申請を行えば、データで管理が可能になり、さらに共有者に人事を含めておけば勝手に見ることができるようになります。そうすることで書類を「かき集めて送付する」ということをしなくてもよくなるわけです。

# :pencil2:使用したツール
+ ChatGPT
+ Google Forms
+ Google Spreadsheets
+ Google Apps Script

# :pencil2:制作までの道のり
## :gear:ChatGPTに聞いてみた！
よくアンケートに使用するGoogle Formsで今回の申請・承認フローは作れたりできるのかなと思いChatGPTにきいてみました。
<details><summary>ChatGPTへの質問-Google Formsで可能かどうか- </summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/06253943-b3dd-1fcf-6332-aed20188ab5c.png)
![スクリーンショット 2024-08-31 195127.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/fed7290f-628e-f4fa-a67b-d72b5e708118.png)
※4はスクリプトのため除外しました。
</details>

また所属長と人事が確認するのは承認後のGoogle Spread Sheetです。承認や却下のみだと「誰の申請だったのか」がわからなくなるため承認する用のGoogle Formsに申請者の情報を反映させることはできないか追加で聞いてみました。
<details><summary>ChatGPTへの質問-承認者フォームに申請者情報は反映できるのか- </summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/5991eba3-935a-7684-857c-452572d8782c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/49f13c66-c1a9-9733-cfac-95f83c14aec7.png)
</details>

承認用フォームに申請者の情報は反映させることが可能と答えをもらえたので、
申請・承認フォーム作成→回答時に承認者へ通知・承認フォームリンク送付設定→申請者情報を承認者フォームへ反映の順番で作成していくことにしました。

## :gear:Google Formsの作成
まず申請者用フォームと承認用フォームを作りました。
<details><summary>申請フォーム</summary>

![申請フォーム.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/5719e829-b6a7-29f9-8617-9914787f3dbb.jpeg)
</details>

<details><summary>承認フォーム</summary>

![承認フォーム.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/62659c53-91b7-1aa5-4cb3-e7d7de0c1a64.jpeg)
</details>

## :gear:Google Spread Sheetsの設定
次に回答時に申請内容が確認できるようにGoogle FormsとGoogle Spread Sheetsをリンクさせ、シート名を「申請シート」と設定しました。

<details><summary>申請シート</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/ac4aa1fd-6bd3-b9f6-3963-4afbb9255881.png)
※入力されている情報はすべて制作する過程で使用したダミーになります。

</details>

:::note info
Google Formsと紐づいているスプレッドシートは編集画面の上記３つの項目「回答」を開くと少し下に「スプレッドシート表示」というボタンが出てくるのでここで作成することができます。
![１.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/93fb62ae-3ace-65b5-2a09-bcde7e780543.png)
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/e01fd677-6f12-d973-8b94-5d6c6cad1fff.png)
:::

## :gear:Google Apps Scriptへの入力
いよいよGASにコードを入力する段階に入ります。
最初ChatGPT先生に全体のスクリプトを教えてほしいと頼みました。

<details><summary>最初のスクリプト</summary>

```js:GAS
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID'; // スプレッドシートID
const APPROVER_EMAIL = 'approver@example.com'; // 承認者のメールアドレス
const FORM_URL = 'YOUR_APPROVAL_FORM_URL'; // 承認用フォームのURL

// 申請が送信されたときに実行される関数
function onFormSubmit(e) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('申請シート');
  const lastRow = sheet.getLastRow();
  const rowData = sheet.getRange(lastRow, 1, 1, sheet.getLastColumn()).getValues()[0];

  // 承認リンクの生成
  const approvalLink = createApprovalLink(lastRow, rowData);
  
  // メールの内容を作成
  const subject = '【承認依頼】残業申請';
  const body = `申請者: ${rowData[0]}\n部署: ${rowData[1]}\n申請内容: ${rowData[2]}\n日時: ${rowData[3]}\n\nこの申請を承認するには、次のリンクをクリックしてください: \n${approvalLink}`;

  // 承認者にメールを送信
  MailApp.sendEmail(APPROVER_EMAIL, subject, body);
}

// 承認リンクの生成
function createApprovalLink(row, rowData) {
  const formUrl = FORM_URL; // 承認用フォームのURL
  // クエリパラメータに含めるデータ
  const params = {
    'entry.XXXX': rowData[0], // 申請者名
    'entry.YYYY': rowData[1], // 部署
    'entry.ZZZZ': rowData[2]  // 申請内容
  };
  
  // クエリパラメータを作成
  const queryString = Object.keys(params)
                            .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
                            .join('&');
  
  return `${formUrl}?${queryString}&row=${row}`;
}

```

</details>

流れはわかるがどこを自分が直せばいいのか分からず聞いてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/b04c2ff9-5e85-f102-027b-d37c57003343.png)

コードは読めないですが、ChatGPTへどこの箇所が自分の手を加えなければいけないのか教えてもらえてとても便利でした。
スプレッドシートと承認者メールアドレスは上の画像の通り修正し、下記は自分がコードをみて修正したいなという部分に手を加えました。

### :paperclip:メールの内容

<details><summary>修正前スクリプト</summary>

```js:GAS
  // メールの内容を作成
  const subject = '【承認依頼】残業申請';
  const body = `申請者: ${rowData[0]}\n部署: ${rowData[1]}\n申請内容: ${rowData[2]}\n日時: ${rowData[3]}\n\nこの申請を承認するには、次のリンクをクリックしてください: \n${approvalLink}`;
```

</details>

突然承認依頼メールが届いて、承認者がびっくりしないようメールの通知に下記の項目が見られるように修正をしました。
+ 申請日時
+ 社員番号
+ 申請者名
+ 残業予定時間
+ 残業する理由

<details><summary>修正後スクリプト</summary>

```js:GAS
  // メールの内容を作成
  const subject = '【承認依頼】残業申請';
  const body = `申請日時: ${rowData[0]}\n` +  // 申請日時
               `社員番号: ${rowData[1]}\n` +  // 社員番号
               `申請者名: ${rowData[2]}\n` +  // 申請者名
               `所属部署: ${rowData[3]}\n` +  // 所属部署
               `残業予定時間: ${rowData[4]}\n` +  // 残業予定時間
               `残業する理由: ${rowData[5]}\n\n` +  // 残業理由
               `この申請を承認するには、次のリンクをクリックしてください: \n${approvalLink}`;
```

:::note warn
この`rowData[]`が重要でスプレッドシートから列を確認し自分で修正をします。一番左の列のタイムスタンプを0列目として(ChatGPT先生が最初を0列目としていたため)右に2,3,4,…とします。
![本文を追 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/b9a928a2-ef4b-68ad-8238-4ec8c51598a2.png)

:::

</details>

### :paperclip:申請者情報を含む承認者フォームリンク生成

<details><summary>修正前スクリプト</summary>

```js:GAS
// 承認リンクの生成
function createApprovalLink(row, rowData) {
  const formUrl = FORM_URL; // 承認用フォームのURL
  // クエリパラメータに含めるデータ
  const params = {
    'entry.XXXX': rowData[0], // 申請者名
    'entry.YYYY': rowData[1], // 部署
    'entry.ZZZZ': rowData[2]  // 申請内容
  };
```

</details>
このコードは所属長・人事が管理確認する承認者フォームにリンクしているGoogle Sheetに申請者情報が含まれている、また承認するときに申請者情報を確認しながら承認できるよう申請者フォームから申請者名・部署・申請内容が自動で反映するようにするスクリプトです。

:::note info
Google Formsの質問には各設問ごとにコードがあります。これが `` entry.XXX `` といったコードであらわされています。これをエントリーコードと呼ぶことにします。
:::

### :paperclip:エントリーコードの場所
承認フォームのどこに申請者の情報を自動反映させたいかというシステムなので、承認フォームの自動反映させる質問のエントリーコードを探します。手順としてはまず、承認フォームから「事前入力したURL」から適当な数値を入力しリンクを取得します。
![３点リーダーを押すと 事前入力したURLを取得が出てきます.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/4e8d43cc-2d43-93ac-3e17-0108cdcc7fdf.png)

<details><summary>適当に入力したフォーム</summary>

![docs.google.com_forms_d_1dg6sBs7KMgYH5CtfZEev0uEHxpBfRD9doLY-Z7c2180_prefill.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/93a252e0-e170-7fca-dd60-45e92f5ced16.png)

</details>

:::note info
適当な情報で問題ないですが、リンクが英語で出てくるので英語表記で入力するとどこの質問がどのエントリーコードなのかわかりやすくなります。
:::

上記のリンクから `` entry.XXX `` という部分を抜粋するとこちらになります。
```js:GAS
entry.1507052026=1111111&entry.658493949=John&entry.1123682456=2hours&entry.833058904=222222&entry.689102455=mia&entry.1906584865=%E6%89%BF%E8%AA%8D&entry.869927844=OK
```
+ 1111111は承認者フォームで入力した申請者の社員番号→ `` entry.1507052026 `` 
+ Johnは承認者フォームで入力した申請者氏名→ `` entry.658493949 `` 
+ 2hoursは承認者フォームで入力した残業予定時間→ `` entry.1123682456 `` 

これで先ほどのスクリプトに入力しました。

<details><summary>エントリーコード入力したスクリプト</summary>

```js:GAS
  // クエリパラメータに含めるデータ
  const params = {
    'entry.833058904': rowData[1], // ダミー
    'entry.1507052026': rowData[1], // 申請者社員番号（エントリーコードが正しいか再確認してください）
    'entry.658493949': rowData[2], // 申請者氏名（エントリーコードが正しいか再確認してください）
    'entry.1123682456': rowData[4], // 残業予定時間（エントリーコードが正しいか再確認してください）
    'row': row // 行番号
  };
```

</details>

:::note warn
原因がわかりませんが、社員番号・申請者氏名・残業予定時間のみで入力すると一番上の欄だけデータを引っ張ってくることができませんでした。順番をひっくり返しても同様の現象がおきました。よって応急措置として**最初にダミーのデータ**を貼り付けることで3つすべての項目を反映させることができました。
:::

## :gear:承認者配信先の変更
現在のスクリプトではGoogle Formsの申請フォームから申請するとメールアドレスを設定した承認者へ通知・URLが届く。その後、申請者の情報を確認しながら承認することが可能になりました。ただ部署によって上司や承認者が異なる場合、部署選択から配信先を変更することができるかChatGPTにさらに追加で聞いてみました。

<details><summary>ChatGPTへの質問-配信先の変更-</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/1876d767-cafe-f91d-2202-0408bed354e6.png)

</details>

ChatGPT回答通り各部署承認者のメールアドレスをスプレッドシートに記入します。
これを「承認者設定シート」とします

<details><summary>承認者設定シート</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/ae617436-3bc7-3cd4-d229-6f631a92584e.png)

</details>

<details><summary>承認者設定コード</summary>

```js:GAS
  // 所属部署に基づいて承認者のメールアドレスを取得
  const department = rowData[3]; // 所属部署が4列目にある場合
  const approverEmail = getApproverEmail(department);
```

```js:GAS
// 承認者メールアドレスをスプレッドシートから取得する関数
function getApproverEmail(department) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('承認者設定');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) { // 1行目はヘッダー行なのでスキップ
    if (data[i][0] === department) {
      return data[i][1]; // メールアドレスを返す
    }
  }
  return 'Default@iiii.jp'; // 所属部署が見つからなかった場合のデフォルトメール
}
```

</details>

以上の追加・修正コードを合わせると全体のスクリプトは下記のようになりました。これで実際に稼働するのかを検証します。(IDやメールアドレスはダミーを使用してます）

<details><summary>最終スクリプト</summary>

```js:GAS
// スプレッドシートとフォームの設定
const SPREADSHEET_ID = 'スプレッドシートID'; // スプレッドシートIDを入力
const FORM_URL = '承認者フォームURL'; // 承認用フォームのURLを入力

// 申請が送信されたときに実行される関数
function onFormSubmit(e) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('申請シート');
  const lastRow = sheet.getLastRow();
  const rowData = sheet.getRange(lastRow, 1, 1, sheet.getLastColumn()).getValues()[0];

  // 所属部署に基づいて承認者のメールアドレスを取得
  const department = rowData[3]; // 所属部署が4列目にある場合
  const approverEmail = getApproverEmail(department);

  // 承認リンクの生成
  const approvalLink = createApprovalLink(rowData, lastRow);

  // メールの内容を作成
  const subject = '【承認依頼】残業申請';
  const body = `申請日時: ${rowData[0]}\n` +  // 申請日時
               `社員番号: ${rowData[1]}\n` +  // 社員番号
               `申請者名: ${rowData[2]}\n` +  // 申請者名
               `所属部署: ${rowData[3]}\n` +  // 所属部署
               `残業予定時間: ${rowData[4]}\n` +  // 残業予定時間
               `残業する理由: ${rowData[5]}\n\n` +  // 残業理由
               `この申請を承認するには、次のリンクをクリックしてください: \n${approvalLink}`;

  // 承認者にメールを送信
  MailApp.sendEmail(approverEmail, subject, body);
}

// 承認者メールアドレスをスプレッドシートから取得する関数
function getApproverEmail(department) {
  const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('承認者設定');
  const data = sheet.getDataRange().getValues();

  for (let i = 1; i < data.length; i++) { // 1行目はヘッダー行なのでスキップ
    if (data[i][0] === department) {
      return data[i][1]; // メールアドレスを返す
    }
  }
  return 'Defaultメール'; // 所属部署が見つからなかった場合のデフォルトメール
}

// 承認リンクの生成
function createApprovalLink(rowData, row) {
  const formUrl = FORM_URL; // 承認用フォームのURL

  // クエリパラメータに含めるデータ
  const params = {
    'entry.833058904': rowData[1], // ダミー（エントリーコードが正しいか再確認してください）
    'entry.1507052026': rowData[1], // 申請者社員番号（エントリーコードが正しいか再確認してください）
    'entry.658493949': rowData[2], // 申請者氏名（エントリーコードが正しいか再確認してください）
    'entry.1123682456': rowData[4], // 残業予定時間（エントリーコードが正しいか再確認してください）
    'row': row // 行番号
  };
  
  // クエリパラメータを作成
  const queryString = Object.keys(params)
                            .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
                            .join('&');
  
  return `${formUrl}?${queryString}`;

}
```

</details>

# :pencil2:実際に使ってみた
上手く稼働するか試してみました。
例
+ 社員番号　777777
+ 申請者　もも
+ 部署　営業部（配信先が違う企画部もやってみる）
+ 残業予定時間　2時間
+ 残業理由　お客様対応

## :gear:申請フォームが承認者へ通知できるか
申請内容を入力します。

<details><summary>申請フォーム（試用）</summary>

![企画部も申請します.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/33b4640f-2694-2048-43cb-59f3a3763557.jpeg)

</details>

<details><summary>承認依頼メール（試用）</summary>

メール（営業部）
![会社PC.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/84278975-6de9-db2a-81ee-d4182bd77e2a.png)
ちゃんとメール来ています。

メール（企画部）
![gmail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/fcb742c3-438b-be49-65cf-c31eb70f2b0f.png)
こちらも問題なさそうです。

</details>

## :gear:承認フォームに申請者の情報が反映しているか
次に承認者メールのURLから申請者の情報が反映しているか確認します。

<details><summary>承認依頼メール（試用）</summary>

![本文を追加.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/ec481386-28d0-b448-b0ca-3932d4a36909.png)

</details>

承認依頼フォームの上3段、自動的に申請者内容が反映できていました。これで申請・承認フローを紙ではなくGoogle Formsを使って作成することができました。

:::note info
承認用でスプレッドシートを出してみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836509/c074a091-0b1b-daeb-084b-93c5c8569f5d.png)
最初にお伝えしたように、この承認用スプレッドシートで所属長・人事（私）は誰がいつどんな内容で何時間申請し、だれがいつ承認したのかを書類を送付することなくこのシートを共有することで確認が可能となりました。
:::

# :pencil2:作ってみて
作ってみて…**とても大変**でした:sob:
Formsはアンケートなどで使っていてもコードとか「これ何言ってんだ？」頭がフリーズすることもたくさんありました。今回作成する前に直属の私のボスが業務4~5時間かけて同じ残業申請のFormsを作成されてました。しかし、
+ 申請だけのGoogle Formsで承認という作業を組み込めない
+ 承認者が部署によって異なるため管理が難しい

この2点の解決策が出ず結果「ボツ」になってました。

今回すごく時間をたくさん使いましたが、ChatGPT先生を使い**100%自分の力では作れなかったものが作れた**ことに感動しました。大袈裟ではなく本当に…すごさ・便利さが身に沁みました。今後も紙削減に向けてシステムを自分で作る時、またChatGPTにお願いします！！