Skip to main content

Browser Implementation Guide

You can build a fully working Work Card scanning system using only a web browser — no native iOS or Android app required. This page covers both the QR code and NFC card approaches, the Ergani API call, and how to handle the 15-minute submission window when connectivity drops.

No app store needed

The browser camera API and Web NFC API are available in modern browsers. A simple web app deployed on your server is enough to run a professional card scanner on a tablet at the entrance.

How it works

Two sides:

  • Worker — shows their QR code on their phone, or taps a physical NFC card on the reader.
  • Employer tablet — reads the QR/NFC, maps it to the employee's ΑΦΜ, and posts the arrival or departure event to the Ergani API within 15 minutes.
Worker's phone Employer's tablet Your server Ergani API
────────────── ───────────────────── ─────────── ──────────
Shows QR/NFC → Reads card or QR code → POST /workcard → WRKCardSE
Looks up employee ΑΦΜ Greece timezone Returns OK
Shows ✅ on screen Handles JWT auth

QR Code approach

Works on any device — iPhone, Android, desktop.

Worker side — show the QR

Generate a QR code from the employee's ΑΦΜ and display it in their browser.

npm install qrcode
import QRCode from 'qrcode'

async function showWorkerQR(afm) {
const canvas = document.getElementById('qr-canvas')
await QRCode.toCanvas(canvas, afm, { width: 300 })
}

The worker opens this page on their phone and holds it up at the entrance.

Employer side — scan with the camera

npm install html5-qrcode
import { Html5QrcodeScanner } from 'html5-qrcode'

const scanner = new Html5QrcodeScanner('reader', { fps: 10, qrbox: 250 })

scanner.render((afm) => {
// afm is the employee's ΑΦΜ read from the QR
handleScan(afm)
})

NFC Card approach

Workers get a cheap blank NFC card (€0.20–0.50 each). They tap it on the employer's Android tablet — no phone needed.

Android + Chrome only

The Web NFC API (NDEFReader) works only on Chrome on Android. It is not supported on iOS — use QR as a fallback for iPhone users.

async function startNFCScanner() {
if (!('NDEFReader' in window)) {
showError('NFC not supported — please use QR code instead')
return
}

const reader = new NDEFReader()
await reader.scan() // prompts for NFC permission

reader.addEventListener('reading', ({ serialNumber }) => {
// serialNumber is the unique ID burned into the card
// e.g. "04:A3:2F:11:BC:44:80"
handleScan(serialNumber)
})
}

You store a mapping of cardId → employee ΑΦΜ in your database. When a card is tapped, look up the employee and proceed.

QR vs NFC

QR CodeNFC Card
Works on iPhone
SpeedSlower (aim camera)Faster (just tap)
Card costFree (worker uses phone)€0.20–0.50 per card
Employer deviceAny phone or tabletAndroid only
Feels professionalOK✅ Yes

Recommendation: support both. Workers without a smartphone use the NFC card. The Ergani API call is identical either way.

Calling the Ergani API

Once you have the employee's ΑΦΜ, post the event from your backend. Never call the Ergani API directly from the browser — keep your JWT token server-side.

// Browser → your server
async function handleScan(employeeId) {
const employee = await getEmployeeByCardId(employeeId) // your DB lookup
if (!employee) { showError('Unknown card'); return }

const eventType = getNextEventType(employee) // 'arrival' or 'departure'

await fetch('/api/workcard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
afm: employee.afm,
branchId: currentBranch.id,
type: eventType,
timestamp: new Date().toISOString()
})
})

showSuccess(`${employee.name}${eventType}`)
}
// Your server → Ergani API
app.post('/api/workcard', async (req, res) => {
const { afm, branchId, type, timestamp } = req.body

const payload = {
f_afm_ergodoti: employer.afm,
f_aa_pararthmatos: branchId,
f_afm_ergazomenos: afm,
f_date: toGreekLocalTime(timestamp), // ⚠️ see note below
f_type: type === 'arrival' ? '0' : '1'
}

const response = await fetch(
'https://eservices.yeka.gr/WebservicesAPI/Api/Documents/WRKCardSE',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${erganiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}
)

res.json(await response.json())
})
Greece timezone — not UTC

f_date must be in Greece local time (UTC+2 in winter, UTC+3 in summer with DST). Sending UTC causes submissions to appear late and get rejected. Convert before sending:

function toGreekLocalTime(isoString) {
return new Date(isoString).toLocaleString('sv-SE', {
timeZone: 'Europe/Athens'
}).replace(' ', 'T')
}

Offline queue — handling the 15-minute window

If the internet drops mid-shift, store scans locally and retry when connectivity returns.

async function submitWithFallback(payload) {
try {
await callErganiAPI(payload)
} catch {
const queue = JSON.parse(localStorage.getItem('wc_queue') || '[]')
queue.push({ payload, queuedAt: Date.now() })
localStorage.setItem('wc_queue', JSON.stringify(queue))
showWarning('Saved offline — will retry when connected')
}
}

async function flushQueue() {
const queue = JSON.parse(localStorage.getItem('wc_queue') || '[]')
if (!queue.length) return

const now = Date.now()
for (const item of queue) {
const ageMinutes = (now - item.queuedAt) / 60000
if (ageMinutes > 15) {
// Mark as late — Ergani will require a justification reason
item.payload.late = true
item.payload.lateReason = 'EMPLOYER_SYSTEM_FAILURE'
}
await callErganiAPI(item.payload)
}

localStorage.removeItem('wc_queue')
}

window.addEventListener('online', flushQueue)
Late submission justification

If more than 15 minutes have passed, the submission is considered εκπρόθεσμη. Accepted justification codes are EMPLOYER_SYSTEM_FAILURE, POWER_TELECOM_OUTAGE, and ERGANI_CONNECTIVITY. You must also notify the Labour Inspectorate (Επιθεώρηση Εργασίας) immediately when the outage occurs and again when it ends. See Work Card overview.

Where to go next