PowerShell で自動勤怠スクリプトを作成し OneDrive 経由で SharePoint に保存

# Windows のサインインログを勤怠に活用し SharePoint にログ保存する
タイムカードをデジタル実装しようと思えば、 Microsoft forms を利用したり Power Apps でスマホボタンを作ったりと方法はいろいろあるが、タイムカードは不要、勤怠の記録は取りたいと要望されると、 Windows のサインイン、サインアウトログを取得して記録する方が確実だろうとなったため、いろいろと調べて PowerShell でタスクスケジューラーを定期実行する方法に落ち着いた。

なお、ファイルサーバーを SharePoint に移行していたため、 OneDrive 経由で SharePoint 上のディレクトリにログを記録する方法を模索して実装している。
## 前提条件
* SharePoint 上でドキュメントライブラリを利用してファイルサーバーとして運用している
* OneDrive for Business で SharePoint ライブラリにショートカットを作成している
* スクリプトがログを保存するディレクトリにはスクリプトを動作させる端末からアクセスできる必要あり
## タスクスケジューラーにタスクを登録するスクリプト
まずは、最終的に毎日実行するスクリプトをタスクスケジューラーから自動実行するために、事前の準備を行う。
スクリプトを実行する際に、端末に実行状態のプロンプト画面が表示されないようにするため、タスクスケジューラーからはまず vbs スクリプトを呼び出す。
```vb:getLog_task.vbs
CreateObject("WScript.Shell").Run "C:\pcmaint\getLog_task.bat",0
```
https://github.com/shokun0803/getLog_script/blob/main/pcmaint/getLog_task.vbs

呼び出された vbs スクリプトからさらに bat ファイルを呼び出して実行する。
この時 bat スクリプト内で PowerShell が実行できるように、 1 行目におまじないを書いておく。以降は PowerShell スクリプトを直接記述可能だ。
```vb
@SETLOCAL&SETLOCAL ENABLEDELAYEDEXPANSION&(chcp 65001>NUL)&PUSHD "%~dp0"&(SET SCRIPT_PATH=%~f0)&(SET SCRIPT_DIR=%~dp0)&(SET SCRIPT_NAME=%~nx0)&(SET SCRIPT_BASE_NAME=%~n0)&(SET SCRIPT_EXTENSION=%~x0)&(SET SCRIPT_ARGUMENTS=%*)&(POWERSHELL -NoLogo -Sta -NoProfile -ExecutionPolicy Unrestricted "&([scriptblock]::create('$OutputEncoding=[Console]::OutputEncoding;'+\"`n\"+((gc -Encoding UTF8 -Path \"!SCRIPT_PATH!\"|?{$_.readcount -gt 1})-join\"`n\")))" !SCRIPT_ARGUMENTS!)&(SET EXIT_CODE=!ERRORLEVEL!)&POPD&PAUSE&EXIT !EXIT_CODE!&ENDLOCAL&GOTO :EOF
```
コードの意味的には簡単に説明すると、 1 行目を飛ばして 2 行目以降を PowerShell コマンドとして読み込み、 PowerShell に渡している。 bat コマンドとしては`GOTO :EOF`でコマンドを終了している。
いろいろと PowerShell コマンドを使いたかったので bat ファイルに PowerShell コマンドを埋め込む方法を採用した。

ここから PowerShell になるが、以下の設定は OneDrive に何らかの問題があり、 SharePoint 上のファイルが読み込めなかったりした場合に警告のポップアップ表示を行うための記述だ。
この記述はスクリプトの最初に記載しておかないと動かないことがあるため注意が必要だ。
```PowerShell
# ポップアップ初期設定
$wsobj = new-object -comobject wscript.shell;
```
次の記述は実際に OneDrive 経由で SharePoint 上のファイルにアクセスするためのパスを作成する記述だ。
OneDrive for Business は、 Windows 上のユーザーディレクトリ内、具体的には`C:\Users\User_name\OneDrive - Company_name\`のような位置にディレクトリが作られる。もちろん`User_name`はそれぞれの端末のユーザーアカウント名が使用されるため、汎用のスクリプトを作るにはいろいろと問題がある。
ユーザーディレクトリを取得する環境変数もあるが、それだと一般に公開する際には`OneDrive - Company_name`部分をそれぞれ変更してもらう必要があるため、OneDrive for Business のディレクトリが収められている環境変数を探して利用している。ちなみに一般向けの OneDrive の場合は環境変数も違っているので注意が必要だ。
```PowerShell
# Server パスをセット
$serverPath = "\file-server\pcmaint"

