Innobi OpsCore : ERP Operations Control Platform

Web-based ERP experience layer that brings customers, projects, vendors, employees, purchase orders, and invoices into a single, opinionated UX. Innobi OpsCore is designed as a clean, production-style shell for a mid-sized B2B company: a modular React SPA with dark/light theming, role-aware navigation, and frictionless CRUD flows for the most common operational entities.

Instead of building yet another static admin template, OpsCore is tuned as a portfolio-grade ERP front-end that mirrors real-world processes: project financials linked to customers, vendor sourcing tied to POs, and approvals captured inline with audit context. The app showcases a layout system (sidebar + header), a reusable design language, and interactive charts that can later be wired to a full Innobi data warehouse or cloud backend.

  • Domain ERP Project Accounting Operations Control
  • Technique SPA Layout System Client-Side Approvals Responsive UI & Dark Mode Recharts Visuals
  • Tech Stack React + TypeScript Vite Tailwind CSS & shadcn/ui Supabase (Postgres + Auth)

Case Study: Innobi OpsCore ERP Experience Layer

6+

Core Operational Modules

<200ms

Route Transition Latency

100%

TypeScript Frontend

SPA

Single-Page Navigation Model

Operational teams typically live in fragmented tools: ERP, spreadsheets, email, and ad hoc dashboards. For a mid-sized B2B company with project-based work, that means customer context, project budgets, vendor spend, and invoices are never visible in one place. The goal for Innobi OpsCore was to build a realistic, modern ERP shell that unifies these flows and demonstrates end-to-end experience design.

OpsCore abstracts the core building blocks of a services ERP—Customers, Projects, Vendors, Purchase Orders, Invoices, and Actual Costs—into a cohesive React application. It uses a flexible layout system, clean table design, and inline status badges to showcase how BI, approvals, and operational CRUD can live together while remaining visually minimal and recruiter-friendly.

The Challenge

  • Scattered operational context: Customers, projects, and vendor spend lived in separate tools, making it hard to see end-to-end profitability for each engagement.
  • Clunky legacy UX: Traditional ERP UIs are dense, inconsistent, and hard to present in a modern BI/engineering portfolio.
  • Approval workflows in email: PO and invoice approvals were conceptualised as email chains rather than structured, auditable actions.

The Solution & Architecture

  • React SPA layout shell: Shared <Layout> component with sidebar, sticky header, and responsive content area for all modules.
  • Module-based routing: React Router drives screens for Customers, Projects, Vendors, Employees, Purchase Orders, Invoices, and Actual Costs.
  • Supabase-backed entities: Tables are designed to plug into a Supabase Postgres model for CRUD and auth, while still working with seeded mock data.
  • Client-side approval store: Lightweight approvals engine for POs and Invoices implemented via a localStorage-backed TypeScript store.
  • Recharts-enabled analytics: Revenue and margin charts embedded directly into the ERP shell to close the loop between operations and BI.

TypeScript Approvals Store (POs & Invoices)

A small, focused approvals module keeps PO/invoice decisions and timestamps in sync with the UI while remaining easy to migrate to a backend later.

approvals.ts
export type ApprovalEntityType = "po" | "invoice";

export type ApprovalStatus = "pending" | "approved" | "rejected";

export interface ApprovalRecord {
  id: string;                // "po:123", "invoice:456"
  entityType: ApprovalEntityType;
  entityId: string;
  status: ApprovalStatus;
  lastActionBy: string;
  lastActionAt: string;      // ISO timestamp
  comment?: string;
}

const STORAGE_KEY = "innobi::opscore::approvals";

function readStore(): Record<string, ApprovalRecord> {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : {};
  } catch {
    return {};
  }
}

function writeStore(data: Record<string, ApprovalRecord>) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}

export function getApproval(entityType: ApprovalEntityType, entityId: string): ApprovalRecord | undefined {
  const store = readStore();
  return store[buildKey(entityType, entityId)];
}

function buildKey(entityType: ApprovalEntityType, entityId: string) {
  return `${entityType}:${entityId}`;
}

interface UpsertApprovalOptions {
  entityType: ApprovalEntityType;
  entityId: string;
  status: Exclude<ApprovalStatus, "pending">;
  user: string;
  comment?: string;
}

export function upsertApproval(options: UpsertApprovalOptions): ApprovalRecord {
  const { entityType, entityId, status, user, comment } = options;
  const key = buildKey(entityType, entityId);
  const store = readStore();

  const record: ApprovalRecord = {
    id: key,
    entityType,
    entityId,
    status,
    lastActionBy: user,
    lastActionAt: new Date().toISOString(),
    comment: comment?.trim() || store[key]?.comment,
  };

  store[key] = record;
  writeStore(store);
  return record;
}

This module powers inline badges such as "Approved" / "Rejected" on PO and invoice tables and can later be replaced by a server-side audit trail with minimal refactoring.