AppSheetで多階層ワークフローを作ってみた【GAS】

AppSheetを使って社内の業務改善をするに当たって立ちはだかる技術の壁が「多段階で承認を必要とするワークフローシステム」の構築。申請者→承認者というだけの単一階層であればそこまで難しくないアプリを作れると思いますが、そこに更に申請者→承認者→上位承認者のような多段階となると・・・

さらに人によってその承認ルートが異なるのが通常の社内ワークフロー。さてどうやって作ろうか?という事で作ってみたサンプルです。

今回利用するアプリ等

以前、労務管理のタイムレコーダーを作りましたが、その中では申請者→承認者の単一な申請・承認の機能を装備しています。その時に作成した仕組みを踏襲しつつも、GASでうまいことハンドリングしてあげる必要があります。

必須要件としては

  • 人によって承認ルートが異なるのでそれをパターン化して人に割当てしてあげる
  • その承認ルートも多階層の上位者がおり、各階層で承認が通過しないと完了しない仕組みにする
  • 申請者は現在の承認状況やコメントを見られるようにサブテーブルで確認する機能
  • 申請中は再編集が出来ないようロックする仕組み。
  • 承認すると対象のワークフローは再承認出来ないように非表示に
  • 同一テーブルを申請者と承認者の両方が更新を行うので、Trigger列を2つ用意してのGAS発火の仕組み

といった感じになります。

AppSheetでタイムレコーダーを作成する

AppSheetで作成するメリット

多段階の承認を要するワークフローは今では企業では欠かせない仕組みです。しかし殆どの企業では外部サービスを利用してワークフローを実現しているでしょう。経費申請用にレシートを添付して申請(Geminiで読み取り可)であったり、有給休暇の申請であったり、はたまたGoogle Workspaceのグループやドライブの作成申請であったり。

これらの外部サービスでシンプルなものでも、月額1ユーザ辺り200円〜300円などが相場。そこに初期導入費用が加わってきます。これらを400名規模であれば年間100万円近い変動費のコストが掛かることになります。AppSheetで作成できればせいぜい30万円の初期コスト(1ヶ月掛かったとしても)で作れます。

作成と言っても殆どのケースで同じような処理の流れであるので、社内で知見を貯めて内製する体制を整えられればGoogle Workspaceにサービスを寄せて外部サービスを断捨離することに繋げられます。オーバースペックな外部サービスを使うのではなくシンプルで自分たちの力量でコントロールできるものから内製していく上ではAppSheetは始めやすいのではないかと思います。

AppSheetでアップしたPDFをGeminiでOCR【GAS】

事前準備

スプレッドシートデータの準備

今回のアプリは非常にシンプルなテンプレートなので、そこまで多くはないのですが利用するにはスプレッドシート上でデータの事前整備が必要です。単なるデータというだけじゃなくこれ自体もロジックの1つとなってるのできちんと整備しないと期待した動きになりません。

userシート

個人の利用者・承認者のすべてのメンバーをここに記述します。メールアドレス、割り当てる承認パターン(次項で作る)、承認ルート名、そしてrole。

roleは過去にも安否確認アプリで使いましたが、ユーザの場合は申請のみ可能です。管理者の場合は承認というパネルが表示されるようになるので承認作業を行えるようになります。

図:単純だけれども重要な設定です

AppSheetで安否確認アプリを作ろう【GAS】

承認パターンシート

userシートでも利用するしGASでも頻繁に参照することになる承認パターンのシートの整備です。例えば総務部の場合の承認ルートであったり、営業部の場合の承認ルートであったり。といった部門や目的別に応じて承認経路は異なります。これを実現するのがこのシート。

patternidを用意しuserシートではこれを割り当てます。

このシートで重要なのは

  • maxstep : 最大承認階層数(最大階層まで承認すると完了を判定する)
  • approve列 : 今回は最大で3つ。approve1〜approve3まで列を用意。現在の承認ステップに応じて次に参照する承認者を変更しています。

4段階以上やりたい場合はapprove4列を追加して上げる必要があります。各approve列はそのレベルでの承認者のメールアドレスを入れることになります(上位承認者)。

