自習用」カテゴリーアーカイブ

[GAS] WEBアプリはフォームの多重送信に注意。特にスマホを使う場合。

Google Apps Script (GAS) はWEBアプリケーションを簡単に作成できるため、改善活動レベルで業務アプリをサクサク構築できます。JavaScript (JS)とHTMLを多少勉強する必要がありますが、業務の自動化を推進する上で強力なツールです。

GASではGET、POSTに対応する処理を行って、対応するHTMLを返すことができます。これをWEBアプリケーションと呼びます。WEBアプリの活用方法として、自作のフォームを使って依頼や連絡、申請の手続きを自動化したいと考える場面もあるでしょう。ここで気を付けたいのが、多重送信の防止です。

WEBアプリを操作する場合、ブラウザの戻る・進むの動作によって、GASに届けたい情報(パラメータ)を送るページを意図せず複数回開いてしまう可能性があります。注文のような1回だけ送りたい情報が何度も送られるようでは混乱を招くので、情報の多重送信を防ぐ仕組みの導入が必要です。
スマホを使う場合は、より多重送信への注意が必要です。PCから操作する場合、余計なタブを閉じたりPCをシャットダウンする際にブラウザも終了されるため、ブラウザの戻る・進むくらいしか意図せずページを開く可能性はありません。ところが、スマホの場合はアプリを終了しない、スマホの電源も切らないで運用されることが多く、多重送信の問題が生じやすい状況です。たとえば、ブラウザのアプリで複数のタブを開いた状態でスマホのホームボタンを押してもアプリが終了されるわけではありません。再度アプリを開いた時点でタブに残っていたページにブラウザがアクセスしようとして、前回アクセスしたときと同じリクエストを出してしまいます。

GETリクエストではURLにパラメータを含むので、同じURLにアクセスすれば同じパラメータが送信されます。別のURLに遷移させない限り、同じURLにアクセスするのを防がないと多重送信が生じます。

POSTリクエストでは、送信されるパラメータがURLに含まれないので、最後に表示されているのがPOSTリクエストへ応答してGASが生成したHTMLだということを忘れてしまうかもしれません。ブラウザ側はPOSTリクエストを覚えていて、再度その画面を開いたときにPOSTリクエストを再送してしまいます。PCでChromeなどブラウザを使っていると戻る・進むによってPOSTリクエストを送る場合に警告が出る場合がありますが、前述したスマホのタブでは警告もなくPOSTリクエストが再送されてしまいます。

多重送信の対策方法は、さいきょうの二重サブミット対策など様々な場所でまとめられています。
まず検討したいのが、GASが受け取るリクエストが過去のリクエストと同一でないか確認できる機構(トークンチェック)を組み込むことです。簡単には、フォームを含むページにアクセスしたときにトークンを発行し、formのhiddenにした要素を使ってほかの回答と一緒にトークンが送信されるようにします。送信されたトークンを過去のトークン(どこかに貯めておく)と比較して、過去に同じトークンを持った回答が送信されていない場合に次の処理に進むようにすれば、多重送信を弾けます。
トークンチェックで多重送信を検知したときに、安易にエラーページを表示するのはかえって危険です。ユーザーが送信に失敗したと思って、最初からフォームの送信作業をやり直すかもしれません。これは正規な手続きなので内容が同じであっても多重送信ではないためシステム側で検出することはできません。トークンチェックで多重送信を検出した場合は、あたかも送信に成功したかのように見せておくとユーザーの勘違いを防げます。

このほかに、フォームの送信ボタンを画面が更新される何度も押すという形の多重送信もあります。この場合はJavaScriptでフォームのボタンを無効にすることで、ボタンを複数回押せなくする方法で対策できます。formタグにonSubmitで回答送信ボタンがdisableになるJavaScriptを仕込むことで実現できます。

[GAS]WEBアプリケーションの見た目を整える

GASでWEBアプリケーションを作れるようになると、さまざまなシーンで電子化・自動化を進められます。よりたくさんの人に使いやすいと感じてもらうためには、多少の工夫が必要です。HTMLの知識があれば、見た目のよい、ぱっと見てわかりやすいWEBアプリケーションを作成することができるでしょう。

WEBページの見た目を改善するのには、HTMLと組み合わせて使われるCSSの作例を調べるのが簡単です。以下にすぐ使えるページを紹介します。

[GAS] Googleドキュメントの途中にパラグラフを挿入する

基準点となる文字列を含んだパラグラフの次に新しいパラグラフを挿入する方法を考えます。新しいパラグラフの中身はダイアログボックスで入力した文字列とします。

function myFunction() {
  var body = DocumentApp.getActiveDocument().getBody();
  
  var paragraphs = body.getParagraphs();
  
  for (var i = 0; i < paragraphs.length; i++) {
    var paragraph = paragraphs[i];
    var index = i;
    var text = paragraph.getText();
    if(text === 'key paragraph'){
      Logger.log('index %s text %s', index, text);
      var child = paragraph;
    }
  }
  
  var childIndex = body.getChildIndex(child);
  Logger.log('childIndex %s', childIndex);
  
  //input text to insert
  var ui = DocumentApp.getUi();
  var response = ui.prompt( '挿入する文字列を記入する。', ui.ButtonSet.OK_CANCEL);
  
  // Process the user's response.
  var button = response.getSelectedButton();
  var textToInsert = response.getResponseText();
  //キャンセルならスクリプト終了
  if (button !== ui.Button.OK) {
    return;
  }
  
  //insertParagraph
  body.insertParagraph(childIndex + 1, textToInsert);
}

