3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターとNFCカードで勤怠管理システムを作成する

3
Last updated at Posted at 2026-03-04

はじめに

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

image.png
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つしかないからです。

  1. 「このUIDは誰のカードか?」を引けること(カードとユーザーの紐付け)
  2. 「いつ出勤して、いつ退勤したか?」を記録できること(勤怠の実績)

この2つを1テーブルに詰め込むこともできますが、運用していくと「カードの登録・変更」と「勤怠の記録」が別のタイミングで発生します。
例えば、カードを紛失して再発行した場合は UIDだけ差し替えたい ですが、過去の勤怠データは当然そのまま残したいです。

そこで今回は、役割を分けて次の構成にしました。

  • NFCカードテーブル:ユーザーとUIDの対応表(マスタ)
    • 目的:UID → ユーザーを特定するための“名簿”
    • 主な更新タイミング:カードの新規登録、カード交換、紛失時の再登録など(たまに)
  • 勤怠管理テーブル:出勤/退勤の履歴(トランザクション)
    • 目的:日々の打刻結果を蓄積する“日誌”
    • 主な更新タイミング:出勤・退勤のたび(頻繁)

この分け方にしておくと、処理の流れも分かりやすくなります。

  1. PaSoRiでUIDを読む
  2. NFCカードテーブルでUIDを検索してユーザーを特定する
  3. 勤怠管理テーブルへ当日の出勤/退勤を登録(または更新)する

テーブルを増やさないぶん、まずは「動く最小構成(MVP)」として組み立てやすいのもメリットです。
あとから「休憩開始/終了」「打刻端末ID」「打刻種別(直行・直帰)」などを足したくなったら、勤怠管理テーブルに列を追加するだけで拡張できます。

image.png

プリザンター側のテーブル

ここからは、プリザンター側で用意する2つのテーブルについて具体的に作っていきます。

ポイントは次の通りです。

  • NFCカードテーブルは「UID → ユーザー」を引くための 対応表(マスタ)
  • 勤怠管理テーブルは出勤・退勤を記録する 履歴(トランザクション)
  • C#アプリからはプリザンターのWeb APIで検索・登録・更新するので、テーブル構造は「APIから扱いやすい」ことを意識します

今回の構成では、カードをかざしたときにまず UIDでNFCカードテーブルを検索してユーザーを特定し、見つかった場合だけ **勤怠管理テーブルに当日の出勤/退勤を登録(または更新)**します。

そのためテーブルを設計する際も、

  1. UIDで検索できること(= NFCカードにUID列がある)
  2. ユーザーと日付で当日の勤怠を一意に扱えること(= 勤怠管理にユーザー列と出勤日列がある)
  3. 出勤時刻・退勤時刻を持てること

の3点を満たすようにしています。

※ここでは「1日1レコード方式」にしていて、出勤時にレコードを作成し、退勤時は同じレコードの退勤時刻を更新する作りにしています。
(打刻ログを全部残したい場合は「1打刻=1レコード」にする方法もありますが、今回は最小構成でシンプルにしました)

NFCカード

プリザンターのユーザーにNFCカードを登録するテーブルを作成します。
テーブルにはユーザーとNFCのUIDを登録するシンプルなテーブル構造にしました。

image.png

ユーザーはプリザンターのユーザーを取得するように[[Users]]を設定しています。

image.png

実際にユーザー001にUIDを登録するとこのようになります。

image.png

勤怠管理テーブル

勤怠管理テーブルは出勤日、出勤時刻、退勤時刻、ユーザーの4つで構成しています。

image.png

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 はプリザンター接続に必要な設定値をまとめたクラスです。
BaseUrlApiKey に接続先と認証情報を設定し、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; }
    }
}

実際に動かしてみる

この様な形で登録されます。

image.png

さいごに

今回は PaSoRi で読み取った NFC のUIDをキーに、プリザンターの2テーブルだけで勤怠打刻を実現しました。
ReadUidFromPasoriAsync() を前回記事の実装に差し替えるだけで動くので、まずは手元の環境で試してみてください。
今後はNFCのUIDカードを登録する仕組みが必要となるので、その辺りを実装できればと思います。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?