技術

【GAS】シフト管理を自動化して業務効率化をしたぺこ!

はらぺこ君
はらぺこ君

みんな、元気ぺこー?
僕は、新型コロナちゃんが全然収まらなくてつらいぺこ。。。 

はらぺこ君
はらぺこ君

今日は、僕のお世話をしてくれるペコシェさんが行った業務効率化についてお話するぺこよー!
ペコッター運営の内部が気になる。業務効率化して空いた時間でサボりたい。みんなからすごいって言われたい人には必見の内容ぺこ♪

ペコシェさん
ペコシェさん

初めまして! ペコッターでアルバイトしているまりもです。
本業としてプログラマをしているので、その知識を生かして業務効率化をしてみました。
個人ブログも書いてるのでよかったらチェックしてみてくださいぺこー!
GitHubはこちらです! 全国のIT企業様からの採用連絡お待ちしてます!

はじめに

ペコッターでは、月間約2万件の予約依頼を頂いていますぺこ。
その予約依頼1つ1つを、ぺコンシェルジュさんが丁寧に対応していますぺこ。
大量の依頼をこなすためには、毎日のシフト管理が重要ぺこけど、そのシフト作成に時間がかかってましたぺこ。
この記事では、そんなシフト作成業務を効率化した内容を書いていますぺこ♪

効率化した業務

ペコッター運営を手伝ってくれている、ぺコンシェルジュさんのシフト管理を効率化したぺこ!
環境・シフト作成までのフローはこんな感じだったぺこ。

■環境
・Googleスプレッドシート
■既存フロー
 ①週に1回、ぺコンシェルジュさん(以下、ペコシェさん)が次週の希望シフトを入力
 ②シフトを取りまとめるペコシェさんが、ぺコシェさんの技量・貢献度を考慮してシフトを作成
 ③シフト作成をした人とは別の人が、完成されたシフトにミスがないかエラーチェックをする

今回は、上記フローの②を自動化したぺこ!

効率化の成果

みんな気になってると思うぺこから、どのくらい効率化できたかを先に書くぺこなー♪

効率化の効果(1ヶ月ごと)
業務内容所要時間(効率化前)所要時間(効率化後)
シフト作成120分40分

シフト作成は毎週1回行っていて、1ヶ月ごとに80分の削減ができたぺこー! 割合だと66%削減ぺこな。
毎週タバコ休憩できる時間が作れた! ってペコシェさんが喜んでたぺこな。

具体的な効率化手順

ここでは、具体的にどうやって定例業務を効率化していったか紹介していくぺこよー!

シフト作成前/作成後

まずはじめに、完成したシフト自動作成を動かしてみるぺこなー♪

シフト作成前
シフト作成後

シフト作成前と、シフト作成後の画像ですぺこ!
右側にあるのがペコシェさんに出してもらう希望シフト、左側が希望シフトを元に作成したシフトですぺこりー♪

今までは、希望シフトを見ながらシフトを作成するのを手動でやってたんぺこけど、今回その手動の部分を自動化したぺこ!

実際のシフト表

次に、シフト表の説明を詳しくしていくぺこー。

上に貼った画像が、実際にペコッターで管理しているシフト表の例ですぺこ!
のび太、しずか、スネ夫…とかの名前はぺコシェさんですぺこ。

それぞれ人ごとに希望シフトを週1回、1週間分入力してもらってるぺこ。

そして、この希望シフトを元に実際にシフトを作っていくのがシフト作成ぺこ!

これが希望シフトを元に作成したシフトですぺこ!列について一個ずつ解説していくぺこな。

O〜R列:ぺコンシェルジュさんが入る列ですぺこ。
N列:ぺコンシェルジュさんが入る列のうち、その時間のリーダー(SV)の人の名前が入る列ですぺこ。誰をSVにするのかはシフト作成の時に決めるぺこ。
A列:5行目には、シフトを作成したペコシェさんの名前が、6行目にはそのシフトに矛盾がないかをチェックした別のペコシェさんの名前が入るぺこ。

シフト作成手順