//メニューを追加
function onOpen(e) {
  DocumentApp.getUi()
      .createMenu('GAS')
      .addItem('insert paragraph', 'myFunction')
      .addToUi();
}


まず、DocumentAppでBodyとその下のparagraphsを取ってきます。各paragraphの中の文字列はgetText()で取れるので、基準点となる文字列が含まれるparagraphを探します。上の例では、paragraphの文字列が「key paragraph」であるという条件で該当するparagraphを指定しました。

基準点となるparagraphオブジェクトが見つかったら、body.getChildIndex(paragraph)のように引数として渡してやると、bodyに対するchildIndexが返ってきます。

基準となるparagraphの childIndexが求まったら、そのparagraphの前に挿入する場合は childIndex を、そのparagraphの後ろに挿入する場合はchildIndex+1を、insertParagraphの引数に渡します。

挿入する文字列を入力するのに使ったダイアログボックスですが、V8エンジンではBrowser.inputbox()が動かないのか、UI.prompt()を使うと動きました。UI.prompt()は返ってきたresponseにさらにgetResponseText()しないとテキストが取れません。

ドキュメントのメニューからスクリプトを動作せさせる工程はGoogle Apps Scriptを使った独自メニューの作り方を参照しました。

テンプレートを別ファイルで用意する場合はGoogle Apps Scriptで議事録テンプレ作成を楽にしたが参考になります。

真の働き方改革 楽をしよう!

私は戦前から続く古びた会社で研究開発の業務に携わりながら、身の回りの業務効率向上―いわゆる働き方改革に取り組んでいます。同僚の多くは真面目で根気よく働くのですが、それゆえに抜本的に働き方、仕事のやり方を変えるという思考には至らないように見えます。たとえば、生産性を上げるためにどのような取り組みをするのかを議論すると、「よく考える」「がんばる」といった精神論レベルのアイデアしか出てこないのが現実です。本稿では、そのような勤勉な方々に向けて、大幅に生産性を向上させるための考え方を提示することを目指しています。

まず、楽をしよう。そして、浮いた時間で新しいことをしよう。

生産性を上げるとは、ある一定の時間に出せる成果の量を増やすことと言い換えられます。つまり、同じ仕事を短時間で処理できれば、生産性が上がったといえます。そこで、頑張って、あるいは気合を入れて、集中して仕事に取り組くことで、ひとつの仕事をいつもより短時間で終わらせることができたとします。ちょっと疲れていませんか?そのペースで一日中働けますか?時間やお金が有限なように、人間の体力や集中力も有限です。気合いを入れて、多くの体力や集中力をひとつの仕事に費やしてしまえば、あとの仕事に支障がでます。一日あるいは一週間というまとまった時間を振り返ってたときに、本当に成果の量は増えるでしょうか。

たとえ話として、走って移動する状況をイメージしてください。普段より気合いを入れて仕事にあたるのは、ジョギングのペースはなくダッシュのペースで走るようなものです。短距離走のペースで長距離走の距離を走ることはできません。長距離を走ろうと思えば、平均的な速度は遅くなります。そこで、別の方法を考える必要が出てきます。別の方法があるのかを考えるときに重要なのが、本来の目的です。もし走ることが目的ではなくて、何かを届けるのが目的なのであれば、駅伝にしてしまえばいい。一人当たりの走る距離を短くすれば速度は上がり、より早く遠くまでタスキを届けられるでしょう。ところで、どうして自分の足で走っているのでしょうか。自転車、自動車、電車、飛行機。 何かを届けるのが目的なら、 自力で走るという条件を外せば、桁違いに速く遠くまで行ける方法があります。冒頭、私が走って移動する状況をイメージしてくださいと書きました。親切なのか無知なのか、仕事の目的ではなく手段を具体的に説明するだけの人はけっこういます。あなたの上司もそんな指示の仕方をしていませんか。

いま取り組んでいる仕事の目的は何でしょうか。おそらく、やり慣れた仕事であれば、どのような成果物が必要なのかを把握されていることでしょう。そこで考えてください。今までのやり方をどのように変えれば、同じ成果物を楽に生み出せるかを。楽にできるということは、より短時間で、体力も集中力も消費せず、ひょっとするとお金もかけずに成果を上げるということです。そのためには、前の段落でたとえ話をしたように、本来の目的以外の前提条件を外して考えることをお勧めします。

楽に成果を上げられるようになったら、時間も体力も余裕ができますね。その浮いた時間で、新しいことをするのが生産性向上です。体力を削って時間を生んでも、あとで帳尻合わせが来ます。時間も体力も消費せずに、同じ成果を上げましょう。

人が手を動かさなくても成果が得られる仕組みをつくろう。

楽に成果を上げるなんて言うは易く行うは難しでは?はい、その通り。簡単ではありません。だから私はこの文章を書いているのです。簡単だったら、みなさんすでにやっておられるでしょう。