# OneDrive 上のパスをセット
$drivePath = [Environment]::GetEnvironmentVariable("OneDriveCommercial", "User")

# パスをセット
$path = Join-Path $drivePath $serverPath
```
実際にはそれぞれの環境に合わせて`$serverPath = ""`の部分を書き換えれば運用できると思われる。残念ながら他の環境では試していないため、検証はできていない。
最終的に`Join-Path`で SharePoint 上の処理スクリプトが存在している場所までのパスを生成している。

次に、実際に実行させる`ps1`拡張子の PowerShell スクリプトをチェック用のファイル名として、パスの存在チェックを実施している。ここでパスが確認できなかったら、最初に記述していたポップアップを利用してユーザーに警告を表示している。
```PowerShell
# 存在チェック用のスクリプト名
$file_name = "getLog_script.ps1";

# 存在チェック用のスクリプトパス
$chkPath = Join-Path $path $file_name;

# パスの存在をチェック
if( -not ( Test-Path -Path $chkPath ) ) {
	# パスが探せないのでエラーを表示して終了
	$result = $wsobj.popup("SharePoint にアクセスできません。`r`n`r`nシステム担当に確認してください。",0,"OneDrive アクセス警告");
	exit;
}
```
最後に実際にログを取得するためのスクリプトが記載された PowerShell スクリプトファイルを実行する。
```PowerShell
# スクリプトファイルを実行
& $chkPath;
```
少し回りくどいことをしているが、その理由として、まずはタスクスケジューラーからバックグラウンドでコマンドプロンプトが表示されずに実行できること、そのため実際の実行スクリプトはローカルに存在している必要がある。
なぜローカルに存在している必要があるかというと、タスクスケジューラーの設定もスクリプトを利用して汎用のタスクを生成しているためだ。ユーザーごとにパスの違うタスクを作るには面倒だったためこのような方法を取っている。

また、ログを取得する PowerShell スクリプトは共有環境に置いておきたかった。その理由としては後からスクリプトを改修しても、全ユーザーでそのまま実行が可能だからだ。
ローカルにスクリプトを置いておくと、何か改修が必要となるたびに全ユーザーのスクリプトを置き換える必要がある。そのため汎用的に呼び出すスクリプトだけを各ユーザーのローカルに配置する方法を取っている。

https://github.com/shokun0803/getLog_script/blob/main/pcmaint/getLog_task.bat

ローカルに配置するファイルが揃ったら、これらスクリプトとタスクスケジューラーを設定するスクリプトを各ユーザーに実行してもらう。

最初のポップアップ設定、パスのセット、存在チェックまでは前のスクリプトと同じだ。
```PowerShell
# ポップアップ初期設定
$wsobj = new-object -comobject wscript.shell;

# Server パスをセット
$serverPath = "\file-server\pcmaint"

# OneDrive 上のパスをセット
$drivePath = [Environment]::GetEnvironmentVariable("OneDriveCommercial", "User")

# パスをセット
$path = Join-Path $drivePath $serverPath

# 存在チェック用のスクリプトパス
$chkPath = Join-Path $path getLog_task.vbs;

