ElectronとMicrosoft365で座席表アプリを作ってみた

Google Workspaceと違い、Microsoft365の場合はウェブアプリを作る機能が無くAPIだけが提供されているので、利用するにはローカル側に土台が必要です。そこで今回、Node.js + Electron + Vue.js + VuetifyおよびMicrosoft Graph API、SVG画像を利用したフリーアドレス対応の座席表アプリを作成してみました。

前回のGoogle Apps ScriptでSVGを使ったイメージマップを操作の内容も踏まえて一通りの機能を装備しています。今回はそんなサンプルです。

※2023年2月、Google Workspaceで動くバージョンとして移植しました。こちらはサーバーレスで動作し、より高機能です。

Google Apps ScriptでSVGを使ったイメージマップを操作 - 基礎編【GAS】

座席表アプリ for Google Workspace

今回使用するファイル等

座席表アプリはインストーラとなっており、マイドキュメント直下のzaseki365フォルダ内にインストールされるようになっています。インストーラ自体は、Inno Setupにて作成されています。

座席管理用のExcelファイルはすべてテーブル化されてる空のデータです。これをOneDrive Businessの特定の場所に配置して利用します。また、今回のアプリの座席の画像データはSVGで作成されており、作成にはInkscapeを利用して加工しています。サンプルのデータは含めていますが、実用するには、後述の事前準備にて、画像データを実際のレイアウト毎に用意したり、場合によってはコードを修正する必要もあります。

図:こんな感じのSVG画像を利用した仕組みです

また、Version 1.7よりTeamSpiritの打刻処理にFirefox Nightlyが使えるようになりました。以下のエントリーを参考に装備しています。

Node.jsとPlaywrightでFirefoxを自動操縦する

事前準備

管理者側の事前準備

Azureプロジェクトの作成

ユーザが利用する為のClient IDやSecretなどを用意する必要があります。以下の手順で用意します。

  1. アプリの登録にて登録を開始する
  2. アプリケーションの登録をクリックする
  3. 名前を入力、リダイレクトURIは「webを選択しhttp://localhost:9999/auth/callback」を入力(127.0.0.1はNG)。今回はPortを9999で指定してるので、替えたい場合はアプリのソースコード側も変更が必要です。
  4. 登録ボタンをクリックする
  5. 出てきた中で、「アプリケーション(クラと書かれているのがクライアントID」なので、このコードをメモしておく
  6. 左サイドバーより、「証明書とシークレット」をクリック
  7. 新しいクライアントシークレット」をクリックする
  8. 今回はとりあえず、6ヶ月で期限切れになるように選択する(最大24ヶ月)
  9. これで「」に「クライアントシークレット」が生成されて手に入りました。このシークレットはこの時だけしか表示されないので、注意してください。
  10. つづけて、左サイドバーより「APIのアクセス許可」をクリックする
  11. Microsoft APIの中にある「Microsoft Graph」をクリックする。
  12. 委任されたアクセス許可」をクリックする
  13. デフォルトでUser.ReadがすでにONなので、以下の項目をを検索してONにしましょう。
    Calendars.ReadWrite
    ChannelMessage.Send
    Chat.Read
    Chat.ReadWrite
    ChatMessage.Send
    email
    Files.ReadWrite
    offline_access
    openid
    Presence.ReadWrite
    profile
    Sites.ReadWrite.All
    User.Read
    User.ReadBasic.All
    

    今回は将来の拡張の為に多くの許可を追加していますが、減らしたい場合は、index.jsやapp.jsのソースコード内のscopeの中身を書き換える必要があります。上記のChannel, Chat, Sitesに関する項目は削除しても問題ありません

  14. アクセス許可の追加をクリックする
  15. 次に左サイドバーより「認証」をクリック
  16. 暗黙の付与にて、「アクセストークン」にチェックを入れる
  17. サポートされているアカウントの種類に於いては、「マルチテナント」にしておきました(企業内で使う場合にはその組織のテナントで良いでしょう)
  18. 保存をクリック
  19. 概要のエンドポイントをクリックすると、いろいろなエンドポイントURLが出る。
  20. 概要のディレクトリテナントの数値はメモっておきます。あとでプログラム中で使用します。)

これで、3つの必要なIDやSecretが用意できました。ユーザ側で登録⇒Azure認証を実行してAccess Tokenを取得するようにします。

図:アプリの登録から全ては始まります。

図:Graphを選択する

図:認証の設定変更に注意

ファイルのアクセス権限設定

ファイルに対してアクセス権限が必要ですが、読み書きのアクセス権限付与自体はアプリ側から行えます。アクセス権限を付与するという権限自体は、OneDrive上で行う必要があるので、以下の手順で行います。この作業はExcelファイルを開いての共有ではできないので注意!!

  1. OneDrive Business上のzaseki.xlsxの横にある「︙」をクリック
  2. アクセス許可の管理をクリック
  3. 直接アクセスの+ボタンをクリックする
  4. アクセス権限付与の権限を与えたい管理者のメアドを入れて、横の鉛筆マークをクリックし、編集可能を選択する
  5. アクセスを許可をクリック
  6. 再度、1.の手順と同じ作業をする
  7. アクセス許可を付与するリンクがでてくるので共有をクリック
  8. リンクのコピーは組織内に限定しましょう(リンクを知ってる組織内のユーザにしておく)
  9. 追加対象管理者ののユーザのアドレスを入れて送信

図:これは読み書きだけのアクセス権限

図:リンクのコピーの設定

SVG画像データの準備

今回のアプリのインストール先をクリックすると、プロジェクトも入っています。resources/app以下のsvgフォルダ内に2つのエリアのSVGファイルが入っています。ParkとMainの2つを用意してあり、この名称を変更する場合はソースコード内も修正が必要です(タブの分岐でその内容が記述されています)。

