GoogleWorkspaceだけで申請承認フローを構築したときのコード

GAS

概要:GoogleWorkspaceだけで申請承認フローを構築したい
構成:GoogleWorkspaceだけで申請承認フローを構築したときの構成

では、コードです。
ちょっと長いのでブロックに分けて書きます。

なお、実際に使用するときは承認者はスプレッドシートの編集権限が必要ですので、組織に編集権限を付けてしまうと楽です。

環境によって変わる値は適宜読み替えてください。

スポンサーリンク

定数

const EPapproval ='社員承認'
const OFapproval ='役員承認'
const EPmaster ='社員マスタ'
const FormANS1 ='フォームの回答 1'
const ApprovalList ='承認済み一覧'
const admin_users = ['hoge@taibonn.com']; //エラーメッセージ受ける人のメアド

定数関数

勝手に定数関数なんて名前つけましたが、あんまり返す値の変わらない関数です。定数みたいなもんです。
滅多に変わらないけど関数で返すようにしておいた方が楽な場合やGASの高速化のために書きます。

//GASのwebアプリを返す
function getWebAppURL(){
  const URL = 'https://script.google.com/a/macros/hoge.co.jp/s/*********/exec'
  return URL;
}

//役員のメールアドレスを返す
function getOfficers(){
  const officers = ['sugoi_erai@taibonn.com','erai@taibonn.com','ma_erai@taibonn.com'];
  return officers;
}

//社員承認シートの取得
function getFirstApprovalSheet() {
  if (getFirstApprovalSheet.firstApprovalSheet) { return getFirstApprovalSheet.firstApprovalSheet; }
  getFirstApprovalSheet.firstApprovalSheet = SpreadsheetApp.getActive().getSheetByName(EPapproval);
  return getFirstApprovalSheet.firstApprovalSheet;
}

//役員承認シートの取得
function getSecondApprovalSheet() {
  if (getSecondApprovalSheet.secondApprovalSheet) { return getSecondApprovalSheet.secondApprovalSheet; }
  getSecondApprovalSheet.secondApprovalSheet = SpreadsheetApp.getActive().getSheetByName(OFapproval);
  return getSecondApprovalSheet.secondApprovalSheet;
}

//社員マスタシートの取得
function getEmployeeMasterSheet() {
  if (getEmployeeMasterSheet.employeeMasterSheet) { return getEmployeeMasterSheet.employeeMasterSheet; }
  getEmployeeMasterSheet.employeeMasterSheet = SpreadsheetApp.getActive().getSheetByName(EPmaster);
  return getEmployeeMasterSheet.employeeMasterSheet;
}

//フォームの回答 1シートの取得
function getApplicantListSheet() {
  if (getApplicantListSheet.ApplicantListSheet) { return getApplicantListSheet.ApplicantListSheet; }
  getApplicantListSheet.ApplicantListSheet = SpreadsheetApp.getActive().getSheetByName(FormANS1);
  return getApplicantListSheet.ApplicantListSheet;
}

//承認済み一覧シートの取得
function getApprovedListSheet() {
  if (getApprovedListSheet.approvedListSheet) { return getApprovedListSheet.approvedListSheet; }
  getApprovedListSheet.approvedListSheet = SpreadsheetApp.getActive().getSheetByName(ApprovalList);
  return getApprovedListSheet.approvedListSheet;
}

メイン処理関数