シフト作成とは、シフト希望を元に、その内容を実際のシフトに落とし込む作業ですぺこ。
基本的に、シフト希望を出してくれているペコシェさんをすべて同一時間に入れているぺこ。
・シフト希望を出してくれているのにシフトに反映されていない
ということが無いようにシフトを作っていますぺこ。

加えて、作成の際に考慮している内容としては、「同一時間に複数ペコシェさんがいた場合、誰を時間帯リーダー(=SV)にするか」ですぺこ。
SVになると、時給が数百円アップするぺこ。ペコシェさんの力量・ペコッターへの貢献度などを総合的に考慮してSVを一人選んでるぺこ。

具体的には、ペコシェさんの1日の勤務時間が多い・経験歴が長い、直近の勤務時間合計が多い、等ですぺこ!

複雑ぺこな。
下の画像を例にとると、
10〜11時:ペコシェさんが1人しかいないから無条件にSV
13〜14時:同一時間にSV可能な人が複数いるので、1日の出勤時間が長いのび太をSVにする
みたいな感じぺこな。

SVになれるペコシェさんが複数いる場合、SVを誰にするのかを決めるのに時間がかかってたぺこ。
そんな時間を短縮したくて、このシフト作成も自動化しちゃったぺこなー♪

シフト作成自動化

シフト表はGoogleスプレッドシートで管理してたぺこから、言語はGoogleAppScriptを使用したぺこ。
チェック処理自体は技術的に難しいことは無くて、希望シフトと実際のシフトを比較していく感じぺこ。
ただ、ペコシェさんのほとんどがプログラミングについて詳しくないぺこから、そんな人でも使いやすいようにした実装上の工夫をいくつかしてあるぺこ。

①任意のタイミングでボタンを押せば処理が開始される
②処理にどれぐらい時間がかかるのかを表示する

実際にシフトWチェックを実行して解説するぺこな。

手順1:Z2セルのはらぺこ君を押下
手順2:開始日終了日をMMDD形式で入力
手順3:確認画面を出力。基本的に1週間ごとに作成するぺこから、1週間の実行時間を書いてるぺこ。
手順4:処理の完了状況を出してるぺこ。これがあるとあとどれくらいで終わるのか分かりやすいぺこな。
シフト作成が完了すると、こうやって表示されるぺこ。びーる飲みたいぺこな嬉しいぺこな。
シフト自動作成が終わった後ですぺこ。

こんな感じで自動でシフトが作成されるぺこりな。
ただ、この場合だとのび太が10時〜18時の間ずっとSVになってるぺこり。一方、ジャイアンは3時間入っているのに1度もSVじゃないぺこり。
実際は、1日の出勤のうち1/3〜1/2がSVになるようにするのがいいんぺこけど、それはまだ実装してないぺこなー。今後の課題ぺこ。

おまけ

Wチェックのソースコードを見せるぺこ♡

  FROM_DATE = Browser.inputBox("シフト作成開始日を入力してぺこ \\n MMDD形式で入力してぺこり。");
  //右上の×️ボタンが押された時は処理終了
  if (FROM_DATE==CANCEL_BTN){
    return;
  }
  
  //シフトチェック開始日エラーチェック0401
  var DataCheck = DateCheck(FROM_DATE);
  
  if (DataCheck !== MSG_OK){
    //入力チェックエラーの場合はエラーメッセージを出力して処理終了
    Browser.msgBox(DataCheck);
    return;
  }

  TO_DATE = Browser.inputBox("シフト作成終了日を入力してぺこ \\n MMDD形式で入力してぺこり。\\n 開始日:" + 
                                Number(FROM_DATE.substr(0,2)) + "月" + 
                                Number(FROM_DATE.substr(2,4)) + "日")
  //右上の×ボタンが押された時は処理終了
  if (TO_DATE==CANCEL_BTN){
    return;
  }
  //シフト作成終了日エラーチェック
  DataCheck = DateCheck(TO_DATE);
  
  if (DataCheck !== MSG_OK){
    //入力チェックエラーの場合はエラーメッセージを出力して処理終了
    Browser.msgBox(DataCheck);
    return;
  }

  //シフト作成開始日と終了日の整合性チェック
  DataCheck = DateFromToCheck(FROM_DATE,TO_DATE);
  if (DataCheck !== MSG_OK){
    //入力チェックエラーの場合はエラーメッセージを出力して処理終了
    Browser.msgBox(DataCheck);
    return;
  }

