// Příkladový klient Profirest API v2: docházkový terminál v příkazové řádce. // // swift run Pichacka // // Při prvním spuštění zařízení spáruje – vypíše kód, který vedoucí potvrdí v administraci (Správa PC / zařízení → // Připojit zařízení). Pak čte příkazy: // p PIN příchod o PIN odchod // pz PIN začátek přestávky kz PIN konec přestávky // k kdo je v práci odparovat zapomenout spárování q konec // // Proměnné prostředí: PROFIREST_BASE_URL (výchozí https://api.mujprofirest.cz), // PROFIREST_TOKENS (soubor s tokeny, výchozí ./profirest-pichacka.json – chraňte ho jako heslo). import Foundation import ProfirestAPI /// Řádek na výstup hned (bez vyrovnávací paměti – terminál i roura ho uvidí okamžitě). func say(_ text: String) { FileHandle.standardOutput.write(Data((text + "\n").utf8)) } func ask(_ text: String) { FileHandle.standardOutput.write(Data(text.utf8)) } /// Celý terminál ve funkci – žádné globální proměnné (main.swift s async kódem je jinak izoluje na hlavní actor). func run() async { let env = ProcessInfo.processInfo.environment func setting(_ name: String, _ fallback: String) -> String { if let v = env[name], !v.isEmpty { return v } return fallback } let api = ProfirestApi(options: .init( baseURL: setting("PROFIREST_BASE_URL", "https://api.mujprofirest.cz"), clientID: "profirest-dochazka", storage: FileTokenStorage(path: setting("PROFIREST_TOKENS", "profirest-pichacka.json")))) // 1) spárování kódem (RFC 8628) – jen poprvé, tokeny pak zůstávají v souboru if !api.isPaired { do { let pairing = try await api.pair( scope: "restaurants:read staff:read staff:authenticate attendance:read attendance:punch", deviceName: "Píchačka (příklad)") say("Zařízení není spárované. Vedoucí potvrdí kód v administraci (Správa PC / zařízení → Připojit zařízení).") say("KÓD: \(pairing.userCode)") say("nebo otevřete: \(pairing.verificationURIComplete)") try await pairing.connected() // ptá se podle intervalu, než vedoucí potvrdí } catch let e as ProfirestOAuthError { switch e.error { case "access_denied": say("Vedoucí párování odmítl.") case "expired_token": say("Kód vypršel (10 minut). Spusťte příklad znovu.") default: say("Párování se nepovedlo: \(e.message)") } exit(1) } catch { say("Párování se nepovedlo: \(error.localizedDescription)") exit(1) } } if api.restaurantID == nil { _ = try? await api.connection() // zapamatuje provozovnu zařízení } guard let rid = api.restaurantID else { say("Provozovna zařízení není známá (GET /v2/connection selhalo). Spusťte příklad znovu.") exit(1) } say("Provozovna \(rid). Příkazy: p/o/pz/kz PIN, k = kdo je v práci, odparovat, q = konec.") let types = ["p": "clock_in", "o": "clock_out", "pz": "break_start", "kz": "break_end"] let states = ["working": "v práci", "on_break": "na přestávce", "off": "mimo práci"] // 2) píchnutí – id vznikne jednou na píchnutí; při opakování (výběr zaměstnance, výpadek) se posílá stejné func punch(type: String, pin: String) async { var body = AttendancePunchCreate(id: ProfirestApi.uuid(), type: type, credential: StaffCredential(method: "pin", secret: pin)) while true { do { let r: AttendancePunch = try await api.post("/restaurants/\(rid)/attendance/punches", body: body) say("OK: \(r.employee.name) – \(states[r.state] ?? r.state)") return } catch let e as ProfirestAPIError { switch e.code { case ErrorCode.staffAmbiguous: // stejný PIN má víc lidí – vybrat a poslat znovu say("Tento PIN má víc lidí:") for (i, c) in e.candidates.enumerated() { say(" \(i + 1)) \(c["name"]?.stringValue ?? "?")") } ask("Číslo: ") let choice = Int((readLine() ?? "").trimmingCharacters(in: .whitespaces)) ?? 0 guard choice >= 1, choice <= e.candidates.count, let id = e.candidates[choice - 1]["id"]?.stringValue else { say("Zrušeno.") return } body.credential?.employeeId = id continue case ErrorCode.staffCredentialsInvalid: say("Neznámý PIN.") case ErrorCode.staffLocked: say("Moc špatných pokusů – zkuste to za \(e.retryAfter.map(String.init) ?? "?") s.") case ErrorCode.attendanceStateConflict: say("Teď to nejde: \(e.title)") case ErrorCode.networkError, ErrorCode.timeout: say("Bez sítě – zkuste znovu.") // skutečný terminál by píchnutí uložil a poslal dávkou case ErrorCode.notPaired: // vedoucí zařízení odpároval say("Zařízení bylo odpárováno. Spusťte příklad znovu.") exit(1) default: say("Chyba: \(e.title) (\(e.code))") } return } catch { say("Chyba: \(error.localizedDescription)") return } } } // 3) smyčka příkazů loop: while let line = readLine() { let parts = line.trimmingCharacters(in: .whitespaces).split(separator: " ", maxSplits: 1).map { $0.trimmingCharacters(in: .whitespaces) } let cmd = parts.first?.lowercased() ?? "" do { if let type = types[cmd], parts.count > 1 { await punch(type: type, pin: parts[1]) } else if cmd == "k" { for p in try await api.all("/restaurants/\(rid)/attendance/presence", as: AttendancePresence.self) { say(" \(p.employee.name): \(states[p.state] ?? p.state)") } } else if cmd == "odparovat" { await api.unpair() say("Spárování zapomenuto.") break loop } else if cmd == "q" { break loop } else if !cmd.isEmpty { say("Příkazy: p/o/pz/kz PIN, k, odparovat, q") } } catch let e as ProfirestAPIError { if e.code == ErrorCode.notPaired { say("Zařízení bylo odpárováno. Spusťte příklad znovu.") exit(1) } say("Chyba: \(e.title) (\(e.code))") } catch { say("Chyba: \(error.localizedDescription)") } } } await run()