Google Apps Scriptで健診予約管理を作ってみた【GAS】

企業によって健康診断の実施時期は異なりますが、どの企業でも総務課が予約を取るのは一苦労な場面があります。所属支店内に於いて様々な部署やメンバーの為に枠を取り、それぞれから希望日を効いて、枠が溢れてしまったら、別の日程に変更を促し、再度取得。最後に健診センターに予約を入れるなどなど。

特に総務の指示に従わず、予約取らないものや、希望日が偏って集中しなんども申込者と日程変更のやり取りをするのは不毛な時間です。そこで、メンバー一覧と枠一覧を用意し、ウェブアプリケーション上で予約を先着順で取って貰えれば、これらの手間のほとんどが不要になります。今回はそんな健診管理システムの簡易版を作ってみました。

今回使用するスプレッドシート

2つのシートがあり、また他の人の予定を探られないように、各人には記号を渡しておき、その記号を持って処理をすすめる方式です。誰がどの記号なのかは、管理者が別途管理する必要があります。

使い方

このアプリは初期セットアップをしなければ利用ができません。手順は簡単で以下の通りです。

  1. スプレッドシートメニューにある「▶セットアップ」を開く
  2. 初期化を実行すると、スプレッドシートのIDがスクリプトプロパティに格納されます。
  3. 送信先を実行すると、管理者向け自動応答メールの送信先のメアド指定がでるので、セットする。
  4. 健診枠シートの残り枠数列以外に、今回取得した健診枠のデータを記述しておく。
  5. 希望日シート記号性別フラグ(0=男、1=女)で人数分用意する。メアドや個人情報は記載しない。
  6. このセットアップが終わったら、ウェブアプリケーションとして公開し、メンバーにはそれぞれ該当の記号を個別にお知らせしてあげる。

図:非常にシンプルな予約画面です。

※現在社内ではこれをElectronで作成し、オプション健診予約や年齢による強制受信のオンオフなど様々な機能を追加しています。

ソースコード

HTML側コード