2段階しかないよという場合はmaxstepは2となり、3列目は空っぽになりますがこれで問題ありません。

図:patternidによって異なる承認経路を設定可能

GAS側の準備

次項のAppSheet側の準備にあるようにテンプレートをコピーした後はGASへの参照が切れています。故に自身でスタンドアローンコンテナのGASファイルを作成して繋げてあげる必要があります。ここではそのGASのファイルの作成手順です。

  1. Google Driveを開いて、左上の新規をクリック
  2. その他 => Google Apps Scriptをクリック
  3. スクリプトエディタが直接開いて警告文が出るのでそのまま作成を続行する
  4. ドライブにはスタンドアローンファイルが生成される
  5. スタンドアローンスクリプトをダブルクリックしてスクリプトエディタを開く
  6. 左上のプロジェクト名がファイル名になるので、無題のプロジェクトではなくきちんと名前を付けましょう。
  7. 今回のサンプルに掲載されてるコードをコピペして保存する

見つからない場合には、Chrome Webstoreから拡張機能インストールしましょう(Google Apps Scriptで検索すると出てくる)。

図:新規から作成することが出来る

スタンドアローンのGoogle Apps Scriptが作れない

AppSheet側の準備

今回のテンプレートではGASを利用しています。2箇所のポイントでGASに対してリクエストを投げてデータの処理をしてもらっています。自身でスタンドアローンのGASを作成した後に、テンプレに対して参照するファイルの変更を行う必要があります。

以下の手順でファイルの参照を変更します。

  1. AppSheetのアプリ開発画面を開く
  2. 左サイドバーのAutomationをクリックする
  3. 申請時ボットと承認時ボットがあるので、まずは申請時ボットをクリックする
  4. eventの次にあるNew Stepをクリックする
  5. call a scriptがセットされてるので、下の方にある「Apps Script Project」のファイルアイコンをクリックする
  6. Pickerで選択画面になるので、自身が作ったスタンドアローンスクリプトを指定する
  7. Function Nameはapproveman(id)でParametersはidに対してAppSheet側の「ID列」を指定してる状態にあります。
  8. 同じく承認時ボットも5.のように変更をかけます。
  9. こちら側はFunction Nameはapproveman2(id)となっていますので要注意。

これで申請時および承認時に発火するGASへの参照を変更することが出来ました。

図:参照するGASファイルを変更しましょう

開発を行う

今回は申請者もそして「承認者」も個人が作業を行うことが前提です。しかし、承認者が例えば「総務部」というような部署の場合、今回の仕組みでは「自分が承認すべき申請」が個別アドレス宛であるため出てこないです。

ゆえに部署メンバー複数名が承認担当といった事例の場合は、スライスで工夫が必要になる点と、その為のGASでのハンドリングを追加してあげる必要性があるので注意が必要です。(スライスの工夫は複数アドレスをカンマ区切りで入れておき、IN関数で自身のアドレスが含まれてるかどうか判定するなどが該当)。

AppSheet側

今回の開発は過去に作成してきた安否確認システムやタイムレコーダアプリでのAppSheetロジックを組み合わせて実現しています。新しい要素的なものは申請中の編集可否のハンドリングといった要素くらいですが、ソレ以外にもちょっとしたロジックを今回は採用しています。

ここではポイントなる点について解説しています。

ダミーの入力欄テクニック

今回のアプリの壁の1つだったのが各々の承認者の承認データを入れる場合に、サブテーブルは用意するのだけれどそのままだと1レコードの中で実現してるので前の人の承認結果とコメントが見えてしまう問題。

AppSheetは融通が効かない部分があるのでこういったケースに於いてダミーの入力欄を設けて、GASでそれをサブテーブルに転記して、ダミー欄の内容をクリアすることで次の承認者は空っぽの状態で入力出来るようにすることが可能です。

また、AppSheetはテーブルとしては「承認欄はRequireで必須」にすることで入力必須項目とすることは出来ますが、同じテーブルで申請のビューも作っているので申請時に承認欄入れないと登録できないのでは?と困る事になります(申請時は当然この欄は使わない為)。

