RFID読取率 日次チェック 設定手順書
0この手順書について
集計ファイルに毎日たまる RFID の読取数を毎朝自動でチェックし、仮説に当てはまるものがあれば工場の Teams チャネルに確認を依頼して、回答を記録するまでの設定手順です。上から順に進めれば設定が終わります。
・フローは一から作らず、設定ファイル(.zip)を取り込む形にしました(4章・5章)。取り込んだ後の設定は、フローの先頭にある4つの値で変えられます(6章)。
・読取率の下限(閾値)を、フローの先頭の値で変えられるようにしました(10章)。
・確認依頼は、工場ごとの Teams チャネルに送ります。チャネル名が工場名のチャネルを、フローが自動で探します(7章)。試験中は、決めた1人のアカウントに送ります(8章)。
システム構成
- 集計ファイルはいまの仕組みのまま毎日更新されます。新しい仕組みは、このファイルを読むだけです。
- 毎朝、Power Automate のフローが起動し、Office Scripts が集計ファイルを読んで、前日分の確認と仮説の判定をします。読取率の下限(閾値)は、フローの先頭の値で変えられます。
- 前日分が入っていなければ、情報システム部に Teams で知らせて終わります。
- 当てはまりがあれば、工場ごとに「日次判定結果」へ記録し、チャネル名が工場名の Teams チャネルに確認依頼のカードを送ります(試験中と、チャネルの無い工場は、決めた方に個人あてで送ります)。
- チャネルのどなたかがカードで回答すると、その内容と回答者が「実証ログ」に記録されます。
- エージェントは、日次判定結果・実証ログ・判定ファイルをナレッジとして参照し、質問に「最新データ/該当した仮説/現場に確認すること」で答えます。
| 章 | 作業 | 目安の時間 | 使う画面 |
|---|---|---|---|
| 1 | 始める前の確認 | 5分 | — |
| 2 | SharePoint リストを2つ作る | 20分 | SharePoint |
| 3 | スクリプトを置く | 5分 | SharePoint |
| 4 | 設定ファイルを作る | 10分 | この手順書の画面 |
| 5 | Power Automate に取り込む | 5分 | Power Automate(ブラウザー) |
| 6 | 取り込んだ後に変える4つの値 | 5分 | Power Automate |
| 7 | 工場ごとのチャネルを作る | 10分 | Teams |
| 8 | 試験する | 15分 | Power Automate・Teams・SharePoint |
| 9 | 本番に切り替える | 5分 | Power Automate |
| 10 | 読取率の下限(閾値)を変える | 2分 | Power Automate |
| 11 | エージェントを更新する | 20分 | Copilot Studio |
| 12 | うまくいかないとき・よくあるご質問 | — | — |
お送りしたファイル
| ファイル | 使う章 |
|---|---|
| 日次チェックのスクリプト(.osts) | 3章(SharePoint の Office Scripts フォルダに置く) |
| 日次チェックのスクリプト(.txt) | .osts が使えない場合だけ(3章の「別の方法」)。付録Aにも全文があります |
| エージェント指示文 Ver8(.txt) | 11章(エージェントの「指示」に貼り替え) |
| 判定ファイル2点(.xlsx) | 11章(ナレッジ)。前回お送りしたもの(ファイル名の末尾が _20261016)から変更はありません |
設定ファイル(.zip)は、御社のサイトやリストの ID が入るため、お送りしていません。4章の画面で作ります。
1始める前の確認5分
- 設定する方が、集計ファイルを編集できること。Power Automate からスクリプトを実行するには、読むだけの処理でもファイルへの書き込み権限が必要です(読み取り専用のブックはエラーになります)。
- 設定する方が、リストとスクリプトを置く SharePoint サイトを編集できること。
- 設定する方が、工場ごとのチャネルを作るチームのメンバーであること。フローは、取り込んだ方のアカウントでチャネルの一覧を取り、カードを投稿します。
- Power Automate をブラウザーで開けること(make.powerautomate.com)。デスクトップのアプリは使いません。Excel Online (Business)・SharePoint・Microsoft Teams は標準コネクタで、Microsoft 365 のライセンスの範囲で使えます。
- Teams 管理センターで Workflows アプリが許可されていること(確認依頼のカードを送るアクションに必要です)。
- 集計ファイルの前日分の更新が毎朝何時までに終わるか(4章で実行時刻を決めるため)。
2SharePoint リストを2つ作る20分
毎朝の判定結果を入れる「日次判定結果」と、拠点の回答を入れる「実証ログ」を、1つのサイトに作ります。工場ごとに分けたいときは、リストの「工場」列で絞り込みます。Teams でリストを見たい場合は、そのチームのサイトに作ってください(2-3)。
2-1 リスト「日次判定結果」を作る
- SharePoint サイトを開き、+ 新規 → リスト を選ぶ。
- 空白のリスト を選び、名前に
日次判定結果と入力して 作成 を押す。 - + 列の追加 から、下の表の列を1つずつ追加する。列の種類を選び、名前をコピーして貼り付け、保存。
| 列名 | 種類 | 入る内容(例) |
|---|---|---|
| タイトル(最初からある列) | — | A工場 2026-09-28 |
工場 | 1 行テキスト | A工場(当てはまりの無い日は「全工場」) |
対象日 | 1 行テキスト | 2026-09-28 |
仮説ID | 1 行テキスト | P04・P12(当てはまりの無い日は「なし」) |
判定内容 | 複数行テキスト | 根拠の数値と、現場に確認すること |
下限 | 1 行テキスト | 85%(その日の判定に使った読取率の下限) |
2-2 リスト「実証ログ」を作る
- 2-1 と同じ手順で、名前
実証ログのリストを作る。 - 下の表の列を追加する。
| 列名 | 種類 | 入る内容(例) |
|---|---|---|
| タイトル(最初からある列) | — | A工場 2026-09-28 |
工場 | 1 行テキスト | A工場 |
対象日 | 1 行テキスト | 2026-09-28 |
仮説ID | 1 行テキスト | P04 |
判定内容 | 複数行テキスト | 確認依頼の内容 |
確認結果 | 1 行テキスト | 仮説のとおり/仮説と違う/確認中 |
確認した内容・原因 | 複数行テキスト | 前処理のデータ連携が夜間に止まっていた |
対応したこと | 複数行テキスト | 連携を再実行し、取り込み直した |
回答者 | 1 行テキスト | カードに回答した方の表示名 |
2-3 Teams でリストを見る(10月16日のご質問)
SharePoint のリストは、Teams(デスクトップ・Web・モバイル)でチャネルのタブとして開けます(Microsoft Lists アプリ)。
- リストを作ったサイトのチームで、チャネルの上の +(タブの追加)を押す。
- リスト(Lists)を選んで 保存。
- 既存のリストを追加 から「日次判定結果」または「実証ログ」を選ぶ。
3スクリプトを置く5分
判定のスクリプト(.osts)を、2章のサイトの ドキュメント(既定のドキュメント ライブラリ)の Office Scripts フォルダに置きます。Excel で貼り付けて登録する作業は要りません。
- 2章のサイトで ドキュメント を開く。
- Office Scripts フォルダが無ければ、+ 新規 → フォルダー で
Office Scriptsを作る(半角スペースを1つ入れます)。 - フォルダを開き、お送りした RFID読取率_日次チェック.osts をドラッグして置く(または アップロード → ファイル)。メールでお送りしたファイルをお使いください。
別の方法:集計ファイルを Excel on the web で開き、自動化 → 新しいスクリプト に .txt の全文を貼り付け、名前を付けて保存 でこのフォルダに保存しても同じです(Microsoft が案内している置き方)。
4設定ファイルを作る10分
Power Automate に取り込む設定ファイル(.zip)を、この画面で作ります。御社のサイト・ライブラリ・リスト・スクリプトの ID を入れる必要があるためです(ID が実在しないと取り込めません)。ID は、下の4つのリンクを開いて出た文字を貼るだけで、画面が読み取ります。
入れた値は、この画面の中だけで使います。どこにも送りません。画面を閉じると消えます。
4-1 サイトのアドレスを入れる
2章でリストを作り、3章でスクリプトを置いたサイトを開いたときのアドレスを貼ります。/sites/サイト名 より後ろ(/Lists/… や ? 以降)は自動で外します。
集計ファイルが別のサイトにある場合
4-2 4つのリンクを開き、出た文字を貼る
リンクを押すと、新しいタブに文字(JSON)が出ます。すべて選んでコピーし(Ctrl+A → Ctrl+C。Mac は ⌘A → ⌘C)、下の欄に貼ります。サインインの画面が出たときは、サインインしてからもう一度リンクを押してください。
4-3 運用の値を入れる
集計ファイルの前日分の更新が終わった後の時刻
これを下回ると「低い」とみなします。分析ルール台帳 Ver3 は 85。取り込んだ後も変えられます(10章)
「前日分が未更新」のお知らせと、チャネルが見つからない工場の確認依頼を受け取る方
入っている間は、確認依頼がすべてこの方に届きます。本番に切り替えるときに空欄にします(9章)
4-4 設定ファイルを作る
保存した .zip を、5章で Power Automate に取り込みます。値を間違えた場合は、ここで直して作り直し、取り込み直してください(取り込み直すと新しいフローができるので、前のフローはオフにしてから削除します)。
5Power Automate に取り込む5分
5-1 取り込む
- Power Automate を開き、左の マイ フロー → 上の インポート → パッケージのインポート (レガシー) を選ぶ。
- アップロード を押し、4章で保存した .zip を選ぶ。「パッケージが正常にアップロードされました。」と出るまで待つ。
- 下の 関連リソース に、SharePoint・Excel Online (Business)・Microsoft Teams の3行が出ます。3つとも、右端のスパナのアイコンを押します。