// フォームの送信で発火する「editイベント」トリガーで実行する関数
function firstApproval(){
  try{
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const document_type = spreadsheet.getName();
    const applicant_list_sheet = getApplicantListSheet(); //フォームの回答
    const first_approval_sheet = getFirstApprovalSheet(); //社員承認
    const second_approval_sheet = getSecondApprovalSheet(); //役員承認
    const row_values = applicant_list_sheet.getRange('B:B').getValues();//申請者のメールアドレスが記載される列
    const last_row = row_values.filter(String).length;
    const col_values = applicant_list_sheet.getRange('1:1').getValues();//ヘッダーの行
    const last_col = col_values.flat().filter(String).length;
    const applicant_line = applicant_list_sheet.getRange(last_row, 1, 1, last_col).getValues(); //申請行を2次元配列で取得
    const applicant = applicant_list_sheet.getRange(last_row, 2).getValue(); //申請者のメールアドレス
    const approvers_obj = getApprovers(applicant);
    const approvers = approvers_obj.approvers;
    const approvers_length = approvers.filter(String).length;//承認者が何人いるか
    const skip_first_flg = approvers_obj.skip_first_flg;
    const id = generateId(last_row - 1);// ヘッダーの分マイナス1行してる

    // 申請行をコピー
    if (skip_first_flg === "Yes"){
      copyLine(applicant_line, second_approval_sheet,id, approvers);
    }else if(skip_first_flg === "No"){
      copyLine(applicant_line, first_approval_sheet,id, approvers);
    }
    let title = document_type;
    const itemname = insertItemName(applicant_list_sheet,applicant_line);
    let body = `<br>承認者各位<br><br> ${document_type} が届いております。承認処理をお願いいたします。<br>`
    body += makeMailbody(applicant,id,itemname);
    let auth_id = 0;
    let index = "";
    for(let j of approvers){
      let link = "";
      index = getWebAppURL()+`?page=determine&id=${id}&approversLength=${approvers_length}&skip=${skip_first_flg}&authId=${auth_id}`;
      link += `<p><a href="${index}">こちらから承認または否認</a></p><br>`;
      submitMail(j, title + " 承認依頼", body, body + link); // 送信先、タイトル、本文
      auth_id++;
    }
    // 申請者にもメール通知
    body = `申請者様\r\n\r\n${document_type}を受け付けました。\r\n申請ID: ${id}\r\n申請の結果はメールでお知らせします。\r\n承認まで今しばらくお待ちください。`;
    submitMail(applicant, title, body);
  }catch(e){
    const applicant_list_sheet = getApplicantListSheet(); //フォームの回答
    const row_values = applicant_list_sheet.getRange('1:1').getValues();
    const last_row = row_values.filter(String).length;
    const err_obj= `該当申請行:${last_row} + \n[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}\n`;
    submitErrMail(err_obj);
  }
}
// webアプリケーションへのアクセスによって実行される関数。
// 実行する関数の割り振りと、承認者がアクセスするwebアプリの画面(承認操作画面のHTML)を返してあげる。
function doGet(e){
  try{
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const document_type = spreadsheet.getName();
    const id = e.parameter.id;
    const skip = e.parameter.skip;
    const auth_id = e.parameter.authId;
    const approvers_length = e.parameter.approversLength;
    const determine_html = HtmlService.createTemplateFromFile("index");
    determine_html.type = document_type;
    determine_html.id = id;
    determine_html.skip = skip;
    determine_html.auth_id = auth_id;
    determine_html.approvers_length = approvers_length;
    return determine_html.evaluate();
  } catch(e){
    const err_obj= `[申請種類]${document_type}\n[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}`;
    submitErrMail(err_obj);
    return err_obj;
  }
}
//承認操作
function submitApprove(id, skip, auth_id, approvers_length){
  try{
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const document_type = spreadsheet.getName();
    let target_sheet = "";
    let next_sheet = "";
    if(skip === 'Yes' || skip === 'last'){
      target_sheet = getSecondApprovalSheet(); //役員承認
      next_sheet = getApprovedListSheet(); //承認済み一覧
    } else if(skip === 'No'){
      target_sheet = getFirstApprovalSheet(); //社員承認
      next_sheet = getSecondApprovalSheet(); //役員承認
    } else{throw new Error("skipパラメータ不正");}

    if(!target_sheet){throw new Error("該当のシートがみつかりませんでした。");}
      const target_row = getIdRow(target_sheet, id);
      const sheet_values = target_sheet.getDataRange().getValues();//二次元配列
      const header_length = sheet_values[0].length;
      // 承認結果を該当行に追加
      const applicant_line = target_sheet.getRange(target_row, 1, 1, header_length).getValues(); //申請行を2次元配列で取得
      // 列を追加する形で結果書き込み
      const target_col = (header_length - (2 - auth_id)); //ヘッダーの長さから承認者IDを引いて人を特定(auth_idは0~2)
      const target_val = applicant_line[0][target_col - 1];
      if(!target_val){throw new Error("該当の申請行がみつかりませんでした。");}
      if(target_val.match('approve:') || target_val.match('reject:')){
      console.log('既に結果があります。');
      return "既に操作済みです。"
    }
    const record = "approve:" + applicant_line[0][target_col - 1]; //approve:erai@taibonn.comみたいな形式
    target_sheet.getRange(target_row,target_col).setValue(record); //セルの値を上書き

    let title ="";
    let body ="";
    //承認者全員の状況をチェック
    const status = checkstatus(target_sheet, id, approvers_length);
    if(!status){throw new Error(`申請ID: ${id} の状態を取得できませんでした。`);}else if(status == "rejected"){return "既に否認された申請です。"}
    const applicant = sheet_values[target_row-1][3];
    //承認者が全員承認しているか確認
    if(status === 'approved'){
      const itemname = insertItemName(target_sheet,applicant_line);
      let body = makeMailbody(applicant,id,itemname);
      const remake_line = applicant_line[0].slice(2,(header_length - 3));//slice(start, end)でendは含めない
      const officers = getOfficers();
      const officers_length = officers.length;
      let index ="";
      let link = "";
      if(skip === 'No'){
        copyLine(remake_line, next_sheet, id, officers);
        //新しい申請情報を取得
        const next_id_row = getIdRow(next_sheet, id);
        const next_applicant_line = next_sheet.getRange(next_id_row, 1, 1, header_length).getValues(); //申請行を2次元配列で取得
        const itemname = insertItemName(target_sheet,next_applicant_line);
        //役員に申請依頼メール出す
        title = `${document_type}承認依頼`;
        let body = `<br>承認者各位<br><br> ${document_type} が届いております。以下の内容を確認いただき、承認処理をお願いいたします。<br>`
        body += makeMailbody(applicant,id,itemname);
        let auth_id = 0;
        for(let j of officers){
          index = getWebAppURL()+`?page=determine&id=${id}&approversLength=${officers_length}&skip=last&authId=${auth_id}`;
          link = `<p><a href="${index}">こちらから承認または否認</a></p><br>`;
          submitMail(j, title, body, body + link); // 送信先、タイトル、本文
          auth_id++;
        }
      }else if(skip === 'Yes' || skip === 'last'){
        let approved_line = applicant_line[0].flat();
        approved_line[1] = "承認済み";
        next_sheet.appendRow(approved_line);

        //申請者と総務に承認結果メール出す
        title = `${document_type}が承認されました`;
        body =`承認結果通知\r\n申請ID: ${id} が承認されました。下記URLから承認結果一覧を確認できます。\r\n`;
        body += makeMailbody(applicant,id,itemname);
        link = 'https://docs.google.com/spreadsheets/d/********'
        const notified = ['somus@taibonn.com', applicant];
        submitMail(notified, title, body, body + link); // 送信先、タイトル、本文
      }
    }
    return "操作が完了しました。"
  } catch(e){
    const err_obj= `[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}`;
    submitErrMail(err_obj);
  }
}
//申請却下処理
function submitReject(reason, id, skip, auth_id, approvers_length){
  try{
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    const document_type = spreadsheet.getName();
    const first_approval_sheet = getFirstApprovalSheet(); //社員承認
    const second_approval_sheet = getSecondApprovalSheet(); //役員承認
    let target_sheet = "";
    let notified = [];
    if(skip === 'Yes' || skip === 'last'){
      target_sheet = second_approval_sheet;
    } else if(skip === 'No'){
      target_sheet = first_approval_sheet;
    } else{throw new Error("skipパラメータ不正");}
    if(!target_sheet){throw new Error("該当のシートがみつかりませんでした。");}
    const target_row = getIdRow(target_sheet, id);
    const sheet_values = target_sheet.getDataRange().getValues();//二次元配列
    const header_length = sheet_values[0].length;
    // 承認結果を該当行に追加
    const applicant_line = target_sheet.getRange(target_row, 1, 1, header_length).getValues(); //申請行を2次元配列で取得
    // 列を追加する形で結果書き込み
    const target_col = (header_length - (2 - auth_id)); //ヘッダーの長さから承認者IDを引いて人を特定(auth_idは0~2)
    const target_val = applicant_line[0][target_col - 1];
    if(!target_val){throw new Error("該当の申請行がみつかりませんでした。");}
    if(target_val.match('approve:') || target_val.match('reject:')){
      return "既に結果があります。";
    }
    //承認者全員の状況をチェック
    const status = checkstatus(target_sheet, id, approvers_length);
    if(!status){throw new Error(`申請ID: ${id} の状態を取得できませんでした。`);}else if(status == "rejected"){return "既に否認された申請です。"}
    const record = "reject:" + applicant_line[0][target_col - 1]; //approve:erai@taibonn.comみたいな形式
    target_sheet.getRange(target_row,target_col).setValue(record); //セルの値を上書き

    const applicant = sheet_values[target_row-1][3];
    if(skip == "No"){
      const approvers_obj = getApprovers(applicant);
      notified = approvers_obj.approvers;
    }else if(skip == "Yes" || skip == "last"){
      notified = getOfficers();
    }
    const itemname = insertItemName(target_sheet,applicant_line);
    const title = `${document_type}否認通知。`;
    let body = `関係者各位<br>申請ID:${id} は${target_val}によって下記理由で否認されましたので通知いたします。<br>否認理由:${reason}`;
    body += makeMailbody(applicant,id,itemname);
    submitMail(applicant,title, body, body, notified);
    return "否認操作が完了しました。"
  } catch(e){
    const err_obj= `[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}`;
    submitErrMail(err_obj);
    return err_obj;
  }
}
スポンサーリンク