実はAppSheetは対象の列をRequireとしていても、ビュー上でその列を予め使わないのだから削除しておく事で、Requireな列を空っぽのままでもレコード登録が出来ます。逆に承認側はこの列を表示するので承認・差し戻しを入れないと「承認実行出来ない」ようにハンドリングすることが可能です。

図:Requireを使ったテクニック

承認履歴のアクションは非表示に

申請者は申請後のデータは承認履歴が見えるので現在の承認状況を把握することが出来るように作っています。しかし、その承認履歴に対してデフォルトだと「データが入れられる状態になってる」ので、これは非常にマズイです。

これはビューでRelatedでリレーションでサブテーブルを表示させた場合デフォルトだと「Addというボタン」が表示されている為。これはアクションで表示してるデフォルトのボタンなので、アクションから該当のボタンのPositionの値を「Hide」にすることで消すことが可能です。

Viewボタンのみは残りますが見られるだけなのでこれで申請者が承認履歴テーブルにレコード追加できちゃうといった問題を解消することが可能です。

図:hideにしてaddボタンを消しておく

AppSheetでドロップダウンとリレーションシップを装備する

SAVE時のイベント処理

申請側および承認側それぞれ同一のテーブルに対して読み書きをしてるので、そのままGASのAutomationを構築しようとすると管理者側が承認作業したのに、申請側のGASが発動みたいなオカシナことが起きてしまいます。これはタイムレコーダの申請フォームでも使ったテクニックですが、このためにスプシの申請シートには「UserTrigger」と「AdminTrigger」の2つのフィールドを用意しています。

そして、以下のロジックを構築します。

  • Actionにて申請時および承認時にそれぞれでUserTriggerもしくはAdminTriggerの列にUNIQUEID関数で値を入れる処理を作る
  • Actionにて申請時だけはステータス欄に「申請中」という文字を入れるものも追加してあります。
  • 各申請および承認における「申請用_Form / 承認用_Form」を開いて、Event Actionでは上記で作成したActionをセットする
  • これでSAVEクリック時に各Trigger列に値がランダム生成される
  • AutomationのGAS側では申請側はAddとUpdateで発火するように、承認側はUpdateのみで発火するようにData change typeを指定する
  • Conditionに於いては申請側は以下の数式を仕掛ける
    [_THISROW_BEFORE].[UserTrigger] <> [_THISROW_AFTER].[UserTrigger]
  • おなじく承認側はは以下のように数式を仕掛ける
    [_THISROW_BEFORE].[AdminTrigger] <> [_THISROW_AFTER].[AdminTrigger]
  • これでそれぞれのアクション時の発火条件を制御できるので申請時に承認側が発火するのを防ぐことが可能です。
  • それぞれのAutomationでは後述の申請時および承認時のGASが発火するようにステップを追加します。

図:event actionにアクションをセットする

図:conditionでTrigger列を見て発火を制御してる

申請ビューの編集ボタン表示切り替え

申請側のビューでは申請時は通常通り作成画面がでますが、申請後は「編集できないようにロック」するために再編集のボタンを非表示にしています。しかしこのままでは「差し戻し時」にも編集しようにも編集出来ません。また、同一のテーブルを元に2つのビューを作ってる関係で、このロックは承認側での編集までこのままではロックされてしまうのです。

よって、これを制御するためにEditボタンについて以下の2つの条件で表示切替を制御させます。

  • 申請ビューの場合、ステータスが「申請中と完了」の場合は表示しない(差し戻しの場合は表示する)
  • 申請_Detailのビューに於いても同様の処置をする
  • 承認用および承認用_Detailビューの場合は常に表示するようにする
  • デフォルトでは表示しない

これはActionに於いて申請テーブルにあるEditを開き、BehaviorにあるOnly if this condition is trueに対して数式を入れることで表示切替をハンドリングすることが可能です。数式としてはSwitch関数を使って判定にContext関数を使い、各ビューのときの挙動をそれぞれ既定して表示・非表示を制御しています。

SWITCH(
  CONTEXT("VIEW"), 
  "申請_Detail", if(IN([ステータス], {"申請中", "完了"}),false,true),
  "申請", if(IN([ステータス], {"申請中", "完了"}),false,true),
  "承認用", true,
  "承認用_Detail", true,
  false
)

