素人が翌日の勤務(シフト)を告げるLINEbotを作った話【9】

勤務確認機能を追加します

 前回書いたとおり、指定日の勤務確認機能を作りたいと思います。勤務を検索する機能(”getWorkDataFromSheet”)はすでに書いてあるので、この関数に日付を渡してあげる方法を考えるのが今回の主な課題です。

日時選択アクションを利用します

 ”getWorkDataFromSheet”に日付を渡すには、渡すデータをデータ型を日付形式にする必要があります。LINE上で「勤務確認,〇〇/××(日付)」とか「勤務確認↩(改行)〇〇/××(日付)」などと送り、日付をGASで文字列→日付形式に変換する方法があるのですが、こうしたコマンド入力は上手くいかないことが多いと思います。コマンドの入力の仕方にはルールがあり、ルールから外れるとこの仕組は動きません。しかし管理者とは違ってユーザーはこんなコマンドなどいちいち覚えられないのです。実際に使い方のマニュアルも見られるようにした上で1ヶ月ログをとってみたのですが、私以外にこの機能を使った人はいませんでした(笑)コマンド入力が機能利用の障害になっていることは明白でした。

 いろいろ調べるうち、LINE MessagingAPIで「日時選択アクション」なる機能が使えることがわかりました。

画像1

LINEの画面上にこんなカレンダーが表示され、日付を選択することができる機能です。これはわかりやすい!そしてなんと言ってもカッコいいです!

図にするとこんな感じ

新規 Microsoft PowerPoint プレゼンテーション

処理の流れはこんな漢字で複雑に見えるんですが、今回新しく記述する"datetimePickerAction"以外は既存の関数なので、少し書き加えるだけで実現できます。

"datetimePickerAction"

function datetimePickerAction(token) {
  var startDate = sheet.getRange(1,2).getValue()
  startDate = Utilities.formatDate(startDate, 'JST', 'yyyy-MM-dd');
  var endDate =sheet.getRange(1,sheet.getLastColumn()).getValue();
  endDate = Utilities.formatDate(endDate, 'JST' , 'yyyy-MM-dd');
    

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
    'headers': {
	'Content-Type': 'application/json',
	'Authorization': 'Bearer ' + channel_access_token, 
	            },
	'method': 'POST',
	'payload': JSON.stringify({
	"replyToken": token, 
	"messages": [
				{
			    "type": "template",
				"altText": "勤務確認",
				"template": {
						    "type": "buttons",
						    "title": "勤務確認",
						    "text": "指定日の勤務者を確認できます",
						    "actions": [
							           {
								       "type": "datetimepicker",
								       "label": "日付を選択してください。",
								       "data": "action=settime",
								       "mode": "date",
                                       "min": startDate,
                                       "max": endDate
							           }
						               ]
					        }
				}
			    ],			
		}),
	});
}

もう丸まんまコピペでOKです。表示されるカレンダーの範囲は、指定しなければ過去から未来までずっと選べるのですが、スプレッドシート上のシフトは毎月書き換えるか追加されていくだけなので、選べる範囲が限定されます。例えば11/1~11/30までのシフトしかシートに書いてないのに、12月の日付を選んでもエラーが出てしまいます。こういったことが起きないように、スプレッドシートから日付の値の上限と下限を設定しています(startDate,endDate)。ここの設定は日付形式でOKかと思いきや、yyyy-MM-dd形式でないと受け付けてくれませんでした。ここでしばらくハマって涙目になったりもしましたが、なんとか乗り越えられました。 これでユーザー画面に表示される日付選択画面が設定できました。

"doPost"

 次に、日付を選択した後の処理を書きます。日付をポチると、ポストバックイベントが発生します。イベントといえば、

 このときにイベント処理のコードを書きました。今回はここにポストバックイベントの処理を書き加えます。

function doPost(e) {
 const webhookData = JSON.parse(e.postData.contents).events[0];
 const eventType = webhookData.type;
 const replyToken = webhookData.replyToken;

 switch(eventType){
   case "follow":  //友だち追加
     follow(webhookData);
     break;
   case "message":  //メッセージ受信
     messageEvent(webhookData);
     break;
   case "unfollow":  //ブロック
     unfollow(webhookData)
     break;
   case "postback":  //ポストバックイベント
     var postbackData = webhookData.postback.params.date
     var targetDate = new Date(postbackData);
     var obj = getWorkDataFromSheet(targetDate);
     var text = obj.text
     sendLineMessageFromReplyToken(replyToken, text)
     break;
 }
}

