Google Apps Scriptで座席表を作ろう - ライブラリ編【GAS】

働き方改革を推し進める一方、制度や仕組みだけがあっても、実際には運用上で次々に障害が出てきます。最近の流行り(IT業界じゃ昔から当たり前)の要素に「フリーアドレス」制度があります。座席を固定せずに、その日好きな席に座って作業をすると・・・。

しかしこの弊害として「誰がどこに座ってるかわからない」「座ってる人が誰なのかわからない」といったコミュニケーション上の問題が生じます(10名程度の小さな事業所ならば不要ですけれどね)。その日に出勤したら席を取ってもらうシステムを用意すれば解決します。カスタマイズ次第では、夢が広がる小さなシステムを作ってみたいと思います。

図:こんな感じのウェブアプリが簡単に作れます

今回使用するスプレッドシートとライブラリ

また、G Suiteでは、現在ログインしてアクセスしているユーザのメアドを自動取得出来るので、社員ID入力を省略も可能です。社員IDではなくメアドをユーザ表に登録しておき、キープリリースの2つのボタンを押すだけで済むものも作成しました(ご要望があったので、コードを少し改造し、余計なコードを削除してあります@2019/12/26)。ソースコードで掲示してる内容は改造前のものです。

※2022年8月4日、より簡単なSVG画像を使ったレスポンシブ対応の座席表管理システムを作りました。

Google Apps ScriptでSVGを使ったイメージマップを操作 – 応用編【GAS】

概要

今回作成するのは、座席表アプリとしては非常に単純で最もシンプルな仕組みで実装しています。ライブラリとしては、Mansonryベースという「jQuery.ShapeShift」を使ってみました。このライブラリ自体はドラッグしてブロックを移動なんかも出来るのですが、今回はその機能はオフにして、スプレッドシート連動の座席表管理アプリを作っています。

仕様としては

  1. 座席はフリーアドレスではない席に対しては固定する為のフラグを用意(チェックボックスで管理
  2. 名前のついていない空いている席をクリックして、キープを押すと席が確定しスプレッドシートに書き込まれる
  3. すでにキープ済みの時にはエラーを返す。
  4. 同じ社員番号での重複キープを防止するためのチェック機能
  5. 固定席のカラーは青や灰色、フリーアドレスはキープするとピンクに変化する
  6. 席の無いエリアおよび通路などは、要素のvisibilityをhiddenする事で実現
  7. 席の確保画面はjQuery Dialogで用意しておく。
  8. 退社時には席をリリースする機能
  9. 各BOXの座席IDはセルの番地とイコールにしておく
  10. 毎日深夜0時に座席データは固定席以外はクリアされる(スクリプトトリガーを使用)
  11. 自分の会社では確保済みシートをクリックした場合、社員の顔写真、電話番号、PC番号、メルアド、Teamsへの直URL Schemeなどが表示されるように別途ダイアログを用意したり、社内の様々な業務(健診予約)なども追加して、活用しています。
  12. 別にテレワーク者の登録画面などもあると便利(今日の外出先や業務内容、時間などを格納するものを用意して)
  13. 個人的には自由度の高いレイアウトを作れるイメージマップ編のほうがオススメです。

キープ済みの席をクリックすると誰なのか?内線番号やSkypeのIDなどを表示するようにすると、なお面白いと思います。基本はスプレッドシートに列を追加して拡張し、プログラム側で読み取ってダイアログに配置するだけの仕組みです。

ソースコード

GAS側コード

//シートの空き状況をチェックする
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:表示用のメイン

<!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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;<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' />&nbsp; キープ</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' />&nbsp; キャンセル</button></span>
</div>
</div>
</div>
<div id="dialog2" title="Basic dialog">
<p>
🍲今日はもうお帰りですか?座席を解放する場合は、IDを入れて、リリースをクリックしてください。
</p>
<div align="center">
<b>あなたのID:</b>&nbsp;&nbsp;<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' />&nbsp; リリース</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' />&nbsp; キャンセル</button></span>
</div>
</div>
</div>
</body>
</html>
  • JavaScriptおよびCSSに関しては別のHTMLファイルに切り出して、起動時に読み込むようにしています。
  • そのため今回のウェブアプリケーションは、GAS側でHtmlService.createTemplateFromFileとしてロードしています。

zaseki.html : 座席用のテンプレート

  <!-- 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にはnothingcabinetなどを指定し、固定席としています。
  • それぞれのidは、スプレッドシート側のセル番地と対応しています。

javascript.html:クライアント側スクリプト

  <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側コード

<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時に座席データのクリアを自動実行する為にはスクリプトトリガーの設置が必要です。以下の作業をしてから利用しましょう。

  1. スプレッドシート上のメニューより「座席表Plus」⇒「setup」を実行。スプレッドシートのIDがスクリプトプロパティに格納されます。
  2. 同じく「座席表Plus」⇒「リセットトリガー設置」を実行。スクリプトトリガーが設置されます。ただし、複数名が実行しないように注意してください。

サンプル

白いタイルは、フリーアドレスの席い該当するので、確保する事が可能です。ただし、ユーザリストに該当の社員IDが登録されていない場合には、確保する事ができません。

現在テスト用の社員IDとして以下のユーザを登録してあります。

ID 表示名
AS400LOVE IBMユーザ
SUPERDOME HPユーザ
INTL8080 INTELユーザ
SUN50 SUNユーザ

サンプルはこちら

関連リンク

Google Apps Scriptで座席表を作ろう - ライブラリ編【GAS】” に対して4件のコメントがあります。

  1. ノア より:

    こちらとても有用な情報で嬉しく思っています。
    なんとか自分の環境に対応させたのですが、
    ※IDはG Suiteユーザならばメールアドレスをスクリプトで取得して、クリックするだけで入力不要にする事でより簡単にする事が出来ます。スプレッドシート側のユーザリストもメアドの登録としておけば、簡単ですね。
    ▲これも実装したいのですが、具体的な手法を教えていただくことは可能でしょうか?
    ※当方HTMLとcssが書ける程度の初心者です。

  2. akanemaru2017 より:

    ノアさん、おはようございます。

    今回のサンプルは、手動で毎回IDを入れてそれを基準に探す仕様にしてました。という事で、G Suiteで利用できるSession.getActiveUser();を使って、メアドを自動取得⇒それを元に探索、キープ、リリースできるようにした改造版を掲示しました。

    主な改造点は

    • var manid = Session.getActiveUser();として、manidを自動取得のメアドに置換
    • LocalStorageを使う部分や、jQuery DIalogのID入力部分の削除

    これくらいです。ただ、今回の自動でメアドを取得して探索して返す手法は、非常によく利用するものなので、ぜひ他のシステムでも流用すると、利用者の余計な負担が減って、利用率も上がると思います(何より、IDは流用できますが、メアドはログイン認証してないと、なりすましができませんから)

    1. ノア より:

      ありがとうございます!望んだ通りの結果になりました。

      毎日使うものだとたった数秒の手間でも、労力の積み重ねがすごいですものね。。。
      これを使えるようになると、今の業務も便利になりそう。。。
      今日で仕事納めですから、お正月休みの間にじっくり研究します!

      本当にありがとうございました!

      1. akanemaru2017 より:

        このアプリは本当に座席表のみですが、色々と機能を追加していくと、非常に便利になると思うので、ぜひ拡張してみてください。
        フリーアドレス推進してる割には、この手のアプリがソリューションでべらぼうに高かったりするので、価値はあると思います。

コメントを残す

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

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