図:ステータスによって表示切り替え

図:表示切替は関数で実現する

管理者パネルの表示切り替え

管理者のロールを持つユーザだけ「承認」パネルを表示するようにしています。申請しかできない一般ユーザには不要であるため。これを行うにはビュー上でshow ifを使って表示制御を行います。

AppSheetにuserシートをテーブルとして追加してるので、自身のメアドに基づくロールを取ってきてshowifの判定に使っています。数式としては以下の通り。

if(LOOKUP(USEREMAIL(),"user","mail","role")="管理者",true,false)

図:showifでロールを見て表示切り替え

自分のものだけ表示するスライス

これはこれまでのアプリでも実装してきたものですが、申請一覧に自分以外の人の申請が見えたらマズイです。また、承認側でも自分以外の人宛の申請が見えたらマズイです。よって申請および承認ビューは元のテーブルのスライスを作ってから、Row filter conditionで制御をしています。

承認側のフィルタの数式の事例は以下の通り。

[次承認メアド] = USEREMAIL()

こうすることで、自分宛のものだけが表示されるようになります。対象フローの承認者が全員見られるようにするには、1つのセルに複数値入れておいてIN関数などで判定させて実装するといった改造が必要になります。

図:承認側のフィルタ

Googleスプレッドシートのドロップダウンで複数値選択

GAS側

同時に申請や承認が発生する可能性があるため、今回のプログラムでは排他制御を取り入れています。順番に入ってきた処理が終わるまで次の処理は待機することにあるので、場合によっては処理に時間が掛かる可能性があります。最大30秒間のウェイトロックは処理が完了後にすぐに解除されますので、実際には30秒も掛からず終わります。

排他制御でGoogle Apps Scriptを安全に実行【GAS】

申請側コード

新規申請時および差し戻し後の再申請に対応するコードです。主に行ってる処理は以下の通り。

  • 最初の承認者のメアドを承認パターンから探索して入れます。
  • 差し戻し時に備えて承認履歴に於いて過去の承認履歴データをここで一括削除をしています。
  • 新しく承認履歴シートに全承認者分の入力欄を作成しています。
  • 最初の承認者に対して、申請が来た旨のメール通知を行っています。

但し、GASでのメールは1日2000通までのQuotaがあるため、各フェーズでのメール通知や利用するメンバー数によっては、1日の上限に達する可能性があります。その場合はAppSheet側でreturnを受け取った後にそちらでNotificationを利用して通知を送ると良いでしょう。

//読み書き先スプレッドシート
var ssid = "ここに読み書きするスプシのIDを入れます";

