Google Apps Scriptで座席表を作ろう - ライブラリ編【GAS】
働き方改革を推し進める一方、制度や仕組みだけがあっても、実際には運用上で次々に障害が出てきます。最近の流行り(IT業界じゃ昔から当たり前)の要素に「フリーアドレス」制度があります。座席を固定せずに、その日好きな席に座って作業をすると・・・。
しかしこの弊害として「誰がどこに座ってるかわからない」「座ってる人が誰なのかわからない」といったコミュニケーション上の問題が生じます(10名程度の小さな事業所ならば不要ですけれどね)。その日に出勤したら席を取ってもらうシステムを用意すれば解決します。カスタマイズ次第では、夢が広がる小さなシステムを作ってみたいと思います。
図:こんな感じのウェブアプリが簡単に作れます
目次
今回使用するスプレッドシートとライブラリ
また、G Suiteでは、現在ログインしてアクセスしているユーザのメアドを自動取得出来るので、社員ID入力を省略も可能です。社員IDではなくメアドをユーザ表に登録しておき、キープとリリースの2つのボタンを押すだけで済むものも作成しました(ご要望があったので、コードを少し改造し、余計なコードを削除してあります@2019/12/26)。ソースコードで掲示してる内容は改造前のものです。
※2022年8月4日、より簡単なSVG画像を使ったレスポンシブ対応の座席表管理システムを作りました。
概要
今回作成するのは、座席表アプリとしては非常に単純で最もシンプルな仕組みで実装しています。ライブラリとしては、Mansonryベースという「jQuery.ShapeShift」を使ってみました。このライブラリ自体はドラッグしてブロックを移動なんかも出来るのですが、今回はその機能はオフにして、スプレッドシート連動の座席表管理アプリを作っています。
仕様としては
- 座席はフリーアドレスではない席に対しては固定する為のフラグを用意(チェックボックスで管理)
- 名前のついていない空いている席をクリックして、キープを押すと席が確定しスプレッドシートに書き込まれる
- すでにキープ済みの時にはエラーを返す。
- 同じ社員番号での重複キープを防止するためのチェック機能
- 固定席のカラーは青や灰色、フリーアドレスはキープするとピンクに変化する
- 席の無いエリアおよび通路などは、要素のvisibilityをhiddenする事で実現
- 席の確保画面はjQuery Dialogで用意しておく。
- 退社時には席をリリースする機能
- 各BOXの座席IDはセルの番地とイコールにしておく
- 毎日深夜0時に座席データは固定席以外はクリアされる(スクリプトトリガーを使用)
- 自分の会社では確保済みシートをクリックした場合、社員の顔写真、電話番号、PC番号、メルアド、Teamsへの直URL Schemeなどが表示されるように別途ダイアログを用意したり、社内の様々な業務(健診予約)なども追加して、活用しています。
- 別にテレワーク者の登録画面などもあると便利(今日の外出先や業務内容、時間などを格納するものを用意して)
- 個人的には自由度の高いレイアウトを作れるイメージマップ編のほうがオススメです。
キープ済みの席をクリックすると誰なのか?内線番号やSkypeのIDなどを表示するようにすると、なお面白いと思います。基本はスプレッドシートに列を追加して拡張し、プログラム側で読み取ってダイアログに配置するだけの仕組みです。
ソースコード
GAS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
//シートの空き状況をチェックする function checkseat(id){ //シートIDを取得する var Properties = PropertiesService.getScriptProperties(); var ssid = Properties.getProperty("sheetid"); var ss = SpreadsheetApp.openById(ssid); var flag = false; //シートデータを探索して、空き状況を返す var sheet = ss.getSheetByName("シートデータ").getRange("A2:C").getValues(); var sslength = sheet.length; for(var i = 0;i<sslength;i++){ //IDが一致したら処理を開始 if(sheet[i][0] == id){ if(sheet[i][1] == true){ //固定フラグがTrueなので座れない }else{ if(sheet[i][2] == ""){ //flagをtrueに flag = true; } } } } //検索結果を返す return JSON.stringify(flag); } //起動時に座席状況をHTML側へ送る関数 function nowsheetlist(){ //シートIDを取得する var Properties = PropertiesService.getScriptProperties(); var ssid = Properties.getProperty("sheetid"); var ss = SpreadsheetApp.openById(ssid); //シートデータをガッツリ取得する var sheet = ss.getSheetByName("シートデータ").getRange("A2:C").getValues(); //シートデータの塊を返す return JSON.stringify(sheet); } //シートを確保する為の関数 function getseatman(manid,tempseat){ //変数を宣言 var msg = ""; var flag = false; var username = ""; var cnt = ""; //シートIDを取得する var Properties = PropertiesService.getScriptProperties(); var ssid = Properties.getProperty("sheetid"); var ss = SpreadsheetApp.openById(ssid); //ユーザがいるかどうかチェック var sheet = ss.getSheetByName("ユーザ表").getRange("A2:B").getValues(); var length = sheet.length; for(var i = 0;i<length;i++){ if(sheet[i][0] == manid){ //見つかった場合 username = sheet[i][1]; //フラグをオン flag = true; } } //フラグチェック if(flag == false){ //エラー処理 return JSON.stringify([flag,"ユーザ情報が見つかりませんでした。"]) } //フラグを初期化 flag = false; //シートの確保 var sheet2 = ss.getSheetByName("シートデータ").getRange("A2:D").getValues(); var length2 = sheet2.length; //ロック開始 //ドキュメントロックを使用する var lock = LockService.getDocumentLock(); //30秒間のロックを取得 try { //ロックを実施する lock.waitLock(30000); //同じ社員IDのデータが存在しないかチェック for(var i = 0;i<length2;i++){ if(sheet2[i][3] == manid){ //ロックを開放する lock.releaseLock(); //エラー処理 return JSON.stringify([flag,"すでにもう席確保してるみたいですよ!!"]) } } //シートの確保 for(var i = 0;i<length2;i++){ //シートの探索 if(sheet2[i][0] == tempseat){ if(sheet2[i][2] == ""){ //書き込み行を特定する cnt = i + 2; //シートが開いてるので確保する ss.getSheetByName("シートデータ").getRange("C" + cnt).setValue(username); ss.getSheetByName("シートデータ").getRange("D" + cnt).setValue(manid); //flagを立てる flag = true; //ループを抜ける break; } } } } catch (e) { //ロック取得できなかった時の処理等を記述する var checkword = "ロックのタイムアウト: 別のプロセスがロックを保持している時間が長すぎました。"; //通常のエラーとロックエラーを区別する if(e.message == checkword){ //ロックエラーの場合 return JSON.stringify([flag,checkword]); }else{ //ソレ以外のエラーの場合 msg = e.message; return JSON.stringify([flag,msg]); } } finally { //ロックを開放する lock.releaseLock(); } //フラグチェック if(flag == false){ //エラー処理 return JSON.stringify([flag,"すでに席は誰かに確保されちゃってました・・・もう一度リロードして作業を行ってください。"]) }else{ //成功した処理 return JSON.stringify([flag,"席の確保が完了しました。"]) } } //深夜0時にデータをクリアするトリガー用関数 function clearsheet(){ //シートIDを取得する var Properties = PropertiesService.getScriptProperties(); var ssid = Properties.getProperty("sheetid"); var ss = SpreadsheetApp.openById(ssid); //シートデータを取得する var sdata = ss.getSheetByName("シートデータ").getRange("A2:D").getValues(); var slength = sdata.length; //ループを回して固定フラグ判定をしながらデータ消去 for(var i = 0;i<slength;i++){ if(sdata[i][1] == true){ //固定席なので何もしない }else{ //フリーアドレスなのでクリアする sdata[i][2] = ""; sdata[i][3] = ""; } } //配列データを一発貼り付け戻し var lastRow = sdata.length; //レコードの数を取得する var lastColumn = sdata[0].length //カラムの数を取得する ss.getSheetByName("シートデータ").getRange(2,1,lastRow,lastColumn).setValues(sdata); } //シートを解放する為の関数 function relseatman(manid){ //変数を宣言する var flag = false; var cnt = ""; //シートIDを取得する var Properties = PropertiesService.getScriptProperties(); var ssid = Properties.getProperty("sheetid"); var ss = SpreadsheetApp.openById(ssid); //ユーザがいるかどうかチェック var sheet = ss.getSheetByName("シートデータ").getRange("A2:D").getValues(); var length = sheet.length; //シートの解放 for(var i = 0;i<length;i++){ //シートの探索 if(sheet[i][3] == manid){ //書き込み行を特定する cnt = i + 2; //シートを開放する ss.getSheetByName("シートデータ").getRange("C" + cnt).setValue(""); ss.getSheetByName("シートデータ").getRange("D" + cnt).setValue(""); //flagを立てる flag = true; //ループを抜ける break; } } //フラグチェック if(flag == false){ //エラー処理 return JSON.stringify([flag,"対象の社員IDが見つからなかったので、座席の解放が出来ませんでした。"]) }else{ //成功した処理 return JSON.stringify([flag,"席の解放が完了しました。"]) } } |
- nowsheetlist関数は起動時やリフレッシュ時に呼び出され、現在のシート状の座席確保状況がHTML側に返されます。
- getsheetman関数は、クリックされてキープされた時、シート上のユーザリストを探索し、ユーザがあれば座席リストを探索し、空いていれば確保する関数です。確保する時は、LockServiceでシートをロックしてるので、ダブルブッキングを防止します。
- clearsheet関数は、毎日0時にスクリプトトリガーから呼び出され、自動的に座席確保状況をクリアします。
- relsheetman関数はクリックされてリリースされた時、シート状のユーザリストを探索し、ユーザがあれば座席リストを探索、シートのユーザIDと一致する場合に、座席をクリアする関数です。
HTML側コード
seatchart.html:表示用のメイン
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 |
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>座席表Plus</title> <!-- jQuery / jQuery UI --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script> <!-- jQuery Touch Punch - Enable Touch Drag and Drop --> <script src="https://officeforest.org/wp/library/shapeshift/jquery.touch-punch.min.js"></script> <!-- jQuery.Shapeshift --> <script src="https://officeforest.org/wp/library/shapeshift/jquery.shapeshift.js"></script> <!-- CSS --> <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css"> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css"> <style> .container { border: 0px dashed #CCC; position: relative; line-height: 40px; } .container > div { background: #FFFFFF; position: absolute; height: 40px; width: 60px; border-style: solid; border-color: #5882FA; font-size: 10px; font-weight:bold; text-align:center; display: inline-block; } .container > div:hover{ background-color: #f5ffaa; -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; transition: all 0.2s ease; } .container > div:active{ background-color: #ffc9d7; -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; transition: all 0.2s ease; } .container > div[data-ss-colspan="2"] { width: 130px; background:#d3ffd7;} .container > div[nothing="1"] { visibility:hidden; } .container > div[cabinet="1"] { background:#A4A4A4; color:#fff;} .container > .ss-placeholder-child { background: transparent; border: 1px dashed blue; } /* ダイアログの背景画像変更用 */ .ui-widget-overlay { background: #000 url(https://officeforest.org/wp/library/forms/halloween.png) 10% 100% repeat-x; opacity: .50; background-size: 100% 100%; filter: Alpha(Opacity=70); } .box { float: left; width: 180px; text-align: center; } .box2 { float: left; width: 180px; text-align: center; } .boxContainer { overflow: hidden; width:100%; } /* clearfix */ .boxContainer:before, .boxContainer:after { content: ""; display: table; } .boxContainer:after { clear: both; } /* For IE 6/7 (trigger hasLayout) */ .boxContainer { zoom: 1; } </style> <!-- Javascript --> <script> //起動時に自動的に起動するスクリプト google.script.run.withSuccessHandler(onSeat).nowsheetlist(); //クリック時に確保する一時座席ID var tempseat = ""; //ダイアログ表示用 $(function() { $( "#dialog" ).dialog({ autoOpen: false, closeText: "保存せずに閉じます", width: 400, height: 200, title: "作業内容", modal: true, show: { effect: "clip", duration: 500 }, hide: { effect: "clip", duration: 500 }, position: { of : 'body', at: 'center', my: 'center' } }); }); //登録用ダイアログ表示設定 $(function() { $( "#dialog2" ).dialog({ autoOpen: false, closeText: "保存せずに閉じます", width: 400, height: 200, title: "作業内容", modal: true, show: { effect: "clip", duration: 500 }, hide: { effect: "clip", duration: 500 }, position: { of : 'body', at: 'center', my: 'center' } }); }); $(function() { $( "input[type=submit], a, button" ) .button() .click(function() { }); }); //シート状況をチャートに反映するコード function onSeat(data){ //データをパースする var json = JSON.parse(data); var length = json.length; //ループを回してシート状況を反映する for(var i = 0;i<length;i++){ //固定フラグがONのデータの場合スルーする if(json[i][1] == true){ // if(json[i][2] == null || json[i][2] == ""){ //スルーする }else{ //既定値を反映する document.getElementById(json[i][0]).innerHTML = json[i][2]; //既定値のみ背景色を変更 document.getElementById(json[i][0]).style.background = "#d3f1ff"; } }else{ //データが空の場合には、スルーし、ある場合には反映する if(json[i][2] == "" || json[i][2] == null){ //データが空なので空として反映する document.getElementById(json[i][0]).innerHTML = json[i][2]; document.getElementById(json[i][0]).style.background = ""; continue; }else{ //キープ済み document.getElementById(json[i][0]).innerHTML = json[i][2]; //キープ済席の背景色を変更 document.getElementById(json[i][0]).style.background = "#f6d3ff"; } } } } //イベントを準備する $(document).ready(function() { //パネルの有効化 $(".container").shapeshift({ enableDrag: false, enableCrossDrop: false, enableResize: false, minColumns: 3, minHeight:50, gutterX:5, gutterY:5, paddingX:5, paddingY:5 }); //パネルのクリックアクションを設定 $('.ss-active-child').on('click', function(){ //クリックした要素のIDを取得 var id = $(this).attr("id"); //一時変数に座席IDを確保 tempseat = id; //DIVの値を取得 var manname = $(this).html(); //取得したID要素を表示 if(id == undefined){ alert("💁この席は座る事が出来ません!!"); }else{ //名前が入ってるかどうかをチェック if(manname != ""){ //確保されてるのでユーザ情報ダイアログを表示する alert("この席はすでに【" + manname + "】さんが座ってますよ。"); return; }else{ //スプレッドシートを参照して空いてるかチェック google.script.run.withSuccessHandler(onSuccess).checkseat(id); } } }); }) //シートの空き状況を取得して処理をする関数 function onSuccess(data){ var json = JSON.parse(data); //返り値を元に処理を分岐 if(json == true){ //シート確保用のダイアログを表示する //ダイアログ表示 $('#dialog').dialog({ title: "席の確保", close : function(){ } }); $( "#dialog" ).dialog( "open" ); $( "#dialog" ).dialog("moveToTop"); document.getElementById("dialog").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi").value = getData("mannum"); }else{ alert("シート取られてしまったようです・・・画面をリロードして、やり直してみてください。"); } } //LocalStorageへのデータの挿入 function setData(key, data){ localStorage.setItem(key, data); } //LocalStorageからのデータの取得 function getData(key){ var ret = localStorage.getItem(key); //null値判定 if(ret == null){ return ""; }else{ return ret; } } //シートをキープする処理 function seatkeep(){ //ダイアログ内のIDを取得する var manid = document.getElementById("wasabi").value; if(manid == ""){ alert("社員IDが入っていませんよ"); document.getElementById("wasabi").focus(); return; }else{ //ダイアログ内のIDをLocalStorageへ記憶する setData("mannum",manid); } //GAS側へシート確保処理をなげる google.script.run.withSuccessHandler(onGetSeat).getseatman(manid,tempseat); //ダイアログを閉じる document.getElementById("wasabi").value = ""; $( "#dialog" ).dialog( "close" ); } //シート確保をキャンセルする関数 function seatcancel(){ //ダイアログを閉じる document.getElementById("wasabi").value = ""; $( "#dialog" ).dialog( "close" ); //メッセージ表示 alert("キャンセルされますた。"); } //シート確保結果 function onGetSeat(data){ //返り値について取得する var json = JSON.parse(data); if(json[0] == true){ //シートが確保出来た場合の処理 alert("シートを確保しました。"); //シートリストのリロード処理 google.script.run.withSuccessHandler(onSeat).nowsheetlist(); }else{ //確保できなかった場合の処理 alert(json[1]); } } //座席の解放ボタンを押した時のダイアログ表示用処理 function releaseseat(){ //シート確保用のダイアログを表示する //ダイアログ表示 $('#dialog2').dialog({ title: "席の解放", close : function(){ } }); $( "#dialog2" ).dialog( "open" ); $( "#dialog2" ).dialog("moveToTop"); document.getElementById("dialog2").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi2").value = getData("mannum"); } //席の解放ボタンクリック時の処理 function release(){ //ダイアログ内のIDを取得する var manid = document.getElementById("wasabi2").value; if(manid == ""){ alert("社員IDが入っていませんよ"); document.getElementById("wasabi2").focus(); return; }else{ //ダイアログ内のIDをLocalStorageへ記憶する setData("mannum",manid); } //GAS側へシート確保処理をなげる google.script.run.withSuccessHandler(onRelSeat).relseatman(manid); //ダイアログを閉じる document.getElementById("wasabi2").value = ""; $( "#dialog2" ).dialog( "close" ); } //シート確保結果 function onRelSeat(data){ //返り値について取得する var json = JSON.parse(data); if(json[0] == true){ //シートが確保出来た場合の処理 alert("シートを解放しました。"); //シートリストのリロード処理 google.script.run.withSuccessHandler(onSeat).nowsheetlist(); }else{ //確保できなかった場合の処理 alert(json[1]); //シートリストのリロード処理 google.script.run.withSuccessHandler(onSeat).nowsheetlist(); } } //シート確保をキャンセルする関数 function seatcancel2(){ //ダイアログを閉じる document.getElementById("wasabi2").value = ""; $( "#dialog2" ).dialog( "close" ); //メッセージ表示 alert("キャンセルされますた。"); } //tomasダイアログを表示する function tomasopen(){ //ダイアログ表示 $('#dialog3').dialog({ title: "Tomas出退勤打刻", close : function(){ } }); $( "#dialog3" ).dialog( "open" ); $( "#dialog3" ).dialog("moveToTop"); document.getElementById("dialog3").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi2").value = getData("mannum"); } </script> </head> <body> <div style="width:1000px"> <!-- 座席用HTMLを呼び出す --> <?!= HtmlService.createHtmlOutputFromFile("zaseki").getContent(); ?> <hr> <div align="right"> <button onClick='tomasopen()' id="tomas" class="create" title='出退勤を打刻する' style="visibility:hidden">出退勤</button> <button onClick='releaseseat()' id="releaseman" class="action" title='席をリリースします。'>座席の解放</button> </div> </div> <div id="dialog" title="Basic dialog"> <p> 今日はこの席でお仕事しますか?座る場合は、IDを入れて、キープをクリックしてください。 </p> <div align="center"> <b>あなたのID:</b> <input type="text" id="wasabi" size="10" maxlength="9" placeholder="例:R91001"> </div> <p></p> <div class='boxContainer'> <div class='box2'> <span><button onClick='seatkeep()' id="button1" style="font-size: 16px;vertical-align: middle" class="ponyo" title='席を確保します。'><img src='https://officeforest.org/wp/library/icons/icon_check2.png' /> キープ</button></span> </div> <div class='box'> <span><button onClick='seatcancel()' id="button2" style="font-size: 16px;vertical-align: middle" class="ponyo" title='キャンセルします。'><img src='https://officeforest.org/wp/library/icons/cross.png' /> キャンセル</button></span> </div> </div> </div> <div id="dialog2" title="Basic dialog"> <p> 🍲今日はもうお帰りですか?座席を解放する場合は、IDを入れて、リリースをクリックしてください。 </p> <div align="center"> <b>あなたのID:</b> <input type="text" id="wasabi2" size="10" maxlength="6" placeholder="例:R91001"> </div> <p></p> <div class='boxContainer'> <div class='box2'> <span><button onClick='release()' id="button3" style="font-size: 16px;vertical-align: middle" class="ponyo" title='席を解放します。'><img src='https://officeforest.org/wp/library/icons/icon_check2.png' /> リリース</button></span> </div> <div class='box'> <span><button onClick='seatcancel2()' id="button4" style="font-size: 16px;vertical-align: middle" class="ponyo" title='キャンセルします。'><img src='https://officeforest.org/wp/library/icons/cross.png' /> キャンセル</button></span> </div> </div> </div> </body> </html> |
- JavaScriptおよびCSSに関しては別のHTMLファイルに切り出して、起動時に読み込むようにしています。
- そのため今回のウェブアプリケーションは、GAS側でHtmlService.createTemplateFromFileとしてロードしています。
zaseki.html : 座席用のテンプレート
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<!-- 1行目--> <div class="container"> <div id="A1"></div> <div id="B1"></div> <div id="D1"></div> <div id="E1"></div> <div id="G1"></div> <div id="H1"></div> <div nothing="1"></div> <div nothing="1" ></div> <div data-ss-colspan="2" id="M1"></div> <div data-ss-colspan="2" id="P1"></div> <div id="S1"></div> <div id="T1"></div> </div> <!-- 2行目--> <div class="container"> <div id="A2"></div> <div cabinet="1">テンポラリ</div> <div id="D2"></div> <div id="E2"></div> <div id="G2"></div> <div id="H2"></div> <div id="J2"></div> <div id="K2"></div> <div id="M2"></div> <div cabinet="1">共有機</div> <div id="P2"></div> <div id="Q2"></div> <div id="S2"></div> <div id="T2"></div> </div> ・・・中略・・・ <!-- 11行目--> <div class="container"> <div id="A11"></div> <div id="B11"></div> <div nothing="1"></div> <div nothing="1"></div> <div nothing="1"></div> <div nothing="1"></div> <div id="J11"></div> <div id="K11"></div> <div id="M11"></div> <div id="N11"></div> <div id="P11"></div> <div id="Q11"></div> <div nothing="1"></div> <div nothing="1"></div> </div> |
- 行単位(横)でセクションを作っていく点に注意!!
- 特定のdivにはnothingやcabinetなどを指定し、固定席としています。
- それぞれのidは、スプレッドシート側のセル番地と対応しています。
javascript.html:クライアント側スクリプト
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
<script> //起動時に自動的に起動するスクリプト google.script.run.withSuccessHandler(onSeat).nowsheetlist(); //クリック時に確保する一時座席ID var tempseat = ""; //登録用ダイアログ表示設定 $(function() { $( "#dialog" ).dialog({ autoOpen: false, closeText: "保存せずに閉じます", width: 400, height: 200, title: "作業内容", modal: true, show: { effect: "clip", duration: 500 }, hide: { effect: "clip", duration: 500 }, position: { of : 'body', at: 'center', my: 'center' } }); }); //登録用ダイアログ表示設定 $(function() { $( "#dialog2" ).dialog({ autoOpen: false, closeText: "保存せずに閉じます", width: 400, height: 200, title: "作業内容", modal: true, show: { effect: "clip", duration: 500 }, hide: { effect: "clip", duration: 500 }, position: { of : 'body', at: 'center', my: 'center' } }); }); $(function() { $( "input[type=submit], a, button" ) .button() .click(function() { }); }); //シート状況をチャートに反映するコード function onSeat(data){ //データをパースする var json = JSON.parse(data); var length = json.length; //ループを回してシート状況を反映する for(var i = 0;i<length;i++){ //固定フラグがONのデータの場合スルーする if(json[i][1] == true){ // if(json[i][2] == null || json[i][2] == ""){ //スルーする }else{ //既定値を反映する document.getElementById(json[i][0]).innerHTML = json[i][2]; //既定値のみ背景色を変更 document.getElementById(json[i][0]).style.background = "#d3f1ff"; } }else{ //データが空の場合には、スルーし、ある場合には反映する if(json[i][2] == "" || json[i][2] == null){ //データが空なので空として反映する document.getElementById(json[i][0]).innerHTML = json[i][2]; document.getElementById(json[i][0]).style.background = "#ffffff"; continue; }else{ //キープ済み document.getElementById(json[i][0]).innerHTML = json[i][2]; //キープ済席の背景色を変更 document.getElementById(json[i][0]).style.background = "#f6d3ff"; } } } } //イベントを準備する $(document).ready(function() { //パネルの有効化 $(".container").shapeshift({ enableDrag: false, enableCrossDrop: false, enableResize: false, minColumns: 3, minHeight:50, gutterX:5, gutterY:5, paddingX:5, paddingY:5 }); //パネルのクリックアクションを設定 $('.ss-active-child').on('click', function(){ //クリックした要素のIDを取得 var id = $(this).attr("id"); //一時変数に座席IDを確保 tempseat = id; //DIVの値を取得 var manname = $(this).html(); //取得したID要素を表示 if(id == undefined){ alert("💁この席は座る事が出来ません!!"); }else{ //名前が入ってるかどうかをチェック if(manname != ""){ //確保されてるのでユーザ情報ダイアログを表示する alert("この席はすでに【" + manname + "】さんが座ってますよ。"); return; }else{ //スプレッドシートを参照して空いてるかチェック google.script.run.withSuccessHandler(onSuccess).checkseat(id); } } }); }) //シートの空き状況を取得して処理をする関数 function onSuccess(data){ var json = JSON.parse(data); //返り値を元に処理を分岐 if(json == true){ //シート確保用のダイアログを表示する //ダイアログ表示 $('#dialog').dialog({ title: "席の確保", close : function(){ } }); $( "#dialog" ).dialog( "open" ); $( "#dialog" ).dialog("moveToTop"); document.getElementById("dialog").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi2").value = getData("mannum"); }else{ alert("シート取られてしまったようです・・・画面をリロードして、やり直してみてください。"); } } //LocalStorageへのデータの挿入 function setData(key, data){ localStorage.setItem(key, data); } //LocalStorageからのデータの取得 function getData(key){ var ret = localStorage.getItem(key); //null値判定 if(ret == null){ return ""; }else{ return ret; } } //シートをキープする処理 function seatkeep(){ //ダイアログ内のIDを取得する var manid = document.getElementById("wasabi").value; if(manid == ""){ alert("社員IDが入っていませんよ"); document.getElementById("wasabi").focus(); return; }else{ //ダイアログ内のIDをLocalStorageへ記憶する setData("mannum",manid); } //GAS側へシート確保処理をなげる google.script.run.withSuccessHandler(onGetSeat).getseatman(manid,tempseat); //ダイアログを閉じる document.getElementById("wasabi").value = ""; $( "#dialog" ).dialog( "close" ); } //シート確保をキャンセルする関数 function seatcancel(){ //ダイアログを閉じる document.getElementById("wasabi").value = ""; $( "#dialog" ).dialog( "close" ); //メッセージ表示 alert("キャンセルされますた。"); } //シート確保結果 function onGetSeat(data){ //返り値について取得する var json = JSON.parse(data); if(json[0] == true){ //シートが確保出来た場合の処理 alert("シートを確保しました。"); //シートリストのリロード処理 google.script.run.withSuccessHandler(onSeat).nowsheetlist(); }else{ //確保できなかった場合の処理 alert(json[1]); } } //座席の解放ボタンを押した時のダイアログ表示用処理 function releaseseat(){ //シート確保用のダイアログを表示する //ダイアログ表示 $('#dialog2').dialog({ title: "席の解放", close : function(){ } }); $( "#dialog2" ).dialog( "open" ); $( "#dialog2" ).dialog("moveToTop"); document.getElementById("dialog2").focus(); //IEに保存されてるIDを呼び出せるなら呼び出す document.getElementById("wasabi2").value = getData("mannum"); } //席の解放ボタンクリック時の処理 function release(){ //ダイアログ内のIDを取得する var manid = document.getElementById("wasabi2").value; if(manid == ""){ alert("社員IDが入っていませんよ"); document.getElementById("wasabi2").focus(); return; }else{ //ダイアログ内のIDをLocalStorageへ記憶する setData("mannum",manid); } //GAS側へシート確保処理をなげる google.script.run.withSuccessHandler(onRelSeat).relseatman(manid); //ダイアログを閉じる document.getElementById("wasabi2").value = ""; $( "#dialog2" ).dialog( "close" ); } //シート確保結果 function onRelSeat(data){ //返り値について取得する var json = JSON.parse(data); if(json[0] == true){ //シートが確保出来た場合の処理 alert("シートを解放しました。"); //シートリストのリロード処理 google.script.run.withSuccessHandler(onSeat).nowsheetlist(); }else{ //確保できなかった場合の処理 alert(json[1]); //シートリストのリロード処理 google.script.run.withSuccessHandler(onSeat).nowsheetlist(); } } //シート確保をキャンセルする関数 function seatcancel2(){ //ダイアログを閉じる document.getElementById("wasabi2").value = ""; $( "#dialog2" ).dialog( "close" ); //メッセージ表示 alert("キャンセルされますた。"); } </script> |
- jQuery.Shapeshiftライブラリで描画させ、GAS側から呼び出した座席リストを各DOM要素のIDに一致する席に確保済みユーザを表示させるようにしています。
- 席の確保、解放それぞれに対する処理を記述しています。
- 一度、シートの確保を行うと、LocalStorageに社員IDをキープしてあります。
- 座席の確保やリリースはそれぞれ用のダイアログを用意して表示するようにしています。jQuery Dialogを利用しています。
CSS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
<style> .container { border: 0px dashed #CCC; position: relative; line-height: 40px; } .container > div { background: #FFFFFF; position: absolute; height: 40px; width: 60px; border-style: solid; border-color: #5882FA; font-size: 10px; font-weight:bold; text-align:center; display: inline-block; } .container > div:hover{ background-color: #f5ffaa; -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; transition: all 0.2s ease; } .container > div:active{ background-color: #ffc9d7; -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; transition: all 0.2s ease; } .container > div[data-ss-colspan="2"] { width: 130px; background:#d3ffd7;} .container > div[nothing="1"] { visibility:hidden; } .container > div[cabinet="1"] { background:#A4A4A4; color:#fff;} .container > .ss-placeholder-child { background: transparent; border: 1px dashed blue; } /* ダイアログの背景画像変更用 */ .ui-widget-overlay { background: #000 url(https://officeforest.org/wp/library/forms/halloween.png) 10% 100% repeat-x; opacity: .50; background-size: 100% 100%; filter: Alpha(Opacity=70); } .box { float: left; width: 180px; text-align: center; } .box2 { float: left; width: 180px; text-align: center; } .boxContainer { overflow: hidden; width:100%; } /* clearfix */ .boxContainer:before, .boxContainer:after { content: ""; display: table; } .boxContainer:after { clear: both; } /* For IE 6/7 (trigger hasLayout) */ .boxContainer { zoom: 1; } </style> |
- ダイアログ用のBox用のCSSおよびShapeShift用のCSSが記述されています。
- cabinetが灰色のBOXで表示される固定確保されてる席を表現
- nothingがBOX自体をvisibility:hiddenで非表示にする対象。通路などのBOXはこれを適用します。
使い方とサンプル
セットアップ
使用する前にスプレッドシート上でセットアップが必要です。また、毎晩0時に座席データのクリアを自動実行する為にはスクリプトトリガーの設置が必要です。以下の作業をしてから利用しましょう。
- スプレッドシート上のメニューより「座席表Plus」⇒「setup」を実行。スプレッドシートのIDがスクリプトプロパティに格納されます。
- 同じく「座席表Plus」⇒「リセットトリガー設置」を実行。スクリプトトリガーが設置されます。ただし、複数名が実行しないように注意してください。
サンプル
白いタイルは、フリーアドレスの席い該当するので、確保する事が可能です。ただし、ユーザリストに該当の社員IDが登録されていない場合には、確保する事ができません。
現在テスト用の社員IDとして以下のユーザを登録してあります。
ID | 表示名 |
AS400LOVE | IBMユーザ |
SUPERDOME | HPユーザ |
INTL8080 | INTELユーザ |
SUN50 | SUNユーザ |
こちらとても有用な情報で嬉しく思っています。
なんとか自分の環境に対応させたのですが、
※IDはG Suiteユーザならばメールアドレスをスクリプトで取得して、クリックするだけで入力不要にする事でより簡単にする事が出来ます。スプレッドシート側のユーザリストもメアドの登録としておけば、簡単ですね。
▲これも実装したいのですが、具体的な手法を教えていただくことは可能でしょうか?
※当方HTMLとcssが書ける程度の初心者です。
ノアさん、おはようございます。
今回のサンプルは、手動で毎回IDを入れてそれを基準に探す仕様にしてました。という事で、G Suiteで利用できるSession.getActiveUser();を使って、メアドを自動取得⇒それを元に探索、キープ、リリースできるようにした改造版を掲示しました。
主な改造点は
これくらいです。ただ、今回の自動でメアドを取得して探索して返す手法は、非常によく利用するものなので、ぜひ他のシステムでも流用すると、利用者の余計な負担が減って、利用率も上がると思います(何より、IDは流用できますが、メアドはログイン認証してないと、なりすましができませんから)
ありがとうございます!望んだ通りの結果になりました。
毎日使うものだとたった数秒の手間でも、労力の積み重ねがすごいですものね。。。
これを使えるようになると、今の業務も便利になりそう。。。
今日で仕事納めですから、お正月休みの間にじっくり研究します!
本当にありがとうございました!
このアプリは本当に座席表のみですが、色々と機能を追加していくと、非常に便利になると思うので、ぜひ拡張してみてください。
フリーアドレス推進してる割には、この手のアプリがソリューションでべらぼうに高かったりするので、価値はあると思います。