# パスの存在をチェック
if( -not ( Test-Path -Path $chkPath ) ) {
	# パスが探せないのでエラーを表示して終了
	$result = $wsobj.popup("SharePoint にアクセスできません。`r`n`r`nシステム担当に確認してください。",0,"OneDrive アクセス警告");
	exit;
}
```
ここから各端末のローカルに必要なファイルの配置を行っていくことになる。
まずはファイルを配置するためのディレクトリを確認し、なければ作成する。
```PowerShell
# ローカルのディレクトリの存在を確認
$dir = "C:\pcmaint";
if( -not ( Test-Path -Path $dir ) ) {
	# ディレクトリがなければ作成
	New-Item $dir -ItemType Directory;
}
```
次に、該当のディレクトリに実行に必要なファイルが存在しているか確認し、ファイルがなければ SharePoint 上の共有ディレクトリからファイルをコピーしてきて配置する。都合 2 つのファイルを確認し配置したら準備は完了だ。
```PowerShell
# 実行スクリプトの存在を確認
$vbsFile = Join-Path $dir getLog_task.vbs;
if( -not ( Test-Path -Path $vbsFile ) ) {
	# なければファイルサーバーからコピー
	$fromFile = Join-Path $path getLog_task.vbs;
	Copy-Item -Path $fromFile -Destination $vbsFile
}
$batFile = Join-Path $dir getLog_task.bat;
if( -not ( Test-Path -Path $batFile ) ) {
	# なければファイルサーバーからコピー
	$fromFile = Join-Path $path getLog_task.bat;
	Copy-Item -Path $fromFile -Destination $batFile
}
```
最後に配置したスクリプトを定期実行するためのタスクをタスクスケジューラーに登録する。
実行時間等は適宜調整してもらえればよいが、一応お昼時間に実行することで業務の負荷にならないようにしている。なお、昼になってもしばらく業務を続けている人がいることを想定して、 12 時 30 分から 30 分程度ディレイを設けて実行している。
実際の実行時間を見ていると、 12 時 30 分から 13 時の間に実行されているようだ。
```PowerShell
# スクリプトの実行をタスクスケジューラーに登録
$action = New-ScheduledTaskAction -Execute $vbsFile;
$trigger = New-ScheduledTaskTrigger -DaysInterval 1 -Daily -At "12:30 PM" -RandomDelay "00:30:00";
$settings = New-ScheduledTaskSettingsSet -Compatibility Win8 -RestartInterval "00:01:00" -RestartCount 3 -ExecutionTimeLimit "01:00:00" -Hidden;

try{
	$tasks = (Get-ScheduledTask -TaskName pcmaint -TaskPath \ -ErrorAction stop);
} catch {
	Register-ScheduledTask -TaskPath \ -TaskName pcmaint -Action $action -Trigger $trigger -Settings $settings;
	$tasks = (Get-ScheduledTask -TaskName pcmaint -TaskPath \ -ErrorAction stop);
}
```
ファイルの存在確認をした際に用意された vbs ファイルまでのパスを使い、実行するスクリプトを`New-ScheduledTaskAction`として設定、トリガーに毎日実行として`New-ScheduledTaskTrigger`を設定している。
これを`try` `catch`でタスクスケジューラーへの登録を実行している。

最後に念のためこのスクリプトを実行したユーザーを csv に登録して終了している。
```PowerShell
# タスク登録が完了したユーザーを登録
$username = $env:UserName;
$td = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss');
$outfile = Join-Path $path setup_user.csv;
$data = @(
	[pscustomobject]@{
		Time = $td
		User = $username
	}
);
$data | Export-Csv -NoTypeInformation $outfile -Encoding UTF8 -Append;
```
この PowerShell スクリプトファイルを各ユーザーに実行してもらい、ログ取得スクリプトの自動実行環境が準備できたことになる。

https://github.com/shokun0803/getLog_script/blob/main/pcmaint/getLog_taskschedule_set.ps1

なお、注意事項として、`ps1`拡張子のスクリプトをファイルサーバー経由やメール添付等で配布しても、ダブルクリックで素直に実行はできない。現在の Windows 環境ではローカル以外から持ち込まれたスクリプトファイル等には「セキュリティ」項目が追加され制限されるため、プロパティから「許可する」にチェックを入れる必要がある。
さらに PowerShell の実行時にも制限が発生することがあるため、管理者権限で実行するか、実行時に表示されるセキュリティの説明に適切に対応する必要がある。当方は簡易マニュアルを用意して対応した。
## Windows のログを取得するスクリプト
今回は Windows 10 、 Windows 11 に対応可能なスクリプトとして作られている。
Windows 10 以降、高速に起動、終了を実行するため、 Windows の停止状態にも複数の状態が存在している。そのため、ログに関しても複数のログが存在しているため、適切に取得してやる必要がある。

最近はテレワークを実施することも増えてきたことから、 VPN で外部から接続し、リモートデスクトップで端末に接続するには、 Windows 端末が起動状態である必要があった。
そのため、ユーザーが端末を終了せずに起動したまま退社することもあり、このままでは端末のログを取得しても出退勤としては使えないことになる。しかし、リモートデスクトップ接続は端末がサインアウトしていても、起動さえしていれば接続が可能であるため、各ユーザーには必ずサインアウト、サインインを実行してもらう事とした。

タスクスケジューラーで自動実行されるスクリプトから、 SharePoint 上のログ取得スクリプトが実行される。問題が発生した場合のポップアップ表示を行うための設定も含め、基本的にはスクリプトの序盤は先ほどのスクリプトと構成は同じだ。
```PowerShell
# ポップアップ初期設定
$wsobj = new-object -comobject wscript.shell;

