EG
Resume

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

  1. Enterprise projects - require more planning than coding
  2. App Router - game changer for React, invest time to understand it
  3. Component libraries - Shadcn UI + customization = powerful combo
  4. 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