// Příkladový klient Profirest API v2: docházkový terminál v příkazové řádce. // // go run ./examples/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). package main import ( "bufio" "context" "errors" "fmt" "os" "strconv" "strings" profirest "github.com/profirest/profirest-api-go" "github.com/profirest/profirest-api-go/model" ) var states = map[string]string{"working": "v práci", "on_break": "na přestávce", "off": "mimo práci"} func state(s string) string { if v, ok := states[s]; ok { return v } return s } func main() { ctx := context.Background() base := os.Getenv("PROFIREST_BASE_URL") if base == "" { base = "https://api.mujprofirest.cz" } tokens := os.Getenv("PROFIREST_TOKENS") if tokens == "" { tokens = "profirest-pichacka.json" } api := profirest.New(profirest.Options{ BaseURL: base, ClientID: "profirest-dochazka", Storage: profirest.NewFileStorage(tokens), }) // 1) spárování kódem (RFC 8628) – jen poprvé, tokeny pak zůstávají v souboru if !api.IsPaired() { pairing, err := api.Pair(ctx, profirest.PairOptions{ Scope: "restaurants:read staff:read staff:authenticate attendance:read attendance:punch", DeviceName: "Píchačka (příklad)", }) if err == nil { fmt.Println("Zařízení není spárované. Vedoucí potvrdí kód v administraci (Správa PC / zařízení → Připojit zařízení).") fmt.Println("KÓD: " + pairing.UserCode) fmt.Println("nebo otevřete: " + pairing.VerificationURIComplete) _, err = pairing.Wait(ctx) // ptá se podle intervalu, než vedoucí potvrdí } if err != nil { var oe *profirest.OAuthError switch { case errors.As(err, &oe) && oe.Code == "access_denied": fmt.Println("Vedoucí párování odmítl.") case errors.As(err, &oe) && oe.Code == "expired_token": fmt.Println("Kód vypršel (10 minut). Spusťte příklad znovu.") default: fmt.Println("Párování se nepovedlo: " + err.Error()) } os.Exit(1) } } if api.RestaurantID() == "" { if _, err := api.Connection(ctx); err != nil { // zapamatuje provozovnu zařízení fmt.Println("Chyba: " + err.Error()) os.Exit(1) } } rid := api.RestaurantID() fmt.Printf("Provozovna %s. Příkazy: p/o/pz/kz PIN, k = kdo je v práci, odparovat, q = konec.\n", rid) types := map[string]string{"p": "clock_in", "o": "clock_out", "pz": "break_start", "kz": "break_end"} in := bufio.NewScanner(os.Stdin) // 3) smyčka příkazů for in.Scan() { parts := strings.Fields(in.Text()) if len(parts) == 0 { continue } cmd := strings.ToLower(parts[0]) var err error switch { case types[cmd] != "" && len(parts) > 1: punch(ctx, api, in, rid, types[cmd], strings.Join(parts[1:], " ")) case cmd == "k": var presence []model.AttendancePresence presence, err = profirest.All[model.AttendancePresence](ctx, api, "/restaurants/"+rid+"/attendance/presence", nil) for _, p := range presence { fmt.Printf(" %s: %s\n", p.Employee.Name, state(p.State)) } case cmd == "odparovat": if err = api.Unpair(ctx); err == nil { fmt.Println("Spárování zapomenuto.") return } case cmd == "q": return default: fmt.Println("Příkazy: p/o/pz/kz PIN, k, odparovat, q") } if err != nil { var e *profirest.APIError if errors.As(err, &e) && e.Code == profirest.CodeNotPaired { // vedoucí zařízení odpároval fmt.Println("Zařízení bylo odpárováno. Spusťte příklad znovu.") os.Exit(1) } if errors.As(err, &e) { fmt.Printf("Chyba: %s (%s)\n", e.Title, e.Code) } else { fmt.Println("Chyba: " + err.Error()) } } } } // 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(ctx context.Context, api *profirest.Client, in *bufio.Scanner, rid, typ, pin string) { body := model.AttendancePunchCreate{ ID: profirest.UUID(), Type: typ, Credential: &model.StaffCredential{Method: "pin", Secret: pin}, } for { r, err := profirest.Post[model.AttendancePunch](ctx, api, "/restaurants/"+rid+"/attendance/punches", body, nil) if err == nil { fmt.Printf("OK: %s – %s\n", r.Employee.Name, state(r.State)) return } var e *profirest.APIError if !errors.As(err, &e) { fmt.Println("Chyba: " + err.Error()) return } switch e.Code { case profirest.CodeNotPaired: // vedoucí zařízení odpároval fmt.Println("Zařízení bylo odpárováno. Spusťte příklad znovu.") os.Exit(1) case profirest.CodeStaffAmbiguous: // stejný PIN má víc lidí – vybrat a poslat znovu fmt.Println("Tento PIN má víc lidí:") for i, c := range e.Candidates { fmt.Printf(" %d) %s\n", i+1, c.Name) } fmt.Print("Číslo: ") choice := 0 if in.Scan() { choice, _ = strconv.Atoi(strings.TrimSpace(in.Text())) } if choice < 1 || choice > len(e.Candidates) { fmt.Println("Zrušeno.") return } id := e.Candidates[choice-1].ID body.Credential.EmployeeID = &id continue case profirest.CodeStaffCredentialsInvalid: fmt.Println("Neznámý PIN.") case profirest.CodeStaffLocked: wait := "chvíli" if e.RetryAfter != nil { wait = fmt.Sprintf("%d s", int(e.RetryAfter.Seconds())) } fmt.Printf("Moc špatných pokusů – zkuste to za %s.\n", wait) case profirest.CodeAttendanceStateConflict: fmt.Println("Teď to nejde: " + e.Title) case profirest.CodeNetworkError, profirest.CodeTimeout: fmt.Println("Bez sítě – zkuste znovu.") // skutečný terminál by píchnutí uložil a poslal dávkou default: fmt.Printf("Chyba: %s (%s)\n", e.Title, e.Code) } return } }