case "postback"以降が今回追加した部分です。

ポストバックイベントが発生したら、ユーザーが選んだ日付を”getWorkDataFromSheet”に送ってシフトを検索、返ってきたシフトをユーザーに返信する、という流れになってます。

"messageEvent"

function messageEvent(webhookData){
 const userId = webhookData.source.userId;
 const nickname = getUserProfile(userId);
 const timestamp = new Date(webhookData.timestamp)
 const message = webhookData.message.text.split("\n");
 const replyToken = webhookData.replyToken;
 const whatToDo = message[0];
 const name = message[1];
 const date = message[2];
 const newshift = message[3];
 const last_row = userSheet.getLastRow();
 const lastRow = logSheet.getLastRow();

 logSheet.getRange(lastRow+1,1).setValue(timestamp);
 logSheet.getRange(lastRow+1,2).setValue(nickname);
 logSheet.getRange(lastRow+1,3).setValue(userId);
 logSheet.getRange(lastRow+1,4).setValue(message[0]);
 logSheet.getRange(lastRow+1,5).setValue(message[1]);
 logSheet.getRange(lastRow+1,6).setValue(message[2]);
 logSheet.getRange(lastRow+1,7).setValue(message[3]);

 switch (whatToDo) {
    case 'こんにちは':
      sendLineMessageFromReplyToken(replyToken, "さようなら");
      break;
    case '勤務確認':
      datetimePickerAction(replyToken);
      break;   
    default:
      return;
      break;  
}
}

もともと書いてあったところに少し書き足すだけです(下から8行~9行目)

テストしてみましょう

キーボードで”勤務確認”と打ってみてください。すると・・・

画像3

画像4

画像5

こんな感じで日付選択画面が返ってきて、日付を選ぶとその日の勤務データが返ってきます!

これで任意の日付から勤務データを返す機能を実装できました!冒頭でも触れたように、”勤務確認”と打たせるのはユーザーフレンドリーの観点から控えたいので、次回は「文字を打たせないための対策」をやりたいと思います!

ここまでのコード

const channel_access_token = "トークン";
const headers = {
  "Content-Type": "application/json; charset=UTF-8",
  "Authorization": "Bearer " + channel_access_token
};
const spreadsheet = SpreadsheetApp.openById("ID");
const sheet = spreadsheet.getSheetByName("勤務表");
const userSheet = spreadsheet.getSheetByName("ユーザー");
const logSheet = spreadsheet.getSheetByName("ログ");


function doPost(e) {
 const webhookData = JSON.parse(e.postData.contents).events[0];
 const eventType = webhookData.type;
 const replyToken = webhookData.replyToken;

 switch(eventType){
   case "follow":  //友だち追加
     follow(webhookData);
     break;
   case "message":  //メッセージ受信
     messageEvent(webhookData);
     break;
   case "unfollow":  //ブロック
     unfollow(webhookData)
     break;
   case "postback":  //ポストバックイベント
     var postbackData = webhookData.postback.params.date
     var targetDate = new Date(postbackData);
     var obj = getWorkDataFromSheet(targetDate);
     var text = obj.text
     sendLineMessageFromReplyToken(replyToken, text)
     break;
 }
}

function follow(webhookData){
 const userId = webhookData.source.userId;
 const nickname = getUserProfile(userId);
 const last_row = userSheet.getLastRow();

 userSheet.getRange(last_row+1,2).setValue(nickname);
 userSheet.getRange(last_row+1,3).setValue(userId);
 userSheet.getDataRange().removeDuplicates([3])  
}