サブ処理関数

社員マスタから申請者に対する承認者を探してきます。
役員はコードにベタで定義しちゃってますが、社員承認のように申請者に対する役員をマスタに設定する方法でもいいでしょう。というか、そっちの方がいいと思います。
書き終わってから気づきましたよね。

//引数によって承認先を取得する
function getApprovers(applicant){
  //二次元配列でシートの内容を取得
  const master_values = getEmployeeMasterSheet().getDataRange().getValues();
  const target = applicant;
  let target_row = 0;
  //二次元配列の中で申請者のメールアドレスで検索し行マスタ中の行を特定。列まで捜査するとメモリエラーになるので列は固定。
  for(let i = 0;i < (master_values.length-1);i++){
    if(master_values[i][4] == target){
      target_row = i;
      break;
    }
  }
  // 申請者のメールアドレスが見つからなかった場合
  if(target_row == 0){
    throw new Error('社員マスタから申請者のメールアドレスが見つかりませんでした。');
  }
  // 申請者が上長かどうか
  const skip_flg = master_values[target_row][6];//G列で固定にしている
  if(skip_flg === "Yes"){
    //上長だったら役員(固定)を返す
    const approver_obj = {
      approvers' : getOfficers(),
      skip_first_flg : skip_flg
    }
    return approver_obj;
  }
  const approver_obj = {
  // 承認者は固定で1~3人
  approvers : [[master_values[target_row][7]],[master_values[target_row][8]],[master_values[target_row][9]]].flat(),
  skip_first_flg : skip_flg
  }
  return approver_obj;
}