こちらは、シフトWチェック開始前の入力部分とそのチェック処理ですぺこ。
MMDD形式で入力してもらって、その日付の整合性をチェックしてるぺこ。

  //ペコシェさんの名前一覧を配列として取得
  ArrayPecoName = GetPecocieName();
  
  //開始日〜終了日において、すでにシフトが入力されていた場合はエラーにする(シフトが上書きされちゃって整合性が終了してしまうため。。。。)
  //該当日付のペコシェさんのシフト(確定版)を二次元配列に格納する(1日ずつ)
  //開始日と終了日の行数取得
  result = GetShiftDays(); 
  if (result !== MSG_OK){
    //入力チェックエラーの場合はエラーメッセージを出力して処理終了
    Browser.msgBox(result);
    return;
  }

  for(let row = FROM_DATE_ROW; row <= TO_DATE_ROW; row = row + TO_FROM_DAT_ROW){
    //ペコシェさん確定シフト
    var ConfirmPecoShiftArray = GetConfirmPecoShiftArray(row);
    for(let r = 0; r< ConfirmPecoShiftArray.length; r++){
      for(let c = 0; c < ConfirmPecoShiftArray[0].length; c++) {
        //該当シフトを横に見ていって、空白でない場合はエラー
        if(trim(ConfirmPecoShiftArray[r][c]) != "" ){
          Browser.msgBox(MSG_SHIFT_CREATE_NG);
          return;
        }          
      }
    } 
  }
  
  //シフトチェック実行前の確認メッセージ出力
  var result = Browser.msgBox("開始日:" + 
                                Number(FROM_DATE.substr(0,2)) + "月" + 
                                Number(FROM_DATE.substr(2,4)) + "日" +
                              "\\n終了日:" + 
                                Number(TO_DATE.substr(0,2)) + "月" + 
                                Number(TO_DATE.substr(2,4)) + "日\\n" +
                                ArrayPecoName[0][0] + "ちゃんから" + ArrayPecoName[0][ArrayPecoName[0].length-1] + "ちゃんまで" +
                               "シフト作成を行うぺこけど、大丈夫ぺこー?" +"\\nちなみに、1週間分作成するのに1~2分くらいかかるぺこな。" ,Browser.Buttons.OK_CANCEL);
  
  //キャンセルボタンor右上の×ボタンが押下されたら処理終了
  if (result == CANCEL_BTN) {
      return;
  }
  
  //シフト作成
  PecoShiftCreate(FROM_DATE_ROW,TO_DATE_ROW)
  
  //完了メッセージ
  Browser.msgBox(MSG_SHIFT_CREATE_OK);

開始日と終了日が入力されたら、確認メッセージを出力して、シフト作成処理を実行するぺこ。

//シフト作成関数。日付単位で実行していく。
function PecoShiftCreate(From_Data_Ret,To_Data_Ret){
  console.log("PecoShiftCreate Start------------------------------:");
  var objSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  objSpreadsheet.toast('ちょっと待っててぺこー!1日あたり10〜20秒くらい処理に時間かかるぺこな。','シフト作成中ぺこ',-1);
 
  //ペコシェさんの名前一覧を配列として取得
  var ArrayPecoName = GetPecocieName();
  
  
  var msg_count = 0
  for(let row = FROM_DATE_ROW; row <= TO_DATE_ROW; row = row + TO_FROM_DAT_ROW){
    CreateShift(row,ArrayPecoName);
    
    //半分超えたら通知
    if(row>= FROM_DATE_ROW + ((TO_DATE_ROW-FROM_DATE_ROW)/2) && msg_count==0){
      msg_count++;
      objSpreadsheet.toast('50%くらい終わったぺこ。','シフト作成中ぺこ',-1);
    }
  }
  objSpreadsheet.toast('お待たせぺこー!。','シフト作成完了ぺこ',5);
  console.log("PecoShiftCreate End------------------------------:");
}

