鍋綿ブログ

C#・SharePoint・SharePoint Framework・Office365を中心に扱うブログです。

SharePoint + Flowで承認フロー付きの申請フォームを作ってみた

SharePointのモダン機能もMicrosoft Flowも機能が拡充されて小慣れて来ましたね。
ワークフロー付きの申請フォームを今の機能でどこまで実現できるかやってみました。

 

 

1.やりたいこと

今回は以下を目指しました。

  1. 申請者は、申請情報の入力 + 申請書の添付 を行うことができる。
  2. 申請者は、承認中に申請情報や申請書を変更できない。
  3. 承認者は、メールで申請を受け取り、メールから承認ができる。
  4. 承認者は、メールを見失ってしまっても画面から承認ができる。
  5. シャレオツ(笑

2.実現した機能の紹介

2-1.申請一覧画面

まずは一覧画面をドーンと貼ろう。

f:id:micknabewata:20181222143215p:plain

申請一覧画面

SharePointっぽくないデザインになっていますが、これはリストのビューに「ビューの書式設定」を適用した結果です。
このリストには複数のユーザーが申請情報を登録する想定になっており、
ビューのフィルタ機能で自分に関連の無い申請情報が表示されないように制御しています。
具体的には以下の情報が表示されます。

  • 登録者が自分である(「編集」「承認依頼」「送信した承認依頼」ボタンが表示される)
  • 承認者が自分である(「受信した承認依頼」ボタンが表示される)

更に、承認状態によってボタンの表示/非表示を制御していますので、承認中に情報を編集されたり、2重で承認依頼が行われることがありません。

 

申請に必要な操作はこの画面から一通り行うことができます。

  • 新規の申請は、画面上部の「新規」ボタンで行う
  • 申請情報を変更したければ、各行の「編集」ボタンで行う
  • 承認依頼は、各行の「承認依頼」ボタンで行う

各画面のキャプチャを貼っておきます。

f:id:micknabewata:20181222144011p:plain

新規登録・編集画面

新規登録画面では、最低限の申請情報を入力します。
全情報を入力しても良いのですが、
今回は「申請書の回覧」というコンセプトを試しているので
申請書に書いてある情報はここで入力しないようにしました。

新規登録した後、承認依頼を開始するまでの間は情報の編集が可能です。
(画面イメージは上記と新規登録画面と同じですので割愛します。)

f:id:micknabewata:20181222144522p:plain

承認依頼画面

ユーザーが「承認依頼」ボタンを押すと表示される承認依頼画面では、
承認者と希望する承認期限、添付ファイル(申請書)を入力するようにしました。
承認者はユーザーによる手動入力にしていますが、
何らかの計算処理で承認者を決定できるならば入力フィールドは不要ですね。

また、承認者もこの画面から承認操作を行うことができます。
承認操作は、「受信した承認依頼」ボタンを押した先の画面で行います。

f:id:micknabewata:20181224080037p:plain

受信した承認依頼画面

見て分かると思いますがこれはSharePointの画面ではなくFlowで用意されている画面です。
承認者が自分であり、申請または却下の操作が必要な申請の一覧が表示されています。
承認はこの画面で承認または却下の操作を行うことができます。
また、「再割り当て」ボタンで承認者を別のユーザーにすることも可能です。
所謂たらい回しですね。

ちなみに上記画像に写っている「送信済み」リンクは、
申請一覧画面から「送信したの承認依頼」ボタンを押した時のリンク先です。

f:id:micknabewata:20181224080716p:plain

送信した承認画面

こちらは自分が申請した承認依頼のうち、承認者の応答待ちである申請の一覧が表示される画面です。

2-2.承認依頼メール

ユーザーが承認依頼をすると、承認者にメールが送信されます。

f:id:micknabewata:20181224081518p:plain

承認依頼メール

承認者はメールから承認/却下が出来て便利です。
メールの内容はカスタマイズ可能ですが、
メールに添付ファイルを付けることができないので
基本的にはメール本文に記載のリンクを踏んで申請情報を確認してから
承認/却下を行うことになるでしょう。

メールが埋もれてしまったら、先ほどの「受信した承認依頼画面」から
承認操作を実施します。

3.どうやって作ったか

3-1.SharePointリスト

サイトの設定画面から「コンテンツ タイプ」をいくつか新規作成し、
リストの設定画面でリストに紐づけました。

f:id:micknabewata:20181224083022p:plain

サイト コンテンツ タイプ

ご覧のとおり、ステータス毎にコンテンツタイプを分けました。
経費申請を親とするコンテンツタイプはすべて同じ列を持っていますが、
ステータス別に編集を許可する列以外は隠し列に設定してあります。
(隠し列はアイテムの新規登録/編集/詳細画面に表示されません。)

f:id:micknabewata:20181224083623p:plain

経費申請(申請待ち)

ご覧の通り、「申請待ち」の状態(新規登録~ユーザーが承認依頼を開始するまで)では「タイトル」「経費種別」のみ入力可能としています。
他はシステム内部で使用したり、申請一覧画面で表示するだけの列です。

f:id:micknabewata:20181224083931p:plain

経費申請(1次承認中)

承認中は、申請情報を編集されては困ります。
よってすべての列を隠し列に設定しています。

3-2.SharePoint ビューの書式設定

以下のJSONを記述しました。

{
  "schema": "https://developer.microsoft.com/json-schemas/sp/view-formatting.schema.json",
  "debugMode": true,
  "hideSelection": true,
  "hideColumnHeader": true,
  "rowFormatter": {
    "elmType": "div",
    "attributes": {
      "class": "ms-borderColor-neutralLight"
    },
    "style": {
      "box-sizing": "border-box",
      "width": "100%",
      "border-width": "1px",
      "border-style": "solid",
      "padding": "0 0 0 20px",
      "margin-bottom": "10px",
      "align-items": "stretch"
    },
    "children": [
      {
        "elmType": "div",
        "style": {
          "flex": "1 0 300px",
          "display": "flex",
          "flex-wrap": "wrap"
        },
        "children": [
          {
            "elmType": "div",
            "style": {
              "flex": "1 0 300px",
              "box-sizing": "border-box",
              "padding": "10px"
            },
            "children": [
              {
                "elmType": "button",
                "attributes": {
                  "class": "ms-fontSize-xl"
                },
                "style": {
                  "line-height": "1.5em",
                  "margin-bottom": "1em",
                  "border": "0",
                  "padding": "0px",
                  "color": "#0077ff",
                  "background-color": "transparent",
                  "cursor": "pointer"
                },
                "txtContent": "[$Title]",
                "customRowAction": {
                  "action": "defaultClick"
                }
              },
              {
                "elmType": "div",
                "style": {
                  "display": "=if([$CostType], '', 'none')",
                  "line-height": "1.5em",
                  "margin-bottom": "8px"
                },
                "txtContent": "='経費種別 ' + [$CostType]"
              },
              {
                "elmType": "div",
                "style": {
                  "display": "=if([$RequestDate], '', 'none')",
                  "line-height": "1.5em",
                  "margin-bottom": "8px"
                },
                "txtContent": "='申請日 ' + toLocaleDateString([$RequestDate])"
              },
              {
                "elmType": "div",
                "style": {
                  "display": "=if([$RequestFileUri], '', 'none')",
                  "line-height": "1.5em",
                  "margin-bottom": "8px"
                },
                "children": [
                  {
                    "elmType": "a",
                    "txtContent": "=[$RequestFileUri.desc]",
                    "attributes": {
                      "href": "=[$RequestFileUri]"
                    },
                    "style": {
                      "font-size": "15px",
                      "font-weight": "bold"
                    }
                  }
                ]
              },
              {
                "elmType": "div",
                "attributes": {
                  "class": "='ms-fontSize-s ms-fontWeight-bold ' + if([$LimitDate] <= @now, 'ms-fontColor-redDark', 'ms-fontColor-neutralPrimary')"
                },
                "style": {
                  "display": "=if([$LimitDate], '', 'none')",
                  "line-height": "1.5em",
                  "margin-bottom": "8px"
                },
                "txtContent": "='承認期限 ' + toLocaleDateString([$LimitDate])"
              },
              {
                "elmType": "div",
                "attributes": {
                  "class": "ms-fontSize-s"
                },
                "style": {
                  "line-height": "1.5em",
                  "margin-bottom": "8px"
                },
                "txtContent": "='最終更新 ' + [$Editor.title] + ', ' + toLocaleString([$Modified])"
              }
            ]
          },
          {
            "elmType": "div",
            "style": {
              "flex": "0 0 170px",
              "display": "flex",
              "flex-direction": "column"
            },
            "children": [
              {
                "elmType": "button",
                "customRowAction": {
                  "action": "editProps"
                },
                "txtContent": "編集",
                "attributes": {
                  "class": "sp-row-button ms-bgColor-neutralLighter ms-fontWeight-semibold"
                },
                "style": {
                  "width": "145px",
                  "height": "32px",
                  "margin": "20px 0 10px 0",
                  "display": "=if(@me == [$Author.email] && ([$MyStatus] == '00.申請待ち' || [$MyStatus] == '90.却下' || [$MyStatus] == '91.取下'), '', 'none')"
                }
              },
              {
                "elmType": "button",
                "customRowAction": {
                  "action": "executeFlow",
                  "actionParams": "{\"id\": \"d28201ed-bf59-4fcf-b344-a6b63bf1a761\"}"
                },
                "txtContent": "承認依頼",
                "attributes": {
                  "class": "sp-row-button"
                },
                "style": {
                  "width": "145px",
                  "height": "32px",
                  "margin": "10px 0",
                  "display": "=if(@me == [$Author.email] && ([$MyStatus] == '00.申請待ち' || [$MyStatus] == '90.却下' || [$MyStatus] == '91.取下'), '', 'none')"
                }
              },
              {
                "elmType": "a",
                "txtContent": "受信した承認依頼",
                "attributes": {
                  "target": "_blank",
                  "href": "https://japan.flow.microsoft.com/manage/environments/Default-8709929d-9ae2-4d48-b76e-431a331f0e90/approvals/received",
                  "class": "sp-row-button ms-bgColor-neutralLighter ms-fontWeight-semibold"
                },
                "style": {
                  "width": "145px",
                  "height": "32px",
                  "line-height": "32px",
                  "margin": "20px 0 10px 0",
                  "text-align": "center",
                  "color": "black",
                  "display": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '90.却下' || [$MyStatus] == '91.取下' || [$MyStatus] == '20.承認', 'none', '')"
                }
              },
              {
                "elmType": "a",
                "txtContent": "送信した承認依頼",
                "attributes": {
                  "target": "_blank",
                  "href": "https://japan.flow.microsoft.com/manage/environments/Default-8709929d-9ae2-4d48-b76e-431a331f0e90/approvals/sent",
                  "class": "sp-row-button ms-bgColor-neutralLighter ms-fontWeight-semibold"
                },
                "style": {
                  "width": "145px",
                  "height": "32px",
                  "line-height": "32px",
                  "margin": "20px 0 10px 0",
                  "text-align": "center",
                  "color": "black",
                  "display": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '90.却下' || [$MyStatus] == '91.取下', '', 'none')"
                }
              }
            ]
          }
        ]
      },
      {
        "elmType": "div",
        "attributes": {
          "class": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '91.取下','ms-bgColor-yellow',if([$MyStatus] == '10.1次承認中' || [$MyStatus] == '11.2次承認中','ms-bgColor-neutralLighter',if([$MyStatus] == '20.承認','ms-bgColor-green','ms-bgColor-redDark'))) + ' ' + if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '91.取下', 'ms-borderColor-redDark', '')"
        },
        "style": {
          "flex": "0 0 173px",
          "display": "flex",
          "justify-content": "center",
          "align-items": "center",
          "border-top-width": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '91.取下', '5px', '0')",
          "border-top-style": "solid",
          "color": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '10.1次承認中' || [$MyStatus] == '11.2次承認中' || [$MyStatus] == '91.取下', 'black', 'white')"
        },
        "children": [
          {
            "elmType": "div",
            "style": {
              "text-align": "center"
            },
            "children": [
              {
                "elmType": "div",
                "attributes": {
                  "class": "ms-fontSize-l"
                },
                "style": {
                  "line-height": "1.5em"
                },
                "txtContent": "[$MyStatus]"
              },
              {
                "elmType": "div",
                "style": {
                  "font-size": "30px",
                  "line-height": "40px",
                  "color": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '10.1次承認中' || [$MyStatus] == '11.2次承認中' || [$MyStatus] == '91.取下', 'black', 'white')"
                },
                "attributes": {
                  "iconName": "=if([$MyStatus] == '00.申請待ち' || [$MyStatus] == '91.取下','Clock',if([$MyStatus] == '10.1次承認中' || [$MyStatus] == '11.2次承認中','Clock',if([$MyStatus] == '20.承認','StatusCircleCheckmark','StatusCircleErrorX')))"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

3-3.Flow

「選択したアイテムの場合」をトリガーにして構築しました。

4.構築のポイント

4-1.コンテンツ タイプを分けることでフィールドの編集可否を制御する

コンテンツ タイプを分けることで、
承認ステータス間で同じ情報を維持しつつ
フィールドの入力可否を制御することができました。

4-2.ビューの書式設定で余計なメニューを消し、かつデザイン性アップ

JSONを書くのがちょっと大変ですが、
公式サイトに情報もありますしGitHubにサンプルも上がっています。
難しい技術は必要なく、気合を入れれば誰でも作れると思います。

 

以上、参考になれば幸いです。