メールのフォーマットは関数に別だししています。段階によって異なる文面はメイン処理にベタ書きしています。

//メール本文を作成する
function makeMailbody(applicant,id,itemname){
  let body = `<br><br>-------------------------------------------------------------------------------------------<br>申請者: ${applicant}<br>申請ID: ${id}<br>申請内容:<br>`;
  for(let i=0; i < itemname.length; i++){
    body += itemname[i]// +"<br>"+i+"test"
  }
  body += '<br>-------------------------------------------------------------------------------------------<br>'
  return body;
}

//項目、値を配列に入れる
function insertItemName(target_sheet,applicant_line){
  var itemname = [];
  const contents_value = getContetsValue(target_sheet,applicant_line);
  for(let i=0; i < contents_value.headers.length; i++){
    if (contents_value.contents[i].match(/[A-Z]{1}[a-z]{2}\s[A-Z]{1}[a-z]{2}\s[0-9]{2}\s[0-9]{4}\s[0-9]{2}\:[0-9]{2}\:[0-9]{2}\s*/) != null) {
    const temp_day = new Date(contents_value.contents[i]);
    contents_value.contents[i] = Utilities.formatDate(temp_day,"JST", "yyyy/MM/dd");
    }
    itemname[i]=contents_value.headers[i] + ": " + contents_value.contents[i] +"<br>"
  }
  return itemname;
}