こちらがチェック処理ぺこ。
概要としては、日付と時間単位でチェック処理を走らせてるぺこ。全部のセルをforで回してるから、チェックに時間かかってるのが難点ぺこ。

//ペコシェさんシフト作成関数
function CreateShift(Data_Ret,ArrayPecoName) {
  console.log("CreateShift Start------------------------------:");
  var row = Data_Ret;
  var objSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  //デビューペコシェさんを取得(研修中を除く)
  var PecoCieName = GetPecocieName_SVOnly();
  

  //ペコシェさんのごとの1日の出勤時間合計を取得(研修中を除く)
  var PecoDayShiftSum = DayShiftSum(PecoCieName,row)
  //出勤時間が最大のペコシェさんが入っている配列番号を取得
  var ArrayPecoMaxWork = MaxNumArray(PecoCieName,PecoDayShiftSum)
  
  //作成するシフト期間における、個人ごとの総労働時間の偏差値を取得
  var ArrayPecoDevValue = PecoDevValue(PecoCieName,PecoDayShiftSum);
  //シフト期間偏差値が最大のペコシェさんが入っている配列番号を取得
  var ArrayPecoDevMaxWork = MaxNumArray(PecoCieName,ArrayPecoDevValue);
  console.log("シフト期間最大偏差値配列番号:"+ArrayPecoDevMaxWork[0] + ":"+PecoCieName[0][ArrayPecoDevMaxWork[0]]+"ちゃん");
  
  //デビューペコシェさんが複数存在する時間帯の行数を格納する配列
  var MultiPecoRow = [];
  var objSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var objSheet = objSpreadsheet.getActiveSheet();

  
  //1日ずつ二次元配列に格納する
  //チェックする日付の行数
  var GetDateRet = Data_Ret;
  //ペコシェさん入力シフト
  var PecoShiftArray = GetPecoShiftArray(GetDateRet);
  
  console.log("PecoShiftArray : " + PecoShiftArray)
  
  //同一時間のSVペコシェさんの人数
  var SVPecoShiftCount = 0;
  //同一時間の研修ペコシェさんの人数
  var TrPecoShiftCount = 0;
  //・同一時間にSV可能な人が一人だけの場合、無条件にSVにする
  for(let r = 0; r < PecoShiftArray.length; r++) {
    
    
    //時間ごとにペコシェさんカウントを初期化
    SVPecoShiftCount = 0;
    TrPecoShiftCount = 0;
    PecoShiftCount = 0;
    for(let column = 0; column < PecoShiftArray[0].length; column++){
      console.log(column + "列目 " + r + "行目");
      console.log(PecoShiftArray[r][column]);
   
      //時間が入力されていたらチェック開始
      if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×"){ 
      
        if(column <= SV_RET_TO - SV_RET_FROM){
          //SVペコシェさんの場合
          SVPecoShiftCount ++;
        }else{
          //研修中ペコシェさんの場合
          TrPecoShiftCount ++;
        }
      
      }
  
    }

これはペコシェさんのシフトをセルに反映させていくシフト作成関数ぺこ。
時間ごとのペコシェさん希望シフトを5パターンに分けてますぺこ。

①ペコシェさんが誰もいない時間の場合
 →シフトを反映させる必要ないぺこ
②研修中ペコシェさんのみの場合
 →研修中ペコシェさんはSVになれないぺこから、単純に架電列に反映させるだけぺこ
③デビューペコシェさんのみの場合
 →1人だけなので、SV列に入れるぺこ
④デビューペコシェさん1人と、研修中ペコシェが1人以上いる場合
 →②と③の組み合わせぺこ
⑤デビューペコシェさんが複数の場合
 →これがよくあるパターンぺこ。その日の労働時間が一番長いペコシェさんをSVにするぺこ

処理①〜⑤のソースを以下に載せるぺこなー!

▼①ペコシェさんが誰もいない時間の場合/②研修中ペコシェさんのみの場合

    if(SVPecoShiftCount == 0 && TrPecoShiftCount == 0 ){
      //①ペコシェさんが誰もいない時間なので対応不要
      continue;
    }else if(SVPecoShiftCount == 0 && TrPecoShiftCount >= 1 ){
      //②研修中ペコシェさんのみなので全員架電列に入れる
      for(let column = 0; column < PecoShiftArray[0].length; column++){
        //シフトが入力されてたら転記する
        if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×"){ 
          //既にペコシェさんの名前が入っていたら左にずらす
          for(let Ret = KADEN_START_RET;Ret < KADEN_START_RET + KADEN_HUMAN_NUM;Ret++){
            
            if(objSheet.getRange(row + r,Ret).getValue() !== ""){
              //ペコシェさんの名前ありのためforを1回スキップ
              continue;
            }
            console.log("ペコシェ名:"+ ArrayPecoName[0][column] + "行:" + (row + r + 1) + "列:" + (SV_RET_FROM + column) )
            AddShift(ArrayPecoName[0][column],row + r,Ret)
            break;
          }
        }
      }

▼③デビューペコシェさんのみの場合/④デビューペコシェさん1人と、研修中ペコシェが1人以上いる場合

}else if(SVPecoShiftCount == 1 && TrPecoShiftCount == 0 ){
      //③デビューペコシェさんのみなので、SVに入れる
      for(let column = 0; column < PecoShiftArray[0].length; column++){
        //シフトが入力されてたら転記する
        if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×"){
            console.log("ペコシェ名:"+ ArrayPecoName[0][column] + "行:" + (row + r) + "列:" + (KADEN_START_RET-1) )
            AddShift(ArrayPecoName[0][column],row + r,KADEN_START_RET-1)
            break;              
        }
      }
      
    }else if(SVPecoShiftCount == 1 && TrPecoShiftCount >= 1 ){
      //④デビューペコシェさんは1人なのでSV、研修中ペコシェさんは全員架電に入れる
      var sv_count = 0;
      for(let column = 0; column < PecoShiftArray[0].length; column++){
        
         //シフトが入力されてたら転記する
        if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×"){
          
          //SVペコシェさんか研修ペコシェさんかで分岐する
          if(column <= (SV_RET_TO - SV_RET_FROM) && sv_count == 0){
            //SVペコシェさん
            AddShift(ArrayPecoName[0][column],row + r,KADEN_START_RET-1)
            sv_count++;
          }else{
            //研修ペコシェさん
            //既にペコシェさんの名前が入っていたら左にずらす
            
            for(let Ret = KADEN_START_RET;Ret < KADEN_START_RET + KADEN_HUMAN_NUM;Ret++){
              if(objSheet.getRange(row + r,Ret).getValue() !== ""){
                //ペコシェさんの名前ありのためforを1回スキップ
                continue;
              }
              console.log("ペコシェ名:"+ ArrayPecoName[0][column] + "行:" + (row + r + 1) + "列:" + (SV_RET_FROM + column) )
              AddShift(ArrayPecoName[0][column],row + r,Ret)
              break;
            }  
          }
        }
      }

