← all projects

pharmacy attendance and payroll

qr and gps clock-in, korean payroll rules, and the bugs that only real staff and real phones find

when
Apr 2026 – Sept 2026
for
a pharmacy with shift staff, built to handle more than one workplace
role
solo: design, build, deploy, operate
stack
next.js, typescript, prisma, postgresql, pwa, web push, windows server
size
168 commits, 62 data models, 34 migrations, ~31k lines of typescript
client work, through neoworks

also in 한국어

what it is

staff scan a qr code at their workplace, the phone checks they're actually there, and that becomes an attendance record. from those records the system works out pay. around that grew work requests and approvals, a shared calendar, tasks, documents, employment contracts with signatures, and later a consultation kiosk for customers. it's a pwa, so staff install it from the browser and get push notifications.

the first working version went up in a single day in april. it kept growing through september as people used it and reported things.

payroll is mostly rules

the payroll function is under 250 lines. the rules inside it took longer. korean labour law pays by the hour, but break time is deducted by shift length (after 4h and after 8h), overtime counts per day and per week without counting the same hour twice, night hours get a premium, public holiday hours get one rate for the first 8 hours and another after, and weekly holiday pay only applies if someone worked enough hours and enough days that week. every one of those is a setting.

one real bug: some employees had their own break rule. if that field was left empty, the code fell back to the global 60 minutes, so people marked "no break" still lost an hour. now an employee override means exactly what it says, and empty means zero.

gps lies a little, all the time

clocking in needs a valid site qr token, a gps reading inside the site radius, and an accuracy that's good enough. that part was fine. auto check-out was not.

the client reported that check-in and check-out were happening at the same time. the record showed someone clocking in 16.3m from the center of a 60m site, then 168 seconds later the phone reported 61.6m and the system checked them out. 1.6m over the line, well inside indoor gps error. the problem was that auto check-out used the same accuracy threshold as a manual punch, which for a 60m site meant accepting readings off by 180m.

so auto check-out got its own, stricter test: accuracy has to be within 60m, and even the nearest edge of the error circle has to be 30m beyond the radius. nothing happens in the first 10 minutes after check-in either. a person pressing a button can use a loose rule. the system ending someone's shift on its own can't.

the reminders that never ran

shift reminders and no-show alerts ran from a scheduled task every 15 minutes. it reported success every time. when i measured two weeks of data, there were about 78 active schedules a day and zero reminders saved. replaying the schedules, 324 notifications should have gone out.

the cause: the server runs windows, the task used powershell 5.1, which defaults to tls 1.0, and the reverse proxy only accepts tls 1.2+. every call failed, and an empty catch {} hid it. the geofence sweep was dead for the same reason. i moved the script into the repo, made it call the app over loopback, and made failures exit non-zero and log.

there was a second bug under it. a reminder could only fire within 14 minutes of its time, on a 15-minute check. one late run and that day's reminder was gone. a shift ending at 23:55 could never get one at all.

double taps

someone's manual entry showed up 5 times in 7.9 seconds, another twice in 0.4 seconds. the screen didn't react, so people kept pressing. i locked the button while submitting, but that doesn't stop requests already in flight. checking for duplicates in code doesn't either: 5 concurrent requests in a local test all passed the check and wrote 5 rows. a unique constraint in the database is what fixed it, with the code check in front to fail quietly. before the migration could run, i had to clean the existing duplicates without losing notes that only one of the pair had.

the back button had the same shape of bug. going back to the scan page and pressing again toggled in and out. there's a 3-minute server-side lock after each punch now.

the kiosk and the printer

the kiosk shows a qr. a customer scans it with their own phone, consents, and verifies once, with no account. the only thing granting access is a token in the url, so it's 32 random bytes, valid for 10 minutes, and burned after use. the health data lookup used to send two approval requests to the customer's phone. sharing one session id across both requests cut it to one.

printing works by having the shop pc pull jobs from the server, so nobody opens a router port. jobs older than 30 minutes expire, because a pc that was off all day shouldn't print twenty strangers' health summaries the next morning.

where it is now

it's in daily use, with 44 staff on the roster as of september, running as a service on a windows server. the last work was the kiosk print queue.