EG
Resume

after finishing the OTC module, they said: "hey, can you also build the Opex Capex reporting?" cool cool cool. 😎

What is Opex Capex?

OPEX = Operational Expenditure (day-to-day expenses) CAPEX = Capital Expenditure (long-term investments)

Mining companies have TONS of both. tracking, reporting, comparing them? that's what this module does.

The Features

1. Interactive Expandable Tables

Users drill down from summary → category → subcategory → individual items.

interface OpexRow {
  id: string;
  category: string;
  subcategory?: string;
  amount: number;
  children?: OpexRow[];
  _expanded?: boolean;
}

const ExpandableTable = ({ data }: { data: OpexRow[] }) => (
  <Table dataSource={data}>
    <Column
      dataIndex="category"
      render={(text, record) => (
        <Space>
          {record.children && (
            <button onClick={() => toggleExpand(record.id)}>
              {record._expanded ? '▼' : '▶'}
            </button>
          )}
          <span>{text}</span>
        </Space>
      )}
    />
    <Column dataIndex="amount" render={formatCurrency} />
  </Table>
);

2. Dynamic Opex Summary Table

18+ columns. users can:

  • Show/hide any column
  • Drag to reorder
  • Pin important columns left
  • Sort by any column
  • Filter by period, category, cost center
// Column management state
const [columns, setColumns] = useState<ColumnConfig[]>(DEFAULT_COLUMNS);
const [pinnedColumns, setPinnedColumns] = useState(['category', 'total']);

const ColumnManager = () => (
  <Dropdown
    overlay={
      <Menu>
        {columns.map(col => (
          <Menu.Item key={col.key}>
            <Checkbox
              checked={columns.includes(col)}
              onChange={() => toggleColumn(col.key)}
            />
            {col.title}
          </Menu.Item>
        ))}
      </Menu>
    }
  >
    <Button icon={<SettingOutlined />}>Manage Columns</Button>
  </Dropdown>
);

3. Budget vs Actual Report

interface BudgetActualRow {
  category: string;
  budget: number;
  actual: number;
  variance: number; // actual - budget
  variancePercent: number;
  status: 'on_track' | 'over_budget' | 'under_budget';
}

// Color-coded variance
const getVarianceColor = (status: string) => {
  switch (status) {
    case 'over_budget': return 'red';
    case 'under_budget': return 'green';
    default: return 'default';
  }
};

The hard part? Budget and Actual data came from DIFFERENT systems. had to:

  1. Fetch from both APIs
  2. Normalize formats
  3. Match by category codes (which were different format in each system 💀)
  4. Calculate variances
  5. Handle missing data gracefully

4. Capex Summary with Table Merging

Some Capex items span multiple periods. needed to merge rows intelligently:

const mergeCapexRows = (data: CapexItem[]): MergedRow[] => {
  const grouped = groupBy(data, 'capexProject');

  return Object.entries(grouped).map(([project, items]) => {
    const totalAmount = items.reduce((sum, item) => sum + item.amount, 0);
    const startDate = min(items.map(i => i.startDate));
    const endDate = max(items.map(i => i.endDate));

    return {
      project,
      items: items.length,
      totalAmount,
      duration: differenceInDays(endDate, startDate),
      rows: items // keep for drill-down
    };
  });
};

Financial Data Challenges

Data validation is EVERYTHING

  • Negative amounts in wrong places
  • Currency mismatches
  • Missing period data
  • Duplicate entries

Used Zod for runtime validation:

const OpexSchema = z.object({
  category: z.string().min(1),
  subcategory: z.string().optional(),
  amount: z.number().positive(),
  currency: z.enum(['IDR', 'USD']),
  period: z.string().regex(/^\d{4}-\d{2}$/),
  costCenter: z.string().regex(/^CC\d{4}$/),
});

const validateOpexData = (data: unknown): OpexData[] => {
  if (Array.isArray(data)) {
    return data.map(item => OpexSchema.parse(item));
  }
  throw new Error('Data must be an array');
};

Results

  • ✅ Users can drill down from summary to individual transactions
  • ✅ 18+ column flexibility without overwhelming users
  • ✅ Real-time Budget vs Actual comparisons
  • ✅ Capex projects properly aggregated across periods
  • ✅ Export to Excel for offline analysis

Performance Optimization

With 10,000+ rows of data, we needed:

  • Virtual scrolling (react-window)
  • Lazy loading for drill-down
  • Pagination on backend
  • Memoization of calculations

What I Learned

  1. Financial data is messy - expect inconsistencies
  2. Table UX is hard - users want power + simplicity
  3. Data transformation - 70% of the code was reshaping data
  4. Business logic - understanding OPEX vs CAPEX was essential

building financial tools sounds boring until you realize: people make million-dollar decisions based on these reports. accuracy matters.

Related Articles