//申請する処理
function approveman(id) {
  //スプレッドシートを各種取得する
  let sheet = SpreadsheetApp.openById(ssid);
  let sinsei = sheet.getSheetByName("申請").getRange("A2:H").getValues();
  let approveptn = sheet.getSheetByName("承認パターン").getRange("A2:F").getValues();
  let history = sheet.getSheetByName("承認履歴");
  let historyman = history.getRange("A2:E").getValues();
  let userlist = sheet.getSheetByName("user").getRange("A2:C").getValues();

  //排他制御開始
  let lock = LockService.getPublicLock();  

  try{
    //30秒間のロックを取得する
    lock.waitLock(30000);

    //idを元に対象レコードを見つける
    let ptnid = "";
    let matchrec = "";
    let mailman = "";
    for(let i = 0;i<sinsei.length;i++){
      //レコードを一個取り出す
      let rec = sinsei[i];

      //idが一致するレコードがあった場合
      if(rec[0] == id){
        mailman = rec[1];

        for(let j = 0;j<userlist.length;j++){
          //レコードを一個取り出す
          let rec2 = userlist[j];

          //メアドが一致するか?
          if(rec2[0] == mailman){
            //patternidを取得する
            ptnid = rec2[1];
          }
        }

        //matchrecに該当レコードの場所を入れる
        matchrec = i + 2;
        break;
      }
    }

    //patternidを元に承認パターンから最初のメアドを取得する
    let firstapprove = "";
    let approverec = "";
    let maxrec = "";
    for(let i = 0;i<approveptn.length;i++){
      //レコードを一個取り出す
      let rec = approveptn[i];

      //patternidが一致するレコードがあった場合
      if(rec[0] == ptnid){
        //最初の承認者のメアドを取得する
        firstapprove = rec[3];

        //承認者一覧レコードを取得する
        approverec = rec;

        //maxstepを取得する
        maxrec = rec[2];
      }
    }

    //取得した最初の承認者のメアドを次承認メアド等に書き込む
    let target = sheet.getSheetByName("申請")
    target.getRange("D" + matchrec).setValue(ptnid);
    target.getRange("F" + matchrec).setValue(maxrec);
    target.getRange("G" + matchrec).setValue(firstapprove);

    //差し戻しの再提出に備えて承認履歴に過去の残骸があったら削除する
    //最終行を取得する
    let lastRow = history.getLastRow() - 2;
    for(let i = lastRow; i >= 1; i--) {
      //レコードを一個取り出す
      let rec = historyman[i];
  
      //チェック対象かどうか?
      if(rec[1] == id){
        //行を削除する
        history.deleteRow(i + 2)
      }
    }
      
    //approverecを元に承認履歴を生成する
    for(let i = 3;i<approverec.length;i++){
      //空の場合はスルーする
      if(approverec[i] == "" || approverec[i] == undefined){
        continue;
      }

      //一時配列を作る
      let temparr = [
        Utilities.getUuid(),
        id,
        approverec[i],
        ""
      ]

      //承認履歴にレコードを追加する
      history.appendRow(temparr);
    }

    //最初の承認者に対してメール通知する
    //メール本文を構築する
    let body = "申請来たよ";
  
    //メールを送信する
    MailApp.sendEmail({
      to: firstapprove,
      subject: '新規ソフトウェア開発申請',
      name:"開発申請",
      body:body,
    });

    //処理を返す
    lock.releaseLock();
    return true;
  }catch(e){
    return false;
  }
}

承認側コード

こちらは管理者側で承認した際に発火するコードです。承認状況に応じて処理が分岐するのでやや複雑です。また、承認履歴に記録するためにダミーの承認状況列やダミーのコメント列を使って入れたものを、承認履歴シートに代入しクリアするといったちょっとしたロジックを使っていたりするので、その仕組を理解する必要があります。

主に以下のような処理をしています。

  • 承認者が選択した承認・差し戻しのデータはダミーの列に入るので、これを承認履歴シートの該当の申請IDのレコードに記録し、ダミー列の値はクリアしています(次の承認者が入力するため)。
  • 現在のstepとmaxstepの数が一致してる場合最終承認者なので、承認されるとステータスは完了となり申請はクローズされます(次承認者アドレス欄はクリアされます)
  • 通常の承認ではstepの数が加算され次の承認者のメアドを取得し、次承認者アドレス欄に記入されます。
  • そして次承認者に対して、承認を促すようにメール通知を送っています。
  • 差し戻しをされた場合、stepは1にリセットされて次承認者アドレスはクリアされます。ステータスは差し戻しに変更されます。
  • また差し戻し時には申請者に対して差し戻しされた旨のメール通知が送られます(ここで申請者は再度申請を編集することが出来るようになります)。
  • 最後にダミーの承認状況およびコメント欄は必ずクリアされます。