# Server パスをセット
$serverPath = "\file-server\timecard_log"

# OneDrive 上のパスをセット
$drivePath = [Environment]::GetEnvironmentVariable("OneDriveCommercial", "User")

# パスをセット
$path = Join-Path $drivePath $serverPath

# 存在チェック用のスクリプトパス
$chkPath = Join-Path $path path_chk.txt;

# パスの存在をチェック
if( -not ( Test-Path -Path $chkPath ) ) {
	# パスが探せないのでエラーを表示して終了
	$result = $wsobj.popup("SharePoint にアクセスできません。`r`n`r`nシステム担当に確認してください。",0,"OneDrive アクセス警告");
	exit;
}
```
このスクリプトではパスとしてログ保存用の専用ディレクトリを用意して設定している。そのため存在チェック用のファイルも同じ場所に用意している必要がある。
`path_chk.txt`という空ファイルを作成し該当ディレクトリに置いているため、チェック用のパスとしても該当のテキストファイルを指定している。
もちろんディレクトリそのものをチェック対象としても問題ない。適切に作り変えて頂きたい。

初期設定が終わったら、次に端末のユーザー情報を取得している。ファイル名生成程度ならば端末名等の取得程度でもいいが、作成されたログのファイル名にユーザーの漢字の名称を付与したかったため、フルネームの取得も行っている。
```PowerShell
# 端末のユーザー名を取得
$username = $env:UserName;
$uinfo = (Get-WmiObject Win32_UserAccount | Where-Object { $_.name -eq $username });
$fullname = $uinfo.FullName;
```
次にログデータの取得を行うため、スクリプト実行当日の日付を取得、ログの保存ディレクトリを年ごと（年度ではない）にしたいため、当年のディレクトリが存在していなかったらディレクトリを作成している。
```PowerShell
# 今日の日付
$td = (Get-Date);