// 指定したシートのヘッダーと受けた二次元配列の申請内容を抽出して返す
// フォームの回答シートから取得した情報じゃないと想定通り動かないので注意
function getContetsValue(target_sheet,contents_values){
  const col_values = target_sheet.getRange('1:1').getValues();//ヘッダーの行
  const last_col = col_values.flat().filter(String).length;
  let headers = Array(last_col-2);//タイムスタンプとメールアドレスを外すので-2
  headers.fill(""); //文字列型でfill()しとかないとundefined+値 になっちゃう
  let contents = Array(headers.length);
  contents.fill("");

  let cnt = 0;
  // タイムスタンプとメールアドレスを外して申請内容のヘッダーだけ取得
  for(let i = 0; i < (last_col); i++){
    if(i >= 2){
      headers[cnt] += col_values[0][i];
      contents[cnt] += contents_values[0][i];
      cnt++;
    }
  }
  const contents_obj = {
    headers: headers,
    contents: contents
  }
  return contents_obj;
}

メール送信の関数です。GASでは簡単にGmailを送信することができますね。

//引数の内容をもとにメールを送信
function submitMail(target, title, body, html, cc){
  try{
    if(!cc){GmailApp.sendEmail(target, title, body, {noReply: true, htmlBody:html});}
    else if(cc){
      for(let i = 0; i < cc.length; i++){
        GmailApp.sendEmail(target, title, body, {noReply: true, htmlBody:html,cc:cc[i]});
      }
    }
    return;
  }catch(e){
    return e;
  }
}

//エラーが発生した時にシステム管理者に送るメール
function submitErrMail(message){
  for (let i = 0; i < admin_users.length; i++){
    submitMail(admin_users[i], 'エラー通知',`${message}\n処理が失敗しました。`);
  }
}

申請にIDを付けたりそのIDを検索したりします。
IDでwebアプリ←→GASで該当申請行を探す仕組みになってます。

//申請IDを作成する
function generateId(id){
  const date = new Date;
  // 1年に一回申請シートクリアする場合は yynnnn形式にする
  // しないならidのみでいいかも
  const yy = date.getFullYear().toString().substr(-2);
  const nnnn = ('0000' + id).slice(-4);
  const result = String(yy) + String(nnnn); //文字列で返す
  return result;
}