楽に成果を上げるのには、3つの方針があります。消す、方法を変える、道具を使うの3つです。

まず、仕事を「消す」ことを考えましょう。昔から続けているというだけで、何の役にも立たない仕事は消す価値があります。やめたら誰かに迷惑がかかるかもしれない?では、その迷惑がかかるかもしれない相手に聞いてみましょう。もし必要ないと言われれば、正々堂々と止めることができます。もし完全に止めることができなくても、ほかの仕事と共通化できれば、2あった仕事を1に減らせます。仕事を消すには関係者との交渉や確認が面倒くさいのですが、その苦労してでもやる価値があります。なんといっても、その仕事、消してしまえば金輪際やらなくていいんですよ。

次に考えるのは、仕事の「方法を変える」ことです。流れを変えるともいえるでしょう。たとえば、予め準備をしておくことを考えます。準備のない状態で問い合わせや指示が来て業務がスタートすると、その場で内容を確認し、適切な対処方法を調べたり考えたり、関連するルールとの整合性を検討したりといった多数の作業に取り掛かります。類似事例の経験があっても、昔のことで詳細は記憶に残っていないかもしれませんし、当時と現在ではルール変更など状況変化があるかもしれません。そのうえ納期はなるべく早くと言われたら、時間のない中でストレスを感じつつ、どうにかそれらしい答えを紡ぎだす羽目になります。そうならないための準備をしましょう。ある程度の経験があれば、業務のパターンが見えてきます。パターンごとに先方に確認すべき事項のリストを作ったり、過去事例をまとめた資料をつくったり、関連する規制情報をすぐに参照できるよう確認手段を明確にしておくといった準備ができるはずです。今の時代、たいていの情報はWEBや共有ネットワーク上に電子データで存在しますから、Wordで資料をつくって外部リソースへリンクを張っておけば、数クリックで必要な情報が集まるようにできます。さらに、電子データで準備をしておけば、容易に使いまわしができます。問い合わせてを受けて、調べて回答するまで数分から数時間かかっていたような業務が、準備資料を検索してコピーするだけのわずか数秒で解決できるようにできます。成果を使いまわせる点は重要です。むしろ、使いまわす前提で資料をまとめましょう。個別の案件にその都度対応していては、結局人間の手間がかかります。そこで、別件のための生み出した成果を使いまわすことを考えましょう。うまくいけば、インプットなしで成果を出せます。

最後に、仕事に「道具を使う」ことを考えましょう。人力で走るより車に乗った方が楽なのは言うまでもありません。車がコスト的に難しいなら、自転車でも人力よりずっとマシでしょう。今日の仕事の大部分である事務仕事にとって、Microsoft OfficeのWordやExcelあるいはG Suite (Google) のドキュメントやスプレッドシートは自転車と同じくらい当たり前の存在です。特に、事務仕事でExcelやスプレッドシートを開かない日はないほどです。これらのソフトウェアは、自転車以上に使うものの技量が問われます。SUM関数やグラフの挿入、罫線の修飾は誰でも使えるかもしれませんが、VLOOKUP関数やデータの並べ替え、データ分析に至っては10人に1人も使えるか怪しいところです。単純な道具の代表であるハサミでも使いようによっては繊細な紙細工を生み出せます。現代のコンピュータ技術の粋を集めて作られたExcelのようなソフトウェアを使いこなせたなら、どれほどのことができるか言うまでもないでしょう。多くの職場には、そんな便利な道具がすでに導入されているのですから、うまく使えるようになれば劇的に事務作業が楽になります。
もっと楽をしたいなら、プログラミングに挑戦しましょう。Officeソフトの使いこなしが自転車レベルとしたら、プログラミングは自動車レベルです。段違いの楽ができます。MS OfficeならVBA、G SuiteならGASというプログラムを自分で作ることで、ほとんどの作業は自動化できます。巷には何冊も書籍が出ていますし、ネット上を検索すれば参考になる情報がいくらでも出てきます。

最後に、他人を動かすのは北風ではなく太陽であることを忘れないで。

ここまで頑張ってきたあなたは、効率化の施策もろもろを組織全体に広げたいと考えることでしょう。ひたすらに前例踏襲に励み、ちっとも業務を効率化しようとしない同僚に苛立ちすら覚えているかもしれません。そこで、思い出していただきたいのが、北風と太陽の寓話です。人は激しい重圧に曝されると身を守る行動にでます。叱責や非難は、行動を変容させるための良い手段ではありません。他人の行動を変えるには、太陽のように温かい心で優しく手を差し伸べることが近道です。

自分ひとりで効率化を進めてきたあなたにとっては納得しがたいことかもしれません。大して努力しようとしない他人のために、なぜ自分が苦労しなければならないのかと。ここで思い出していただきたいのが、あなた自身がここまで来るのに積み重ねた努力と苦労の大きさです。それを他人にも強いるのですか?全ての人類が自分自身の成長を常に望んでいるわけではありませんよね。

