← All case files

Case file 003Attendance & payroll

The problem

“Our fingerprint machine knows who came late. Payroll doesn’t. Every month, someone works it out by hand.”

Solved

For
A company running shifts across branches
Built
HR & payroll app: web admin, phone app, machine bridge
Stack
Next.js · Expo · Prisma · Postgres · ZKTeco ADMS
Result
Number to confirm
Before: month end, a calculator and a punch log3D pixel film · made by me

01 · The pain

The machine knew who was late. Payroll didn’t.

The fingerprint machine keeps a log of codes and times. Turning that into salaries meant matching every line to a person, a shift, a late rule, an overtime rate and any advance. By hand. Every month.

Punches to match
0
Example team
40 people · 26 days

And one wrong line means someone’s salary is wrong.

1024	2026-08-01 08:31:59
1027	2026-08-01 18:54:04
1040	2026-08-01 08:11:59
1003	2026-08-01 20:04:40
1037	2026-08-01 08:43:03
1025	2026-08-01 18:52:04
1033	2026-08-01 09:16:42
1030	2026-08-01 18:49:44
1002	2026-08-01 08:08:39
1016	2026-08-01 18:18:52
1023	2026-08-01 08:27:34
1017	2026-08-01 18:35:21
1029	2026-08-02 08:23:34
1010	2026-08-02 18:38:27
1012	2026-08-02 08:04:46
1009	2026-08-02 18:42:37
1038	2026-08-02 08:28:23
1026	2026-08-02 18:11:37
1001	2026-08-02 08:45:45
1018	2026-08-02 18:14:17
1034	2026-08-02 08:35:03
1036	2026-08-02 18:10:49
1012	2026-08-02 08:26:46
1015	2026-08-02 18:19:59
1012	2026-08-03 08:34:53
1029	2026-08-03 18:13:29
1016	2026-08-03 08:29:56
1032	2026-08-03 18:22:35
1036	2026-08-03 08:19:44
1036	2026-08-03 20:14:20
1002	2026-08-03 09:44:48
1010	2026-08-03 18:10:36
1026	2026-08-03 08:53:14
1028	2026-08-03 18:30:18
1028	2026-08-03 09:53:21
1012	2026-08-03 18:32:55
1018	2026-08-04 08:04:35
1027	2026-08-04 18:49:35
1003	2026-08-04 08:28:26
1034	2026-08-04 20:53:19
1004	2026-08-04 08:03:45
1031	2026-08-04 20:23:48
1018	2026-08-04 08:12:09
1024	2026-08-04 18:38:07
1035	2026-08-04 09:59:08
1011	2026-08-04 18:34:40
1002	2026-08-04 09:15:07
1020	2026-08-04 18:29:09
1009	2026-08-05 08:27:30
1040	2026-08-05 18:09:16
1032	2026-08-05 08:29:05
1030	2026-08-05 20:06:19
1009	2026-08-05 08:37:33
1035	2026-08-05 18:29:07
1026	2026-08-05 08:31:46
1040	2026-08-05 18:51:19
1037	2026-08-05 08:51:57
1038	2026-08-05 18:02:42
1004	2026-08-05 08:02:57
1001	2026-08-05 18:02:26
1005	2026-08-06 08:03:20
1009	2026-08-06 18:45:55
1015	2026-08-06 08:31:30
1006	2026-08-06 18:05:16
1001	2026-08-06 09:29:18
1022	2026-08-06 18:34:54
1038	2026-08-06 08:33:44
1004	2026-08-06 18:56:39
1001	2026-08-06 08:44:47
1030	2026-08-06 18:02:13
1027	2026-08-06 08:26:37
1010	2026-08-06 20:51:56
1004	2026-08-07 08:19:46
1036	2026-08-07 18:19:40
1013	2026-08-07 08:57:14
1028	2026-08-07 18:38:41
1024	2026-08-07 08:37:32
1032	2026-08-07 20:16:59
1035	2026-08-07 08:38:08
1027	2026-08-07 18:37:11
1038	2026-08-07 08:10:45
1002	2026-08-07 18:45:48
1006	2026-08-07 08:35:32
1007	2026-08-07 18:09:39
1012	2026-08-08 09:19:50
1021	2026-08-08 18:55:11
1039	2026-08-08 08:27:18
1024	2026-08-08 20:34:10
1039	2026-08-08 08:40:03
1025	2026-08-08 20:07:38
1003	2026-08-08 08:50:39
1023	2026-08-08 18:19:06
1001	2026-08-08 09:40:06
1019	2026-08-08 18:21:20
1005	2026-08-08 08:44:00
1018	2026-08-08 20:11:55
1039	2026-08-09 08:44:36
1034	2026-08-09 18:21:47
1006	2026-08-09 08:57:55
1012	2026-08-09 18:40:41
1016	2026-08-09 08:52:36
1036	2026-08-09 18:07:54
1017	2026-08-09 08:07:16
1031	2026-08-09 18:38:46
1011	2026-08-09 08:32:24
1001	2026-08-09 18:21:40
1038	2026-08-09 08:01:23
1001	2026-08-09 20:08:44
1007	2026-08-10 09:41:09
1026	2026-08-10 18:43:22
1025	2026-08-10 08:38:41
1030	2026-08-10 20:16:23
1011	2026-08-10 08:16:54
1036	2026-08-10 18:11:19
1037	2026-08-10 08:32:37
1021	2026-08-10 18:20:59
1002	2026-08-10 08:45:01
1038	2026-08-10 18:23:04
1025	2026-08-10 08:07:29
1006	2026-08-10 18:54:17
1007	2026-08-11 08:04:42
1024	2026-08-11 18:32:02
1020	2026-08-11 08:20:00
1030	2026-08-11 20:22:29
1014	2026-08-11 09:30:22
1030	2026-08-11 18:26:18
1038	2026-08-11 09:22:45
1023	2026-08-11 20:13:40
1013	2026-08-11 08:36:48
1010	2026-08-11 18:54:48
1028	2026-08-11 08:21:37
1018	2026-08-11 20:16:24
1030	2026-08-12 08:18:55
1032	2026-08-12 18:22:36
1026	2026-08-12 09:49:25
1012	2026-08-12 18:07:02
1013	2026-08-12 08:22:10
1005	2026-08-12 20:18:23
1016	2026-08-12 08:40:24
1010	2026-08-12 18:45:12
ATTLOG · device PIN · date · time

