はじめに
前回の記事、WinFormsでPaSoRi RC-S300とNTAG 215 NFCカード読み込ませるでプログラムでPaSoRiが動いたので、プリザンターに勤怠管理システムを導入してみる。

3,000円くらいしたけど、もっと安いものは無いのだろうか。
環境
- Windows 11 Pro
- Visual Studio 2026
- C#(.NET 10)
- NTAG 215 NFCカード
- PaSoRi RC-S300
- プリザンター(バージョン1.4.20.0)
構成イメージ
C#のプログラムから、PaSoRi経由でNFCカードが読み込まれたら、NFCカードテーブルでIDが存在するユーザーを取得し、データが存在する場合は勤怠管理テーブルにユーザーID、出勤日、出勤時刻、退勤時刻を登録する、というシンプルな構成です。
シーケンス図
プリザンターのテーブルを作る
今回プリザンター側で用意するテーブルは、あえて 2つだけ にしています。
理由はシンプルで、NFC勤怠で必要なことは大きく分けて次の2つしかないからです。
- 「このUIDは誰のカードか?」を引けること(カードとユーザーの紐付け)
- 「いつ出勤して、いつ退勤したか?」を記録できること(勤怠の実績)
この2つを1テーブルに詰め込むこともできますが、運用していくと「カードの登録・変更」と「勤怠の記録」が別のタイミングで発生します。
例えば、カードを紛失して再発行した場合は UIDだけ差し替えたい ですが、過去の勤怠データは当然そのまま残したいです。
そこで今回は、役割を分けて次の構成にしました。
-
NFCカードテーブル:ユーザーとUIDの対応表(マスタ)
- 目的:UID → ユーザーを特定するための“名簿”
- 主な更新タイミング:カードの新規登録、カード交換、紛失時の再登録など(たまに)
-
勤怠管理テーブル:出勤/退勤の履歴(トランザクション)
- 目的:日々の打刻結果を蓄積する“日誌”
- 主な更新タイミング:出勤・退勤のたび(頻繁)
この分け方にしておくと、処理の流れも分かりやすくなります。
- PaSoRiでUIDを読む
- NFCカードテーブルでUIDを検索してユーザーを特定する
- 勤怠管理テーブルへ当日の出勤/退勤を登録(または更新)する
テーブルを増やさないぶん、まずは「動く最小構成(MVP)」として組み立てやすいのもメリットです。
あとから「休憩開始/終了」「打刻端末ID」「打刻種別(直行・直帰)」などを足したくなったら、勤怠管理テーブルに列を追加するだけで拡張できます。
プリザンター側のテーブル
ここからは、プリザンター側で用意する2つのテーブルについて具体的に作っていきます。
ポイントは次の通りです。
- NFCカードテーブルは「UID → ユーザー」を引くための 対応表(マスタ)
- 勤怠管理テーブルは出勤・退勤を記録する 履歴(トランザクション)
- C#アプリからはプリザンターのWeb APIで検索・登録・更新するので、テーブル構造は「APIから扱いやすい」ことを意識します
今回の構成では、カードをかざしたときにまず UIDでNFCカードテーブルを検索してユーザーを特定し、見つかった場合だけ **勤怠管理テーブルに当日の出勤/退勤を登録(または更新)**します。
そのためテーブルを設計する際も、
- UIDで検索できること(= NFCカードにUID列がある)
- ユーザーと日付で当日の勤怠を一意に扱えること(= 勤怠管理にユーザー列と出勤日列がある)
- 出勤時刻・退勤時刻を持てること
の3点を満たすようにしています。
※ここでは「1日1レコード方式」にしていて、出勤時にレコードを作成し、退勤時は同じレコードの退勤時刻を更新する作りにしています。
(打刻ログを全部残したい場合は「1打刻=1レコード」にする方法もありますが、今回は最小構成でシンプルにしました)
NFCカード
プリザンターのユーザーにNFCカードを登録するテーブルを作成します。
テーブルにはユーザーとNFCのUIDを登録するシンプルなテーブル構造にしました。
ユーザーはプリザンターのユーザーを取得するように[[Users]]を設定しています。
実際にユーザー001にUIDを登録するとこのようになります。
勤怠管理テーブル
勤怠管理テーブルは出勤日、出勤時刻、退勤時刻、ユーザーの4つで構成しています。
C#のソースコード
前回の記事で書いたプログラムを参考にWinFormsでとりあえず作りました。ボタンイベントでの発火となります。
画面側(Form1.cs)
Form1.cs はボタン押下を起点に、PaSoRiからUIDを取得してプリザンターの「NFCカード」テーブルでユーザーを特定します。
当日の勤怠レコードが無ければ出勤(DateA)を作成し、既にあって退勤未入力なら退勤(DateB)を更新します。
PaSoRi読み取りは ReadUidFromPasoriAsync() を前回記事の実装に差し替えるだけで動く想定です。
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace NfcKintaiSample
{
public partial class Form1 : Form
{
private readonly PleasanterClient _pleasanter;
public Form1()
{
InitializeComponent();
_pleasanter = new PleasanterClient(new PleasanterClientOptions
{
BaseUrl = "http://localhost", // 例: http://localhost or https://your-pleasanter
ApiKey = "YOUR_API_KEY", // 例: APIキーを使う場合
NfcCardSiteId = 10736, // NFCカード
AttendanceSiteId = 10738 // 勤怠管理テーブル
});
}
private async void btnScan_Click(object sender, EventArgs e)
{
btnScan.Enabled = false;
try
{
Log("カードをかざしてください…");
// 1) PaSoRiからUIDを読む(ここを前回記事の実装に差し替え)
var uid = await ReadUidFromPasoriAsync();
if (string.IsNullOrWhiteSpace(uid))
{
Log("UIDが取得できませんでした。");
return;
}
Log($"UID: {uid}");
// 2) NFCカードテーブルでUIDに紐づくユーザーを検索(ClassB=UID)
var user = await _pleasanter.FindUserByUidAsync(uid);
if (user is null)
{
Log("未登録カードです(NFCカードテーブルにUIDを登録してください)。");
return;
}
Log($"ユーザー特定: {user.UserDisplay}(UserId={user.UserId})");
// 3) 今日の勤怠レコードを検索(ClassA=ユーザー, DateC=出勤日)
var today = DateOnly.FromDateTime(DateTime.Now);
var record = await _pleasanter.FindTodayAttendanceAsync(user.UserId, today);
// 4) 出勤 or 退勤
var now = DateTime.Now;
if (record is null)
{
// 当日レコードなし = 出勤
await _pleasanter.CreateAttendanceAsync(user.UserId, today, clockIn: now);
Log($"出勤登録しました: {now:yyyy/MM/dd HH:mm:ss}");
}
else if (record.ClockOut is null)
{
// 当日レコードあり & 退勤なし = 退勤
await _pleasanter.UpdateClockOutAsync(record.ResultId, clockOut: now);
Log($"退勤登録しました: {now:yyyy/MM/dd HH:mm:ss}");
}
else
{
Log("本日はすでに出勤・退勤済みです。");
}
}
catch (Exception ex)
{
Log("エラー: " + ex.Message);
}
finally
{
btnScan.Enabled = true;
}
}
// ----------------------------------------
// ここが差し替えポイント(前回記事のPaSoRi読み取り)
// ----------------------------------------
private Task<string> ReadUidFromPasoriAsync()
{
// TODO: 前回記事の「PaSoRiでNTAG215のUIDを読む処理」を呼び出す
// 例: return Task.FromResult(uidHexString);
// デモ用(削除して差し替えてください)
return Task.FromResult("04A1B2C3D4E5F6");
}
private void Log(string message)
{
txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}{Environment.NewLine}");
}
}
}
プリザンターとのデータ連携を行うクラスを作成
PleasanterClientOptions はプリザンター接続に必要な設定値をまとめたクラスです。
BaseUrl と ApiKey に接続先と認証情報を設定し、NfcCardSiteId / AttendanceSiteId に各テーブル(サイト)のIDを指定します。
環境ごとに変わるのは基本ここだけなので、まずこの値を自分のプリザンターに合わせて変更します。
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace NfcKintaiSample
{
public sealed class PleasanterClientOptions
{
public required string BaseUrl { get; init; }
public string? ApiKey { get; init; }
public required long NfcCardSiteId { get; init; }
public required long AttendanceSiteId { get; init; }
}
public sealed class PleasanterClient
{
private readonly PleasanterClientOptions _opt;
private readonly HttpClient _http;
private readonly JsonSerializerOptions _json;
public PleasanterClient(PleasanterClientOptions options)
{
_opt = options;
_http = new HttpClient { BaseAddress = new Uri(_opt.BaseUrl.TrimEnd('/') + "/") };
// ★認証(環境に合わせて差し替え)
// 例: APIキーをヘッダーに付ける運用の場合
if (!string.IsNullOrWhiteSpace(_opt.ApiKey))
{
_http.DefaultRequestHeaders.Add("X-Api-Key", _opt.ApiKey);
}
_json = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}
// --- 1) UIDからユーザー特定(NFCカード:ClassB=UID / ClassA=ユーザー) ---
public async Task<NfcUser?> FindUserByUidAsync(string uid)
{
// 実際のAPIパラメータやエンドポイントは環境で異なることがあるので、
// まずは「NFCカードサイトからClassBで検索 → 1件目のClassA(=ユーザー)を取る」方針で書いています。
var body = new
{
// siteId を指定して検索
siteId = _opt.NfcCardSiteId,
view = new
{
columnFilterHash = new
{
ClassB = uid
}
}
};
var res = await PostJsonAsync("api/items/get", body);
var payload = await ReadJsonAsync<PleasanterItemsResponse>(res);
var first = payload?.Data?.Length > 0 ? payload.Data[0] : null;
if (first is null) return null;
// ClassAがユーザー。プリザンターのユーザーIDをどう返すかは環境で差があるため、
// ここは「数値IDが取れる」前提のサンプルにしています。
if (!long.TryParse(first.ClassA, out var userId)) return null;
return new NfcUser
{
UserId = userId,
UserDisplay = first.ClassA
};
}
// --- 2) 当日の勤怠(ClassA=ユーザー, DateC=出勤日)を検索 ---
public async Task<AttendanceRecord?> FindTodayAttendanceAsync(long userId, DateOnly date)
{
var body = new
{
siteId = _opt.AttendanceSiteId,
view = new
{
columnFilterHash = new
{
ClassA = userId.ToString(),
DateC = date.ToString("yyyy-MM-dd")
}
}
};
var res = await PostJsonAsync("api/items/get", body);
var payload = await ReadJsonAsync<PleasanterItemsResponse>(res);
var first = payload?.Data?.Length > 0 ? payload.Data[0] : null;
if (first is null) return null;
return new AttendanceRecord
{
ResultId = first.ResultId,
ClockIn = ParseDateTimeNullable(first.DateA),
ClockOut = ParseDateTimeNullable(first.DateB)
};
}
// --- 3) 出勤登録(DateA) ---
public async Task CreateAttendanceAsync(long userId, DateOnly date, DateTime clockIn)
{
var body = new
{
siteId = _opt.AttendanceSiteId,
data = new
{
ClassA = userId.ToString(),
DateC = date.ToString("yyyy-MM-dd"),
DateA = clockIn.ToString("yyyy-MM-ddTHH:mm:ss")
}
};
var res = await PostJsonAsync("api/items/create", body);
res.EnsureSuccessStatusCode();
}
// --- 4) 退勤更新(DateB) ---
public async Task UpdateClockOutAsync(long resultId, DateTime clockOut)
{
var body = new
{
id = resultId,
data = new
{
DateB = clockOut.ToString("yyyy-MM-ddTHH:mm:ss")
}
};
var res = await PostJsonAsync("api/items/update", body);
res.EnsureSuccessStatusCode();
}
// ------------------------------
// HTTP helper
// ------------------------------
private async Task<HttpResponseMessage> PostJsonAsync(string path, object body)
{
var json = JsonSerializer.Serialize(body, _json);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
var res = await _http.PostAsync(path, content);
if (!res.IsSuccessStatusCode)
{
var txt = await res.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Pleasanter API error: {(int)res.StatusCode} {res.ReasonPhrase}\n{txt}");
}
return res;
}
private async Task<T?> ReadJsonAsync<T>(HttpResponseMessage res)
{
var json = await res.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(json, _json);
}
private static DateTime? ParseDateTimeNullable(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return null;
return DateTime.TryParse(s, out var dt) ? dt : null;
}
}
// ---- DTO ----
public sealed class NfcUser
{
public required long UserId { get; init; }
public required string UserDisplay { get; init; }
}
public sealed class AttendanceRecord
{
public required long ResultId { get; init; }
public DateTime? ClockIn { get; init; }
public DateTime? ClockOut { get; init; }
}
public sealed class PleasanterItemsResponse
{
[JsonPropertyName("data")]
public PleasanterItem[]? Data { get; set; }
}
public sealed class PleasanterItem
{
[JsonPropertyName("resultId")]
public long ResultId { get; set; }
[JsonPropertyName("classA")]
public string? ClassA { get; set; }
[JsonPropertyName("classB")]
public string? ClassB { get; set; }
[JsonPropertyName("dateA")]
public string? DateA { get; set; }
[JsonPropertyName("dateB")]
public string? DateB { get; set; }
[JsonPropertyName("dateC")]
public string? DateC { get; set; }
}
}
実際に動かしてみる
この様な形で登録されます。
さいごに
今回は PaSoRi で読み取った NFC のUIDをキーに、プリザンターの2テーブルだけで勤怠打刻を実現しました。
ReadUidFromPasoriAsync() を前回記事の実装に差し替えるだけで動くので、まずは手元の環境で試してみてください。
今後はNFCのUIDカードを登録する仕組みが必要となるので、その辺りを実装できればと思います。