今回のパーツはシンプルに四角にテキストを1個追加してるだけのものです。このSVGファイル自体は、Inkscapeで作成後に各パーツに対して

  • gタグをくくるように、クリック時のイベントコードを追加。完成形は以下の通り。これで1座席分。seatinfo関数で座席位置を特定し、Excelを参照する仕組みになっています。
    <a href="javascript:void(0);" onclick="seatinfo($(this).children().attr('id'));" id="a23">
        <g id="seatA2" transform="matrix(0.84466019,0,0,0.88518024,17.616658,98.837363)">
            <rect 
                style="stroke-width:3" 
                id="A2" width="94.380722" 
                height="69.397591" 
                x="24.057831" 
                y="45.33976" 
                class="bar"
                fill="#afdde9" 
                stroke="blue" />
            <text xml:space="preserve"
                style="font-size:16.31940592px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';fill:#1a1a1a;stroke-width:0.0867171"
                x="26.065416" 
                y="89.968636" 
                id="textA2" 
                transform="scale(0.99324444,1.0068015)">
            <tspan sodipodi:role="line" 
                id="spanA2" 
                style="fill:#1a1a1a;stroke-width:0.0867171;font-size:16.31940592px"
                x="26.065416" 
                y="89.968636"> </tspan>
            </text>
        </g>
    </a>
  • gタグにIDを付与(Excelの座席ナンバーがコレ)
  • rectに対して固有のIDを付与(Excelのrecttagの値がコレ)
  • rectに対して、classとしてbarを付与
  • rectに対して、styleを削除しておき代わりにFillに色指定、strokeをblueに指定、stroke-widthに3を指定
  • 同じパーツの下のほうにあるtspanタグに対してIDを付与(Excelのtexttagの値がコレ)
  • tspanの間は半角スペース一個分入れておく(でないと保存時にtspanが消える可能性がある)

この内容に基づいて、Excelのzaseki.xlsxに於けるzasekiシートのデータの準備をします。誰にもキープさせたくない場合には、固定フラグをTRUEにして割当ユーザに固定席の名称を入れておけばOKです。

Excelファイルのルームは対象となるSVGファイルがPark側なのか?Main側なのかを示しています(検索時にタブ切り替えで利用しています)

また、上記の場合手打ちで修正していますが、Inkscape上でも以下の手順でID等を修正可能です(四角と外枠、テキストをグループ化してあります)。

  1. メニューから編集⇒XMLエディタを開く
  2. 対象の座席オブジェクトをクリックすると、対象のエレメントに飛ぶ
  3. 右サイドバーに内容が表示される
  4. どれがパーツのID、シートID、texttag, recttagなのかよく確認して、Excel Onlineのそれと同じ値を入れてあげる
  5. 入力したら保存する

レイヤーとオブジェクトからはIDなどの書き換えが出来ないので要注意。また、tspanだけは何故か、Inkscape上からIDが変更が反映されなかったので、このあたりのID割当はVSCodeなどで手動で行ったほうが良いかもしれません。

図:パーツ情報をInkscape上から修正する

ExcelのUserlistを用意

すでにキープされている人の座席をクリック時にはGraph APIからプレゼンス情報の他にExcelファイルからも追加のデータを表示するようにしています。アプリのマスタ画面より、社員マスタから登録するとuserlistシートにデータが登録されますので、管理者の人はユーザの情報を登録しておきましょう。部署名やメアド(ソレに基づくM365上のIDが自動的に書き込まれます)等など

この作業は、管理者もAzure認証後にマスタ設定を開いてユーザの追加から行います。ファイルに対して直接書き込みで追加したりはしません。また、この登録作業をすることで、ファイルに対するアクセス権限も追加されますので、必ず全ユーザ分行いましょう(アクセス権限付与の権限がない人はユーザ追加は出来ません)

図:ユーザの追加画面

ExcelファイルへのURLの準備

OneDriveに配置したzaseki.xlsxのファイルのIDおよびオーナーである管理者自身のユーザIDを取得し、アクセスさせる為のURLを構築する必要があります。アクセスに利用するURLは以下の通りです。これをユーザ側に渡して設定に入れてもらう必要があります。

//ExcelのファイルへのURL
https://graph.microsoft.com/v1.0/users/オーナーのユーザID/drive/items/ファイルのID/workbook/

以下の手順にてGraph Explorerを使って調べます。

  1. Graph Explorerの右上をクリックして、Microsoft365のアカウントでログインしておく
  2. OneDriveにて事前にファイルは適当な場所に配置しておく。ファイル名はzaseki.xlsxとします。
  3. 検索窓にて、「https://graph.microsoft.com/v1.0/me/drive/root/search(q='zaseki.xlsx')」を入力する
  4. アクセス許可の修正をクリックして、同意をクリックしておく。管理者権限は不要です。
  5. クエリの実行をクリック
  6. 下の応答のプレビューに色々出てくる。この中でnameがzaseki.xlsxに関する情報を見つける
  7. idがファイルのIDになるので控えておく。
  8. 続けて検索窓にて、「https://graph.microsoft.com/v1.0/me」を入力して、クエリの実行をクリック
  9. idがオーナーのユーザのIDになるので控えておく

あとは上記のURLのオーナーのユーザIDとファイルのIDにそれぞれの値を割り当ててURLを構築し、ユーザ側に登録してもらいます。

図:情報を調べる必要がある

座席の自動開放処理

Power Automateの仕組みを利用する事で、毎日0時に座席および在宅勤務登録のデータを自動でクリアする事が可能です。この設定をしておくことで、座席の開放忘れをしたとしても、翌日には普通に座席のキープが可能になります。わざわざ手動で開放する必要もなくなりますが、基本は帰宅時には退勤処理をクリックして開放するようにしましょう。

上記の自動データクリア用のPower Automateをダウンロードしたのち、以下の手順でインポート、書き込み先などを修正します。

  1. Power Automateを開く
  2. 上記にある「インポート」をクリック
  3. アップロードボタンをクリックして、ZIPファイルを指定する
  4. インポートオプションは「新しく作成する」を選択する
  5. 関連リソースは自身のアカウントを選択し保存をクリックします
  6. インポートボタンが押せるようになるので、クリックする
  7. 完了してマイフローを開いても何故か出てこないので、ページをリロードする
  8. 対象のフローは無効化された状態になってるので、編集をクリックする
  9. 色々手直しをして保存する(Excelファイルのパスやアカウント、実行する時間など)
  10. フローチェッカーをクリックして、このフローをオンにするをクリックする

このフローによって、毎日0時に自動的にzasekiおよびremoteのシートをクリアします。zasekiについては固定フラグがFalseのものだけが処理対象になるので、固定席はクリアされることはありません。在宅勤務登録もクリアされます。

Recurrenceの値をいじれば、実行する時刻を変更する事が可能です。プレミアムコネクタは利用していないので、フリーのアカウントで利用が可能です。

Power Automateのスケジュール済みクラウドフローを使う

図:2つのシートをクリアする自動化処理

図:2つのExcelフローは手直しが必要

エラー送信先の指定