02 · The fix

Punches in. Payslips out.

One sample employee’s month, and the payslip the system builds from it. Change any day and watch the salary follow.

Sample employee · Rs 60,000 / month · shift 09:00–18:00

Late rule: 15+ min late costs 1 hour · overtime × 1.5

Day 4 · what the machine sent

1042 2026-08-04 09:27:12 → in
1042 2026-08-04 18:02:40 → out

Click any day to make it late, overtime, absent or leave. The payslip redoes itself.

Payslip · AugustSample

Base salary26 working days
Rs 60,000
Overtime9.3 h × Rs 256 × 1.5
Rs 3,577
Late deduction3 late × 1 h
− Rs 769
Unpaid days1 × Rs 2,308
− Rs 2,308
Advance installmentmonthly repayment
− Rs 5,000

Net payRs 55,500

Worked out by the same rules the real payroll engine runs.

03 · How it works

Three steps, and machines that all speak differently.

  1. Every punch lands in one place

    Fingerprint machines send punches straight to the app. Staff away from a machine clock in from the phone app, but only inside their branch's geofence.

  2. Punches become attendance

    Each punch is matched to an employee by their device code. The first of the day is clock-in, the next is clock-out, extras are ignored. Shifts, late rules and leave are applied on top.

  3. Payroll runs itself, HR approves

    At month end the engine builds every payslip: overtime, late deductions, unpaid days, advances. HR reviews and approves; staff see their own slip in the app.

The hard part

Three ways in, one record out.

Newer ZKTeco machines call the server themselves and send plain text lines. Older ones never call: a small agent on site reads their log and remembers the last punch it saw, so nothing is counted twice. Phones send a location that has to be inside the branch’s radius.

All three are turned into the same shape: device code, date, time. Only then does the app decide whether it is a clock-in, a clock-out, or noise.

Newer machines push

POST /iclock/cdata?SN=…&table=ATTLOG 1042 2026-08-04 09:27:12

Older machines get pulled

agent reads the device log resumes after the last punch it saw

Phones check distance

You are 450m from Head Office (allowed radius: 200m)

Becomes

code 1042 · 2026-08-04 · 09:27 → clock-in, 27 min late

04 · What changed

Payroll became something HR approves, not something HR builds.

People on payroll
—

Metric to confirm · staff paid through it

Month-end payroll time
—

Metric to confirm · days by hand → minutes

Salary disputes
—

Metric to confirm · before vs after