自分の業務を効率化して多少の余裕がある。同僚にも役立つ効率化の具体的な手法と効果を知っている。そんな立場だからこそ、最小の労力で同僚の業務を効率化する提案ができるはずです。人間なんて現金なものですから、自分が労せずして楽をできると知れば、こちらの話に乗ってくれる可能性が高くなります。少し頑張るだけで楽をできるなら、自分も工夫してみようと考える仲間が現れるかもしれません。

まず、楽をしよう。次に、他人を楽させよう。そして、働きやすい環境を実現しよう。

[GAS] WEBアプリケーションでJavaScriptを使う例 QRコードの読み込み

GAS上でJavaScriptやCSSをHTMLと別ファイルで管理し、埋め込む方法

GASのWEBアプリケーションでは、GASを使ってhtmlファイルを生成し、それを表示している。普通のWEBのように、htmlファイルにJavaScriptを埋め込むこともできる。

JavaScriptやCSSをHTMLファイルに直で書き込むと読みにくいので、別のファイルに書いたJavaScriptやCSSをHTMLファイルで読み込む形をとることが多い。GASではファイルの扱いが面倒くさいが、以下のページの記述がわかりやすい。

GASでHTML/CSS/JavaScriptを使ってWebアプリケーションを作る方法

やっていることは、GASでHTMLファイルにJavaScriptやCSSを埋め込んでしまい、マージされたHTMLを表示させるというものだ。

GASのプロジェクトではHTMLファイルとしてJavaScriptやCSSを作っておく。このとき、HTMLファイルといえどもJavaScriptならscriptタグとその中身、CSSならstyleタグとその中身だけを書いておく。また、HTMLのテンプレートとなるファイルには、<?!= HtmlService.createHtmlOutputFromFile(‘ 読み込むファイル名 ‘).getContent(); ?> と記述しておく。

関連 [GAS] Google Apps Script のHtmlServiceまとめ

JavaScriptの利用例 ブラウザ上でQRコードを読み込む

JavaScriptを使うといろいろ可能性が広がる。

たとえば、スマホのブラウザ (Chrome, Safariなど) からQRコードを読み込ませることもできる。ユーザーにブラウザ以外のアプリをインストールさせる必要がない。

たとえば、次に示すページでは、フォームのテキストボックスにQRコードを読んだ結果を書き込むシンプルな方法を紹介している。

ネイティブアプリ不要!モバイルWebサイトにQRコードリーダーを実装する方法 LazarSoft/jsqrcodeを使った例。写真を撮ってQRコード読み込み。

[HTML5] QRコードリーダーを作成する cozmo/jsQRを使った例。ビデオで撮ってQRコードを検出すると結果がテキストで出てくる。

QRコードの生成や印刷の条件

QRコードの生成はGoogleスプレッドシートとGoogle Charts APIを使うのが楽。QRコードに埋め込みたい文字列とGoogle Chartsを呼び出す式を並べれば一気にQRコードを生成できる。
Google Charts APIのQRコードの説明

1個1個つくるならQRコードの生みの親デンソーウェーブのQRQRも使い勝手がいい。PCにソフトウェアをインストールすれば大量生成も可能。

QRコードの仕様を学びたければKeyenceの資料がわかりやすい。

[GAS]DriveAppでファイルを移動する

stackoverflowの説明が端的でわかりやすい。Googleドライブ上のファイルがどのフォルダに入っているかは、いわゆるフォルダ構成とはイメージが異なる。Googleドライブであるファイルがどのフォルダに入っているのかは、Gmailのラベルに相当するものと考えればよい。同時に複数のフォルダに同一のファイルが所属できるのも、Gmailのラベルを思い出せばおかしなことではないだろう。

新しいファイルの生成は、ルートフォルダである「マイドライブ」で行われる。これは新しいファイルに「マイドライブ」のラベルが付いた状態だと思えばいい。Googleドライブ上でどこかのフォルダにファイルを移動させたければ、移動先のfolderに対してfolder.addFile(ファイル)を行ったうえで、「マイドライブ」フォルダからremoveFile()すればよい。addFileする前にファイルのgetParents()をしておけば、移動元のフォルダ(「マイドライブ」だろう)を取得できる

日本語だとGASで業務効率化 ~スプレッドシートからGoogleドキュメントを作成する~の説明通りにやればファイルを移動できる。ほかのサイトだと、copyして元のファイルを削除しているケースが見られるが、これだとcopyした意味がない。

[GAS]どこで勤務しているか所在情報を共有する仕組み

小さな組織から大きな組織まで、出欠や勤怠をまとめた一覧表を用意している場合が多い。よくある方式が、大きなホワイトボードに氏名が並んでいて、マグネットで自分がどこにいるのかを示す形である。少人数のうちはホワイトボードでもいいが、100人を超えるような組織で、かつ居場所が散在している場合には、自分の所在を変更するためホワイトボードまで行くだけで相当な手間がある。ホワイトボードから離れた場所にいるものには他人の情報を確認できないし、忘れたまま出かけてしまった場合は残っている人に連絡して情報を書き換えてもらう必要がある。そもそも在宅勤務・テレワークの同僚がいれば、休みなのか働いているのかホワイトボードでは共有できない。このような不便を解消するため、Google Apps Scriptを使って所在情報を共有する仕組みを開発した。