<!DOCTYPE HTML>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <style>
      .form-control{
        width:60%;
      }
    </style>
    <script>
      //健診受信可能日を格納する
      var masterman = "";
      
      //性別フラグを格納する
      var sflag = ""
      
      //入力されたコードをチェックする
      function checkman(){
        //入力された値をチェック
        var validata = document.getElementById("checkcode").value;
        
        //空チェック
        if(validata == ""){
          alert("コードが入っていませんよ");
          return;
        }
        
        //コードの認証をする為、GAS側へデータを送る
        google.script.run.withSuccessHandler(onSuccess).codeapprove(validata);
        
        //プログレス表示をONにする
        document.getElementById("mainform").style.display = "none";
        document.getElementById("progress").style.display = "block";
        
      }
      
      //健診可能日を取得し、selectboxへ反映する
      function onSuccess(data){
        var json = JSON.parse(data);
        
        if(json[0] == "NG"){
          //NGフラグの場合、処理を中断
          alert(json[1]);
          
          //プログレス表示をONにする
          document.getElementById("mainform").style.display = "block";
          document.getElementById("progress").style.display = "none";
         
          return;
        }else{
          //OKフラグの場合、受診可能な日程のselectを生成する
          var array = json[1];
          sflag = json[2];
        
          //arrayの値に基づきHTML組み立て
          var alength = array.length;
          var html = "<option value='0'>受診日程を選択</option>";
          
          for(var i = 0;i<alength;i++){
            html += "<option value='" + array[i][0] + "'>" + array[i][7] + "</option>"
          }
          
          //selectboxを反映する
          document.getElementById("selectman").innerHTML = html;
          
          //プログレス表示をOFFにする
          document.getElementById("subform").style.display = "block";
          document.getElementById("progress").style.display = "none";
          
          //sflagに応じてオンオフ
          if(sflag == 0){
            //男性枠なので非表示にする
            document.getElementById("women").style.display = "none";
          }else{
            //女性枠なので表示する
            document.getElementById("women").style.display = "block";
          }
        }
      }
      
      //健診予約登録を行うためのルーチン
      function kenyoyaku(){
        //ユーザIDと選択した枠のIDを取得する
        var userid = document.getElementById("checkcode").value;
        var wakuid = document.getElementById("selectman").value;
        
        //コード認証と日付枠取得処理を実行する
        if(wakuid == 0){
          alert("枠日程が選択されていませんよ。");
          document.getElementById("selectman").focus();
          return;
        }else{
          //GAS側へ処理を投げる
          google.script.run.withSuccessHandler(onKensin).getkensin(userid,wakuid,0);
        }

        //プログレス表示をONにする
        document.getElementById("subform").style.display = "none";
        document.getElementById("progress").style.display = "block";
      }
     
      //健診予約結果を処理する
      function onKensin(data){
        var json = JSON.parse(data);
        
        //サーバーからのメッセージ表示
        alert(json[1]);
        
        switch(json[0]){
          case "NG":
            //プログレス表示をONにする
            document.getElementById("subform").style.display = "block";
            document.getElementById("progress").style.display = "none";
            break;
          case "OK" :
            //プログレス解除してトップページに戻す
            document.getElementById("mainform").style.display = "block";
            document.getElementById("progress").style.display = "none";
            
            //コードをリセットする
            document.getElementById("checkcode").value = "";
            break;
          case "Fail":
            //枠のプルダウンを再生成する
            document.getElementById("selectman").innerHTML = "";
            var userid = document.getElementById("checkcode").value;
            google.script.run.withSuccessHandler(onSuccess).codeapprove(userid);
            break;
        }
      }

    </script>
  </head>
  <body>
      <nav class="navbar navbar-default">
        <div class="container">
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">健診予約</a>
          </div>
        </div>
    </nav>
      <div class="container">
          <!-- 記号を送る入力欄 -->
          <div id="mainform" style="display:block">
            <h1>記号の入力</h1>
            <form name="fm">
                <label for="textArea" class="control-label">事前にメールで渡されてるコードを入力</label>
                <p><input type="text" pattern="\d*" id="checkcode" name="name" size="30" maxlength="20"></p><br>
                <a href="#" class="btn btn-primary btn-lg" onclick="checkman()">送信する</a>
            </form>
          </div>
          
          <!-- 現在選択できる日程の提示 -->
          <div id="subform" style="display:none">
            <h1>日程の選択</h1>
            <p id="women" style="display:none">受診可能枠の記号Eは「エコー」、記号Mは「マンモグラフィ」の意味です。お間違えのないよう。</p>
            <form name="fm2" style="display:block;">
              <div class="form-group">
                <label class="control-label">現在予約可能な日程一覧</label>
                <br>
                <div>
                  <select class="form-control" id="selectman" name="number"></select>
                </div>
              </div><p><br>
               <a href="#" class="btn btn-primary btn-lg" onclick="kenyoyaku()">予約する</a>
            </form>
          </div>
          
          <!-- プログレス表示 -->
          <div id="progress" style="display:none;">
            <center>
              <p><b>作業中・・・・</b></p>
              <img border='0' src='https://officeforest.org/wp/library/icons/spinner.gif' width='32' height='32'>
            </center>
          </div>
      </div>
      <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
  </body>
</html>
  • 今回はモバイルデバイス対応の為に、Booststrapおよびviewport設定を使っています。
  • 性別によって、列挙する健診枠を変更しています。
  • また、枠が埋まっているものについては、一覧表示させず、予約時にも残枠数チェックを入れるようにしています。
  • 主な機能は、記号の認証と健診枠表示、予約登録の3種類のみです。

GAS側コード

//ウェブアプリケーションを表示する
function doGet(e) {
  var html = HtmlService.createHtmlOutputFromFile('index')
              .addMetaTag("viewport", "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=10.0")
              .setTitle("健診予約窓口");
  return html;
}

