Skip to content
Work / HR portal

One portal for 500 people,and the numbers always agree.

An enterprise HR portal covering attendance, leave, onboarding, payroll and reports, with each role seeing exactly what it should.

500+employees on the portal every day

ClientEnterprise, internal
My roleFrontend Developer
When2025
What I ownedArchitecture, state, tables, access
Built withReact, Redux Toolkit, Tailwind
StatusLive
Screenshot: HR portal payroll view

The problem

HR software fails quietly: a leave gets approved but attendance still shows an absence, and payroll pays the wrong amount a month later. With 500 people, small drifts become real money. Every module had to stay in agreement without a person reconciling them.

The engineering version
Eight modules, each with its own data shape, sharing entities like employees and dates. Naive cross-module state produced cascading re-renders on large tables and inconsistent derived numbers. The fix was structural: isolated slices, normalized entities, a middleware bus for cross-module events, and memoized selectors feeding virtualized tables.
HR portal five modules, one source of truth
Five HR modules that work on their own andstill agree with each otherAttendanceslice + entityAdapterLeaveslice + approvalsPayrollslice + virtual tableOnboardingslice + RBAC guardsReportsselectors + CSVApprove a leave once, and attendance,payroll and reports all updateshared middleware: leave.approved ->attendance.recalc, payroll.adjust; memoizedselectors keep 500+ rows instant
Each module is its own box. The band underneath is what keeps them honest.

What was hard, and what I did about it

Modules that stay in agreement

Approving a leave has to change attendance, payroll and reports at once, without any of them polling the others.

What I did. Gave every module its own isolated state and put a small shared event layer under all of them, so one approval fans out to every module that cares.

Implementation notes
One RTK slice per module with entityAdapter. Custom middleware listens for cross-module actions (leave.approved) and dispatches follow-ups. No module imports another module's state directly.

Big tables that feel instant

Monthly payroll runs and attendance matrices are hundreds of rows by dozens of columns, and they re-rendered on every keystroke.

What I did. Made the tables draw only the rows on screen and recompute only what changed.

Implementation notes
Memoized Reselect selectors per row, virtualized rendering, column filtering in selectors rather than in components, CSV export from the same selectors.

Right people, right views

Admins, HR managers and employees share one app but must never see each other's data.

What I did. Built the views to adapt to the role and guarded every route and action on that role.

Implementation notes
Role in auth state, route guards at the router level, action permissions checked in middleware before dispatch reaches a slice.
Screenshot: Attendance matrix
Attendance matrix
Screenshot: Leave approval hierarchy
Leave approval hierarchy
500+employees on the portal every day
8modules, one source of truth
Livein production since 2025

Ask me about

  • How the cross-module middleware avoids circular updates
  • Where normalized state paid off and where it was overkill
  • How role-based access is enforced without sprinkling checks in components

Built with React, Redux Toolkit, TypeScript, Tailwind CSS, REST.

Next case study

Pipeline inspection classifier

Open Pipeline inspection
Lakshay Agarwal, 2026Updated September 2026. Noida / Delhi NCR.