// Příklad: docházkový terminál (píchačka) v příkazové řádce nad Profirest API v2. // // PROFIREST_BASE_URL adresa API (výchozí https://api.mujprofirest.cz) // PROFIREST_TOKENS soubor s tokeny (výchozí ./profirest-pichacka.json) – chraňte ho jako heslo // // Na terminálu se nikdo nepřihlašuje: každé píchnutí nese PIN zaměstnance. using System.Text; using Profirest.Api; using Profirest.Api.Models; using Profirest.Api.Storage; Console.OutputEncoding = Encoding.UTF8; Console.InputEncoding = Encoding.UTF8; var api = new ProfirestApi(new ProfirestApiOptions { BaseUrl = Environment.GetEnvironmentVariable("PROFIREST_BASE_URL") ?? "https://api.mujprofirest.cz", ClientId = "profirest-dochazka", // tokeny přežijí restart; soubor se zamyká, takže obnovu tokenu udělá jen jeden proces Storage = new FileTokenStorage(Environment.GetEnvironmentVariable("PROFIREST_TOKENS") ?? "profirest-pichacka.json"), OnUnpaired = _ => Console.WriteLine("Zařízení bylo odpárováno – spusťte příklad znovu a spárujte ho."), }); // 1) Spárování kódem (jen poprvé): vedoucí kód zadá v administraci (Správa PC / zařízení → Připojit zařízení). if (!api.IsPaired) { var pairing = await api.PairAsync(new PairOptions { Scope = "restaurants:read staff:read staff:authenticate attendance:read attendance:punch", DeviceName = "Píchačka (příklad)", }); Console.WriteLine("KÓD: " + pairing.UserCode); Console.WriteLine("Potvrďte ho v administraci, nebo otevřete " + pairing.VerificationUriComplete); try { await pairing.Connected; // čeká, dokud vedoucí kód nepotvrdí (výpadek sítě přečká) } catch (ProfirestOAuthException e) when (e.Error is "access_denied" or "expired_token") { Console.WriteLine(e.Error == "access_denied" ? "Vedoucí párování odmítl." : "Kód vypršel (10 minut bez potvrzení) – spusťte příklad znovu."); return 1; } } // provozovnu zařízení si klient pamatuje z GET /v2/connection if (api.RestaurantId == null) { await api.ConnectionAsync(); } var rid = api.RestaurantId!; var punchesPath = $"/restaurants/{rid}/attendance/punches"; Console.WriteLine($"Provozovna {rid}. Příkazy: p PIN = příchod, o PIN = odchod, pz PIN = začátek přestávky,"); Console.WriteLine("kz PIN = konec přestávky, k = kdo je v práci, odparovat = odpárovat a skončit, q = konec."); var types = new Dictionary { ["p"] = "clock_in", ["o"] = "clock_out", ["pz"] = "break_start", ["kz"] = "break_end", }; string? line; while ((line = Console.ReadLine()) != null) { var parts = line.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) { continue; } var cmd = parts[0].ToLowerInvariant(); if (cmd == "q") { break; } if (cmd == "odparovat") { await api.UnpairAsync(); // zapomene tokeny a zneplatní je i na serveru Console.WriteLine("Zařízení je odpárované."); break; } try { if (cmd == "k") { // seznam po stránkách – AllAsync projde všechny foreach (var p in await api.AllAsync($"/restaurants/{rid}/attendance/presence")) { Console.WriteLine($" {p.Employee.Name} – {Stav(p.State)}"); } } else if (types.TryGetValue(cmd, out var type) && parts.Length == 2) { await Punch(type, parts[1]); } else { Console.WriteLine("Neznámý příkaz."); } } catch (ProfirestApiException e) { Console.WriteLine(Describe(e)); } } return 0; async Task Punch(string type, string pin) { // Id vznikne JEDNOU na píchnutí (UUIDv7) – opakování se stejným Id server nikdy nezapíše dvakrát var body = new AttendancePunchCreate { Id = ProfirestApi.Uuid(), Type = type, Credential = new StaffCredential { Method = "pin", Secret = pin }, }; for (;;) { try { var punch = await api.PostAsync(punchesPath, body); Console.WriteLine($"OK: {punch!.Employee.Name} – {Stav(punch.State)}"); return; } catch (ProfirestApiException e) when (e.Code == ErrorCodes.StaffAmbiguous) { // stejný PIN má víc lidí: vybrat a poslat znovu se stejným Id a s employee_id Console.WriteLine("Tento PIN má víc zaměstnanců, vyberte číslo:"); for (var i = 0; i < e.Candidates.Count; i++) { Console.WriteLine($" {i + 1}) {e.Candidates[i].Name}"); } if (!int.TryParse(Console.ReadLine(), out var n) || n < 1 || n > e.Candidates.Count) { Console.WriteLine("Píchnutí zrušeno."); return; } body.Credential.EmployeeId = e.Candidates[n - 1].Id; } } } // stav zaměstnance česky (nové hodnoty mohou přibýt – pak se vypíše, jak přišla) static string Stav(string state) => state switch { "working" => "v práci", "on_break" => "na přestávce", "off" => "mimo práci", _ => state, }; // chyby podle kódu (Code), ne podle HTTP stavu – kódy se nemění static string Describe(ProfirestApiException e) => e.Code switch { ErrorCodes.StaffCredentialsInvalid => "Neznámý PIN.", ErrorCodes.StaffLocked => $"Příliš mnoho špatných PINů – zkuste to za {e.RetryAfter ?? 60} s.", ErrorCodes.AttendanceStateConflict => "Nejde: " + e.Title, ErrorCodes.NetworkError or ErrorCodes.Timeout => "Bez sítě – zkuste znovu.", ErrorCodes.NotPaired => "Zařízení není spárované – spusťte příklad znovu.", _ => $"Chyba: {e.Title} ({e.Code})", };