- 右に インポートの設定 が開きます。一覧からご自分のアカウントの接続を選び、保存 を押す。一覧が空のときは + 新規作成 で接続を作り、一覧の更新 を押してから選びます。

- 3つとも接続が入ると、インポート が押せるようになります。押して、しばらく待つ。

- 「すべてのパッケージ リソースが正常にインポートされました。」と出たら、フローを開く を押す。

5-2 オンにする
取り込んだ直後のフローはオフです。フローの詳細の画面で、上の 有効 のスイッチをオンにします(オフのままでは 実行 もできません)。

6取り込んだ後に変える4つの値5分
フローの先頭に、運用で変える値を4つ並べています。4章で入れた値がそのまま入っているので、変えたいときだけ直します。直し方:フローを 編集 で開き、カードを押して 値 の欄を書き換え、右上の 保存。

| カードの名前 | 入れる値 | いつ変えるか |
|---|---|---|
| 読取率の下限 | 数字(%)。例 85・90 | 閾値を変えるとき(10章) |
| 情シスの通知先 | メールアドレス(Teams にサインインするアドレス) | 担当の方が替わるとき |
| テスト送信先 | 試験する方のメールアドレス。本番は空欄 | 試験のとき(8章)・本番に切り替えるとき(9章) |
| チームID | 工場ごとのチャネルがあるチームの ID(4章で自動で入ります) | チャネルを別のチームに作るとき |
7工場ごとのチャネルを作る10分
確認依頼のカードは、チャネル名が工場名と同じチャネルに届きます。フローが毎朝、チームの標準チャネルの一覧から自動で探します。
- 4章の「チームID」のチーム(ふつうはリストを作ったサイトのチーム)で、チャネルを追加(または … → チャネルを作成)を選ぶ。
- チャネル名を工場名にする。集計ファイルのブロックの見出しと同じ表記(例
A工場)か、「工場」を除いた名前(例A)。 - 種類は標準にする(プライベートや共有のチャネルには送れません)。
- そのチャネルを見る工場の方を、チームのメンバーにする。
8試験する15分
8-1 1人のアカウントで試す
- 6章の「テスト送信先」に、試験する方のアドレスが入っていることを確認する。
- フローを 編集 で開き、判定 を押して、targetDate に判定したい過去の日(例
2026-09-28)、expectedDate に-(半角ハイフン)を入れて 保存。 - 右上の テスト → 手動 → テスト → フローの実行 で実行する。
- Teams の ワークフロー(Workflows)とのチャットに、工場ごとのカードが届くこと、リスト「日次判定結果」に工場ごとの行が入ることを確認する。
- カードで結果を選び、回答する を押す。カードが「ご回答ありがとうございました。…」に変わり、リスト「実証ログ」に行が入ることを確認する。

