/** * Příkladový klient Profirest API v2 v TypeScriptu: docházkový terminál v příkazové řádce (Node 18+). * * npx tsc pichacka.ts --module commonjs --target es2020 --lib es2020,dom (vedle profirest-api.js a .d.ts) * node pichacka.js * * 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 ProfirestApi = require('./profirest-api'); // moduly Node bez balíčku @types/node (s ním: import * as fs from 'fs' …) declare const process: any; declare function require(name: string): any; const fs = require('fs'); const readline = require('readline'); /** Úložiště tokenů v JSON souboru (klient čeká getItem / setItem / removeItem). */ function fileStorage(path: string): ProfirestApi.Storage { const read = (): Record => { try { return JSON.parse(fs.readFileSync(path, 'utf8')); } catch { return {}; } }; const write = (data: Record) => fs.writeFileSync(path, JSON.stringify(data), { mode: 0o600 }); return { getItem: (k) => read()[k] ?? null, setItem: (k, v) => { const d = read(); d[k] = v; write(d); }, removeItem: (k) => { const d = read(); delete d[k]; write(d); }, }; } const api = new ProfirestApi({ baseUrl: process.env.PROFIREST_BASE_URL || 'https://api.mujprofirest.cz', clientId: 'profirest-dochazka', storage: fileStorage(process.env.PROFIREST_TOKENS || './profirest-pichacka.json'), }); const STATES: Record = { working: 'v práci', on_break: 'na přestávce', off: 'mimo práci' }; const TYPES: Record = { p: 'clock_in', o: 'clock_out', pz: 'break_start', kz: 'break_end', }; const rl = readline.createInterface({ input: process.stdin }); const lines = rl[Symbol.asyncIterator](); async function readLine(): Promise { const r = await lines.next(); return r.done ? null : String(r.value); } /** Píchnutí: id vznikne jednou; při výběru zaměstnance se posílá znovu se stejným id. */ async function punch(rid: string, type: ProfirestApi.Models.AttendancePunchCreate['type'], pin: string): Promise { const body: ProfirestApi.Models.AttendancePunchCreate = { id: ProfirestApi.uuid(), type, credential: { method: 'pin', secret: pin }, }; for (;;) { try { const r = await api.post(`/restaurants/${rid}/attendance/punches`, body); // typ AttendancePunch console.log(`OK: ${r.employee.name} – ${STATES[r.state] || r.state}`); return; } catch (e) { if (!(e instanceof ProfirestApi.ApiError)) { throw e; } switch (e.code) { case 'staff_ambiguous': { // stejný PIN má víc lidí – vybrat a poslat znovu console.log('Tento PIN má víc lidí:'); e.candidates.forEach((c, i) => console.log(` ${i + 1}) ${c.name}`)); const chosen = e.candidates[Number(await readLine()) - 1]; if (!chosen) { console.log('Zrušeno.'); return; } body.credential = { method: 'pin', secret: pin, employee_id: chosen.id }; continue; } case 'staff_credentials_invalid': console.log('Neznámý PIN.'); return; case 'staff_locked': console.log(`Moc špatných pokusů – zkuste to za ${e.retryAfter} s.`); return; case 'attendance_state_conflict': console.log(`Teď to nejde: ${e.title}`); return; case 'network_error': case 'timeout': console.log('Bez sítě – zkuste znovu.'); return; // terminál by uložil a poslal dávkou default: console.log(`Chyba: ${e.title} (${e.code})`); return; } } } } async function main(): Promise { // 1) spárování kódem (RFC 8628) – jen poprvé, tokeny pak zůstávají v souboru if (!api.isPaired()) { const pairing = await api.pair({ scope: 'restaurants:read staff:read staff:authenticate attendance:read attendance:punch', deviceName: 'Píchačka (příklad)', }); console.log('Zařízení není spárované. Vedoucí potvrdí kód v administraci (Správa PC / zařízení → Připojit zařízení).'); console.log(`KÓD: ${pairing.userCode}`); console.log(`nebo otevřete: ${pairing.verificationUriComplete}`); try { await pairing.connected; } catch (e) { const err = e instanceof ProfirestApi.OAuthError ? e.error : String(e); console.log(err === 'access_denied' ? 'Vedoucí párování odmítl.' : err === 'expired_token' ? 'Kód vypršel (10 minut). Spusťte příklad znovu.' : `Párování se nepovedlo: ${err}`); return 1; } } if (api.restaurantId === null) { await api.connection(); } const rid = api.restaurantId as string; console.log(`Provozovna ${rid}. Příkazy: p/o/pz/kz PIN, k = kdo je v práci, odparovat, q = konec.`); // 2) smyčka příkazů for (let line = await readLine(); line !== null; line = await readLine()) { const [cmd = '', pin] = line.trim().split(/\s+/, 2); try { if (TYPES[cmd.toLowerCase()] && pin) { await punch(rid, TYPES[cmd.toLowerCase()], pin); } else if (cmd === 'k') { for (const p of await api.all(`/restaurants/${rid}/attendance/presence`)) { console.log(` ${p.employee.name}: ${STATES[p.state] || p.state}`); } } else if (cmd === 'odparovat') { await api.unpair(); console.log('Spárování zapomenuto.'); break; } else if (cmd === 'q') { break; } else if (cmd !== '') { console.log('Příkazy: p/o/pz/kz PIN, k, odparovat, q'); } } catch (e) { if (e instanceof ProfirestApi.ApiError && e.code === 'not_paired') { console.log('Zařízení bylo odpárováno. Spusťte příklad znovu.'); return 1; } console.log(`Chyba: ${e instanceof Error ? e.message : String(e)}`); } } return 0; } main().then((code) => { rl.close(); process.exitCode = code; });