現在まだ、エラーを検知した時のエラー送信先の指定が空になっています。この機能は、エラーを検知するとエラーメッセージをファイルにし、その内容を指定のURL先にPOSTで送信して送りつける機能で、自分の場合はPower Automateにて構築したエラー受信用のフローにて受け取り、OneDriveに内容を保存するようにしています。

その内容は以下の通り。将来的にこのURLを設定から保存できるようにする予定です。

Electronでクラッシュ検知してログを送信する

ユーザ側の事前準備

アプリの設定

アプリのインストール後まずは、右上のボタンから「アプリの設定」をクリックし、前述のAzureプロジェクトのClientID類の登録やExcelファイルへのURL、社内で利用する場合にはプロキシーサーバのアドレスを登録します。

  1. ユーザ設定にて、社員番号・自分の氏名・所属ユニット・自身のメールアドレスを登録
  2. プロキシーサーバを利用してる場合は通過する為に必要なので登録しておく(例:http://hogehoge.com:1234)
  3. Azure認証設定には、テナントID、クライアントID、クライアントシークレットを登録する
  4. Excel Online設定には前述で組み立てたExcelファイルへのURLを入力
  5. TeamSpirit打刻を利用する場合は、TSログイン設定にログインURL、ユーザID、パスワードを入れる
  6. TeamSpirit打刻にChromeではなく、Firefoxを利用する場合は、TS自動化オプションにて、Firefoxを入手をクリック⇒管理者権限を求められたらキャンセル⇒インストールを行う
  7. なおかつ、Firefoxで操縦するにチェックを入れる(Chromeの場合はチェックを入れない)
  8. 設定保存をクリックしたら、一旦アプリを終了しもう一度アプリを再起動する

図:設定を入力して保存、再起動する

図:Firefoxで操縦する場合の設定

Azure認証の実行

再起動させたら、次にアプリの右上にあるAzure認証をクリックして、Microsoft365アカウントの認証作業を行います。取得したAccess Tokenは暗号化されて保存されます。

  1. サインインをクリック
  2. Microsoft365アカウントとパスワードを入力して認証を実行する
  3. 最初の1回だけ要求されてるアクセス許可の画面が出るので、組織の代理として同意するにチェックを入れる
  4. 承諾をクリックする
  5. アクセストークンを取得したら完了。
  6. アクセストークンをクリアしたり認証し直したい場合は、サインアウトを実行します。
  7. アプリを再起動します。

再起動が完了すると、Excel Onlineからシートデータやユーザのデータ、在宅勤務してる人のデータが取得されて表示されます。

図:サインインを実行する

図:アクセス許可の付与をする

カレンダーの共有

同じ組織内でカレンダー情報を共有しておくと、他人が座席をクリック時にその人の当面のスケジュールが出てくるようになっています。共有手順は以下の通りです(ウェブのOutloook Businessで作業する事例)

  1. Outlook Businessを開く
  2. 予定表を開く
  3. 個人の予定表にある「予定表」という自分のカレンダーを見つけて、その横にある「︙」をクリック
  4. 共有とアクセス許可をクリック
  5. 所属組織内の人について、「タイトルと場所を閲覧可能」に変更する
  6. 閉じる

これで、後述のユーザの詳細情報にカレンダー情報が第三者に対して表示されるようになります。こちらについても、Graph APIで変更が出来るので、右上のメニューボタンから「カレンダー許可」をクリックすると、共有しない設定だった場合は「タイトルと場所を閲覧可能」にし、「タイトルと場所を閲覧可能」の場合は共有しないに設定されます。これはデフォルトの予定表のみに行います。

図:これがないとスケジュールが相手にわからない

図:ボタン一つで所属組織内での公開設定を変更できる

使い方

座席のキープ

座りたい席をクリックし、ダイアログが出てくるので、「キープ」をクリックするだけ。Excelの対象のレコードにユーザ名と社員番号が書き込まれてキープされます。すでに取得されてる場合は、ダブルブッキング防止用の処理が入ってるので、他の席を探す必要があります。

この処理の他に、Teamsのプレゼンス情報(離席中や忙しいなどのアレ)が、自動で「連絡可能」に変更されます。また、Outlookカレンダーにも座席取得したというイベントが登録されます(定時の18:00までの予定で自動登録)

※自分の場合さらに、ここから勤怠アプリの自動打刻の処理を加えたり、Teamsの特定チャンネルに出勤した旨のメッセージの自動投稿などを加えています。キープ一個でいっぺんに片付ける事で楽になります。

カレンダーは同一日付内の場合、先に登録してあったイベントを削除してから登録し直します。

図:座席をキープはこれだけ

図:TeamsとCalendarにも登録変更処理

座席のリリース

アプリの右上の退勤処理をクリックすることで席を開放します。帰宅時にだけ実行します。ダイアログが出てくるので、リリースをクリックするとExcel Onlineのレコードから社員名と社員番号を消去して、他の人が取得出来るようになります。前述の管理者設定にてPower Automateによる自動消去を入れてる場合は、開放を忘れても毎日0時に自動的に開放するようになります。

また、リリース時に自動的にTeamsのプレゼンス情報を「オフライン」に変更します(カレンダーの登録はそのままです)。

※自分の場合さらに、ここから勤怠アプリの自動退勤打刻処理などを加えています。

図:座席のリリース処理

在宅勤務登録

在宅勤務をする人は「在宅勤務」タブをクリックすると、右上に「在宅登録」のボタンが出てくるので、これをクリック。YESをクリックすることでExcelのremoteシートに情報が登録されて、座席キープ時と同じくTeamsのプレゼンスを連絡可能にし、Calendarにも登録がされます。また、在宅と座席のダブルブッキングは出来ないように制限が掛けられています。

退勤時は座席の場合と同様に退勤処理を行えば、在宅勤務登録が削除されてクリアされます。

図:在宅勤務一覧に登録されます

出勤者の検索

座席および在宅勤務者の情報を串刺し検索して、出勤状況を一覧で表示⇒さらにその場所を表示する機能を搭載しています。

  1. 右上の「検索」をクリックして、氏名や社員番号を入力検索を実行
  2. 見つかった情報の一覧が出てきます。
  3. 座席の場合はジャンプをクリックすると、その座席の縁取りが赤く表示されて、対象のタブがアクティブになります。
  4. 検索窓を空にして検索を実行すると、検索状態が解除されます。

図:検索結果が一覧表示される

図:赤枠が座ってる場所

ユーザの詳細情報

すでに登録のある座席のクリックや、在宅勤務のアクションのアイコンをクリックすると、そのユーザの詳細データが表示されます。事前にマスタ編集の社員マスタに情報を登録し、userlistシートに書き込まれてる必要性があります。

対象者のプロファイル画像、メールアドレスそして、共有されてる場合カレンダーの情報が表示されるようになっています。

図:対象者の画像やメアド、1日のカレンダー情報が表示されます

TeamSpirit勤怠打刻

設定に於いてTeamSpiritのログインURLを入力保存してる場合、IDとパスワードを使い、Chrome/Firefoxを自動操縦して、打刻処理を行う事が可能です。座席の登録/リリース時および在宅勤務登録/勤務終了時にそれぞれ発動するようになっています。

Firefoxで操縦する場合は別途ユーザのAppDataディレクトリ以下にFirefoxがインストールされており、また一度起動しておいて、ユーザプロファイルが作成されてる必要があるので、Firefox Nightlyをインストールしたら、一度手動で起動しておきましょう。

図:この部分をクリックして打刻します

図:ログインにFirefox自動操縦で処理中の様子

ソースコード解説

今回のアプリは、SVGの操作だけじゃなく、Microsoft Graph APIを利用してTeamsプレゼンスの変更、プロファイル画像の取得、カレンダーの登録Excel Onlineファイルのレコードの追加・削除・更新の処理を装備しています。以下のエントリーでも記述していますが、公式リファレンスが間違っていたり情報が不足していたりして、作成するのに非常に苦労したので、ここで該当の部分の解説を残しておきます。

VBAからGraph APIでExcel Onlineを読み書きしてみた - 実装編

プロファイル画像を取得する

Microsoft365のユーザのプロファイル画像は、ユーザが自由に変更する事が可能です。この情報はバイナリデータとしてMicrosoft365に保存されており、アプリでこれを取得して表示するにはバイナリデータをjpg⇒Base64に変換して表示させるロジックが必要です。

//tokenを格納する
token = g_token[1];

//送信用エンドポイントを構築する
var endpoint = "https://graph.microsoft.com/beta/users/" + uid + "/photos/360x360/$value";

//リクエストヘッダ
var headers = {
    "Authorization": "Bearer " + token,
    //"Content-Type":"image/jpg",
    encoding: null
}

//リクエストオプション
var options = {
    method: 'GET',
    agent: agent,
    headers: headers
}

//ステータスコード用
var status

//URLリクエスト
fetch(endpoint, options)
.then((res) => {
    //ステータスコードを取得
    status = res.status;

    //body部分を取得
    return res;
})
.then((response) => {
    let contentType =response.headers.get('content-type')

    //画像を生成する
    let stream  = fs.createWriteStream('./temp/test.jpg')
    response.body.pipe(stream)

    return "test"
})
.then((ret) => {
    if (status == 200) {
        //ファイルのパスを指定
        let jpgfile = path.join(__dirname, '/temp/test.jpg')

        //ファイルを読み込む
        fs.readFile(jpgfile, 'base64', function(err, data) {
            var imageType = 'image/jpeg';
            var imageSrc = `data:${imageType};base64,${data}`;

             //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", imageSrc]);
                return;
            }, 500);
        });
    } else {
        var message = "エラー:" + status + "\n" + ret.error.message;

        //メッセージを返す
        //次の送信のためにウェイトを入れる
        setTimeout(function () {
            callback(["NG", message]);
            return;
        }, 500);
        return;
    }
})
.catch((err) => {
    //エラーメッセージ
    callback(["NG", status + "エラー:\n" + err]);
    return;
});
  • uidは対象ユーザのMicrosoft365上のidです。予め取得しておき、渡す必要があります。
  • tokenはAccess Tokenです。リフレッシュした後に最新のトークンを渡す必要があります。
  • fs.createWriteStreamにて取得したバイナリデータをjpgのデータに変換しています。(temp以下に一時的にtest.jpgで生成しておく)
  • fs.readFileにて、base64にデータを変換しています。
  • エンドポイントの360x360が画像のサイズで最大、648x648で取得が可能です。