所在情報をまとめるスプレッドシートを作成し、さらに各ユーザーの利便性のためWEBアプリケーションを経由してスプレッドシートに記録されている自信の情報を読み書きできるようにした。さらに、 外部から所定のメールアドレスへメールを送り、GmailをGASから操作して所所在情報を書き換えられるようにした。 所在情報を社外から書き換えたい場合でも G Suiteに社内ネットワークからしかアクセスできないという制限を踏まえたためである。

スプレッドシートの構成は以下の通り。
所在の情報の確認でも最も短時間で反応する必要がある場面を総務部門の担当者が代表電話で連絡を受けた場合と判断し、氏名をあいうえお順で並べたシートを本体とした。
A列 ID 検索に使う
B列 氏名
C列 読み仮名
D列 本日の所在
E列 本日の所在 付加情報、連絡事項
F列 本日の所在 更新時刻
G列 翌日の所在
H列 翌日の所在 付加情報、連絡事項
I列 翌日の所在 更新時刻
J列 予定の最終有効日

これとは別に、所属部門ごとに氏名を並べて所在を見やすくしたシートを作成した。各人の漢字氏名を検索キーとして、あいうえお順のシートからINDEX関数とMATCH関数を使って情報を反映した。

勤怠程度の情報ならGoogle Apps Script で勤怠フォームを作って出欠確認を自動化するGAS|オフィスの業務を改善・効率化した活用事例を紹介する(1)|非エンジニアのための「Google Apps Script」あたりを参考にした方がいいかもしれない。前者のリンクにも書かれているように、システムを使う場所、組織の都合に適したシステム構築が必要である。

まずはWEBアプリケーションに設定するスクリプトを以下に示す。検索のキーとなるidは、全角数字で入力されても半角数字に変換されて処理される。利用者のリテラシーが高くない場合、全角数字と半角数字の区別がつかない可能性があるためだ。ユーザー数が増えれば、その分だけリテラシーの高くないユーザーを含む可能性が高まる。

GASのHtmlServiceでhtmlファイル側に変数を渡したいときは、13行目のようにする。

//参考ページhttps://www.indetail.co.jp/blog/181219/
//https://dackdive.hateblo.jp/entry/2015/02/01/010540
function doGet(e) {
  // 表示したいHTMLのファイル名を指定(拡張子は記載しない)
  var id = e.parameter.id;
  
  if(!id){
  //idがない場合はid入力欄を表示
  var html = HtmlService.createTemplateFromFile('inputid');
  }else{
  //idがある場合は、現在の記入内容と情報記入欄を表示
  var html = HtmlService.createTemplateFromFile('index');
  html.id = toNumber(id);
  }
  
  //htmlを出力
  return html.evaluate();
}

function doPost(e) {
  //POSTで渡された値を取得
  var id = toNumber(e.parameter.id);
  var target = e.parameter.target;
  
  // SSIDからスプレッドシートの取得
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  
  //id対象者の行数を取得
  var row = rowInterest(id, 'ID');
  //id対象者の情報を取得
  var info = getData(id);
 
  //本日の情報を処理
  if(target === 'today'){
    info.placeToday = e.parameter.whereIs;
    info.commentToday = e.parameter.comment;
    info.dateTodayChange = new Date();
    
    //書き込み配列を用意
    var writeArray = [[info.placeToday,info.commentToday,info.dateTodayChange]]
    var column = 4;
  }
  
  //翌日の情報を処理
  if(target === 'NextDay'){
    info.placeNextDay = e.parameter.whereIs;
    info.commentNextDay = e.parameter.comment;
    info.dateNextDayChange = new Date();
    var dateString = e.parameter.validDate;
    dateString += "T23:59:59+0900";
    info.validDate = new Date(dateString);
    
    //予定の取り消し
    if(info.placeNextDay === 'delete'){
      info.placeNextDay = '';
      info.commentNextDay = '';
      info.validDate = '';
    }
    //書き込み配列を用意
    var writeArray = [[info.placeNextDay,info.commentNextDay,info.dateNextDayChange, info.validDate]]
    var column = 7;
  }
  //書き込み
  sheet.getRange(row, column,1,writeArray[0].length).setValues(writeArray);
  SpreadsheetApp.flush()
  
  // スプレッドシートのデータ挿入後、元の画面に戻す
  var html = HtmlService.createTemplateFromFile('index');
  html.id = id;
  html.info = info;
  return html.evaluate();
}

function getSheet(name){
  // SSIDからスプレッドシートの取得
  var ssId = 'ここにファイルIDを記入する';
  var ss = SpreadsheetApp.openById(ssId);

  // 指定されたシート名からシートを取得して返却
  var sheet = ss.getSheetByName(name);
  return sheet;
}

function getData(id) {
  // 指定したシートからデータを取得
  var data = getSheet('あいうえお順').getDataRange().getValues();
  
  // 該当ユーザーの情報を取得
  var row = rowInterest(toNumber(id), 'ID') ;
  
  var info = {
  ID:data[row -1][0],
  fullname:data[row -1][1],
  fullnameKana:data[row -1][2],
  placeToday:data[row -1][3],
  commentToday:data[row -1][4],
  dateTodayChange:data[row -1][5],
  placeNextDay:data[row -1][6],
  commentNextDay:data[row -1][7],
  dateNextDayChange:data[row -1][8],
  validDate:data[row -1][9]
  };
  info.hasToday = info.placeToday !== '';
  info.hasNextDay = info.placeNextDay !== '';
  
  return info;
}