function messageEvent(webhookData){
 const userId = webhookData.source.userId;
 const nickname = getUserProfile(userId);
 const timestamp = new Date(webhookData.timestamp)
 const message = webhookData.message.text.split("\n");
 const replyToken = webhookData.replyToken;
 const whatToDo = message[0];
 const name = message[1];
 const date = message[2];
 const newshift = message[3];
 const last_row = userSheet.getLastRow();
 const lastRow = logSheet.getLastRow();

 logSheet.getRange(lastRow+1,1).setValue(timestamp);
 logSheet.getRange(lastRow+1,2).setValue(nickname);
 logSheet.getRange(lastRow+1,3).setValue(userId);
 logSheet.getRange(lastRow+1,4).setValue(message[0]);
 logSheet.getRange(lastRow+1,5).setValue(message[1]);
 logSheet.getRange(lastRow+1,6).setValue(message[2]);
 logSheet.getRange(lastRow+1,7).setValue(message[3]);

 switch (whatToDo) {
    case 'こんにちは':
      sendLineMessageFromReplyToken(replyToken, "さようなら");
      break;
    case '勤務確認':
      datetimePickerAction(replyToken);
      break;  
    default:
      return;
      break;  
}
}

function unfollow(webhookData){
 const data = userSheet.getDataRange()
 const userId = webhookData.source.userId;
 const userFinder = data.createTextFinder(userId).findAll();
 for ( var i = 0; i < userFinder.length; i++ ) {
       var userRow = userFinder[i].getRow();
       var user = userFinder[i].offset(0,1).getValue()
   userSheet.deleteRows(userRow);
 }
}

function getUserProfile(userId){ 
 const Url = 'https://api.line.me/v2/bot/profile/' + userId;
 const userProfile = UrlFetchApp.fetch(Url,{
   'headers': {
     'Authorization' :  'Bearer ' + channel_access_token,
   },
 })
 return JSON.parse(userProfile).displayName;
}