| 試したいこと | targetDate | expectedDate | 期待する結果 |
|---|---|---|---|
| 過去の日を判定する | 2026-09-28 など | - | その日に当てはまった工場のカードが、テスト送信先に届く |
| 未更新の通知 | 空欄 | まだデータがない日(例:明日の日付) | 判定はせず、情シスの通知先に「…のデータがまだ入っていません。」が届く |
| 当てはまりなし | 当てはまらない日 | - | カードは届かず、日次判定結果に「全工場/なし」の1行が入る |
8-2 チャネル1つで試す
工場のチャネルに実際に届くことは、当社の環境ではまだ確認できていません(試験用のチームが無いため)。本番の前に、チャネル1つで確かめてください。
- 7章で作ったチャネルが、8-1 で当てはまった工場のものであることを確認する(違う場合は、当てはまった工場のチャネルを1つ作る)。
- 6章の「テスト送信先」を空欄にして保存し、8-1 と同じ日で実行する。
- そのチャネルにカードが届き、ほかの工場のカードは情シスの通知先に届くことを確認する。
9本番に切り替える5分
- 残りの工場のチャネルを、7章の手順で作る(8-2 でチャネルに届くことを確かめてから)。
- 判定 の targetDate と expectedDate を空欄に戻す。
- 「テスト送信先」を空欄にし、「チームID」に値が入っていることを確認する(空欄だと、確認依頼はすべて情シスの通知先に届きます)。
- 試験で入った行を、リスト「日次判定結果」と「実証ログ」から削除する(試験の回答を、エージェントが現場で確認した結果として答えないようにするため)。
- 「毎朝の起動」の時刻が、集計ファイルの更新が終わった後になっているかを確認する。
- 保存 して、フローがオンになっていることを確認する。翌朝から自動で動きます。
10読取率の下限(閾値)を変える2分
フローの 読取率の下限 の値を変えて 保存 します。次の朝の判定から使われます。判定の結果と「日次判定結果」の 下限 の列に、その日に使った値が入ります。
| 下限を使う判定 | 内容 |
|---|---|
| P06 機器の故障 | その日、ゲートの8割以上の商品が下限を下回った(前日は8割以上が下限以上) |
| P01・P03 日ズレ | 3日前の日のゲート全体の読取率が下限を下回り、読取数が1〜3日後の前処理・仕上の枚数とほぼ一致した |
| P17(期間の傾向) | その商品だけ、期間の中央値が下限を下回っている |
空欄、または50〜100以外の数を入れた場合は、85% で判定します(分析ルール台帳 Ver3 の値)。
11Copilot Studio のエージェントを更新する20分
11-1 指示を貼り替える
- Copilot Studio でエージェントを開き、概要 の 指示 の 編集 を押す。
- 入っている文章をすべて消し、お送りした「エージェント指示文 Ver8」の全文を貼り付けて 保存。
11-2 ナレッジを追加する
- ナレッジ → ナレッジの追加 を開く。
- SharePoint を選び、2章で作ったリスト「日次判定結果」と「実証ログ」を選んで追加する(画面の表示に従ってサイトとリストを選びます)。
- 状態が 準備完了 になるまで待つ。初回は数分〜数十分かかることがあります。
- テスト パネルで「最新日に該当した拠点と、現場に確認することを教えて」と聞き、最新データ・当日に該当した仮説・現場に確認すること の3つで答えることを確認する。
12うまくいかないとき・よくあるご質問
12-1 うまくいかないとき
| 症状 | 考えられる原因 | 対処 |
|---|---|---|
| 4章で「…がありません」と出る | リスト名・列名・フォルダ名が違う/貼った文字が途中で切れている | 2章・3章の名前を確認し、リンクを開き直してすべて選んでコピーし直す |
| 5章で パッケージのインポート (レガシー) が出ない | 環境がソリューションでの管理を必須にしている/取り込む権限が無い | 管理者にご確認ください(取り込みには、その環境の作成者の権限が要ります) |
| 取り込みで「…のインポートに失敗しました」 | 4章の ID と、取り込む方のアクセス権・接続のアカウントが合っていない | リストとスクリプトのあるサイトを開ける方の接続を選ぶ。直らない場合はエラーの文をお知らせください |
| 「判定」が 403 エラー | 集計ファイルへの書き込み権限がない | 設定する方に、ファイルの編集権限を付ける |
| 「判定」が 502 エラー | ブックが読み取り専用になっている | 読み取り専用の設定を外す |
| 実行が失敗し、しばらくして再実行される | 1回の実行が120秒を超えた | スクリプトは読むだけなので、再実行しても結果は同じです。続く場合はご連絡ください |
| 「対象の月のシート…が見つかりませんでした」 | 月のシート名が「工場読取データ (N月)」の形と違う/対象月のシートがまだない | シート名と、targetDate の日付を確認する |
| 毎日「まだ入っていません」になる | 前日分の更新が、フローの実行時刻より後 | 「毎朝の起動」の時刻を遅らせる |
| 「ユーザー詳細が見つかりませんでした」 | 通知先のアドレスが Teams のサインインのアドレスと違う | 6章の通知先を、Teams にサインインするアドレスに直す |
| チャネルにカードが届かない | テスト送信先が入っている/チャネル名が工場名と違う/プライベート チャネル/フローの所有者がチームのメンバーでない | 6章・7章を確認する。届かない工場の分は情シスの通知先に届いています |
| カードがどこにも届かない | Workflows アプリが許可されていない | Teams 管理センターで許可する |
| ファイルがロックされて更新できない | Excel のコネクタの使用後、最大6分ロックされることがある | 数分待つ。フローの時刻を更新作業とずらす |
| エージェントが古い結果を答える | ナレッジへの反映(4〜6時間ごとの同期が目安)がまだ | 反映を待つ。急ぐ内容は Teams の通知で確認する |
12-2 よくあるご質問
| ご質問 | 回答 |
|---|---|
| カードに回答が無いときは? | 実証ログには記録されません。フローは最長30日回答を待ち、期限を過ぎた工場の分は「情シスの通知先」に「回答が無いまま期限を過ぎた」旨のメッセージが届きます。実行は失敗にならず、ほかの工場のカードにも影響しません(カードを送れなかった場合も同じ動きです。当社の環境で、送れなかった場合を確認)。 |
| 回答のあとのチャットも記録される? | 記録されるのは、カードの入力欄(確認した結果・確認した内容・原因・対応したこと)と回答した方の名前だけです。その後のチャットのやり取りは記録されません。 |
| パソコンの電源を切っても動く? | 動きます。クラウド フローは Microsoft のクラウドで動きます。ブラウザーで作成・管理できます。 |
| 何人分のフローが要る? | どなたか1人のアカウントに1本で足ります。担当の方が替わる場合に備えて、フローの詳細の画面の 共同所有者 にもう1人追加しておくと、作成した方がいなくなってもフローは動き続けます。ただし、作成した方の接続を使うアクションは失敗することがあるため、担当が替わるときは接続を新しい担当の方のものに付け替えてください。 |
| フローが止まることは? | 14日間失敗が続いたフローは、自動でオフになります。失敗の通知が届いたら12-1 の表で確認してください。 |
| スクリプトを新しくしたら? | .osts を同じ名前で置き換えれば、そのまま使えます。引数が変わる版をお送りした場合は、4章で設定ファイルを作り直して取り込み直します(Microsoft の案内で、引数が変わったときはスクリプトの実行のアクションを作り直す必要があるため)。 |
| 設定ファイルを取り込み直したら? | 取り込み直すと新しいフローができます。前のフローは、フローの詳細の画面で 有効 をオフにしてから削除してください(オンのまま残すと、毎朝の確認依頼が二重になります)。6章で変えた値は、作り直した設定ファイルの値になります。 |
A付録A スクリプト全文
この画面に載せるため、ファイル名などの表記を一般的な名前に置き換えています。お使いいただくのは、メールでお送りした .osts(または .txt)です。
/**
* RFID読取率 日次チェック(Office Scripts)
* 株式会社デジライズ 2026-10-19 版(10/16 お打合せ反映:読取率の下限を引数 rateLow で変更可能に)
*
* 集計ファイルの月別シート(工場読取データ (N月))を読み、
* 対象日のデータで仮説に当てはまるものを判定して、Power Automate に結果を返します。
* ブックの中身は書き換えません(読み取りだけ)。
*
* 使い方:SharePoint のライブラリに置いた .osts を、Power Automate の「SharePoint ライブラリからスクリプトを実行する」で呼び出します。
* (Excel on the web の[自動化]→[新しいスクリプト]に全文を貼り付けて登録し、「スクリプトの実行」で呼び出すこともできます)
* 戻り値:判定結果(DailyResult)を JSON にした文字列
* 引数 :targetDate … 判定する日(YYYY-MM-DD)。空欄ならデータの最終日
* expectedDate … 本来入っているはずの日。空欄なら日本時間の前日。"-" なら確認しない(過去日の試験用)
* mode … "daily"(毎日の判定)または "period"(月内の傾向の判定。拠点への通知には使わない)
* rateLow … 読取率の下限(%)。これを下回ると「低い」とみなす。空欄なら 85(分析ルール台帳 Ver3)。例:90
* 使う判定:P06(ゲートの8割以上の商品が下限未満)・P01/P03(ゲート全体が下限未満)・P17(その商品だけ期間中央値が下限未満)
* 毎日の判定:対象日 … P04(対象数0・分母0の商品)、P06、P12(出荷のみ)
* 3日前 … P04(対象数の急減で戻らないもの)、P01・P03(日ズレ。ゲート全体)
* 当月の全日で読取数が0のゲートは「未取得」として判定から外します。
*/
// ---------- 閾値(分析判定閾値マスタ Ver3。指示文の【閾値】と必ず揃える) ----------
const RATE_LOW_DEFAULT = 0.85; // 85%未満=異常(既定。引数 rateLow で変えられる)
const RATE_HI = 1.0; // 100%超=要注意
const QTY_WARN = 0.10; // 前日比10%以上=要注意
const QTY_ABN = 0.20; // 前日比20%以上=異常候補
const BUSY = 1.2; // 繁忙日=中央値の1.2倍以上
// ---------- 仮説マスタ(判定できる11件。名称と「現場に確認すること」) ----------
const HYP: { [id: string]: { name: string; check: string } } = {
P01: { name: "回収と前処理でチップを読ませる日が異っている", check: "回収ゲートの通過と前処理の通過のサイクルがどうなっているのか確認する" },
P03: { name: "出荷日と仕上日がずれている", check: "出荷ゲートの通過と仕上の通過のサイクルがどうなっているのか確認する" },
P04: { name: "前処理・仕上など工程データが欠損している", check: "対象工場のゲートが集計対象に入っているか、いつから連携が止まっているかをシステム側で確認する" },
P05: { name: "分子・分母、対象顧客、日付の集計ロジックが不整合", check: "分子・分母それぞれの集計の抽出条件(対象得意先、対象商品、日付の締め時刻)が一致しているか確認する" },
P06: { name: "リーダー、本体または電源系が故障している", check: "対象日にリーダー本体の再起動・電源断・保守作業がなかったか、機器のエラーログが残っていないか確認する" },
P08: { name: "アンテナの出力・感度が低下している", check: "アンテナの出力設定値、設置後の経過年数、直近の調整履歴を確認する" },
P12: { name: "未タグ品が処理数に含まれている", check: "対象商品にタグが全数装着されているか、未装着品の運用ルールがあるか確認する" },
P17: { name: "RFIDタグの向きが読取安定エリアに適合していない", check: "該当商品のタグ縫い付け位置と、ゲート通過時の向きを現物で確認する" },
P19: { name: "濡れた状態での読取(回収が出荷より恒常的に低い)", check: "回収時の濡れ具合と、ゲート通過時の積み方を確認する" },
P20: { name: "枚数が多い日ほど読取率が下がる", check: "繁忙日のゲート通過の速度・積載量を確認する" },
P21: { name: "繁忙日に読取率の低下が偏る", check: "繁忙日の作業手順(通過速度・積み方)が通常日と違っていないか確認する" },
};
// ---------- 型 ----------
interface Rec {
factory: string; gate: string; custName: string; cd: string; name: string;
date: string; num: number | null; den: number | null; rate: number | null;
}
interface Hit {
hypothesisId: string; hypothesisName: string; factory: string; gate: string;
targetDate: string; scope: string; evidence: string; check: string;
}
interface Group {
factory: string; hitCount: number; hypothesisIds: string; text: string;
cardText: string; // Teams のカード(JSON)に埋め込む用。改行や引用符をエスケープ済み
}
interface DailyResult {
targetDate: string; latestDate: string; expectedDate: string; isUpdated: boolean;
message: string; hitCount: number; hits: Hit[]; groups: Group[]; notAcquired: string[];
rateLow: number; // この判定に使った読取率の下限(%)
}
// ---------- 入口 ----------
function main(workbook: ExcelScript.Workbook, targetDate?: string, expectedDate?: string, mode?: string, rateLow?: number): string {
// 読み込むのは対象月と前月のシートだけ(実行時間の上限120秒に収めるため)
// 対象日も確認日も指定しない試験(expectedDate = "-")のときは、全部の月を読む
const jstToday = new Date(Date.now() + 9 * 3600000).toISOString().slice(0, 10);
const readAll = !targetDate && expectedDate === "-";
const ref = targetDate || (expectedDate && expectedDate !== "-" ? expectedDate : jstToday);
const m = parseInt(ref.slice(5, 7)), pm = m === 1 ? 12 : m - 1;
const sheets: { name: string; values: (string | number | boolean)[][] }[] = [];
for (const ws of workbook.getWorksheets()) {
const name = ws.getName();
if (!isMonthSheet(name)) continue;
const mm = name.match(/\((\d+)月\)/);
if (!mm) continue;
if (!readAll && parseInt(mm[1]) !== m && parseInt(mm[1]) !== pm) continue;
const used = ws.getUsedRange(true);
if (!used) continue;
// 使われている範囲は「値のある最初の行・列」から始まるため、A1 から最後の行・列までを読む(列の位置で読むので)
const all = ws.getRangeByIndexes(0, 0, used.getRowIndex() + used.getRowCount(), used.getColumnIndex() + used.getColumnCount());
sheets.push({ name: name, values: all.getValues() });
}
// expectedDate が空欄なら「日本時間の前日」。"-" なら最新データの確認をしない(過去日の試験用)
const jstYesterday = new Date(Date.now() + 9 * 3600000 - 86400000).toISOString().slice(0, 10);
const exp = expectedDate === "-" ? "" : (expectedDate || jstYesterday);
// 結果は JSON の文字列で返す(フローの「作成」で json() に通して使う)
return JSON.stringify(runChecks(sheets, targetDate || "", exp, mode || "daily", toRateLow(rateLow)));
}
// 読取率の下限を 0〜1 の比率にする(90 でも 0.9 でもよい。空欄・範囲外は既定の 85%)
function toRateLow(v?: number): number {
const n = parseFloat(String(v)); // フローから文字で渡されても読む
if (isNaN(n) || n <= 0) return RATE_LOW_DEFAULT;
const r = n > 1 ? n / 100 : n;
return r >= 0.5 && r <= 1 ? r : RATE_LOW_DEFAULT;
}
// ---------- 本体(ブックに依存しない部分。手元の検証でもこのまま使う) ----------
function runChecks(sheets: { name: string; values: (string | number | boolean)[][] }[], targetDate: string, expectedDate: string, mode: string, low: number): DailyResult {
const lowPct = Math.round(low * 1000) / 10;
// 1) 月別シートを商品単位のレコードに展開
let recs: Rec[] = [];
for (const s of sheets) {
const m = s.name.match(/\((\d+)月\)/);
recs = recs.concat(extractSheet(s.values, m ? parseInt(m[1]) : 0));
}
// 2) 最新データの確認
let latest = "";
for (const r of recs) {
if (((r.num ?? 0) > 0 || (r.den ?? 0) > 0) && r.date > latest) latest = r.date;
}
if (!latest) {
return { targetDate: targetDate, latestDate: "", expectedDate: expectedDate, isUpdated: false,
message: "対象の月のシート(工場読取データ (N月))にデータが見つかりませんでした。シート名と対象日を確認してください。",
hitCount: 0, hits: [], groups: [], notAcquired: [], rateLow: lowPct };
}
const D = targetDate || latest;
const isUpdated = expectedDate ? latest >= expectedDate : true;
// 対象日を指定せずに最新データを待っている場合、まだ入っていなければ判定しない(古い日の再通知を防ぐ)
if (!isUpdated && !targetDate) {
return { targetDate: "", latestDate: latest, expectedDate: expectedDate, isUpdated: false,
message: "集計ファイルの最新日が " + latest + " で、" + expectedDate + " のデータがまだ入っていません。",
hitCount: 0, hits: [], groups: [], notAcquired: [], rateLow: lowPct };
}
// 対象日の月と前月だけを使う(毎日の判定はこの範囲で足りる)
const useMonths = mode === "period-all" ? null : [D.slice(0, 7), addDays(D.slice(0, 7) + "-01", -1).slice(0, 7)];
if (useMonths) recs = recs.filter(r => useMonths.indexOf(r.date.slice(0, 7)) >= 0);
const hits: Hit[] = [];
const notAcquired: string[] = [];
const idx = buildIndex(recs);
// 当月の全日で読取数0のゲート=未取得(異常として数えず、判定からも外す)
const ym = D.slice(0, 7);
const skipGate: { [k: string]: boolean } = {};
for (const fg of Object.keys(idx.byFG)) {
const xs = idx.byFG[fg].filter(r => r.date.slice(0, 7) === ym && r.date <= D);
if (xs.length > 0 && xs.every(r => (r.num ?? 0) === 0)) {
skipGate[fg] = true;
notAcquired.push(fg.replace("|", " ") + "(" + ym + "は読取数0のため未取得として除外)");
}
}
if (mode === "daily") {
dailyChecks(D, idx, skipGate, hits, low);
// 日ズレ(P01・P03)は1〜3日後の枚数と比べるため、3日前の日を判定する
lagChecks(addDays(D, -3), idx, skipGate, hits, low);
} else {
periodChecks(idx, skipGate, hits, low);
}
const groups = groupByFactory(hits);
let message = "";
if (!isUpdated) message = "集計ファイルの最新日が " + latest + " で、" + expectedDate + " のデータがまだ入っていません。";
else if (hits.length === 0) message = D + " のデータで、仮説に当てはまるものはありませんでした。";
else message = D + " のデータで、" + groups.length + "工場・" + hits.length + "件が仮説に当てはまりました(読取率の下限 " + lowPct + "%)。";
return { targetDate: D, latestDate: latest, expectedDate: expectedDate, isUpdated: isUpdated,
message: message, hitCount: hits.length, hits: hits, groups: groups, notAcquired: notAcquired, rateLow: lowPct };
}
// ---------- シートの読み取り ----------
function isMonthSheet(name: string): boolean {
return name.indexOf("工場読取データ") === 0 && name.indexOf("テンプレート") < 0 && name.indexOf("OLD") < 0 && name.indexOf("重複") < 0;
}
function toDate(v: string | number | boolean): string {
if (typeof v === "number" && v > 40000 && v < 60000) {
const ms = Date.UTC(1899, 11, 30) + Math.floor(v) * 86400000;
return new Date(ms).toISOString().slice(0, 10);
}
if (typeof v === "string") {
const m = v.match(/^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})/);
if (m) return m[1] + "-" + ("0" + m[2]).slice(-2) + "-" + ("0" + m[3]).slice(-2);
}
return "";
}
function numOrNull(v: string | number | boolean): number | null {
return typeof v === "number" ? v : null;
}
function extractSheet(rows: (string | number | boolean)[][], month: number): Rec[] {
const out: Rec[] = [];
const heads: number[] = [];
for (let i = 0; i < rows.length; i++) {
if (String(rows[i][0] ?? "").indexOf("ゲート読取率") >= 0) heads.push(i);
}
for (let bi = 0; bi < heads.length; bi++) {
const h = heads[bi];
const title = String(rows[h][0]).replace(/ /g, " ").trim();
const factory = title.split(" ")[0];
const gate = title.indexOf("回収ゲート") >= 0 ? "回収" : (title.indexOf("出荷ゲート") >= 0 ? "出荷" : "不明");
const custRow = rows[h + 1], hdrRow = rows[h + 2];
if (!hdrRow || hdrRow[0] !== "No") continue;
const custName = String(custRow[1] ?? "");
// 日付の列(6列目から3列おき)。同じ日付は初出だけ使い、シート末尾の翌月の日付は除く
const dateCols: { c: number; d: string }[] = [];
const seen: { [d: string]: boolean } = {};
for (let c = 5; c < custRow.length; c += 3) {
const d = toDate(custRow[c]);
if (!d || seen[d]) continue;
if (month && parseInt(d.slice(5, 7)) !== month) continue;
seen[d] = true;
dateCols.push({ c: c, d: d });
}
const end = bi + 1 < heads.length ? heads[bi + 1] : rows.length;
for (let i = h + 3; i < end; i++) {
const r = rows[i];
if (!r || typeof r[0] !== "number") continue;
const name = r[1];
if (name === "" || name === null || name === undefined) continue;
for (const dc of dateCols) {
if (dc.c + 1 >= r.length) continue;
const num = numOrNull(r[dc.c]), den = numOrNull(r[dc.c + 1]);
if (num === null && den === null) continue;
out.push({ factory: factory, gate: gate, custName: custName, cd: String(r[2]), name: String(name), date: dc.d,
num: num, den: den, rate: (num !== null && den) ? num / den : null });
}
}
}
return out;
}
// ---------- 索引 ----------
interface Idx {
byFG: { [fg: string]: Rec[] };
byFGD: { [fgd: string]: Rec[] };
byFGP: { [fgp: string]: Rec[] };
byFGPD: { [fgpd: string]: Rec };
}
function buildIndex(recs: Rec[]): Idx {
const idx: Idx = { byFG: {}, byFGD: {}, byFGP: {}, byFGPD: {} };
for (const r of recs) {
const fg = r.factory + "|" + r.gate;
(idx.byFG[fg] = idx.byFG[fg] || []).push(r);
(idx.byFGD[fg + "|" + r.date] = idx.byFGD[fg + "|" + r.date] || []).push(r);
(idx.byFGP[fg + "|" + r.cd] = idx.byFGP[fg + "|" + r.cd] || []).push(r);
idx.byFGPD[fg + "|" + r.cd + "|" + r.date] = r;
}
for (const k of Object.keys(idx.byFGP)) idx.byFGP[k].sort((a, b) => a.date < b.date ? -1 : 1);
return idx;
}
function addDays(d: string, n: number): string {
const t = Date.UTC(parseInt(d.slice(0, 4)), parseInt(d.slice(5, 7)) - 1, parseInt(d.slice(8, 10))) + n * 86400000;
return new Date(t).toISOString().slice(0, 10);
}
function pct(x: number): string { return (x * 100).toFixed(1) + "%"; }
function fmtLow(x: number): string { return (Math.round(x * 1000) / 10) + "%"; }
function signedPct(x: number): string { return (x >= 0 ? "+" : "") + Math.round(x * 100) + "%"; }
function median(xs: number[]): number | null {
if (xs.length === 0) return null;
const s = xs.slice().sort((a, b) => a - b), n = s.length;
return n % 2 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2;
}
function dayTotal(xs: Rec[]): { num: number; den: number } {
let num = 0, den = 0;
for (const x of xs) { num += x.num ?? 0; den += x.den ?? 0; }
return { num: num, den: den };
}
function push(hits: Hit[], id: string, fg: string, date: string, scope: string, evidence: string) {
const [f, g] = fg.split("|");
hits.push({ hypothesisId: id, hypothesisName: HYP[id].name, factory: f, gate: g, targetDate: date,
scope: scope, evidence: evidence, check: HYP[id].check });
}
// ---------- 毎日の判定(対象日 D) ----------
function dailyChecks(D: string, idx: Idx, skip: { [k: string]: boolean }, hits: Hit[], low: number) {
const prev = addDays(D, -1);
for (const fg of Object.keys(idx.byFG)) {
if (skip[fg]) continue;
const today = idx.byFGD[fg + "|" + D] || [];
const yday = idx.byFGD[fg + "|" + prev] || [];
if (today.length === 0) continue;
// P04-1 対象数(分母)が0なのに読取数がある商品
const zeroDen = today.filter(r => r.den === 0 && (r.num ?? 0) > 0);
if (zeroDen.length > 0) {
const ex = zeroDen.slice(0, 3).map(r => r.name + "(" + r.cd + ")読取" + r.num + "枚").join("、");
push(hits, "P04", fg, D, "商品単位", "対象数(分母)が0なのに読取数がある商品が" + zeroDen.length + "件。例:" + ex + "。対象工程のデータ未取込の疑い");
}
// P04-2 ゲート全体の対象数が0になった(前日はあった)。急減の判定は3日前の日で行う(lagChecks)
const a = dayTotal(yday), b = dayTotal(today);
if (a.den > 0 && b.den === 0) {
push(hits, "P04", fg, D, "ゲート全体", "ゲート全体の対象数が0(前日は" + a.den + "枚)。前処理・仕上のデータが入っていない疑い");
}
// P06 その日、8割以上の商品が下限(既定85%)未満(前日は8割以上が正常)
const vv = today.filter(r => r.den);
const pv = yday.filter(r => r.den);
if (vv.length >= 3 && pv.length > 0) {
const lows = vv.filter(r => r.rate !== null && r.rate < low);
const pok = pv.filter(r => r.rate !== null && r.rate >= low);
if (lows.length / vv.length >= 0.8 && pok.length / pv.length >= 0.8) {
push(hits, "P06", fg, D, "ゲート全体", vv.length + "商品中" + lows.length + "商品(" + Math.round(lows.length / vv.length * 100) + "%)が" + fmtLow(low) + "未満。前日は正常で、特定商品ではなくゲート全体の事象");
}
}
// P12 出荷ゲートのみ:仕上(分母)だけ前日比20%以上増え、出荷(分子)がほぼ横ばい
if (fg.split("|")[1] === "出荷") {
const p12: string[] = [];
for (const r of today) {
const y = idx.byFGPD[fg + "|" + r.cd + "|" + prev];
if (!y || !y.den || !y.num || r.den === null || r.num === null) continue;
const dd = (r.den - y.den) / y.den, dn = (r.num - y.num) / y.num;
if (dd >= QTY_ABN && Math.abs(dn) < QTY_WARN) p12.push(r.name + "(" + r.cd + ")仕上" + signedPct(dd) + "・出荷" + signedPct(dn));
}
if (p12.length > 0) {
push(hits, "P12", fg, D, "商品単位", "仕上だけ増えて出荷が増えていない商品が" + p12.length + "件。例:" + p12.slice(0, 3).join("、") + "。未タグ品混入の候補");
}
}
}
}
// ---------- 3日前の日の判定:対象数の急減(P04)と日ズレ(P01・P03) ----------
// どちらも「1〜3日後の枚数」と比べるため、3日分のデータがそろった日を対象にする
function lagChecks(D: string, idx: Idx, skip: { [k: string]: boolean }, hits: Hit[], low: number) {
for (const fg of Object.keys(idx.byFG)) {
if (skip[fg]) continue;
// P04-3 ゲート全体の対象数が前日比20%以上急減し、3日後までに戻らない
const a = dayTotal(idx.byFGD[fg + "|" + addDays(D, -1)] || []), b = dayTotal(idx.byFGD[fg + "|" + D] || []);
if (a.den > 0 && b.den > 0) {
const ch = (b.den - a.den) / a.den;
const fwd: number[] = [];
// データのない日(休業日など)は飛ばし、データのある次の3日で見る
for (let k = 1; k <= 7 && fwd.length < 3; k++) { const z = idx.byFGD[fg + "|" + addDays(D, k)]; if (z) fwd.push(dayTotal(z).den); }
if (ch <= -QTY_ABN && !(fwd.length > 0 && Math.max(...fwd) >= a.den * (1 - QTY_WARN))) {
push(hits, "P04", fg, D, "ゲート全体", "ゲート全体の対象数が前日比" + signedPct(ch) + "に急減(" + a.den + "→" + b.den + "枚)し、" + fwd.length + "日後までに戻っていない");
}
}
// 日ズレは、分子・分母の両方がある行だけで合計する
const t = dayTotal((idx.byFGD[fg + "|" + D] || []).filter(r => r.num !== null && r.den !== null));
if (t.den <= 0 || t.num <= 0) continue;
const rate = t.num / t.den;
if (rate >= low) continue;
const gate = fg.split("|")[1];
const id = gate === "回収" ? "P01" : "P03";
const lbl = gate === "回収" ? "前処理" : "仕上";
for (let lag = 1; lag <= 3; lag++) {
const y = dayTotal((idx.byFGD[fg + "|" + addDays(D, lag)] || []).filter(r => r.num !== null && r.den !== null));
if (y.den <= 0) continue;
const diff = Math.abs(t.num - y.den) / y.den;
if (diff <= QTY_WARN) {
push(hits, id, fg, D, "ゲート全体", "当日の読取数" + t.num + "枚が" + lag + "日後の" + lbl + y.den + "枚とほぼ一致(差" + Math.round(diff * 100) + "%)。当日読取率" + pct(rate));
break;
}
}
}
}
// ---------- 月内の傾向(P05・P08・P17・P19・P20・P21) ----------
function periodChecks(idx: Idx, skip: { [k: string]: boolean }, hits: Hit[], low: number) {
const med: { [fgp: string]: number | null } = {};
for (const k of Object.keys(idx.byFGP)) {
med[k] = median(idx.byFGP[k].filter(x => x.den && x.rate !== null).map(x => x.rate as number));
}
const label = "(期間通算)";
for (const k of Object.keys(idx.byFGP)) {
const [f, g, p] = k.split("|"); const fg = f + "|" + g;
if (skip[fg]) continue;
const v = idx.byFGP[k]; const last = v[v.length - 1];
const vv = v.filter(x => x.den && x.rate !== null);
const m = med[k];
// P05 恒常的に100%超
if (m !== null && m > RATE_HI && vv.length >= 10) push(hits, "P05", fg, label, "商品単位", last.name + "(" + p + ")の期間中央値" + pct(m) + "(" + vv.length + "日)。100%超は集計ロジック不整合の候補");
// P19 同一商品で回収が出荷より10pt以上低い
if (g === "回収" && m !== null) {
const m2 = med[f + "|出荷|" + p];
if (m2 !== null && m2 !== undefined && m2 - m >= 0.10) push(hits, "P19", fg, label, "商品単位", last.name + "(" + p + ")回収" + pct(m) + "に対し出荷" + pct(m2));
}
if (vv.length >= 10) {
// P20 枚数と読取率の相関
const xs = vv.map(x => x.den as number), ys = vv.map(x => x.rate as number);
const n = xs.length, mx = xs.reduce((s, x) => s + x, 0) / n, my = ys.reduce((s, y) => s + y, 0) / n;
const sx = Math.sqrt(xs.reduce((s, x) => s + (x - mx) ** 2, 0)), sy = Math.sqrt(ys.reduce((s, y) => s + (y - my) ** 2, 0));
if (sx > 0 && sy > 0) {
const r = xs.reduce((s, x, i) => s + (x - mx) * (ys[i] - my), 0) / (sx * sy);
if (r <= -0.30) push(hits, "P20", fg, label, "商品単位", last.name + "(" + p + ")枚数と読取率の相関係数 " + r.toFixed(2) + "(" + n + "日)");
}
// P21 繁忙日に低下が偏る
const mq = median(xs) as number;
const busy = vv.filter(x => (x.den as number) >= mq * BUSY).map(x => x.rate as number);
const norm = vv.filter(x => (x.den as number) < mq * BUSY).map(x => x.rate as number);
if (busy.length >= 3 && norm.length >= 3) {
const gap = norm.reduce((s, x) => s + x, 0) / norm.length - busy.reduce((s, x) => s + x, 0) / busy.length;
if (gap >= 0.05) push(hits, "P21", fg, label, "商品単位", last.name + "(" + p + ")繁忙日の平均が通常日より" + (gap * 100).toFixed(1) + "pt低い");
}
}
}
// P17 特定商品だけ恒常的に低い(ゲート全体は正常)
const byGate: { [fg: string]: { p: string; m: number; name: string }[] } = {};
for (const k of Object.keys(med)) {
const m = med[k]; if (m === null) continue;
const [f, g, p] = k.split("|"); const fg = f + "|" + g;
const v = idx.byFGP[k];
(byGate[fg] = byGate[fg] || []).push({ p: p, m: m, name: v[v.length - 1].name });
}
for (const fg of Object.keys(byGate)) {
if (skip[fg]) continue;
const ms = byGate[fg]; if (ms.length < 3) continue;
const ok = median(ms.map(x => x.m)) as number;
if (ok < low) continue;
for (const x of ms) if (x.m < low) push(hits, "P17", fg, label, "商品単位", x.name + "(" + x.p + ")だけ期間中央値" + pct(x.m) + "(他商品の中央値" + pct(ok) + ")");
}
// P08 直近4週の週平均が単調に10pt以上低下
for (const k of Object.keys(idx.byFGP)) {
const [f, g, p] = k.split("|"); const fg = f + "|" + g;
if (skip[fg]) continue;
const vv = idx.byFGP[k].filter(x => x.den && x.rate !== null);
if (vv.length < 28) continue;
const wk: { [w: string]: number[] } = {};
for (const x of vv) { const w = isoWeek(x.date); (wk[w] = wk[w] || []).push(x.rate as number); }
const ks = Object.keys(wk).sort(); if (ks.length < 4) continue;
const avg = ks.map(w => wk[w].reduce((s, y) => s + y, 0) / wk[w].length);
const t = avg.slice(-4);
if (t[0] > t[1] && t[1] > t[2] && t[2] > t[3] && t[0] - t[3] >= 0.10) push(hits, "P08", fg, label, "商品単位", vv[vv.length - 1].name + "(" + p + ")直近4週の週平均が単調低下(" + pct(t[0]) + "→" + pct(t[3]) + ")");
}
}
function isoWeek(d: string): string {
const dt = new Date(Date.UTC(parseInt(d.slice(0, 4)), parseInt(d.slice(5, 7)) - 1, parseInt(d.slice(8, 10))));
const day = (dt.getUTCDay() + 6) % 7;
dt.setUTCDate(dt.getUTCDate() - day + 3);
const y = dt.getUTCFullYear();
const first = new Date(Date.UTC(y, 0, 4));
const wn = 1 + Math.round(((dt.getTime() - first.getTime()) / 86400000 - 3 + ((first.getUTCDay() + 6) % 7)) / 7);
return y + "-" + ("0" + wn).slice(-2);
}
// ---------- 工場ごとにまとめる(Teams には1工場1通) ----------
function groupByFactory(hits: Hit[]): Group[] {
const by: { [f: string]: Hit[] } = {};
for (const h of hits) (by[h.factory] = by[h.factory] || []).push(h);
const out: Group[] = [];
for (const f of Object.keys(by).sort()) {
const hs = by[f];
const ids = hs.map(h => h.hypothesisId).filter((x, i, a) => a.indexOf(x) === i).join("・");
// Teams のカードで改行されるよう、行の区切りは空行(\n\n)にする
const text = hs.map(h => "**■" + h.gate + "ゲート " + h.targetDate + " " + h.hypothesisId + " " + h.hypothesisName + "**" +
"\n\n根拠:" + h.evidence + "\n\n現場に確認すること:" + h.check).join("\n\n");
out.push({ factory: f, hitCount: hs.length, hypothesisIds: ids, text: text, cardText: JSON.stringify(text).slice(1, -1) });
}
return out;
}
B付録B 確認依頼カード(Adaptive Card 1.4)
設定ファイルのフローに組み込んであります。文言を変えたいときの参考です(フローの「カードをチャネルに送る」「カードを個人に送る」の メッセージ の欄)。
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "読取状況の確認のお願い",
"weight": "Bolder",
"size": "Medium",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "工場",
"value": "@{items('工場ごと')?['factory']}"
},
{
"title": "対象日",
"value": "@{outputs('判定結果')?['targetDate']}"
},
{
"title": "該当件数",
"value": "@{items('工場ごと')?['hitCount']}件(@{items('工場ごと')?['hypothesisIds']})"
}
]
},
{
"type": "TextBlock",
"text": "@{items('工場ごと')?['cardText']}",
"wrap": true,
"spacing": "Medium"
},
{
"type": "TextBlock",
"text": "現場で確認した結果を入力し、[回答する]を押してください。",
"wrap": true,
"spacing": "Large",
"weight": "Bolder"
},
{
"type": "Input.ChoiceSet",
"id": "result",
"label": "確認した結果",
"isRequired": true,
"errorMessage": "確認した結果を選んでください",
"style": "expanded",
"choices": [
{
"title": "仮説のとおりだった(原因が分かった)",
"value": "仮説のとおり"
},
{
"title": "仮説とは違った",
"value": "仮説と違う"
},
{
"title": "まだ確認中・分からない",
"value": "確認中"
}
]
},
{
"type": "Input.Text",
"id": "cause",
"label": "確認した内容・原因",
"isMultiline": true,
"placeholder": "例:前日の仕上が夜間にずれ込み、翌朝に出荷ゲートを通していた"
},
{
"type": "Input.Text",
"id": "action",
"label": "対応したこと",
"isMultiline": true,
"placeholder": "例:出荷ゲートを通す時刻を仕上の当日中に変更した"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "回答する"
}
]
}
C付録C フローの中身
| アクションの名前 | 種類 | コネクタ | 内容 |
|---|---|---|---|
| 毎朝の起動 | 繰り返し | 組み込み | 集計ファイルの前日分の更新が終わった後の時刻にする(更新中に動かさない) |
| 読取率の下限 | 変数を初期化する | 組み込み | 単位は %。これを下回ると「低い」とみなす(例:85/90)。変えたら次の朝から反映 |
| 情シスの通知先 | 変数を初期化する | 組み込み | 未更新のお知らせと、チャネルが見つからない工場の確認依頼を送る方のメールアドレス |
| テスト送信先 | 変数を初期化する | 組み込み | 試験中は試験する方のメールアドレス(すべての確認依頼がこの方に届く)。本番は空欄にする |
| チームID | 変数を初期化する | 組み込み | 工場ごとのチャネルがあるチーム。チャネル名が工場名と同じチャネルに確認依頼を送る |
| 判定 | SharePoint ライブラリからスクリプトを実行する | Excel Online (Business) | 集計ファイルを読むだけ(書き換えない)。targetDate=判定する日(空欄=データの最終日)、expectedDate=入っているはずの日(空欄=日本時間の前日、- =確認しない) |
| 判定結果 | 作成 | 組み込み | スクリプトが返した JSON を読む |
| 前日分が入っているか | 条件 | 組み込み | |
| はい:チャネルを探すか | 条件 | 組み込み | |
| はい:チャネルの一覧 | チャネルを一覧表示する | Microsoft Teams | チームのチャネルの一覧(チャネル名で工場を探す) |
| はい:該当なしの記録 | 条件 | 組み込み | |
| はい:該当なしを記録 | 項目を作成する | SharePoint | エージェントが「最新日は該当なし」と答えられるよう、該当が無い日も1行残す |
| はい:工場ごと | それぞれに適用する | 組み込み | 工場ごとに1通。同時実行10(オフのままだと、1つ目の工場が回答するまで次の工場にカードが届かない) |
| 繰り返しの中:日次判定結果に記録 | 項目を作成する | SharePoint | |
| 繰り返しの中:その工場のチャネル | アレイのフィルター処理 | 組み込み | チャネル名が工場名(「工場」を除いた名前でもよい)と同じ標準チャネル(プライベート・共有チャネルには送れないため除く) |
| 繰り返しの中:送り先 | 条件 | 組み込み | テスト送信先が空欄で、工場のチャネルが見つかればチャネルへ。それ以外は個人あて |
| はい:カードをチャネルに送る | アダプティブ カードをポストして応答を待つ | Microsoft Teams | 工場のチャネルに確認依頼を送り、最初の回答を待つ(最大30日) |
| いいえ:カードを個人に送る | アダプティブ カードをポストして応答を待つ | Microsoft Teams | 試験中はテスト送信先へ。本番でチャネルが見つからない工場は情シスの通知先へ |
| 繰り返しの中:回答 | 作成 | 組み込み | |
| 繰り返しの中:回答なしの連絡 | チャットまたはチャネルでメッセージを投稿する | Microsoft Teams | 回答が無いまま期限を過ぎた・カードを送れなかったときだけ動く(実行を失敗で終わらせず、情シスに知らせる) |
| 繰り返しの中:実証ログに記録 | 項目を作成する | SharePoint | 回答があったときだけ記録する(回答が無いまま30日たつと、この工場の分は記録されずに終わる) |
| 繰り返しの中:この工場の終わり | 作成 | 組み込み | 回答の記録か、回答なしの連絡のどちらかが済んだら終わる |
| いいえ:未更新の通知 | チャットまたはチャネルでメッセージを投稿する | Microsoft Teams |