function rowInterest(keyValue, keyType){
  // SSIDからスプレッドシートの取得
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  var data =sheet.getDataRange().getValues();

  var rowInterest = 0;
  var columnInterest = 0;
  if(keyType ==='ID'){
    columnInterest = 0;
  }
  var data =sheet.getDataRange().getValues();

  for(var i=1;i<data.length;i++){
    if(keyValue === data[i][columnInterest]){
      rowInterest = i;
    }
  }
  
  Logger.log(rowInterest);
  return rowInterest + 1;
}

//毎日AM0時から人が動き出すまでの時間に実行する。
//本日の所在を消去し、値を空白にする。
//翌日の所在が記入されていれば、その内容を本日の所在に転記する。
//翌日の所在の最終有効日を当日の日付と比較し、最終日であれば翌日の所在を消去する。

function monitoringDaily(){
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  var data =sheet.getDataRange().getValues();
  
  //現在時刻
  var date = new Date();
  
  //本日の所在を消去し、値を空白にする。
  var lastrow = data.length;
  sheet.getRange(4,4,lastrow-3,3).clearContent();
  
  //翌日の所在が記入されていれば、その内容を本日の所在に転記する。
  for(var i=3;i<data.length;i++){
    var info = {
      ID:data[i][0],
      fullname:data[i][1],
      fullnameKana:data[i][2],
      placeToday:data[i][3],
      commentToday:data[i][4],
      dateTodayChange:data[i][5],
      placeNextDay:data[i][6],
      commentNextDay:data[i][7],
      dateNextDayChange:data[i][8],
      validDate:data[i][9]
    };
    info.hasToday = info.placeToday !== '';
    info.hasNextDay = info.placeNextDay !== '';
    
    if(info.hasNextDay){
      //書き込み配列を用意
      var writeArray = [[info.placeNextDay,info.commentNextDay,info.dateNextDayChange]]
      var column = 4;
      sheet.getRange(i + 1, column,1,writeArray[0].length).setValues(writeArray);
    }
    //翌日の所在の最終有効日を当日の日付と比較し、最終日であれば予定を消去する。
    Logger.log(info.validDate < date);
    if(info.validDate < date){
      var column = 7;
      sheet.getRange(i + 1, column,1,4).clearContent();
    }
  }
}

function toNumber(str){
  str = String(str);
  var numVal = str.replace(/[0-9]/g, function(s) {
    return String.fromCharCode(s.charCodeAt(0) - 65248);
  });
  return Number(numVal);
}

function getNextDay(){
  var date = new Date();
  date.setDate(date.getDate() + 1);
  var dateNextDay = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
  return dateNextDay;
}

次に、WEBアプリケーションで使用するHTMLのテンプレートを示す。idを未指定の場合はidを入力するページを、idが指定されていれば現在の情報の確認と新しい情報の入力ページを表示する。
formで与える選択肢の値は適切に変更する。日付を入力させる欄は、予め翌日の日付が入るようにした。

以下の例では、所在地の選択肢が事業所1~5であるが、たとえば選択肢のひとつに「在宅勤務」を用意しておけば、在宅勤務者の勤務開始や終了をほかの従業員と同様に情報共有できる。

//ID入力用
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>IDを入力してください</h2>
    <form method="get" action="ここにWEBアプリケーションのURLを記入する">
      <p>ID: <input type="text" name="id" value="" size="20" /></p>
      <input type="submit" value="送信する">
    
  </form>
  </body>