function getIdRow(sheet, id){
  try{
    let target_row = undefined;
    const sheet_values = sheet.getDataRange().getValues();//二次元配列
    for(let i = 1; i <= sheet_values.length -1 ; i++){
      if(sheet_values[i][0] == id){
        target_row = i + 1; //申請IDの要素番号(行)を取得
        break;
      }
    }
    if(!target_row){throw new Error(`該当の申請ID(${id})が見つかりませんでした。`);}
    return target_row;
  }catch(e){
    const applicant_list_sheet = getApplicantListSheet(); //フォームの回答
    const row_values = applicant_list_sheet.getRange('1:1').getValues();
    const last_row = row_values.filter(String).length;
    const err_obj= `該当申請行:${last_row} + \n[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}\n`;
    submitErrMail(err_obj);
  }
}

あとはスプレッドシートの値を操作するような細かい関数です。

//次の承認段階シートに該当行をコピーする
function copyLine(applicant_line, target_sheet, id, approvers){
  try{
    const one_dim_array = applicant_line.flat();
    let line = [id];
    line.push("承認待ち");
    line = line.concat(one_dim_array);
    line = line.concat(approvers);
    target_sheet.appendRow(line);
    return line;
  }catch(e){
    const err_obj= `[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}`;
    console.log(err_obj);
    return e;
  }
}

//同じ段階で承認者の全員が承認あるいは1人でも却下していればステータスを変える
function checkstatus(target_sheet, id,approvers_length){
  try{
    // ターゲットシートの値を全取得
    const sheet_values = target_sheet.getDataRange().getValues();//二次元配列
    // 申請IDの行を取得
    let target_row = undefined;
    for(let i = 1; i <= sheet_values.length -1 ; i++){
      if(sheet_values[i][0] == id){
        target_row = i; //申請IDの行を取得
        break;
      }
    }
    if(!target_row){throw new Error(`該当の申請ID(${id})が見つかりませんでした。`);}

    //承認者の数とapproveのカウントが同じだったら全員承認したと判定
    const answers = sheet_values[target_row].flat().slice(-3);//承認者は3人までで固定なので
    let cnt = 0;
    for (let i = 0; i < answers.length; i++) {
      if(answers[i].match(/approve:*/)){
        cnt++;
      }else if(answers[i].match(/reject:*/)){
        //1つでも否認があれば状態列を「否認」にする
        target_sheet.getRange(target_row + 1, 2).setValue('否認');
        return 'rejected';
        break;
      }
    }
    if(approvers_length == cnt){
      target_sheet.getRange(target_row + 1, 2).setValue('承認済み');
      return 'approved';
    }
    return 'waiting';
  }catch(e){
    const err_obj= `[名前]${e.name}\n[場所] ${e.fileName}(${e.lineNumber}行目)\n[メッセージ]${e.message}\n[StackTrace]\n${e.stack}`;
    submitErrMail(err_obj);
  }
}

HTMLとjavascript

正直HTMLとクライアントサイドのjavascriptを書くのが一番ハマりました。
クライアントサイドのjavascriptって手軽にデバッグできないんですかね?

<!DOCTYPE html>
<html>
  <head>
  <base target="_top">