# 年のディレクトリを確認、存在していなければ作成
$year = Join-Path $path $td.Year;
if( -not ( Test-Path -Path $year ) ) {
	# ディレクトリがなければ作成
	New-Item $year -ItemType Directory;
}
```
次に、それぞれのユーザーのログファイルの存在を確認し、まだ存在していなければ新規にファイルを生成することから開始している。
Windows には端末が使用されていれば、それなりにログが残っている。このスクリプトを実行した時からのログではなく、ログが残っていれば本年のログをすべて取得して初期ファイルとして作成するように構成している。

具体的には、まだログファイルが生成されていなかった場合、その年の 1 月 1 日`yyyy-01-01 00:00:00`から本日`yyyy-MM-dd 23:59:59`を`AddDays(-1)`した前日までのログを取得しようと試みる（本日のサインアウト時間はまだない）。
```PowerShell
# 自分のファイルを確認、存在してなければ今年のファイルを新規生成
$fullname = $fullname.Replace("　", " ");
$file_name = $fullname.Replace(" ", "_") + ".csv";
$file = Join-Path $year $file_name;
if( -not ( Test-Path -Path $file ) ) {
	# 取得開始日時、終了日時設定
	$sd = Get-Date $td.ToString('yyyy-01-01 00:00:00');
	$date = Get-Date $td.ToString('yyyy-MM-dd 23:59:59');
	$ed = $date.AddDays(-1);
```
Windows 10 以降で起動と終了（サインイン、サインアウト）のログを取得しようとする場合、コードで`7001`と`7002`を取得すればよい。
```PowerShell
	# 起動終了時間の取得
	$array = GET-WinEvent -FilterHashTable @{LogName='System'; StartTime=$sd; EndTime=$ed} | Where-Object{$_.Id -eq 7001 -or $_.Id -eq 7002} | select-Object TimeCreated,Id,Message | Sort-Object -Property @{E = {Get-Date $_.TimeCreated}};
```
取得したログデータから日時情報を適切に整形している。これは過去の勤怠情報取得スクリプトが生成していた形式に合わせるための記載であるため、それぞれの事情に合わせて成形してほしい。
```PowerShell
	# 日時データのみ成形
	$csvlist = @();
	$flag = 0;
	foreach ($data in $array) {
		if( $data.Id -eq "7001" ) {
			$attend = $data.TimeCreated.ToString('HH:mm:ss');
			$flag = 1;
		} elseif( $data.Id -eq "7002" -and $flag -eq 0 ) {
			$csvlist += @(
				[PSCustomObject]@{
					date = $data.TimeCreated.ToString('MM-dd')
					leav = ""
					attend = $data.TimeCreated.ToString('HH:mm:ss')
				}
			);
			$flag = 0;
		} elseif( $data.Id -eq "7002" -and $flag -eq 1 ) {
			$csvlist += @(
				[PSCustomObject]@{
					date = $data.TimeCreated.ToString('MM-dd')
					leav = $attend
					attend = $data.TimeCreated.ToString('HH:mm:ss')
				}
			);
			$flag = 0;
		}
	}
```
最後にできあがったログデータを新規ファイルとして csv に書き込んでいる。
```PowerShell
	# 新規ファイルの書き込み
	$csvlist | ConvertTo-Csv -NoTypeInformation | Select-Object -Skip 1 | % {$_ -replace '"',''} | Set-Content $file -Encoding UTF8;
```
すでにそれぞれのユーザーのログファイルが作られている場合、取得データも一部のみとし、既存のファイルに追記する形で保存している。

まずはすでに存在している csv ファイルを開き、最後の1行を取得、日付データに修正しスクリプトで使えるようにしている。
```PowerShell
# 自分のファイルが存在している場合は、追記のみ実施
} else {
	# CSVファイルの最終日時を取得
	$last = Get-Content $file | Select-Object -Last 1;
	$date = Get-Date $td.ToString('yyyy-' + $last.split(",")[0] + " " + $last.split(",")[2]);
```
取得した最終データを元に、スタート日時を`$date.AddMinutes(1)`として、最終ログ日時から 1 分後を設定している。これはログの最後の時間を含めず、同日のログ以降のサインイン、サインアウトのログを取り逃さないようにするための指定だ。
```PowerShell
	# 取得開始日時、終了日時設定（既存ファイルの最終時間から前日の24時まで）
	$sd = $date.AddMinutes(1);
	$date = Get-Date $td.ToString('yyyy-MM-dd 23:59:59');
	$ed = $date.AddDays(-1);
	# 起動終了時間の取得
	$array = GET-WinEvent -FilterHashTable @{LogName='System'; StartTime=$sd; EndTime=$ed} | Where-Object{$_.Id -eq 7001 -or $_.Id -eq 7002} | select-Object TimeCreated,Id,Message | Sort-Object -Property @{E = {Get-Date $_.TimeCreated}};
```
ログデータを取得する方法、ログデータの成形に関しては新規ファイル作成時と同じだ。
```PowerShell
	# 日時データのみ成形
	$csvlist = @();
	$flag = 0;
	foreach ($data in $array) {
		if( $data.Id -eq "7001" ) {
			$attend = $data.TimeCreated.ToString('HH:mm:ss');
			$flag = 1;
		} elseif( $data.Id -eq "7002" -and $flag -eq 0 ) {
			$csvlist += @(
				[PSCustomObject]@{
					date = $data.TimeCreated.ToString('MM-dd')
					leav = ""
					attend = $data.TimeCreated.ToString('HH:mm:ss')
				}
			);
			$flag = 0;
		} elseif( $data.Id -eq "7002" -and $flag -eq 1 ) {
			$csvlist += @(
				[PSCustomObject]@{
					date = $data.TimeCreated.ToString('MM-dd')
					leav = $attend
					attend = $data.TimeCreated.ToString('HH:mm:ss')
				}
			);
			$flag = 0;
		}
	}
```
最後に既存ファイルに追記する形でログを記録している。
```PowerShell
	# 既存ファイルへの追記
	$csvlist | ConvertTo-Csv -NoTypeInformation | Select-Object -Skip 1 | % {$_ -replace '"',''} | Add-Content $file -Encoding UTF8;

}
```
## サインアウトのログなし警告をポップアップ表示する
ここからはログの記録には必須ではないが、ユーザーがサインアウトをしないで放置してしまうと、出退勤のログとして意味をなさなくなってしまうため、サインイン、サインアウトのログがなければ警告を表示するようにしている。

前日サインアウトしていなければ警告を表示するが、前日とは前営業日のことであり、月曜日から見た場合、前日とは金曜日のサインアウトログを確認する必要がある。
日曜日に出勤して仕事をしている場合、日曜日に土曜日のサインアウトを確認して警告を表示してしまわないように、一応日曜日も判定条件として取得している。
```PowerShell
# ここからサインアウトしていない場合の警告を表示するスクリプト
# 前日のサインイン、サインアウトを調べる基準日の算出
if( $td.DayOfWeek.value__ -eq 1 ) {
	# 月曜
	$sd = Get-Date $td.AddDays(-3).ToString('yyyy-MM-dd 00:00:00');
	$ed = Get-Date $td.AddDays(-3).ToString('yyyy-MM-dd 23:59:59');
} elseif( $td.DayOfWeek.value__ -eq 0 ) {
	# 日曜
	$sd = Get-Date $td.AddDays(-2).ToString('yyyy-MM-dd 00:00:00');
	$ed = Get-Date $td.AddDays(-2).ToString('yyyy-MM-dd 23:59:59');
} else {
	# それ以外
	$sd = Get-Date $td.AddDays(-1).ToString('yyyy-MM-dd 00:00:00');
	$ed = Get-Date $td.AddDays(-1).ToString('yyyy-MM-dd 23:59:59');
}
```
基準日として取得した「前日」のログを取得する。
```PowerShell
# 前日のデータを取得
$data = GET-WinEvent -FilterHashTable @{LogName='System'; StartTime=$sd; EndTime=$ed} | Where-Object{$_.Id -eq 6005 -or $_.Id -eq 6006 -or $_.Id -eq 6008 -or $_.Id -eq 7002 -or $_.Id -eq 7001} | select-Object TimeCreated,Id,Message;
```
次に警告に表示するため、最後のログオフデータを取得している。一応過去 1 週間程度を取得して最後の 1 行を取得しているが、それ以上ログが存在しない（ 1 週間以上休暇とか）場合は警告にも表示ができない。
```PowerShell
# 最後のログオフデータを取得
$lastsd = Get-Date $td.AddDays(-7).ToString('yyyy-MM-dd 00:00:00');
$lasted = Get-Date $td.AddDays(-1).ToString('yyyy-MM-dd 23:59:59');
$lastlog = GET-WinEvent -FilterHashTable @{LogName='System'; StartTime=$lastsd; EndTime=$lasted} | Where-Object{$_.Id -eq 7002} | select-Object -First 1 TimeCreated;
$lastlog = $lastlog.TimeCreated;
```
前日のデータを元に警告を表示している。基準日の日程にログがない`$data.Count -ne 0`場合にサインアウト、サインインそれぞれを判断して警告をポップアップ表示している。
```PowerShell
# 前日のデータを元に警告を表示
if( $data.Count -ne 0 ) {
	$attend = $data | select @{L = "TimeCreated"; E = {Get-Date $_.TimeCreated}},Id | sort TimeCreated -Descending | where {$_.Id -eq 7001};
	$leav = $data | select @{L = "TimeCreated"; E = {Get-Date $_.TimeCreated}},Id | sort TimeCreated | where {$_.Id -eq 7002};
	if( -not ($leav) ) {
		# 前日サインアウトなし警告表示
		$result = $wsobj.popup("前日のサインアウト記録がありません。`r`n`r`n業務終了後には必ずサインアウトを実施してください。`r`n`r`n最終サインアウト日時：$lastlog",0,"サインアウト警告");
	}
	if( -not ($attend) ) {
		# 前日サインインなし警告表示
		$result = $wsobj.popup("前日のサインイン記録がありません。`r`n`r`n業務終了後には必ずサインアウトを実施してください。",0,"サインイン警告");
	}
} else {
```
前日のログがない場合でも、土日祝日だった場合は問題ないこともある。別途土日祝日判定スクリプトを実行し、戻ってきたコードが 1 だった場合は平日だったと判定し、警告を表示している。
なお、個人の休暇は判定できないため、警告表示もそのように記載している。
```PowerShell
	# 前日の記録なし、土日祝日を判定
	$date = $sd.ToString('yyyyMMdd');

  # パスをセット
	$path = Join-Path  $PSScriptRoot check_holiday.ps1;

	# 土日祝日判定スクリプトを実行
	."$path" $date;

	if( $LastExitCode -eq 1 ) {
		# 前日の記録なし、休みだったかも
		$result = $wsobj.popup("前日のサインイン、サインアウト記録がありません。`r`n`r`n業務終了後には必ずサインアウトを実施してください。`r`n`r`n休みだった場合はこのメッセージを無視してください。`r`n`r`n最終ログオフ日時：$lastlog",0,"前日の記録なし警告");
	}
}
```
なお、土日祝日判定のスクリプトは他の方の作成したスクリプトをほぼそのまま流用させて頂いているため、該当のサイトを確認して頂きたい。該当のサイトは GitHub 上の土日判定スクリプト内に記載している。

https://github.com/shokun0803/getLog_script/blob/main/pcmaint/getLog_script.ps1

https://github.com/shokun0803/getLog_script/blob/main/pcmaint/check_holiday.ps1

サインアウトの記録として注意することがひとつある。
例えば Windows Update で再起動を要求してきた場合、一見再起動してサインアウトされた状態のように見えるが、実は再起動後、自動的にサインインされて、ロック状態（パスワードの入力画面）になっているだけである。
Windows Update があったから、インストールして再起動してそのまま退社してしまうと、翌日のサインインの記録が残らなくなってしまう。

今回のスクリプトは、残すログデータとしては当日の最後のサインアウトとかではなく、すべてのログを残すようにしている。これは帰宅後にリモートデスクトップ接続で夜間帯の緊急業務を実施したとしてもログとして残すためと、最初と最後の取得ではこのような場合長時間勤務していたことになるため、すべてのデータを残す設定としている。

しかし、警告表示は前日のサインアウト情報がなければ警告してしまうため、警告されてしまうという事と、再起動後にサインインされていることから、翌日のサインインの記録が残らず、一晩中勤務していたことになってしまうため、注意が必要だ。

中小企業の DX 施策として Microsoft 365 の環境があれば使える仕組みであるので、試してみてはどうだろうか。ファイルの保存先をローカル NAS 等に変更すれば SharePoint がなくても使えるはずだ。適切に書き換えてみてもらいたい。