</html>
//IDをキーとして情報の確認と書き換えをする
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      h2 {background-color:palegreen;}
      table{
        border: solid 1px #000000;
        border-collapse: collapse;
      }
      th {border: solid 1px #000000}
      td {border: solid 1px #000000}
    </style>
  </head>
  <body>
    <table>
      <?
        // スプレッドシートからデータを取得
        // 該当ユーザーの情報を取得
        var info = getData(id);
        
        // 本日の予定の見出し作成
        output.append('<tr>');
        output.append('<th>氏名</th>');
        output.append('<th>本日の所在</th>');
        output.append('<th>本日の所在 連絡事項</th>');
        output.append('<th>本日の所在 更新時刻</th>');
        output.append('</tr>');
        //本日情報に値があれば表示
        if(info.hasToday){
          output.append('<tr>');
          output.append('<th>' + info.fullname + '</th>');
          output.append('<th>' + info.placeToday + '</th>');
          output.append('<th>' + info.commentToday + '</th>');
          output.append('<th>' + Utilities.formatDate(info.dateTodayChange, 'JST', 'yyyy年M月d日 H時m分') + '</th>');
          output.append('</tr>');
        }else{
          output.append('<tr>');
          output.append('<th>' + info.fullname + '</th>');
          output.append('<th>未入力</th>');
          output.append('<th></th>');
          output.append('<th></th>');
          output.append('</tr>');
        }

        // 翌日の予定の見出し作成
        output.append('<tr>');
        output.append('<th>予定の有効期限</th>');
        output.append('<th>翌日の所在</th>');
        output.append('<th>翌日の所在 連絡事項</th>');
        output.append('<th>翌日の所在 更新時刻</th>');
        output.append('</tr>');
        //本日情報に値があれば表示
        if(info.hasNextDay){
          output.append('<tr>');
          output.append('<th>' + Utilities.formatDate(info.validDate, 'JST', 'yyyy年M月d日 H時m分') + '</th>');
          output.append('<th>' + info.placeNextDay + '</th>');
          output.append('<th>' + info.commentNextDay + '</th>');
          output.append('<th>' + Utilities.formatDate(info.dateNextDayChange, 'JST', 'yyyy年M月d日 H時m分') + '</th>');
          output.append('</tr>');
        }else{
          output.append('<tr>');
          output.append('<th></th>');
          output.append('<th>未入力</th>');
          output.append('<th></th>');
          output.append('<th></th>');
          output.append('</tr>');
        }
      ?>
     </table>
   
    <h3>所在情報を更新するには、以下のフォームに記入して、更新ボタンを押してください。</h3>
    <h2>本日の所在情報を登録する</h2>
    
    <form method="post" action="ここにWEBアプリケーションのURLを記入する。末尾はexec?id=<?= id ?>">
      <input type="hidden" name="target" value= "today">
      <input type="hidden" name="id" value= "<?= id ?>">
      所在:<label><input type="radio" name="whereIs" value="事業所1">事業所1</label>
      <label><input type="radio" name="whereIs" value="事業所2">事業所2</label>
      <label><input type="radio" name="whereIs" value="事業所3">事業所3</label>
      <label><input type="radio" name="whereIs" value="事業所4">事業所4</label>
      <label><input type="radio" name="whereIs" value="事業所5">事業所5</label><br><br>
	
      <label><input type="radio" name="whereIs" value="退勤" required>退勤</label>
      <label><input type="radio" name="whereIs" value="出張">出張</label>
      <label><input type="radio" name="whereIs" value="休暇">休暇</label>
      <label><input type="radio" name="whereIs" value="未出社">未出社(詳細は下の連絡事項欄に記入する)</label><br><br>
	
      <label>連絡事項: <input type="text" name="comment"></label><br><br>

     <input type="submit" value="本日の所在を更新する">
     </form>
     
  <h2>翌日の所在情報を登録する</h2>
    <form method="post" action="ここにWEBアプリケーションのURLを記入する。末尾はexec?id=<?= id ?>">
    <input type="hidden" name="target" value= "NextDay">
    <input type="hidden" name="id" value= "<?= id ?>">
    <label><input type="radio" name="whereIs" value="出張">出張</label>
    <label><input type="radio" name="whereIs" value="休暇" required>休暇</label>
    <label><input type="radio" name="whereIs" value="時差出勤">時差出勤</label>
    <label><input type="radio" name="whereIs" value="delete">予定の取り消し</label><br><br>
    
    <label>連絡事項: <input type="text" name="comment"></label><br><br>
    
    予定の最終有効日: <input type="date" name="validDate" value="<?= getNextDay() ?>"><br><br>
    <input type="submit" value="翌日以降の所在を更新する">
    </form>

  </body>
</html>

メールを読み書きして所在情報を書き換えるスクリプトを以下に示す。社外から所在情報を書き換えたいケースとして、突然の休暇、公共交通機関の事故など出社が遅れる、あるいは退勤処理をし忘れて退勤したという3通りを想定した。1分、5分など、一定時間ごとに発火するトリガーをかけておく。

GoogleフォームはG Suiteの外からでも回答できるので、フォームのスクリプトと組み合わせれば外部から勤務場所を入力するのに使える。このときは社員IDなどを予め入力したフォームのURLを配布しておくとよい。

function getMail(){
  var sheetname = 'あいうえお順';
  var sheet = getSheet(sheetname);
  var start = 0;
  var max = 500;
  var threads = GmailApp.search('label:shozai is:unread',start,max);

  for(var n in threads){
    var thread = threads[n];
    
    var msgs = thread.getMessages();
    for(m in msgs){
      var msg = msgs[m];
      var id = msg.getSubject();
      var body = msg.getBody();
      var date = msg.getDate();
      
      
      //所在情報に反映
      id = toNumber(id);
      var row = rowInterest(id, 'ID');
      //メール本文に「休暇」があれば休暇、「退勤」があれば退勤、それ以外は未出社に設定する
      var whereIs = '未出社';
      if ( body.match(/休暇/)) {
        whereIs = '休暇';
      }
      if ( body.match(/退勤/)) {
        whereIs = '退勤';
      }
      //書き込み配列を用意
      var writeArray = [[whereIs,body,date]]
      var column = 4;
      sheet.getRange(row, column,1,writeArray[0].length).setValues(writeArray);

      //メールを既読にする
      thread.markRead();
    }
  }
}

[GAS] フォームの送信時トリガーの多重実行問題を防止する

スプレッドシートをフォームとリンクして、フォームからの書き込みがあったときにスクリプトを動作させるトリガーが「フォームの送信時」トリガーである。これにバグがあるようで、フォームに回答が1回しかないのに、トリガーをかけているスクリプトが複数回実行されることがある。あとから実行されたスクリプトではeに値が入っていないので、ファイルへの書き込むなどに支障がでる。

代表的な回避策としては、スクリプトをスプレッドシートではなくフォームの方に記述し、スクリプトを介してスプレッドシートへの値書き込みを行うというものがある。ほかには、スプレッドシートのスクリプトでsleepを数秒かけたり、eの値が入っていないときに処理を止める条件分岐を付ける手法がある。

フォームのスクリプトの基本的な作り方はGASのonFormSubmit()でハマったことを参照。あとはreferenceでも読んで試してみる。スプレッドシートに書き込むには、appendRowすればいい。フォームへの回答の中身は以下で確認できる。ファイルのアップロードは、Drive上のファイルIDが回答になる。

function onFormSubmit(e){
  var mail = e.response.getRespondentEmail();
  Logger.log(mail);
  var date = e.response.getTimestamp();
  Logger.log(date);
  
  var itemResponses = e.response.getItemResponses();

  for (var i = 0; i < itemResponses.length; i++) {
    var itemResponse = itemResponses[i];
    var title = itemResponse.getItem().getTitle();
    var value = itemResponse.getResponse();
    Logger.log('["%s"] Title "%s", Value "%s"', i, title, value);
  }
}

便利そうなやつ
Googleフォームのプルダウンにスプレッドシートの内容を反映させる

[GAS] GASを使ってもらうようにする導入資料

GASを知らない同僚にGASを使う価値を理解してもらい、学習を始めるきっかけを与えるために有用な資料を集める。

GASの価値を理解してもらう

Google Apps Script (GAS) で毎週 30 分の雑務を自動化した話
毎月 500 時間の社内ルーティンワークを GAS を使って自動化した話
ともにグロービスのYuki Tanabe氏が社内業務にGASを活用したケースを紹介している。ありがちな事務作業が対象で、自職場での活用イメージがわきやすい。

GASの学習を始める最初の一歩

非プログラマの事務員にGASをいきなり書かせるのは難しい。まずはマクロやちょっとした関数として使ってもらう。Google Apps Script ハンズオン資料にあるような具体的なケースを与えて、一緒にプログラムを作る経験を積ませる。慣れてきたらGAS ビギナーが GAS を使いこなすために知るべきこと 10 選で紹介されているような一段階高度な内容を教えるとともに、自分だけでプログラムを描けるように励ます。

【保存版】初心者向け実務で使えるGoogle Apps Script完全マニュアル
実例が非常に豊富

G Suite Developer Apps Script リファレンス
Googleの公式リファレンス。Document, Spreadsheet, Formなど、それぞれのアプリケーションでどのようなスクリプトを書けるのかを調べられる。辞書的に使う。

マンガで分かる JavaScriptプログラミング講座
プログラミング初学者にJavaScriptでプログラムをつくる方法や概念を伝えるのに便利なサイト。Google Apps ScriptはJavaScript 1.6を基に作られているため、プログラムをどのように書くか、どのようなデータ処理が可能なのかはJavaScriptを学ぶ必要がある。書籍版もある。
2020/2/11 追記 G suiteではV8エンジンが利用可能になり、ECMAScript 5で書けるようになった日本語解説

JavaScript リファレンス
JavaScriptの日本語リファレンス。条件分岐や抽出といったデータ処理はこちらを参照する。GAS独自の機能以外をカバーする。
JavaScriptは広く使われている言語なので、ググれば何かしらヒントが得られるはず。

GAS使わなくても便利な機能があることも同時に教える

非プログラマには、プログラムを書くのは魔法を覚えるようなもの。はじめは何でもできるような気がするが、仕事では投入する時間やリソースと利益を秤にかける必要がある。車輪の再発明をしないためにも、既存の機能を学ぶことも推奨する。

たとえば、Google ドキュメントの音声認識機能(ツール>音声入力)。音声からかなり正確に文章を生成してくれる。まさに魔法のようだ。音声入力はキーボード操作が得意でない層にアピールするだろう。音声入力以外に翻訳機能もある。

https://twitter.com/master2020hitu/status/1190294814127882240

[GAS] Google Apps Scriptでハッシュ化する関数

GASで送信したメールのリンクを踏むと受領確認できる仕組みにはURLクエリパラメータを使えばいい。しかし、ユーザーIDのような意味のある値をあからさまにURLに付加するのはやりたくない。そんなときは受け渡すパラメータの値をハッシュ化すればいい。

Google Apps ScriptではcomputeDigest関数でアルゴリズムを指定して簡単にハッシュ値を取得できる。使用可能なアルゴリズムは、MD2, MD5, SHA-1, SHA-256, SHA-384, SHA-512, (2019/11/1現在)。

たとえば、スプレッドシートの何行目の値を編集するかをクエリで指示する際に、行数を指定する情報とユーザーIDを結合した文字列をハッシュ化したものをスプレッドシートの同じ行に書き込んでおく。このハッシュ値で検索すれば、あからさまにユーザーIDや行を特定する情報をパラメータに含まなくても、該当する行を選択することができる。

参考
スプレッドシートで覚えるブロックチェーン |「もしかして渡した値」「入れ替わってる!?」