</head>
<body>
  <h1>
    <?=type?>の承認操作
  </h1>
  <br>
    申請ID:<span id="applicant_id"><?=id?></span>
  <br>
  skip_type:<span id="skip"><?=skip?></span>
  <br>
  auth_id:<span id="auth_id"><?=auth_id?></span>
  <br>
  approvers_length:<span id="approvers_length"><?=approvers_length?></span>
  <br>
  <div name="ap_div">
    <h3>
      申請を承認
    </h3>
    <input type="button" value="承認" id="approve">
    <br>
    <br>
  </div>
  <div name="rj_div">
    <h3>
      申請を却下する場合は却下理由記載の上、却下ボタンをクリックしてください。
    </h3>
    <textarea id="reason" rows="10" cols="80" placeholder="却下理由を入力"></textarea>
    <br>
    <input type="button" value="却下" id="reject">
    <br>
  </div>
  <script type="text/javascript">
  document.getElementById("approve").onclick = approve_onclick;
  document.getElementById("reject").onclick = reject_onclick;

  function approve_success(result){
    document.getElementById("reason").value = "";
    alert(result);
  }
  function reject_success(result){
    document.getElementById("reason").value = "";
    alert(result);
  }

  function approve_onclick(){
    const id = document.getElementById("applicant_id").textContent;
    const skip = document.getElementById("skip").textContent;
    const auth_id = document.getElementById("auth_id").textContent;
    const approvers_length = document.getElementById("approvers_length").textContent;
    alert("操作が完了するまで少々お待ちください。");
    google.script.run
      .withSuccessHandler(approve_success)
      .submitApprove(id, skip, auth_id, approvers_length);
  }

  function reject_onclick(){
    let flag = 0;
    if(document.getElementById("reason").value == ""){
      flag = 1;
    }
    if(flag){
      window.alert("却下理由を記入してください"); // 入力漏れがあれば警告ダイアログを表示
      return false; // 送信を中止
    }
    const reason = document.getElementById("reason").value;
    const id = document.getElementById("applicant_id").textContent;
    const skip = document.getElementById("skip").textContent;
    const auth_id = document.getElementById("auth_id").textContent;
    const approvers_length = document.getElementById("approvers_length").textContent;
    alert("操作が完了するまで少々お待ちください。");
    google.script.run
      .withSuccessHandler(reject_success)
      .submitReject(reason, id, skip, auth_id, approvers_length);
    }
  </script>
</body>
</html>

GASではクライアント側のHTML&jsとサーバー側のGASで値をやり取りしたり、関数を実行させたり連携するためのAPIが用意されています。
今回はそれに大いに助けられています。
google.script.runは使いこなせると楽しそうですね。でもHTMLは楽しくないですね。

まとめ

完走おめでとうございます。
長かったですよね。

GASは環境構築なしで思い立ったらすぐバシバシコーディングできるので楽しいです。
今回は結構重めの仕組みでしたが、基本コピペで動くのかな?

承認フローというとしっかりしたシステムを買ったりオプションで継ぎ足してったりしてお金がかかりますが、社内で回すものという点ではあんまりしっかりしたものは必要ないと思っています。

今回の記事がどなたかの役に立つことを期待しています。
気が向いたらこの仕組みを作るうえで得られたGASコーディングのノウハウやポイントをまとめようと思います。

