// Příkladový klient Profirest API v2: docházkový terminál v příkazové řádce. // // dart run example/pichacka.dart // // 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 vedle příkladu – chraňte ho jako heslo). // // Ve Flutteru je postup stejný: místo FileStorage dejte úložiště aplikace (viz README) a kód ukažte na displeji. import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:profirest_api/io.dart'; import 'package:profirest_api/profirest_api.dart'; const types = {'p': 'clock_in', 'o': 'clock_out', 'pz': 'break_start', 'kz': 'break_end'}; const states = {'working': 'v práci', 'on_break': 'na přestávce', 'off': 'mimo práci'}; late final StreamIterator input; /// Další řádek ze vstupu, null na konci. Future readLine() async => await input.moveNext() ? input.current : null; Future main() async { stdout.encoding = utf8; input = StreamIterator(stdin.transform(utf8.decoder).transform(const LineSplitter())); final env = Platform.environment; final tokens = env['PROFIREST_TOKENS'] ?? '${File.fromUri(Platform.script).parent.path}${Platform.pathSeparator}profirest-pichacka.json'; final api = ProfirestApi( baseUrl: env['PROFIREST_BASE_URL'] ?? 'https://api.mujprofirest.cz', clientId: 'profirest-dochazka', storage: FileStorage(tokens), ); // 1) spárování kódem (RFC 8628) – jen poprvé, tokeny pak zůstávají v souboru if (!await api.isPaired()) { try { final pairing = await api.pair( scope: 'restaurants:read staff:read staff:authenticate attendance:read attendance:punch', deviceName: 'Píchačka (příklad)', ); print('Zařízení není spárované. Vedoucí potvrdí kód v administraci (Správa PC / zařízení → Připojit zařízení).'); print('KÓD: ${pairing.userCode}'); print('nebo otevřete: ${pairing.verificationUriComplete}'); await pairing.connected; // ptá se podle intervalu, než vedoucí potvrdí } on OAuthError catch (e) { print(switch (e.error) { 'access_denied' => 'Vedoucí párování odmítl.', 'expired_token' => 'Kód vypršel (10 minut). Spusťte příklad znovu.', _ => 'Párování se nepovedlo: ${e.message}', }); exit(1); } on ApiError catch (e) { print('Párování se nepovedlo: ${e.message}'); exit(1); } } if (await api.restaurantId() == null) { await api.connection(); // zapamatuje provozovnu zařízení } final rid = (await api.restaurantId())!; print('Provozovna $rid. Příkazy: p/o/pz/kz PIN, k = kdo je v práci, odparovat, q = konec.'); // 3) smyčka příkazů for (;;) { final line = await readLine(); if (line == null) { break; } final parts = line.trim().split(RegExp(r'\s+')); final cmd = parts[0].toLowerCase(); final arg = parts.length > 1 ? parts.sublist(1).join(' ') : null; try { if (types.containsKey(cmd) && arg != null) { await punch(api, rid, types[cmd]!, arg); } else if (cmd == 'k') { for (final p in await api.all('/restaurants/$rid/attendance/presence')) { print(' ${p['employee']['name']}: ${states[p['state']] ?? p['state']}'); } } else if (cmd == 'odparovat') { await api.unpair(); print('Spárování zapomenuto.'); break; } else if (cmd == 'q') { break; } else if (cmd.isNotEmpty) { print('Příkazy: p/o/pz/kz PIN, k, odparovat, q'); } } on ApiError catch (e) { if (e.code == ErrorCode.notPaired) { // vedoucí zařízení odpároval print('Zařízení bylo odpárováno. Spusťte příklad znovu.'); exit(1); } print('Chyba: ${e.title} (${e.code})'); } } await input.cancel(); exit(0); } // 2) píchnutí – id vznikne jednou na píchnutí; při opakování (výběr zaměstnance, výpadek) se posílá stejné Future punch(ProfirestApi api, String rid, String type, String pin) async { final credential = {'method': 'pin', 'secret': pin}; final body = {'id': ProfirestApi.uuid(), 'type': type, 'credential': credential}; for (;;) { try { final r = await api.post('/restaurants/$rid/attendance/punches', body); print('OK: ${r['employee']['name']} – ${states[r['state']] ?? r['state']}'); return; } on ApiError catch (e) { switch (e.code) { case ErrorCode.staffAmbiguous: // stejný PIN má víc lidí – vybrat a poslat znovu print('Tento PIN má víc lidí:'); for (var i = 0; i < e.candidates.length; i++) { print(' ${i + 1}) ${e.candidates[i]['name']}'); } stdout.write('Číslo: '); final choice = int.tryParse((await readLine() ?? '').trim()) ?? 0; if (choice < 1 || choice > e.candidates.length) { print('Zrušeno.'); return; } credential['employee_id'] = e.candidates[choice - 1]['id'] as Object; continue; case ErrorCode.staffCredentialsInvalid: print('Neznámý PIN.'); case ErrorCode.staffLocked: print('Moc špatných pokusů – zkuste to za ${e.retryAfter} s.'); case ErrorCode.attendanceStateConflict: print('Teď to nejde: ${e.title}'); case ErrorCode.networkError: case ErrorCode.timeout: print('Bez sítě – zkuste znovu.'); // skutečný terminál by píchnutí uložil a poslal dávkou default: print('Chyba: ${e.title} (${e.code})'); } return; } } }