//承認する処理
function approveman2(id) {
  //スプレッドシートを各種取得する
  let sheet = SpreadsheetApp.openById(ssid);
  let ss = sheet.getSheetByName("申請");
  let sinsei = ss.getRange("A2:I").getValues();
  let approveptn = sheet.getSheetByName("承認パターン").getRange("A2:F").getValues();
  let history = sheet.getSheetByName("承認履歴");
  let historyman = history.getRange("A2:E").getValues();
  let userlist = sheet.getSheetByName("user").getRange("A2:C").getValues();

  //各種変数
  let ptnid = "";
  let matchrec = "";    //申請の書き込み位置
  let mailman = "";
  let dumappro = "";    //ダミー承認結果
  let dumcomment = "";  //ダミーコメント
  let nowstep = "";     //step
  let appmail = "";     //次承認メアド

  //排他制御開始
  let lock = LockService.getPublicLock();  

  try{
    //30秒間のロックを取得する
    lock.waitLock(30000);

    //申請シートから情報を取得する
    for(let i = 0;i<sinsei.length;i++){
      //レコードを一個取り出す
      let rec = sinsei[i];
      console.log(rec)

      //idが一致するレコードがあった場合
      if(rec[0] == id){
        //申請者、承認結果、コメントを取得する
        mailman = rec[1];
        dumappro = rec[7];
        dumcomment = rec[8];
        console.log(dumcomment)
        
        nowstep = rec[4];
        maxstep = rec[5];
        appmail = rec[6];

        for(let j = 0;j<userlist.length;j++){
          //レコードを一個取り出す
          let rec2 = userlist[j];

          //メアドが一致するか?
          if(rec2[0] == mailman){
            //patternidを取得する
            ptnid = rec2[1];
          }
        }

        //matchrecに該当レコードの場所を入れる
        matchrec = i + 2;
        break;
      }
    }

    //承認履歴にデータを書き込む
    for(let i = 0;i<historyman.length;i++){
      //レコードを一個取り出す
      let rec = historyman[i];

      //idと一致するか?
      if(id == rec[1]){
        //メールアドレスが一致するか?
        if(appmail == rec[2]){
          //書き込み位置
          let cnt = i + 2;

          //書き込み
          history.getRange("D" + cnt).setValue(dumappro);
          history.getRange("E" + cnt).setValue(dumcomment);
          break;
        }
      }
    }

    //承認状況に応じて処理をする
    if(dumappro == "承認"){
      //maxstepに到達しているか?
      if(maxstep == nowstep){
        //ステータスを完了にする
        ss.getRange("L" + matchrec).setValue("完了");

        //次承認メアドを空にする
        ss.getRange("G" + matchrec).setValue("");

        //本人に差し戻し通知する
        let mailbody = "承認完了しました";
        MailApp.sendEmail({
          to: mailman,
          subject: '新規ソフトウェア開発申請:完了',
          name:"承認結果",
          body:mailbody,
        });

      }else{
        //stepを加算する
        nowstep = Number(nowstep) + 1;

        //ptnidを元に承認パターンを特定する
        for(let i = 0;i<approveptn.length;i++){
          //レコードを一個取り出す
          let rec = approveptn[i];

          //ptnidと一致するか?
          if(ptnid == rec[0]){
            //スタート位置を指定
            let start = 2 + nowstep;

            //次承認メアドを取得する
            let nextappro = rec[start];

            //stepと次承認メアドを書き込む
            ss.getRange("E" + matchrec).setValue(nowstep);
            ss.getRange("G" + matchrec).setValue(nextappro);

            //nextapproに対してメールで通知
            let body = "申請来たよ";
          
            //メールを送信する
            MailApp.sendEmail({
              to: nextappro,
              subject: '新規ソフトウェア開発申請',
              name:"開発申請",
              body:body,
            });

            break;
          }
        }
      }
    }else{
      //差し戻しされた場合の処理
      ss.getRange("E" + matchrec).setValue(1);    //承認ステップを1にリセット
      ss.getRange("G" + matchrec).setValue("");   //次承認メアドを空にする

      //ステータスを差し戻しにする
      ss.getRange("L" + matchrec).setValue("差し戻し");

      //本人に差し戻し通知する
      let body2 = "差し戻しされました。";
      MailApp.sendEmail({
        to: mailman,
        subject: '新規ソフトウェア開発申請:差し戻し',
        name:"承認結果",
        body:body2,
      });
    }

    //ダミー承認とダミーコメント,次承認メアドを空にする
    ss.getRange("H" + matchrec).setValue("");
    ss.getRange("I" + matchrec).setValue("");
    
    //処理を返す
    lock.releaseLock();
    return true;
  }catch(e){
    lock.releaseLock();
    return false;
  }
}

直接開くURLを生成する