//IDを取得し認証後、健診可能日を返す
function codeapprove(userid){
  var Properties = PropertiesService.getScriptProperties();
  var ssid = Properties.getProperty("sheetid");
  
  //希望日シートにて対象のコードがあるかチェック
  var ss = SpreadsheetApp.openById(ssid);
  var sheet = ss.getSheetByName("希望日").getRange("A2:C").getValues();
  var length = sheet.length;
  
  //フラグ類
  var cflag = false;
  var sflag = "";   //0 = 男, 1 = 女
  
  //シートを探索しコードの有無をチェック
  for(var i = 0;i<length;i++){
    if(userid == sheet[i][0]){
      //コードが見つかったらフラグをtrueにする
      cflag = true;
      sflag = sheet[i][2];
      break;
    }
  }
  
  //フラグの状態によって処理を分岐
  if(cflag == false){
    //IDが見つからなかった場合の処理
    var msg = "コードが未登録もしくは発見できませんでした";
    return JSON.stringify(["NG",msg]);
  }
  
  //現在受診可能な健診枠データを取得する
  var array = [];
  var sheet2 = ss.getSheetByName("健診枠").getRange("A2:I").getValues();
  var slength = sheet2.length;
  
  for(var i = 0;i<slength;i++){
    //sflagに基づいて判定
    if(sflag == sheet2[i][6]){
      //性別が一致したら、残り枠数をチェック
      if(sheet2[i][8] > 0){
        //枠数が1以上あるので、配列にpushする
        array.push(sheet2[i]);
      }
    }  
  }
  
  //検索結果をリターンする
  return JSON.stringify(["OK",array,sflag]);
}

//健診予約の処理を行うメインルーチン
function getkensin(userid,wakuid,skipflg){
  //ドキュメントロックを使用する
  var lock = LockService.getDocumentLock();

  try {
    //30秒間の排他ロックを取得する
    lock.waitLock(30000);
    
    var Properties = PropertiesService.getScriptProperties();
    var ssid = Properties.getProperty("sheetid");
    
    //希望日シートにて対象のコードがあるかチェック
    var ss = SpreadsheetApp.openById(ssid);
    var sheet = ss.getSheetByName("希望日").getRange("A2:C").getValues();
    var length = sheet.length;
    
    //フラグ類
    var cflag = false;
    var sflag = "";   //0 = 男, 1 = 女
    var kibou = "";
    var counter = 2;
    
    //シートを探索しコードの有無をチェック
    for(var i = 0;i<length;i++){
      if(userid == sheet[i][0]){
        //コードが見つかったらフラグをtrueにする
        cflag = true;
        sflag = sheet[i][2];
        kibou = sheet[i][1];
        break;
      }
      
      //カウンターを回す
      counter = counter + 1;
      
    }
    
    //すでにkibouに値が入ってる時の確認処理入れる?
    if(skipflg == 1){
      //kibouの値の有無を無視してスルーする
    }else{
      if(kibou == ""){
        //まだ予約取っていないので何もしない
      }else{
        //すでに入ってるので予約日変更するか確認するためにreturnする
        var msg = "すでにもう予約が入っています。予約の変更は直接担当者にご連絡ください。"
        return JSON.stringify(["Fail",msg]);
      }
    }

    //現在受診可能な健診枠データを取得する
    var sheet2 = ss.getSheetByName("健診枠").getRange("A2:I").getValues();
    var slength = sheet2.length;
    var wakuname = "";

    //健診残り枠数をチェックする
    for(var i = 0;i<slength;i++){
      if(wakuid == sheet2[i][0]){
        //残り枠数を取得
        var zanwaku = sheet2[i][8];
        var wakuname = sheet2[i][7];
      
        //残が0の場合Failを返す
        if(zanwaku == 0){
          //残り枠が0になっちゃったので
          var msg = "残り枠が0になってしまいました。再度、別の枠を取り直してください。"
          lock.releaseLock();
          return JSON.stringify(["Fail",msg]);
        }else{
          //枠がまだ余ってるのでループを抜ける
          break;
        }
      }
    }
    
    //希望日シートに希望日ID(wakuid)を書き込む
    ss.getSheetByName("希望日").getRange("B" + counter).setValue(wakuid);
    
    //ロックを開放する
    lock.releaseLock();
    
    //メール送信
    var Properties = PropertiesService.getScriptProperties();
    var tomail = Properties.getProperty("mail");
    var mail = tomail;
    var body = "記号" + userid + "さんから、" + wakuid + "の予約が入りましたよ。ご確認くださいません。<br>"
             + "尚、このメールは自動送信されているので、返信はできませんよ。" ;

    MailApp.sendEmail({
        to:mail,
        subject:"【健診予約】予約が入りましたよ",
        htmlBody: body,
        noReply: true
    });
   
    //値を返す
    var msg = wakuname + "にて、予約が完了しました。";
    return JSON.stringify(["OK",msg]);

  } catch(e) {
    //ロック取得できなかった時の処理等を記述する
    var checkword = "ロックのタイムアウト: 別のプロセスがロックを保持している時間が長すぎました。";
    
    //通常のエラーとロックエラーを区別する
    if(e.message == checkword){
      //ロックエラーの場合
      msg = "誰かまだ使ってるみたい。しばらく待ってから再度アクセスしてみてください。";
    }else{
      //ソレ以外のエラーの場合
      msg = e.message;
    }   
    
    //ロックを開放する
    lock.releaseLock();
    
    //NG結果をリターンする
    return JSON.stringify(["NG",msg]);
  }

}
  • 予約のルーチンは、バッティング防止の為に排他制御を利用しています。
  • 記号の認証探索と予約のルーチンの2つがメイン機能です。