▼⑤デビューペコシェさんが複数の場合

 }else if(SVPecoShiftCount >= 2){      
      //⑤デビューペコシェさんが複数なので、誰をSVにするかを決定する(研修中ペコシェさんは架電に入れる)
      //その日の労働時間が一番長い人をSVにする。
      
      //あとでシフト調整ロジックを動かす時に使う
      MultiPecoRow.push(r);
      
      //シフトを入力しているペコシェさん一覧を取得(配列)
      var ArrayWorkPeco = [];
      for(let column = 0; column < PecoShiftArray[0].length; column++){
        if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×" && column <= (SV_RET_TO - SV_RET_FROM)){
          ArrayWorkPeco.push(column);
        }
      }
      
      //その日シフトに入っている時間が最大値のペコシェさんの人数を取得
      var maxworkhuman = 0;
      //ArrayPecoMaxWork 出勤時間が最大のペコシェさんが入っている配列 [saotam(0),azusa(1),rui(3)]
      //ArrayWorkPeco 出勤しているペコシェさんが入っている配列 [saotam(0),rui(3)]
      for (i = 0;i<ArrayWorkPeco.length;i++){
        if (ArrayPecoMaxWork.indexOf(ArrayWorkPeco[i]) >= 0){
          maxworkhuman++;
        }
      }
      
      var count = 0;
      if(maxworkhuman ==0){
        //どのペコシェさんも最大ではないのでとりあえず仮に一番左のペコシェさんをSVにする
        
        for(let column = 0; column < PecoShiftArray[0].length; column++){
          //シフトが入力されてたら転記する
          if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×"){
            if(count==0){
              //SVにする
              AddShift(ArrayPecoName[0][column],row + r,KADEN_START_RET-1)
              count++;
            }else{
              //架電にする
              for(let Ret = KADEN_START_RET;Ret < KADEN_START_RET + KADEN_HUMAN_NUM;Ret++){
                if(objSheet.getRange(row + r,Ret).getValue() !== ""){
                  //ペコシェさんの名前ありのためforを1回スキップ
                  continue;
                }
                console.log("ペコシェ名:"+ ArrayPecoName[0][column] + "行:" + (row + r + 1) + "列:" + (SV_RET_FROM + column) )
                AddShift(ArrayPecoName[0][column],row + r,Ret)
                break;
              }
            }
          }
        }
        
      }else if(maxworkhuman=1){
        //最大のペコシェさんが1人なのでその人をSVにする
        for(let column = 0; column < PecoShiftArray[0].length; column++){
          //シフトが入力されてたら転記する
          if(PecoShiftArray[r][column] != "" && PecoShiftArray[r][column] != "×"){
            if(column == ArrayPecoMaxWork[0]){
              //出勤時間が一番長いのでSVにする
              AddShift(ArrayPecoName[0][column],row + r,KADEN_START_RET-1)
            }else{
              //架電にする
              for(let Ret = KADEN_START_RET;Ret < KADEN_START_RET + KADEN_HUMAN_NUM;Ret++){
                if(objSheet.getRange(row + r,Ret).getValue() !== ""){
                  //ペコシェさんの名前ありのためforを1回スキップ
                  continue;
                }
                console.log("ペコシェ名:"+ ArrayPecoName[0][column] + "行:" + (row + r + 1) + "列:" + (SV_RET_FROM + column) )
                AddShift(ArrayPecoName[0][column],row + r,Ret)
                break;
              }
            }
          }
        }
      }
  console.log("CreateShift End------------------------------:");
}