今回のサンプルには含めていませんが、申請者や承認者に通知するメールの本文に対象アプリの対象ビューの対象レコードを直接開くURLを含めておくとユーザの手間が大幅に減り、利便性が向上します。主な利点は、

  • スマフォのAppSheetアプリを開いてる場合にアプリの切り替えが不要です(アカウントの切り替えまでは出来ない)
  • 開いた後に対象のビューを開く手間が削減出来ます。
  • さらに対象の申請レコードを開く手間を削減出来ます。

このような作業を実現するURLの構築ですが以下のようなURLを構築します。

https://www.appsheet.com/start/アプリのID#view=開くビューの名前&row=レコードのキー列の値
  • アプリのIDは共有時のOpen Browser Linkで取得が出来ます。
  • 開くビューは例えば「承認者_Detail」といったビューの名前を指定します。
  • レコードのキー列の値ですが、通常はID列をKeyにしてると思います。ここは今回の事例ならばUNIQUEID()で生成していますが、この値を当てはめます。

使ってみる

ここでは実際のアプリの使い方の説明になります。基本スマートフォンでの操作を元に説明をしています。

申請者側

申請してみる

ユーザロールだけがついたユーザはアプリを開くと申請パネルだけが表示されます。管理者の場合はこの他に承認パネルが表示されるように仕組みが備わっています。申請する手順は以下の通りです。

  1. 右下の+ボタンをクリックする
  2. 申請者のアドレスは自動で入力されます。
  3. 申請内容に適当な文字入力をします。
  4. SAVEをクリックすると承認ルートに従って最初の承認者に通知が飛び、申請中ステータスになります

図:申請の作成画面

申請中の様子

申請を行うと対象のエントリーはユーザは削除は出来ますが、再編集は「管理者により差し戻しされるまで編集不可」になります。そして、開いてみると、以下のような情報を常に確認出来ます。

  • ステータス:現在の申請がどのような状況なのか?最後の承認者まで承認されると「完了」となり、差し戻されると「差し戻し」となります。
  • 次の承認者:現在承認作業中となる次のレベルの承認者のメアドがわかります。
  • 承認履歴:この申請に対する各承認者の承認状況とコメントを見ることが出来ます。

現在の進行状況を確認できるのはワークフローとしては必要な機能なので、それをサブテーブルである承認履歴テーブルを使って実現しています。

図:ワークフロー的機能があります

承認者側

承認を実行する

userに於いて管理者のロールを持ってるユーザには「承認パネル」が表示されます。この承認パネルには「次承認者メアド」で指定されてるものと自分のメアドが一致したものだけが表示される仕組みになっています。よって、ここに表示されてるということは?自分が承認するフェーズにあるタスクということになります。

承認方法は以下の通り

  1. 対象の申請をクリックする
  2. 右下の編集ボタンをクリックする
  3. 申請内容とステータス、承認パネルとコメント欄が出てくる。
  4. 承認パネルで、承認 or 差し戻しをクリックする(選択必須項目です)
  5. コメント欄には承認時のコメントを入れる
  6. SAVEをクリックするとその時点での状況に応じて後ろで処理が行われます。

図:管理者の承認画面

注意点

今回のワークフローアプリでは前述にもあるように「自分が承認フェーズにあるタスクだけが見える」ので、承認実行すると申請は管理者からは見えなくなり、次の承認者は見えるようになるという仕組みになっています。例えば以下のようなケースに対応するには改造する必要があります。

  • 申請側のように承認後も編集できない形で見えるように残しておく場合は同じロジックを装備する必要があります。
  • また自分に承認フェーズがあるものを承認できるので、前承認者をすっ飛ばして承認出来るようにしたい場合には工夫が必要です。
  • またその場合、自分の承認フェーズじゃないけれど、対象の申請が見えるようにしてほしいといったケース。
  • 差し戻しの場合、前承認者に差し戻されるのではなく申請者のレベルにまで差し戻しされますので要注意。

他にも細かい承認にあたっての承認後の申請書ファイルの作成であったり、色々な希望を実現する為の最低限のテンプレであるため現場で実際に使うためには色々と改造が必要になります。

関連リンク

コメントを残す

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

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