function sendLineMessageFromReplyToken(token, replyText) {
var url = "https://api.line.me/v2/bot/message/reply";
var headers = {
 "Content-Type": "application/json; charset=UTF-8",
 "Authorization": "Bearer " + channel_access_token
};
var postData = {
 "replyToken": token,
 "messages": [{
   "type": "text",
   "text": replyText
 }]
};
var options = {
 "method": "POST",
 "headers": headers,
 "payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}

function sendLineMessageUsingBroadcast(text) {
var Url = "https://api.line.me/v2/bot/message/broadcast";
var postData = {
   "messages": [{
   "type": "text",
   "text":  text,
 }
             ]};
var options = {
 "method": "POST",
 "headers": headers,
 "payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(Url, options);
}

function sendLineMessageFromUserId(text,userId) {
var url = "https://api.line.me/v2/bot/message/push";
var postData = {
 "to": userId,
 "messages": [{
   "type": "text",
   "text":  text,
 }]
};
var options = {
 "method": "POST",
 "headers": headers,
 "payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}

function sendLineMessageToAdmin(text) {
var url = "https://api.line.me/v2/bot/message/push";
var postData = {
 "to": "ユーザーID",
 "messages": [{
   "type": "text",
   "text":  text,
 }]
};
var options = {
 "method": "POST",
 "headers": headers,
 "payload": JSON.stringify(postData)
};
return UrlFetchApp.fetch(url, options);
}

function sendTomorrowsShiftEverynight(){
 var today = new Date();
 today.setDate(today.getDate()+1)
 const obj = getWorkDataFromSheet(today)
 const text = obj.text
 if(obj.workers == "0" ){
   return;  
 }
 sendLineMessageUsingBroadcast(text);
}

function getWorkDataFromSheet(targetDate){ 
 const week = ["日","月","火","水","木","金","土"];
 const lastRow = sheet.getLastRow();
 const month = Utilities.formatDate(targetDate, 'JST', 'MM');
 const day = Utilities.formatDate(targetDate, 'JST', 'dd');
 const date = month + "/" + day 
 const dateFinder = sheet.createTextFinder(date).findAll();
 
   for ( var i = 0; i < dateFinder.length; i++ ) {
       var dateColumn = dateFinder[i].getColumn();
   }
 
 const data = sheet.getRange(1,dateColumn,lastRow);
 let earlyTurnList = [];
 let dayShiftList = [];
 let lateTurnList =[];
 let nightShiftList = [];
 let afterNightShiftList = [];
 let holidayList = [];
 let paidVacationList = [];
 
 
 let earlyTurnFinder = data.createTextFinder('早').findAll();
 for ( let i = 0; i < earlyTurnFinder.length; i++ ) {
   let earlyTurn = earlyTurnFinder[i].offset( 0 , - dateColumn + 1).getValue();
   earlyTurnList.push(earlyTurn)
  }
 
 let dayShiftFinder = data.createTextFinder('日').findAll();
 for ( let i = 0; i < dayShiftFinder.length; i++ ) {
   let dayShift = dayShiftFinder[i].offset( 0 , - dateColumn + 1).getValue();
   dayShiftList.push(dayShift)
  }  
 
 let lateTurnFinder = data.createTextFinder('遅').findAll();
 for ( let i = 0; i < lateTurnFinder.length; i++ ) {
   let lateTurn = lateTurnFinder[i].offset( 0 , - dateColumn + 1).getValue();
   lateTurnList.push(lateTurn)
  } 
 
 let nightShiftFinder = data.createTextFinder('夜').findAll();
 for ( let i = 0; i < nightShiftFinder.length; i++ ) {
   let nightShift = nightShiftFinder[i].offset( 0 , - dateColumn + 1).getValue();
   nightShiftList.push(nightShift)
  }
 
 let afterNightShiftFinder = data.createTextFinder('明').findAll();
 for ( let i = 0; i < afterNightShiftFinder.length; i++ ) {
   let afterNightShift = afterNightShiftFinder[i].offset( 0 , - dateColumn + 1).getValue();
   afterNightShiftList.push(afterNightShift)
  }
 
 let holidayFinder = data.createTextFinder('公').findAll();
 for ( let i = 0; i < holidayFinder.length; i++ ) {
   let holiday = holidayFinder[i].offset( 0 , - dateColumn + 1).getValue();
   holidayList.push(holiday)
  }
 
 let paidVacationFinder = data.createTextFinder('有').findAll();
 for ( let i = 0; i < paidVacationFinder.length; i++ ) {
   let paidVacation = paidVacationFinder[i].offset( 0 , - dateColumn + 1).getValue();
   paidVacationList.push(paidVacation)
  }

 var text = date +"("+week[targetDate.getDay()]+") 勤務者一覧"+"\n\n" 
 if(earlyTurnList.length > 0){
   text = text +  "早番 : " + earlyTurnList +"\n\n"
 }  
 if(dayShiftList.length > 0){
   text = text +  "日勤 : " + dayShiftList +"\n\n"
 }  
 if(lateTurnList.length > 0){
   text = text +  "遅番 : " + lateTurnList +"\n\n"
 }
 if(nightShiftList.length > 0){
   text = text +  "夜勤 : " + nightShiftList +"\n\n"
 }
 if(afterNightShiftList.length > 0){
   text = text +  "明け : " + afterNightShiftList +"\n\n"
 }  
 if(holidayList.length + paidVacationList.length > 0){
  text = text +  "公・休・有 : \n" + holidayList+ "," + paidVacationList + "\n\n" ;
  }
 
 if(dayShiftList.length + earlyTurnList.length + lateTurnList.length + nightShiftList.length + afterNightShiftList.length < 1 ){
   var workers = 0; 
 }
 
 return  {text: text, workers: workers}; 
}

function datetimePickerAction(token) {
 var startDate = sheet.getRange(1,2).getValue()
 startDate = Utilities.formatDate(startDate, 'JST', 'yyyy-MM-dd');
 var endDate =sheet.getRange(1,sheet.getLastColumn()).getValue();
 endDate = Utilities.formatDate(endDate, 'JST' , 'yyyy-MM-dd');
   

 UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
   'headers': {
	'Content-Type': 'application/json',
	'Authorization': 'Bearer ' + channel_access_token, 
	            },
	'method': 'POST',
	'payload': JSON.stringify({
	"replyToken": token, 
	"messages": [
				{
			    "type": "template",
				"altText": "勤務確認",
				"template": {
						    "type": "buttons",
						    "title": "勤務確認",
						    "text": "指定日の勤務者を確認できます",
						    "actions": [
							           {
								       "type": "datetimepicker",
								       "label": "日付を選択してください。",
								       "data": "action=settime",
								       "mode": "date",
                                      "min": startDate,
                                      "max": endDate
							           }
						               ]
					        }
				}
			    ],			
		}),
	});
}

この記事が気に入ったらサポートをしてみませんか?