import cz.mujprofirest.api.ApiException; import cz.mujprofirest.api.ErrorCode; import cz.mujprofirest.api.FileStorage; import cz.mujprofirest.api.OAuthException; import cz.mujprofirest.api.PairOptions; import cz.mujprofirest.api.Pairing; import cz.mujprofirest.api.ProfirestApi; import cz.mujprofirest.api.model.AttendancePresence; import cz.mujprofirest.api.model.AttendancePunch; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Příklad: docházkový terminál v příkazové řádce nad Profirest API v2. * * Spuštění (Java 11+): java -cp profirest-api-1.0.0.jar Pichacka.java * Proměnné prostředí: PROFIREST_BASE_URL (výchozí https://api.mujprofirest.cz), * PROFIREST_TOKENS (výchozí ./profirest-pichacka.json – tokeny zařízení, nikomu je nedávejte) */ public class Pichacka { static final String SCOPE = "restaurants:read staff:read staff:authenticate attendance:read attendance:punch"; static final PrintStream out = new PrintStream(System.out, true, StandardCharsets.UTF_8); static BufferedReader in; static ProfirestApi api; public static void main(String[] args) throws IOException { String base = env("PROFIREST_BASE_URL", "https://api.mujprofirest.cz"); String tokens = env("PROFIREST_TOKENS", "./profirest-pichacka.json"); in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); // Klient: tokeny v souboru, obnovuje je sám (jednou, i když běží víc vláken nebo procesů). api = ProfirestApi.builder() .baseUrl(base) .clientId("profirest-dochazka") .storage(new FileStorage(tokens)) .onUnpaired(reason -> out.println("Zařízení bylo odpárováno – spusťte příklad znovu a spárujte ho.")) .build(); // 1) Spárování kódem: vedoucí ho zadá v administraci (Správa PC / zařízení → Připojit zařízení). if (!api.isPaired()) { Pairing pairing = api.pair(PairOptions.builder().scope(SCOPE).deviceName("Píchačka (příklad)").build()); out.println("KÓD: " + pairing.getUserCode()); out.println("Potvrďte ho na " + pairing.getVerificationUriComplete()); try { pairing.awaitConnected(); // blokuje, dokud vedoucí kód nepotvrdí (dotazuje se po 5 s) } catch (OAuthException e) { out.println("access_denied".equals(e.getError()) ? "Vedoucí párování odmítl." : "expired_token".equals(e.getError()) ? "Kód vypršel (10 minut bez potvrzení)." : "Párování selhalo: " + e.getMessage()); System.exit(1); } } if (api.getRestaurantId() == null) { api.connection(); // provozovnu si klient zapamatuje v úložišti } out.println("Provozovna " + api.getRestaurantId()); out.println("Příkazy: p PIN = příchod, o PIN = odchod, pz PIN = začátek přestávky, kz PIN = konec přestávky,"); out.println(" k = kdo je v práci, odparovat = odpárovat zařízení, q = konec"); // 2) Příkazy po řádcích ze stdin. String line; while ((line = prompt()) != null) { String[] w = line.trim().split("\\s+"); String cmd = w[0]; Map types = new LinkedHashMap<>(); types.put("p", "clock_in"); types.put("o", "clock_out"); types.put("pz", "break_start"); types.put("kz", "break_end"); if (cmd.isEmpty()) { continue; } else if (cmd.equals("q")) { break; } else if (types.containsKey(cmd) && w.length == 2) { punch(types.get(cmd), w[1]); } else if (cmd.equals("k")) { whoIsIn(); } else if (cmd.equals("odparovat")) { api.unpair(); // zapomene tokeny a zneplatní je na serveru out.println("Zařízení je odpárované."); break; } else { out.println("Neznámý příkaz."); } } } /** Jedno píchnutí. id vznikne jednou – opakování se stejným id server nikdy nezapíše dvakrát. */ static void punch(String type, String pin) throws IOException { Map credential = new LinkedHashMap<>(); credential.put("method", "pin"); credential.put("secret", pin); Map body = new LinkedHashMap<>(); body.put("id", ProfirestApi.uuid()); body.put("type", type); body.put("credential", credential); String path = "/restaurants/" + api.getRestaurantId() + "/attendance/punches"; for (;;) { try { AttendancePunch r = api.post(path, body, AttendancePunch::fromJson); out.println("OK: " + r.employee.name + " – " + stav(r.state)); return; } catch (ApiException e) { switch (e.getCode()) { case "staff_ambiguous": // stejný PIN má víc lidí: vybrat a poslat znovu se stejným id List> c = e.getCandidates(); out.println("Tento PIN má víc zaměstnanců:"); for (int i = 0; i < c.size(); i++) { out.println(" " + (i + 1) + ") " + c.get(i).get("name")); } String choice = prompt(); int n; try { n = Integer.parseInt(choice == null ? "" : choice.trim()); } catch (NumberFormatException x) { n = 0; } if (n < 1 || n > c.size()) { out.println("Zrušeno."); return; } credential.put("employee_id", String.valueOf(c.get(n - 1).get("id"))); continue; case "staff_credentials_invalid": out.println("Neznámý PIN."); return; case "staff_locked": out.println("Příliš mnoho špatných PINů – zkuste to za " + e.getRetryAfter() + " s."); return; case "attendance_state_conflict": out.println(e.getTitle()); // např. odchod bez příchodu return; case ErrorCode.NETWORK_ERROR: case ErrorCode.TIMEOUT: out.println("Bez sítě – zkuste znovu."); return; default: out.println(e.getTitle() + " (" + e.getCode() + ")"); return; } } } } /** Stav zaměstnance česky (nové hodnoty mohou přibýt – pak se vypíše, jak přišla). */ static String stav(String state) { if ("working".equals(state)) { return "v práci"; } if ("on_break".equals(state)) { return "na přestávce"; } return "off".equals(state) ? "mimo práci" : state; } /** Kdo je v práci: všechny stránky seznamu přítomnosti. */ static void whoIsIn() { try { List all = api.all("/restaurants/" + api.getRestaurantId() + "/attendance/presence", null, AttendancePresence::fromJson); for (AttendancePresence p : all) { out.println(" " + p.employee.name + " – " + stav(p.state)); } } catch (ApiException e) { out.println(e.getTitle() + " (" + e.getCode() + ")"); } } static String prompt() throws IOException { out.print("> "); out.flush(); return in.readLine(); } static String env(String name, String fallback) { String v = System.getenv(name); return v == null || v.isEmpty() ? fallback : v; } }