ユーザのMicrosoft365上のIDを取得する

数々のユーザに関する情報にアクセスする場合、メールアドレスではなく別途取得しておいたMicrosoft365のUIDを利用します。社員マスタに登録時に取得して記録しておきます(この値は不変なので1回取得しておけばオッケー)

//mailからo365uidを取得するメインルーチン
function o365uidgetter(access_token, mail, callback) {
    //送信用エンドポイントを構築する
    var endpoint = "https://graph.microsoft.com/v1.0/users/" + mail;
  
    //リクエストヘッダ
    var headers = {
      Authorization: "Bearer " + access_token,
    }
  
    //リクエストオプション
    var options = {
      method: 'GET',
      agent: agent,
      headers: headers
    }
  
    //ステータスコード用
    var status
  
    //URLリクエスト
    fetch(endpoint, options)
      .then((res) => {
        //ステータスコードを取得
        status = res.status;
  
        //body部分を取得
        return res.json()
      })
      .then((jsonData) => {
        //データのパース
        var ret = jsonData;
  
        if (status == 200) {  
          //UIDを返す
          //次の送信のためにウェイトを入れる
          setTimeout(function () {
            callback(["OK", ret.id]);
            return;
          }, 500);
        } else {
          if (status == 403) {
            var message = "エラー:" + status + "\n" + "そのメアドは存在しないか?情報にアクセスできません。";
          } else {
            //その他のエラー
            var message = "エラー:" + status + "\n" + ret.error.message;
          }
  
          //メッセージを返す
          //次の送信のためにウェイトを入れる
          setTimeout(function () {
            callback(["NG", message]);
            return;
          }, 500);
          return;
        }
      })
      .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
      });
}
  • リクエストには対象ユーザのメールアドレスが必要です

Teamsのプレゼンスの取得

対象ユーザの現在のプレゼンス情報を取得し表示する場合に使用します。

//tokenを格納する
token = g_token[1];

//送信用エンドポイントを構築
let endpoint = "https://graph.microsoft.com/beta/users/" + uid + "/presence"

//リクエストヘッダ
var headers = {
    "Authorization": "Bearer " + token
}

//リクエストオプション
var options = {
    method: 'GET',
    agent: agent,
    headers: headers
}

//ステータスコード用
var status

