Mind-ID Portal Recruitment was my first enterprise-scale project. big company, big expectations, big learning curve. 📈
What is MIND ID?
MIND ID = Mining Industry Indonesia They're a holding company managing multiple mining subsidiaries:
- ANTAM
- Freeport Indonesia
- Bukit Asam
- Timah
- and more
Each subsidiary had their own recruitment process. HR at holding level had zero visibility. chaos.
The Goal
One portal where:
- Job seekers can apply to any subsidiary
- Subsidiaries can manage their own recruitment
- Holding HR can monitor everything
- Approval workflows are standardized
Tech Stack
Frontend:
- Next.js 14 (App Router - was new for me then)
- Shadcn UI (component library)
- TypeScript
- Tailwind CSS
Backend:
- NestJS (new for me too)
- PostgreSQL
- RESTful API
What I Built
1. Landing Page
Public-facing page showing all job openings across subsidiaries:
// app/page.tsx (Server Component)
async function JobListings() {
const jobs = await jobService.getActiveJobs();
const subsidiaries = await subsidiaryService.getAll();
return (
<div className="container mx-auto py-8">
<SubsidiaryFilter subsidiaries={subsidiaries} />
<JobGrid jobs={jobs} />
<Pagination total={jobs.length} />
</div>
);
}
2. Multi-Tier Approval Flow
This was complex. Different roles can approve at different stages:
type ApprovalStage = 'hr初审' | 'manager复审' | 'director终审';
interface ApprovalWorkflow {
stages: ApprovalStage[];
currentStage: number;
status: 'pending' | 'approved' | 'rejected';
approvers: {
stage: ApprovalStage;
userId: string;
status: 'pending' | 'approved' | 'rejected';
comment?: string;
}[];
}
const ApprovalFlow = ({ workflow }: { workflow: ApprovalWorkflow }) => (
<div className="space-y-4">
{workflow.stages.map((stage, index) => (
<ApprovalStageCard
key={stage}
stage={stage}
isActive={index === workflow.currentStage}
approver={workflow.approvers.find(a => a.stage === stage)}
/>
))}
</div>
);
3. Candidate Screening
Tools to help HR screen candidates efficiently:
interface CandidateScorecard {
candidateId: string;
criteria: {
name: string;
weight: number; // importance 1-5
score: number; // 1-5
}[];
totalScore: number;
recommendation: 'strong_hire' | 'hire' | 'no_hire' | 'strong_no_hire';
}
// Calculate weighted score
const calculateScore = (scorecard: CandidateScorecard) => {
const totalWeight = scorecard.criteria.reduce((sum, c) => sum + c.weight, 0);
const weightedSum = scorecard.criteria.reduce(
(sum, c) => sum + (c.score * c.weight),
0
);
return weightedSum / totalWeight;
};
4. Dashboard Analytics
HR can see:
- Applications by stage
- Time-to-hire metrics
- Subsidiary comparison
- Source of hire
// Analytics dashboard
const RecruitmentDashboard = () => {
const { data: stats } = useQuery(['recruitment-stats'], () =>
fetch('/api/analytics/recruitment').then(res => res.json())
);
return (
<div className="grid grid-cols-4 gap-4">
<StatCard title="Total Applications" value={stats.total} />
<StatCard title="In Progress" value={stats.inProgress} />
<StatCard title="Hired" value={stats.hired} />
<StatCard title="Avg. Time to Hire" value={`${stats.avgDays} days`} />
</div>
);
};
Challenges
Next.js 14 App Router (when it was new)
App Router changed everything about how I thought about React. Server Components by default. I spent extra time understanding the mental model.
Shadcn UI
amazing component library but needed customization for enterprise feel. learned a lot about modifying Radix primitives.
Multi-Subsidiary Complexity
Each subsidiary wanted slightly different:
- Application forms
- Approval workflows
- Branding
Had to build flexible configuration system.
Best Practices Implemented
learned from seniors and implemented:
// 1. Consistent error handling
class ApiError extends Error {
constructor(
message: string,
public statusCode: number,
public code: string
) {
super(message);
}
}
// 2. Type-safe API responses
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: ApiError };
// 3. Zod validation everywhere
const CreateJobSchema = z.object({
title: z.string().min(3).max(100),
description: z.string().min(50),
subsidiaryId: z.string().uuid(),
requirements: z.array(z.string()),
deadline: z.date().min(new Date()),
});
// 4. Proper git workflow
// feature branches, code review, meaningful commits
Results
- ✅ Unified recruitment across 5+ subsidiaries
- ✅ 40% faster screening process
- ✅ Multi-tier approval workflow implemented
- ✅ Better candidate experience
- ✅ Real-time analytics for HR
What I Learned
- Enterprise projects - require more planning than coding
- App Router - game changer for React, invest time to understand it
- Component libraries - Shadcn UI + customization = powerful combo
- Stakeholder management - multiple subsidiaries = multiple opinions
this project taught me: big projects don't have to be messy. proper planning, good code practices, and communication make everything smoother.
Related Articles
Resona Perdania Corporate Website - Modern B2B Identity
Revamped corporate website with Next.js, Strapi CMS, custom plugins, and Japanese-inspired B2B branding.
PAMAFix Opex Capex - Financial Reporting Module
Built complex financial reporting with expandable tables, dynamic summaries, and Budget vs Actual comparisons.
Kalla Property Management - Full-Stack Real Estate System
Built a comprehensive property management system for Kalla Group's real estate portfolio.