カレンダーに登録してもらう

フォーム送信完了と共に、そのままGoogle Calenderへ連続登録も良いのですが、登録するかしないかはユーザに選択の余地があったほうが良いこともあります。また、健診だと個人のカレンダーアプリに登録する人もいるでしょう。
そういった人たちに、手軽に健診予約情報を登録してもらうには、URLを叩くと登録画面が出るようにするとGoodです。今回のサンプルには搭載していませんが、以下のようなURLを生成し、メールで送ってあげると、丁寧ですね。予め対象のカレンダーにログインしてある必要があります。

Google Calenderへ登録する

Google Calenderへイベント登録するURLの構成は以下のような感じになります。

http://www.google.com/calendar/event?action=TEMPLATE&text=健康診断&details=定期健康診断を受ける日&location=東京都小笠原村沖ノ鳥島&dates=dates=20200207T043000Z/20200207T063000Z

Calenderに登録時に注意すべきは、日付です。textはタイトル、detailsが本文、Locationが住所で、これらは素直につなげれば良いですが、日付だけはそのままの状態では登録できません。
また、健診なので時間の設定も必要です。この時間は13:00に健診予定がある時は、+9:00した時間でなければなりません。また、日付形式もUTCの形式でと整えて開始時間と終了時間それぞれをスラッシュでつなげたものを必要とします。
上記の事例でsと、2020年2月7日13:30~15:30の予定となっているのですが、datesより後ろはなんだか時間が変。しかしこれで正しいのです。元の時間に+9:00でUTC形式にするとこのような日付になります.

//元の日付の形式
2020-02-07T13:00:00+09:00

//UTC形式へ変換する関数
var getUTC = function(date_str){
    var date = new Date(date_str);
    return date.getUTCFullYear() +
      zerofill(date.getUTCMonth()+1) +
      zerofill(date.getUTCDate()) +
      'T' +
      zerofill(date.getUTCHours()) +
      zerofill(date.getUTCMinutes()) +
      zerofill(date.getUTCSeconds()) +
      'Z';
};

//0埋めする関数
var zerofill = function(num){
  return ('0'+num).slice(-2);
}

Outlookへ登録する

outlookの場合は割と素直なURLで構築が可能です。

https://outlook.office.com/owa/?path=/calendar/action/compose&rru=addevent&startdt=2020-02-07T13:00:00&enddt=2020-02-07T15:00:00&subject=健康診断&body=定期健康診断を受ける&location=東京都小笠原村沖ノ鳥島

startdtが開始時間、enddtが終了時間、subjectがタイトルで、bodyが本文、Locationが住所になります。+9:00する時点は同じですが、UTCに変換は不要です。

2020年12月1日現在、+9:00を付与してると正しい日時登録がなされなくなり、これを外してあげるとうまく動作したので、UTC変換は勝手にやってくれてるようなので、正しい日時が新規画面に入らない場合には、T12:00:00+09:00といった表記は、+09:00は削ったほうが良いです。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)