//URLリクエスト
fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res.json();
    })
    .then((ret) => {
        if (status == 200) {

            //availabilityとactivityを取得する
            let coloricon = "";
            let acttext = "";
            let avail = ret.availability;
            let active = ret.activity;

            //availで色アイコンを決定
            switch (avail) {
                case "Offline":
                case "PresenceUnknown":
                    coloricon = "🔘";
                    acttext = "オフライン";
                    break;
                case "Available":
                case "AvailableIdle":
                    coloricon = "🟢";
                    acttext = "応答可能";
                    break;
                case "DoNotDisturb":
                case "Busy":
                case "BusyIdle":
                    coloricon = "🔴";
                    acttext = "応答不可";
                    break;
                case "BeRightBack":
                    coloricon = "🟡";
                    acttext = "一時退席中";
                    break;
                case "Away":
                    coloricon = "🟡";
                    acttext = "退席中";
                    break;
            }

            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", coloricon + acttext]);
                return;
            }, 500);
        } else {
            var message = "エラー:" + status + "\n" + ret.error.message;

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
  • リクエストには対象ユーザのMicrosoft365上のUIDが必要です。
  • 返ってくる値のうち、availabilityに応じて内容を判定し、配列で呼び出し元に返しています。

Teamsのプレゼンスの変更

Teamsのプレゼンス情報は、委任されたアクセス許可の場合は、Betaの「setUserPreferredPresence」でなければ変更が出来ません。また、このプレゼンスは6種類あり、以下のような区分になっています。

※現在、APIからのプレゼンスメッセージの変更は未対応です。なぜか、Power Automateからだけは特殊な方法で変更が可能ですが。

availability activity Teams上表記
Available Available 連絡可能
Busy Busy 取り込み中
DoNotDisturb DoNotDisturb 応答不可
BeRightBack BeRightBack 一時離席中
Away Away 退席中
Offline OffWork オフライン
//プレゼンスを変更する
function setPresense(action,access_token,callback){
    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        'Content-Type': 'application/json'
    }

    //リクエストボディを作成する
    let tarr;
    let changemsg = "";
    if(action == "Available"){
        tarr = {
            "availability": action,
            "activity": action,
            "expirationDuration": "PT8H"
        }

        changemsg = "Teamsのプレゼンスを連絡可能に変更しました。";

    }else{
        tarr = {
            //"sessionId": store.get("clientid"),
            "availability": action,
            "activity": "OffWork",
            "expirationDuration": "PT8H"
        }

        changemsg = "Teamsのプレゼンスをオフラインに変更しました。";
    }

    //リクエストオプション
    var options = {
        method: 'POST',
        agent: agent,
        headers: headers,
        body: JSON.stringify(tarr),
    }

    //自分自身のUIDを取得
    let uid = store.get("my365uid");

    //エンドポイント作成
    //setUserPreferredPresenceに変更
    let prepoint = "https://graph.microsoft.com/beta/users/" + uid + "/presence/setUserPreferredPresence"

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(prepoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;
        //body部分を取得
        return res;
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 200) {

            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", changemsg]);
                return;
            }, 500);
        } else {
            if (status == 403) {
                var message = "エラー:" + status + "\nプレゼンス変更失敗";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • actionに前述のavailabilityの値やactivityの値を格納して渡す
  • expirationDurationは有効にしておく時間で、PT8Hで8時間有効になります。
  • リクエストエンドポイントはBetaである必要があります。
  • 基本、availabilityとofflineの2つだけを本プログラムでは利用しています(それぞれ出勤時・退勤時にセットします)

カレンダー情報を取得

対象ユーザのカレンダー情報の取得で利用しますが、相手が組織に対して情報を公開・共有していないと取得が出来ません。また、取得する情報はselectにて選択的に指定が可能なので、タイトル・開始時刻・終了時刻の3つを今回取得しています。

また、リクエストにはユーザのMicrosoft365上のUIDが必要になります。

//tokenを格納する
token = g_token[1];

//送信用エンドポイントを構築
let endpoint = "https://graph.microsoft.com/v1.0/users/" + uid + "/events?$select=subject,start,end";

//リクエストヘッダ
var headers = {
    "Authorization": "Bearer " + token
}

//リクエストオプション
var options = {
    method: 'GET',
    agent: agent,
    headers: headers
}

//ステータスコード用
var status

//URLリクエスト
fetch(endpoint, options)
.then((res) => {
    //ステータスコードを取得
    status = res.status;

    //body部分を取得
    return res.json();
})
.then((ret) => {
    if (status == 200) {
        //イベントデータをまとめる
        let body = ret.value;
        let barray = [];

        if(body.length == 0){
            //データがないのでそのまま返す
        }else{
            for(var i = 0;i<body.length;i++){
                //レコードを一個取り出す
                let rec = body[i];

                //一時配列を用意する
                let temparr = [];

                //データを追加する
                temparr.push(rec.subject);
                temparr.push(utc2tokyo(rec.start.dateTime,0));
                temparr.push(utc2tokyo(rec.end.dateTime,0));
                temparr.push(utc2tokyo(rec.start.dateTime,1));

                //barrayに追加する
                barray.push(temparr);
            }
        }

        //次の送信のためにウェイトを入れる
        setTimeout(function () {
            callback(["OK", barray]);
            return;
        }, 500);
    } else {
        var message = "エラー:" + status + "\n" + ret.error.message;

        //メッセージを返す
        //次の送信のためにウェイトを入れる
        setTimeout(function () {
            callback(["NG", message]);
            return;
        }, 500);
        return;
    }
})
.catch((err) => {
    //エラーメッセージ
    callback(["NG", status + "エラー:\n" + err]);
    return;
});

//UTCタイムをTokyoに変換する
function utc2tokyo(datetime,flg){
    //日付を取得
    let dt = new Date(datetime);

    //JSTに変換
    dt.setTime(dt.getTime() + 1000*60*60*9);

    //日付を取得
    let yearman = dt.getFullYear();
    let monthman = paddingZero(dt.getMonth() + 1);
    let dateman = paddingZero(dt.getDate());

    //時間を取得
    let hourman = paddingZero(dt.getHours());
    let mins = paddingZero(dt.getMinutes());

    //整形して返す
    if(flg == 0){
        var strDate = monthman + "/" + dateman + " - " + hourman + ":" + mins;
    }else{
        var strDate = String(yearman) + String(monthman) + String(dateman) + String(hourman) + String(mins)
    }

    return strDate;
}
  • 返ってくる値がUTCでの日付時刻になってるので、utc2tokyoにて9時間加算した日付と時刻に変換しています。
  • レンダラ側ではVuetifyのタイムラインコンポーネントを利用して表示させています。
  • ユーザの標準の予定表のデータのみを取得します。また、取得できるデータはリクエスト時の時刻以降のデータになります。

カレンダー登録の削除

登録時に一時的な変数に日付とカレンダーIDをキープしておき、同一日付内でのシートのキープ時には、先に登録されてるカレンダーイベントを削除し、新たに登録し直すというコードを追加し、同一日で席を変えた場合に、カレンダーにいくつも登録がされるのを防ぐ対処を追加しました。

//今日の日付を取得
let tempdate = todaynum();
let setcalflg = 0;

//保存済みの日付データと照合する
let seatdate = store.get("seatdate");

if(seatdate == tempdate){
    //同一日付であるので、一度カレンダーイベントを削除してから登録
    setcalflg = 1;
}else{
    //新規なのでそのままカレンダーを登録する
    setcalflg = 0;
}

//カレンダーの座席イベントを削除する
function delCalendarEvent(access_token,flg,callback){
    //処理しないフラグの判定
    if(flg == 0){
        console.log("削除すべきイベントなし")
        //そのまま処理をせずにコールバック
        callback(["OK","なにもしない"]);
        return;
    }

    //カレンダーのイベントIDを取得
    let eventid = store.get("calevent");

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token
    }

    //リクエストオプション
    var options = {
        method: 'DELETE',
        agent: agent,
        headers: headers
    }

    //リクエストURL
    let requrl = calpoint + "/" + eventid;

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(requrl, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;
        //body部分を取得
        return res;
    })
    .then((jsonData) => {
        if (status == 204) {
            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                //現在のイベント削除完了
                console.log("イベント削除完了")
                callback(["OK", "カレンダー削除完了"]);
                return;
            }, 500);
        } else {
            var message = "エラー:" + status + "\nカレンダーイベントの削除に失敗しました";

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}

//今日の日付を数値形式で取得する
function todaynum(){
    //日付を取得
    let tempdate = "";
    let nowdate = new Date();

    //yyyymmddで取得
    let yearman = nowdate.getFullYear();
    let monthman = paddingZero(nowdate.getMonth() + 1);
    let dateman = paddingZero(nowdate.getDate());

    //一旦テキストとして結合
    tempdate = String(yearman) + String(monthman) + String(dateman);

    //数値形式に変換して返す
    return Number(tempdate);
}
  • delCalendarEventを実行する前に、キープ済みのseatdateと今日の日付を比較して同一の場合は削除を実行する
  • 同一日付じゃない場合には、flg == 0としてカレンダー削除イベントを飛ばしてcallbackで返し、続けてカレンダー登録イベントを実行します。
  • カレンダー登録時にstore.set("seatdate",日付);で日付を登録し、store.set("calevent",カレンダーID)でカレンダーIDをキープしておきます。
  • イベントの削除は非常にシンプルなので、カレンダーのエンドポイントにカレンダーIDを付け加えるだけ
  • カレンダー書き込み時にレスポンスデータからresponse.idとしてカレンダーIDを取っておくのを忘れずに

カレンダーに書き込み

出勤時のみスケジュールに、在宅やキープした座席番号データを書き込みします。時刻の指定フォーマットがややハマるポイントです。

//カレンダー登録用の日時の生成(yyyy-MM-ddThh:mm:ssの形式)
function makeNowDate(){
     //現在の日付を生成
    let nowdate = new Date();

    let yearman = nowdate.getFullYear();
    let monthman = paddingZero(nowdate.getMonth() + 1);
    let dateman = paddingZero(nowdate.getDate());
    let hourman = paddingZero(nowdate.getHours());

    //日付を組み立て
    //Zをつけてしまうと、UTCになってしまうので削除しておく
    let tempdate = yearman + "-" + monthman + "-" + dateman + "T" + hourman + ":00:00";

    //19時退勤固定
    let eighthour = "19";

    let tempdate2 = yearman + "-" + monthman + "-" + dateman + "T" + eighthour + ":00:00";

    //配列で返す
    return [tempdate,tempdate2];
}

//カレンダーに出勤状況を登録
function setWorkCalendar(temp,timeman,access_token,flg,callback){
    if(flg == 0){
        //placemanから出勤場所を取得
        var seatrec = temp.args2;
        var seatname = seatrec[0];
        var areaname = seatrec[1];
    }

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        'Content-Type': 'application/json'
    }

    //リクエストボディを作成する(2022-08-21T08:00:00)
    let tarr = "";
    if(flg == 0){
        tarr = {
            "subject": areaname + "の" + seatname + "にて出勤",
            "start": {
                "dateTime": timeman[0],
                "timeZone": "Asia/Tokyo"
            },
            "end": {
                "dateTime": timeman[1],
                "timeZone": "Asia/Tokyo"
            }
        }
    }else{
        tarr = {
            "subject": "在宅勤務にて出勤",
            "start": {
                "dateTime": timeman[0],
                "timeZone": "Asia/Tokyo"
            },
            "end": {
                "dateTime": timeman[1],
                "timeZone": "Asia/Tokyo"
            }
        }
    }

    //リクエストオプション
    var options = {
        method: 'POST',
        agent: agent,
        headers: headers,
        body: JSON.stringify(tarr),
    }

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(calpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;
        //body部分を取得
        return res.json()
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 201) {
            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", "カレンダー登録完了"]);
                return;
            }, 500);
        } else {
            if (status == 403) {
                var message = "エラー:" + status + "\nカレンダー登録に失敗しました";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • makenowdate関数で、開始時刻と終了時刻(19時固定にしてあります)を生成します。フォーマットの最後にZが付くとUTCになってしまうので、外す必要があります。
  • リクエストボディのTimezoneはAsia/Tokyoを指定します
  • デフォルトの予定表にだけ読み書きを行います。
  • 成功時のステータスが201であることに注意が必要です。

Excel Online関係の処理

永続セッション情報を取得する

Excel Onlineは読み書きをする場合に、永続セッションと呼ばれるものを利用しないと一時処理扱いとなってしまい、リアルタイムにデータが反映しなくなってしまいます。タイムラグが数分あるため、必ず処理をする場合はworkbook-session-idという永続セッション情報を取得してからヘッダーに追加して処理を行わせます。

//永続セッション情報を取得する
function getSessionId(access_token,callback){
    //エンドポイントURLを構築
    let excelpath = store.get("excelpath");
    let endpoint = excelpath + "createSession";

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        'Content-Type': 'application/json'
    }

    //リクエストボディ
    let tarr ={
        "persistChanges": true
    }

    //リクエストオプション
    var options = {
        method: 'POST',
        agent: agent,
        headers: headers,
        body:JSON.stringify(tarr),
    }

    //ステータスコード用
    var status
    
    //URLリクエスト
    fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res.json()
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 201) {
            //レコードデータだけを取得する
            let session = ret.id;

            //session-idを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", session]);
                return;
            }, 500);
        } else {
            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", ret.error.message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • persistChangesをtrueにするリクエストボディを追加する必要があります。
  • 成功時のステータスは201であるので注意
  • 成功すると長い文字列のworkbook-session-idと呼ばれる値が取得出来るので、これを次の処理へと渡してあげる

テーブルデータを取得する

範囲指定ではなくテーブル単位のデータの取得を行います。まるごと取得する上で、workbook-session-idが必要です。またこの時個々のレコードの行番号であるrowIndexの値も取得しておく必要があります(後で更新や削除する場合に必要)

//Excel Onlineのテーブルデータを全取得して返す
function getExcelTableData(tablename,session,access_token,callback){
    //エンドポイントURLを構築
    let excelpath = store.get("excelpath");
    let endpoint = excelpath + "tables/" + tablename + "/rows";

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        "workbook-session-id": session
    }

    //リクエストオプション
    var options = {
        method: 'GET',
        agent: agent,
        headers: headers
    }

    //ステータスコード用
    var status
    
    //URLリクエスト
    fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res.json()
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 200) {
            //レコードデータだけを取得する
            let recs = ret.value;

            //valueだけを取り出して配列で返す
            let temparr = [];
            let rec = [];
            for(var i = 0;i<recs.length;i++){
                //レコードを一個取り出す
                rec = [];
                rec = recs[i].values;

                //インデックスを取り出す
                let indexman = recs[i].index;

                //インデックスをrecに追加する
                rec[0].push(indexman);

                //valuesのデータを取り出す
                temparr.push(rec[0]);
            }

            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", temparr]);
                return;
            }, 500);
        } else {
            if (status == 403) {
                var message = "エラー:" + status + "\n" + tablename + "のデータの取得に失敗しました。";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
  })
  .catch((err) => {
    //エラーメッセージ
    callback(["NG", status + "エラー:\n" + err]);
    return;
  });
}
  • valuesがレコードデータとなっていて、別にあるindexがrowindexとなっているので別々に取得して1つの配列に結合させています。
  • table名を変えれば他のテーブルの値をまるごと取得する事が可能です。