実装の方針としては、時間(行)ごとにペコシェさんの人数をカウントして、その人数によってifを分岐させてる感じぺこなー。
ただ、この方法だとすべてのセルをforで見にいってるぺこから、処理に時間がかかるしPCに負荷がかかるぺこから、もっとうまい方法ないかなーって実装したペコシェさんが嘆いてたぺこ。

ペコシェさん
ペコシェさん

手動でシフトを作成している時の注意点をヒアリングした時、考慮している点が多くて、その内容をプログラムに落とし込むのがすごい大変でした。。。
今後の改題としては、
①処理速度向上
②SVを決定するロジックの修正
 (現状、1日の労働時間が長い人=SVになってしまっている)
です! ここまで読んでいただきありがとうございます!

おわりに

今回はペコッターの運営をしてくれているペコシェさん達が、自分たちの業務をどう効率化しているかっていう内容をお届けしたぺこー!

これを見て、僕私ならもっとうまくやれる! ペコッター大好き! 運営を支えたい! って思ってくれた人は是非僕の会社で働いてほちいぺこー! 
アルバイトでも大歓迎ぺこりよ! 在宅もOKぺこ!
ペコッターで働きたきたい方はこちら

この記事の反響がよかったら、また別の効率化についての記事も書くぺこなー!
SNSでシェアしてほちいぺこ♡

トップへ戻る
Top
タイトルとURLをコピーしました