Inspiration

I work as an SAP ABAP/HCM consultant, and for months I've been deep in the guts of a real employee leave-management system — fixing quota calculation bugs, approval workflows, staffing edge cases. What struck me wasn't that the system was broken; it's that even when it works correctly, the actual approval decision is still just one manager looking at a list and deciding case by case, with no consistent logic behind who gets approved when requests collide, and no real explanation when someone is rejected.

When I saw this hackathon's theme — Where Operations Research Meets Innovation — it clicked immediately: leave approval isn't just an HR workflow, it's a constrained optimization problem. Employees compete for a limited resource (staffing capacity) under quota constraints, and the actual right tool for that is math, not manual judgment.

What it does

Smart Leave Approval Optimizer takes a batch of pending leave requests and, instead of approving them one at a time, formulates the entire batch as an Integer Linear Programming (ILP) problem:

$$\max \sum_{i} w_i \cdot x_i \quad \text{where } w_i = \alpha \cdot \text{waiting_days}_i + \beta \cdot \text{seniority}_i$$

subject to hard constraints on minimum daily staffing per department and remaining leave quota per employee. The solver (PuLP/CBC) decides every request in under ~1.1 seconds — even with ~75-100 overlapping requests.

Crucially, every rejection comes with a concrete, data-grounded reason (not a black-box "no"), a manager can run "What-If" simulations to force-approve a specific request and instantly see which other approvals would get displaced, and there's an optional AI chatbot (OpenAI/Anthropic API) for natural-language questions like "Is it safe to approve Andi's leave Aug 10–15?", with a deterministic local fallback when no API key is provided. Since this is a global hackathon, the entire interface — including the dynamically generated explanations — is fully bilingual (Indonesian/English).

How I built it

  • Python + PuLP (CBC solver) for the ILP core
  • Streamlit for the interactive dashboard — staffing heatmaps, KPI cards, and live sliders to retune priority weights ($\alpha$, $\beta$) and minimum staffing thresholds
  • A synthetic dataset generator simulating a realistic mid-size company: 40 employees, 5 departments, 75 overlapping leave requests
  • A centralized lang.py translation module so bilingual support covers everything — static labels and every dynamically generated explanation string, not just the UI chrome

Challenges I ran into

Explainability on a MIP, not an LP. My first instinct for "why was this rejected" was to use shadow prices / dual values from the constraints — a standard OR technique. But because approve/reject is inherently a binary decision ($x_i \in {0,1}$), this is a Mixed Integer Program, and CBC doesn't return valid duals for integer constraints. I had to design a separate post-solve heuristic: check individual quota exhaustion first, then check departmental staffing bottlenecks (naming exactly which higher-priority requests "won" that slot), and only fall back to a general priority trade-off explanation when neither hard constraint applies.

An invisible-looking bug that wasn't actually a bug. After adding live sliders for the priority weights, I adjusted seniority weight from 1.5 to 5.0 and the approved/rejected counts looked completely unchanged — I assumed the slider was broken. Testing revealed the objective score had changed significantly, but only two borderline requests actually flipped status in that dataset, so the headline counts masked a real change underneath. The fix was as much about UX as code: surface the objective score directly so the impact is never invisible.

Silent API failures. Early on, a failed OpenAI/Anthropic call (bad key, rate limit) would silently fall through to the local fallback with no indication anything had gone wrong — a failed call looked identical to a working one. I had to make failures surface explicitly before handing off to the fallback engine.

Accomplishments that I'm proud of

  • A genuinely explainable ILP system — every decision traces back to a specific, verifiable constraint, not a black box
  • Solve time consistently under ~1.1 seconds even with heavy request overlap
  • Full bilingual parity, including generated explanation text, not just UI labels

What I learned

Explainability techniques that work for pure LP (shadow prices) don't transfer to MIP — integer models often need a purpose-built explanation layer designed alongside the model, not bolted on afterward. I also learned that a parameter change can have a real, measurable effect on the objective function while barely moving the headline output — which is a UX lesson as much as an OR one: always surface the underlying number, not just the summary count.

What's next

  • Real HRIS/SAP integration, replacing the synthetic dataset with live employee and quota data
  • Half-day / partial-day leave support
  • Role-based authentication for employees, department managers, and HR admins
  • A fairness constraint to cap consecutive rejections for the same employee

Built With

Share this project:

Updates

Submission history