テーブルにレコードを追加する

在宅勤務登録時に使用。テーブルに対して1行レコードを新規に追加する。workbook-session-idが必要です。

//在宅ワークデータをExcel Onlineに追記する処理
function setzaiwork(temp,session,access_token,callback){
    //引数を分解
    let uid = temp.uid;
    let uname = temp.uname;
    let mailman = temp.mail;
    let starttime = temp.starttime;
    let endtime = temp.endtime;

    //エンドポイントURLを構築
    let excelpath = store.get("excelpath");
    let endpoint = excelpath + "tables/remote/rows/add";

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        "workbook-session-id": session,
        "Content-type": "application/json"
    }

    //リクエストボディ
    let tarr = {
        "values": [
          [
            uid,
            uname,
            mailman,
            "在宅勤務",
            starttime,
            endtime
          ]
        ]
    }

    //リクエストオプション
    var options = {
        method: 'POST',
        agent: agent,
        headers: headers,
        body: JSON.stringify(tarr),
    }

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res.json()
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 200 || status == 201) {
            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", "在宅登録完了しました。"]);
                return;
            }, 500);
        } else {
            if (status == 403) {
                var message = "エラー:" + status + "\n" + tablename + "のデータの取得に失敗しました。";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • valuesで配列を構築し1行分を作成する
  • rows/addで追加するエンドポイントになります。
  • 一番下に1行追加してくれるます。取得後はシートデータを再取得してrowindexの値も取り直すようにしましょう。

テーブルからレコードを削除する

在宅ワーク登録に於いて、退勤時にテーブルから該当のレコードを削除する場合に利用しています。テーブルデータ取得時に得たrowIndexの値workbook-session-idが必要です。公式ドキュメントだとitemAtの記述が乗っておらず、非常に苦労した部分の1つ。

//在宅ワークデータをExcel Onlineに追記する処理
function setzaiworkdel(rowindex,session,access_token,callback){
    //エンドポイントURLを構築
    let excelpath = store.get("excelpath");
    let endpoint = excelpath + "tables/remote/rows/itemAt(index=" + rowindex + ")";

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        "workbook-session-id": session
    }

    //リクエストオプション
    var options = {
        method: 'DELETE',
        agent: agent,
        headers: headers
    }

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res;
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 204) {
            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["OK", "在宅登録解除完了しました。"]);
                return;
            }, 500);
        } else {
            if (status == 403) {
                var message = "エラー:" + status + "\n" + tablename + "のデータの取得に失敗しました。";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • リクエストメソッドはDELETEを利用。rowindexはitemAtに続けて指定が必要
  • workbook-session-idを利用しないとリアルタイムに書き込みが反映しないので注意。
  • 成功時のステータスは204なので注意

rowIndexによってレコードの位置を特定し、テーブルからそのレコードだけを削除するので、探索するようなコードは不要です。

テーブルのレコードを更新する

座席データのキープ時・リリース時などに利用しています。rowIndexの値の指定が必要で、workbook-session-idが必要です。公式ドキュメントだとitemAtの記述が乗っておらず、非常に苦労した部分の1つ。また、Delete時と違い、/rangeをエンドポイントに必要になります。

//シートキープの為のリクエスト処理
function setSeatKeep(temp,session,flg,access_token,callback){
    //引数を分解
    let seatid = temp.args;
    let seatrec = temp.args2;
    let indexman = temp.args3;

    //エンドポイントURLを構築
    let excelpath = store.get("excelpath");
    let endpoint = excelpath + "tables/zaseki/rows/itemAt(index=" + indexman + ")/range";

    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        "workbook-session-id": session
    }

    //ユーザ情報を取得する
    let uid = "";
    let uname = ""
    if(flg == true){
        uid = store.get("id");
        uname = store.get("username");
    }
    
    //リクエストボディを生成する
    let tarr ={
        "index" : indexman,
        "values": [
            [
                seatid,
                seatrec[1],
                seatrec[2],
                seatrec[3],
                seatrec[4],
                uname,
                uid,
                seatrec[7]
            ]
        ]
    }

    //リクエストオプション
    var options = {
        method: 'PATCH',
        agent: agent,
        headers: headers,
        body: JSON.stringify(tarr),
    }

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res.json()
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 200) {
            //UIDを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                if(flg == true){
                    callback(["OK", "シートキープされました。"]);
                }else{
                    callback(["OK", "シートはリリースされました。"]);
                }
                return;
            }, 500);
        } else {
            if (status == 403) {
                var message = "エラー:" + status + "\n" + tablename + "のデータの取得に失敗しました。";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • リクエストボディのindexにrowindexを指定すると共に、エンドポイントにもitemAtで同じrowindexの指定が必要
  • valuesに1行分のデータを用意するので、書き換えしない列でも上書きする為の値が必要になるので注意。
  • リリース時は値を空にして書き込めば良い(社員番号と社員氏名の列の部分だけを空にする)

アクセスするユーザの権限を追加

マスタ編集の社員マスタ追加時に、zaseki.xlsxに対して対象のユーザの読み書き権限を追加する時に使用しています。workbook-session-idが必要です。

//新規にzaseki.xlsxに対してユーザの共有権限を追加
function addUserPermission(mail,access_token,callback){
    //excelpathからitemidだけ取り出す
    let excelpath = store.get("excelpath");
    let accpoint = excelpath.replace("\/workbook\/","")

    //エンドポイントURLを構築
    let endpoint = accpoint + "/invite";
    
    //リクエストヘッダ
    var headers = {
        "Authorization": "Bearer " + access_token,
        "Content-type": "application/json"
    }
    
    //リクエストボディ
    let tarr = {
        "recipients": [
            {
              "email": mail
            }
          ],
          "message": "座席表アプリ用ファイルへのアクセス権限付与",
          "requireSignIn": true,
          "sendInvitation": false,
          "roles": ["write"]
    }

    //リクエストオプション
    var options = {
        method: 'POST',
        agent: agent,
        headers: headers,
        body: JSON.stringify(tarr),
    }

    //ステータスコード用
    var status

    //URLリクエスト
    fetch(endpoint, options)
    .then((res) => {
        //ステータスコードを取得
        status = res.status;

        //body部分を取得
        return res.json()
    })
    .then((jsonData) => {
        //データのパース
        var ret = jsonData;

        if (status == 200) {
            //UIDを返す
            //次の送信のためにウェイトを入れる
            console.log("新規ユーザ権限追加に成功。")

            setTimeout(function () {
                callback(["OK", "新規ユーザ権限追加に成功。"]);
                return;
            }, 500);
            return;
        } else {
            console.log("新規ユーザ権限追加は失敗しました。")
            if (status == 403) {
                var message = "エラー:" + status + "\n" + ": userlistへの新規ユーザ権限追加に失敗しました。";
            } else {
                //その他のエラー
                var message = "エラー:" + status + "\n" + ret.error.message;
            }

            //メッセージを返す
            //次の送信のためにウェイトを入れる
            setTimeout(function () {
                callback(["NG", message]);
                return;
            }, 500);
            return;
        }
    })
    .catch((err) => {
        //エラーメッセージ
        callback(["NG", status + "エラー:\n" + err]);
        return;
    });
}
  • 必要なのは、対象ユーザのメールアドレスであってMicrosoft365のuidではないので注意。
  • Excelへのパスからworkbookの文字列を除去したURLに/inviteをつなげたURLがエンドポイントになるので注意。
  • リクエストボディがやや複雑。rolesはwriteだけあれば読み書きが可能になります。
  • 招待状の送信可否と送信する場合のメッセージを指定可能です。

SVGのロードや写真のロード上の注意点

起動時に2つのSVGデータをHTML上に差し込む必要があり、またユーザの詳細情報時にはそのユーザのプロファイル画像を差し込む必要があります。しかし、今回はフレームワークにVueおよびVuetifyを利用しており初期化が終わった後のHTMLに対して、単純にjQuery等でHTMLの書き換えというのは動かないので、変数にこれらの値を格納しておき、書き換える場所に対してはバインドさせて読み込ませるようにします。

また、Vuetifyのタブは標準機能のまま使うと、切り替えても何も表示されないので、今回はタブクリックでDivの表示を切り替えるように仕組みを替えています。

<div v-show="grid1 == 2">
	<div id="map_park" v-html="rawHtml"></div>					
</div>

上記の例あと、rawHtmlにSVGデータが格納されており、これを入れ替える事で、SVGの差し込みと後からSVGデータの操作を可能にしています。

<v-avatar
    class="ma-3"
    size="125"
    tile
>
    <v-img 
        alt="イメージ画像"
        :src="rawimage"
    ></v-img>
</v-avatar>

上記の例も同様で、srcにBase64エンコードしたDataUriによる画像データが入っており、ここを入れ替えれば写真が入れ替わる仕組みです。

改訂履歴

  • 2022/11/22 - Version 1.8
    • FirefoxでTeamSpiritを操縦する場合、networkidle2が動かないので、削除した
  • 2022/11/17 - Version 1.7
    • TeamSpiritログイン用にUID,PW,URLの入力欄を追加
    • FireFoxダウンロード用設定を追加
    • TeamSpirit出勤打刻処理を装備(TSURL登録時に発火)
    • TeamSpirit退勤打刻処理を装備(TSURL登録時に発火)
    • calendarPermissionを変更してlimitedReadをボタン1つで実現する(カレンダーの組織内閲覧権限をボタンひとつで切り替え可)
    • iframeのサブドメインURLを生成する処理を追加
    • FirefoxはNightlyにし、操作出来るようにした。
    • settingウィンドウにLoadingを追加
    • proxy設定が間違っていた場合に起動で聞くなくなる現象があるので修正した
    • Graph API Token Refresh失敗時にスピナーをキャンセルするようにした
    • 起動時にfirefoxのプロファイルディレクトリを特定するコードを追加
  • 2022/11/11 - Version 1.5
    • 在宅勤務登録のカレンダー登録時に於ける日付の処理がおかしかったので修正した
    • シートキープ時の同一日付判定で同一日の場合、カレンダー登録を削除して登録し直すようにした。
    • カレンダー登録削除エラーが出た場合は、一次保存の日付データを削除する処理を追加
    • 座席リリースでソート順が残ってたので空にするようにした
    • Power Automateのデータクリアが失敗してる(ループにて)のを修正
  • 2022/09/01 - Version 1.3
    • 初版リリース

関連リンク

コメントを残す

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

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