// Příklad: docházkový terminál v příkazové řádce nad Profirest API v2 (Kotlin). // // Překlad a spuštění: kotlinc -cp profirest-api-1.0.0.jar Pichacka.kt -include-runtime -d pichacka.jar // java -cp pichacka.jar:profirest-api-1.0.0.jar PichackaKt // 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) 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.ProfirestApi import cz.mujprofirest.api.model.AttendancePresence import cz.mujprofirest.api.model.AttendancePunch import java.io.PrintStream import kotlin.system.exitProcess const val SCOPE = "restaurants:read staff:read staff:authenticate attendance:read attendance:punch" val out = PrintStream(System.out, true, "UTF-8") val input = System.`in`.bufferedReader(Charsets.UTF_8) fun env(name: String, fallback: String) = System.getenv(name)?.takeIf { it.isNotEmpty() } ?: fallback fun prompt(): String? { out.print("> ") out.flush() return input.readLine() } fun main() { // Klient: tokeny v souboru, obnovuje je sám (jednou, i když běží víc vláken nebo procesů). // Klient blokuje – v aplikaci pro Android ho volejte z withContext(Dispatchers.IO) { … }. val api = ProfirestApi.builder() .baseUrl(env("PROFIREST_BASE_URL", "https://api.mujprofirest.cz")) .clientId("profirest-dochazka") .storage(FileStorage(env("PROFIREST_TOKENS", "./profirest-pichacka.json"))) .onUnpaired { 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) { val pairing = api.pair(PairOptions.builder().scope(SCOPE).deviceName("Píchačka (příklad)").build()) out.println("KÓD: ${pairing.userCode}") out.println("Potvrďte ho na ${pairing.verificationUriComplete}") try { pairing.awaitConnected() // blokuje, dokud vedoucí kód nepotvrdí (dotazuje se po 5 s) } catch (e: OAuthException) { out.println(when (e.error) { "access_denied" -> "Vedoucí párování odmítl." "expired_token" -> "Kód vypršel (10 minut bez potvrzení)." else -> "Párování selhalo: ${e.message}" }) exitProcess(1) } } if (api.restaurantId == null) api.connection() // provozovnu si klient zapamatuje v úložišti out.println("Provozovna ${api.restaurantId}") 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") val types = mapOf("p" to "clock_in", "o" to "clock_out", "pz" to "break_start", "kz" to "break_end") // 2) Příkazy po řádcích ze stdin; EOF (null) = konec. while (true) { val w = (prompt() ?: break).trim().split(Regex("\\s+")) when { w[0].isEmpty() -> continue w[0] == "q" -> break w[0] in types && w.size == 2 -> punch(api, types.getValue(w[0]), w[1]) w[0] == "k" -> whoIsIn(api) w[0] == "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. */ fun punch(api: ProfirestApi, type: String, pin: String) { val credential = mutableMapOf("method" to "pin", "secret" to pin) val body = mapOf("id" to ProfirestApi.uuid(), "type" to type, "credential" to credential) val path = "/restaurants/${api.restaurantId}/attendance/punches" while (true) { try { val r = api.post(path, body, AttendancePunch::fromJson) out.println("OK: ${r.employee.name} – ${stav(r.state)}") return } catch (e: ApiException) { when (e.code) { "staff_ambiguous" -> { // stejný PIN má víc lidí: vybrat a poslat znovu se stejným id out.println("Tento PIN má víc zaměstnanců:") e.candidates.forEachIndexed { i, c -> out.println(" ${i + 1}) ${c["name"]}") } val n = prompt()?.trim()?.toIntOrNull() if (n == null || n !in 1..e.candidates.size) { out.println("Zrušeno.") return } credential["employee_id"] = e.candidates[n - 1]["id"].toString() } "staff_credentials_invalid" -> return out.println("Neznámý PIN.") "staff_locked" -> return out.println("Příliš mnoho špatných PINů – zkuste to za ${e.retryAfter} s.") "attendance_state_conflict" -> return out.println(e.title) // např. odchod bez příchodu ErrorCode.NETWORK_ERROR, ErrorCode.TIMEOUT -> return out.println("Bez sítě – zkuste znovu.") else -> return out.println("${e.title} (${e.code})") } } } } /** Stav zaměstnance česky (nové hodnoty mohou přibýt – pak se vypíše, jak přišla). */ fun stav(state: String?): String? = when (state) { "working" -> "v práci" "on_break" -> "na přestávce" "off" -> "mimo práci" else -> state } /** Kdo je v práci: všechny stránky seznamu přítomnosti. */ fun whoIsIn(api: ProfirestApi) { try { api.all("/restaurants/${api.restaurantId}/attendance/presence", null, AttendancePresence::fromJson) .forEach { out.println(" ${it.employee.name} – ${stav(it.state)}") } } catch (e: ApiException) { out.println("${e.title} (${e.code})") } }