コメント

  1. はるひろ より:

    はじめまして、はるひろと申します。

    社内で残業申請などのワークフローを構築しています。
    こちらの記事がとても参考になりました。

    1点ご質問させてください。

    承認先の指定についてですが、通常の申請先及び承認権限持ちの人数は決まっていて申請先候補者が休みなどの場合、別の承認先に承認を得るのですがその場合の処理などご教授頂ければ幸いです。

    あと、ぶしつけなお願いですがもし記事のスプレッドシートなど閲覧させて頂ければありがたいです。

    • たいぼん より:

      コメントありがとうございます!
      確かに承認権限持ちの人数が決まっていればこのフローが使えそうですね!

      残業申請ということですので、即日で承認される必要がありそうですね。
      ですが、このフローは承認者が承認できないパターンを考慮していません。
      そこで、承認者をGoogleグループ等のメーリスで運用するのはいかがでしょうか?

      また、思い付きですが、
      1. 承認者が1人でも承認すればOKとする
      2. 承認者のGmailに不在時自動返信メール設定し、特定の文字列を受けたらもう一人の承認者にメールを転送する
      というような変更は可能かもしれませんね。

      もしお手伝いできるようでしたら下記メールアドレス宛にメールやチャット送っていただいても構いませんので質問あれば教えてください。
      taibonn.public@gmail.com

      また、スプレッドシートは公開できるような状態で残しておらず、すみません。
      整理したら公開できるようにしますね。

      もしよければまた顛末など書きこんでいただけると嬉しいです。
      では、頑張ってください!

  2. あし より:

    お世話になっております。
    あしと申します。

    コードの公開ありがとうございます。
    早速参考させていただいております。

    一点以下のエラーが出ていて、解決に悩んでいます。
    不躾なお願いで恐縮ですが、原因もしくは解決法などありましたらご教授いただければ幸いです。

    以下は、エラーメールとして送付された内容になります。

    該当申請行:1 +
    [名前]Error
    [場所] undefined(undefined行目)
    [メッセージ]社員マスタから申請者のメールアドレスが見つかりませんでした。
    [StackTrace]
    Error: 社員マスタから申請者のメールアドレスが見つかりませんでした。
    at getApprovers (コード:290:11)
    at firstApproval (コード:76:27)
    at __GS_INTERNAL_top_function_call__.gs:1:8

    処理が失敗しました。

    • たいぼん より:

      見ていただきありがとうございます!
      自分で作っておいてうろ覚えですみませんが、申請者に対する承認者が見つかっていないエラーのようです。
      流れとしてはGoogleForms→スプレッドシートに回答が連携され、その行に含まれる申請者を取得します。
      その申請者と同じ文字列を社員マスタから探します。
      居たら申請者のよこに書いてある承認者を取得します。

      社員マスタを探す処理が↓これなのですが、社員マスタにGoogleFormsと同じ申請者の文字列がないと該当のエラーが出ます。
      for(let i = 0;i < (master_values.length-1);i++){ if(master_values[i][4] == target){ target_row = i; break; } } もしかしたら、フォームとスプシのほうでメールアドレスが違う、社員マスタに載っていないかもしれません。 申請者や承認者のメアドが記載されているセルをGASで取得できているか確認できるといいと思います。 デバッグするなら↑の処理のifのところで止めて(ブレークポイント)master_values[i][4]とtargetを1つずつ確認してみてください! (master_values[i][4]の[4]が列を示しています。0がAなので4はEです。ご自身の環境によってセル(配列)を指定している箇所を変更する必要があります。)

      • あし より:

        先日はありがとうございました。
        やはりメールアドレスの列が原因だったようです。
        列番号を変更してみるとうまくいきました。

        社員マスタ関連のエラーがまだ出ているのですが、エラーが出ない場合は問題なく処理をすることができました。
        社内SEが捗りそうです。

        ありがとうございました。

  3. sai より:

    はじめまして、saiと申します。

    コードの公開ありがとうございます。
    こちらの記事がとても参考になりました。
    大変長いコードで私では作ることができないと思います。
    早速参考させていただいております。

    一点以下のエラーが出ていて、解決に悩んでいます。
    不躾なお願いで恐縮ですが、原因もしくは解決法などありましたらご教授いただければ幸いです。

    以下は、エラーメールとして送付された内容になります。
    [名前]TypeError
    [場所] undefined(undefined行目)
    [メッセージ]Cannot read property ‘parameter’ of undefined
    [StackTrace]
    TypeError: Cannot read property ‘parameter’ of undefined
    at doGet (定数:110:18)
    at __GS_INTERNAL_top_function_call__.gs:1:8
    処理が失敗しました。

    また、doGet(e)関数で、実行を行うと、ReferenceError: document_type is not defined
    となり、
    const err_obj= ` [申請種類] ${document_type} \n[名前] ${e.name}\n[場所] ${e.fileName} ${e.lineNumber}行目)\n[メッセージ] ${e.message}\n[StackTrace]\n${e.stack}`;
    の行の[申請種類] ${document_type} を消すとエラーなく実行できましたが、
    承認操作画面のHTMLでReferenceError: document_type is not defined(行 122、ファイル「定数」)と画面に出てきてしまいます。

タイトルとURLをコピーしました