Hire Me
โ† All Projects
๐Ÿšฉ Flagship Project In Progress

Construction Inventory
Management System

A multi-user platform replacing the paper-and-WhatsApp workflows on Nigerian construction sites. Built with ASP.NET Core, SQL Server, and Vanilla JS โ€” designed from day one to scale into a complete company operating system.

ASP.NET Core C# / .NET 9 SQL Server EF Core Vanilla JS JWT BCrypt Plesk

What it is

The Construction IMS is a production-oriented web application designed to digitise the operations of Nigerian construction companies. Most sites today run on paper records, WhatsApp messages, and verbal handoffs โ€” systems that break down the moment a project gets complicated or a key person is unavailable.

This system gives companies a central platform where every material, piece of equipment, and worker is tracked per project โ€” with the right people having the right access, and a full audit trail of every change.

3
Inventory Types
โˆž
Projects per Company
5+
Worker Roles
1
Unified Platform

What I was solving

Construction sites in Nigeria โ€” and many other places โ€” operate with almost no digital infrastructure. Inventory is tracked in notebooks. Workers are managed through phone calls. Materials go missing because nobody knows how much was ordered vs how much arrived. Project managers juggle multiple WhatsApp groups that contain critical information nobody can search or audit.

I built this because I've worked on construction sites myself. I know exactly what that chaos looks like โ€” and I know what a well-designed system could fix.

๐Ÿ“‹
Paper Records
Inventory tracked in notebooks. No search, no history, no access control.
๐Ÿ“ฑ
WhatsApp as a Database
Critical project updates buried in group chats. No audit trail. No structure.
๐Ÿ”‘
No Access Control
Everyone sees everything. No way to give a worker site access without full admin.
๐Ÿ“Š
No Vendor Tracking
No record of which vendor charged what, or whether materials were good quality.

What it does

๐Ÿ”

Authentication Pipeline

Full JWT-based stateless authentication. Users register, verify their email, log in, and receive a signed token. Every protected API route checks the token via Authorization middleware before any data is returned. Passwords are hashed with BCrypt โ€” never stored in plain text.

๐Ÿ—๏ธ

Project-Specific Role-Based Access

This was one of the hardest design decisions: a user can be a Builder on Project A and a Worker on Project B. There are no global roles. Permissions are resolved at the project level every time a request comes in. This mirrors how real construction companies actually operate.

๐Ÿ“ฆ

Dynamic Inventory Management

Each project can have up to three inventory types: Building Materials, Building Equipment, and Site Workers. The system checks which types already exist before showing the create option โ€” preventing duplicates. Tables are generated dynamically from the API response, not hardcoded in HTML.

๐Ÿ”

Search, Sort & Filter

Projects can be searched by name, address, or creation date in real time. The list can be sorted by any of those fields. When fewer than 4 projects exist, the sort controls are hidden โ€” small details like this keep the UI clean without cluttering it with options the user doesn't need yet.

๐Ÿ“

Update History Tracking

Every time a project is edited, the system stores what changed โ€” old title vs new title, old address vs new address โ€” with a timestamp. This creates a full audit trail viewable in a modal, so project managers can always see who changed what and when.

See it in action

A live deployment, screenshot walkthrough, and video tour of the system โ€” showing the full flow from login through inventory management.

01 โ€” Live App

Try the deployed system

The IMS is deployed on Plesk with a live SQL Server database. Click below to open the real application.

02 โ€” Screenshots

Key screens walkthrough

A visual tour through the main flows โ€” from authentication through project management to inventory tracking.

๐Ÿ–ผ๏ธ Login Screen Replace with: assets/images/ims-login.png
Login & Authentication
๐Ÿ–ผ๏ธ Dashboard Replace with: assets/images/ims-dashboard.png
Project Dashboard
๐Ÿ–ผ๏ธ Project View Replace with: assets/images/ims-project.png
Project Detail View
๐Ÿ–ผ๏ธ Inventory Table Replace with: assets/images/ims-inventory.png
Inventory Management
๐Ÿ–ผ๏ธ Role Management Replace with: assets/images/ims-roles.png
Project-Specific Roles
๐Ÿ–ผ๏ธ Update History Replace with: assets/images/ims-history.png
Audit Trail Modal
03 โ€” Video Walkthrough

Full system tour

A screen recording walking through every major feature โ€” from signup and email verification through to inventory management and audit history.

โ–ถ
Walkthrough video coming soon

A full screen recording of the system in action will be added here โ€” covering the complete flow from registration through project and inventory management.

How I built this

This is where the real engineering decisions live. Not just what the app does โ€” but the specific patterns and techniques I used to build it, and why.

Design Pattern

Factory Pattern for Inventory Tables

The app has three inventory types โ€” Materials, Equipment, and Workers โ€” each with different table columns, different row data, and different register buttons. Instead of writing three separate render functions, I built one function getInventoryRenderConfig(type) that returns a configuration object (headers, row template, button text) depending on the type passed in. The render function then uses that config without knowing which type it's working with.

What I said to myself

"One function gives me the right table setup depending on inventory type"

โ†’
What it's called

Factory Pattern โ€” a function that produces different objects based on the input type, without the caller needing to know the details

js / main.js
function getInventoryRenderConfig(type) {
  switch (type) {
    case 'materials':
      return {
        headers: `<th>NAME</th><th>UNIT COST</th>...`,
        renderRow: (obj, sn) => `<tr><td>${obj.name}</td>...</tr>`,
        buttonText: 'Register Material'
      };
    case 'equipment':
      return { /* different config */ };
    case 'workers':
      return { /* different config */ };
  }
}
Asynchronous Programming

async/await for Sequential API Calls

When a user loads their dashboard, I need to do two things in order: first fetch their profile (to get their name), then fetch their projects. Using async/await makes this sequential and readable โ€” each line waits for the previous one to complete before moving on. If the token is missing or expired, I redirect to login immediately without making unnecessary API calls.

What I said to myself

"Get the user name first, then load the projects, and handle errors at each step"

โ†’
What it's called

Asynchronous programming with sequential execution โ€” async/await ensures each API call completes before the next begins, without blocking the browser

js / main.js
async function loadProjects() {
  const token = localStorage.getItem("authToken");
  if (!token) {
    window.location.href = "/login.html"; // Guard clause โ€” stop early
    return;
  }

  // First API call โ€” fetch profile
  const profileResponse = await fetch("api/profile/me", {
    headers: { "Authorization": `Bearer ${token}` }
  });
  const profile = await profileResponse.json();

  // Second API call โ€” fetch projects (runs after profile is done)
  const response = await fetch('/api/projects', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  const projects = await response.json();
}
Defensive Programming

Race Condition Prevention

When a user clicks on a project card, the app fetches that project's details from the API. If the user clicks the same card twice quickly โ€” or clicks two cards in a row before the first response arrives โ€” two requests fire at the same time and the UI can end up in a broken state. I solved this with a simple boolean flag: isFetchingProjectPage. If a fetch is already running, any new click is ignored until it completes.

What I said to myself

"If someone clicks twice, I don't want it to load twice"

โ†’
What it's called

Race condition prevention using a mutex flag โ€” a boolean lock that ensures only one async operation runs at a time

js / main.js
let isFetchingProjectPage = false; // Lock flag

wrapper.addEventListener('click', async () => {
  if (isFetchingProjectPage) return; // Ignore click if already loading

  isFetchingProjectPage = true; // Acquire lock
  try {
    await fetchAndDisplayProjectPage(project.id);
  } catch (err) {
    console.error("Error displaying project page:", err);
  } finally {
    isFetchingProjectPage = false; // Always release lock, even on error
  }
});
Architecture

Separation of Concerns

The project page rendering is split into small, focused functions โ€” each one doing exactly one job. renderProjectPage coordinates but doesn't do the work itself. renderProjectHeader builds only the header. setupCreateInventoryButton handles only that button's logic. setupCloseProjectPageButton handles only the close. This means if the header breaks, I know exactly where to look โ€” and I can change one part without touching the others.

What I said to myself

"Each function should only do one thing"

โ†’
What it's called

Separation of Concerns and the Single Responsibility Principle (SRP) โ€” each function has one job and one reason to change

js / main.js
// Coordinator โ€” calls others but does no work itself
function renderProjectPage({ id, name, type, address }) {
  ensureProjectPageContainer();       // Sets up the DOM container
  renderProjectHeader(id, name, address); // Builds the header HTML
  setupCreateInventoryButton(id, type);   // Wires up the create button
  setupCloseProjectPageButton();          // Wires up the close button
}
DOM & Events

Event Delegation

Project cards are created dynamically โ€” they don't exist in the HTML when the page loads. Because of this, I can't attach event listeners to them directly on page load. Instead, I attach one listener to the document and check whether the clicked element is the one I care about. This also means I only need one listener no matter how many cards exist โ€” cleaner and more memory-efficient.

What I said to myself

"I can't add listeners to elements that don't exist yet, so I listen on the parent instead"

โ†’
What it's called

Event delegation โ€” attaching a single listener to a parent element and using event.target to determine which child was actually clicked

js / main.js
// One listener on document handles clicks from many dynamic elements
document.addEventListener('mousedown', function (event) {
  if (event.target.classList.contains('show-history')) {
    // This fires for any .show-history element, even ones added later
    projectIdForModal = event.target.getAttribute('project-id');
    fetchUpdateDetails(projectIdForModal);
  }
});
Data Validation

Duplicate Detection with Independent Field Flagging

When a user adds a project, I check whether a project with the same name and address already exists. But instead of just blocking the submission with a generic error, I flag each field independently โ€” if the name matches, the name field border turns red. If the address matches, the address field border turns red. Only if both match is the submission blocked. This gives the user precise feedback about exactly what the problem is.

What I said to myself

"Show them which field is the problem, not just a generic error message"

โ†’
What it's called

Duplicate detection with field-level validation feedback โ€” checking uniqueness constraints client-side before making an API call

js / main.js
function checkForDuplicateProjectInputs(title, address) {
  let nameMatch = false;
  let addressMatch = false;

  for (let i = 0; i < projectListTitles.length; i++) {
    if (existingTitle === enteredTitle) nameMatch = true;
    if (existingAddress === enteredAddress) addressMatch = true;
  }

  // Flag each field independently โ€” red border on the offending input
  if (nameMatch) addProjectTitle.style.border = '2px solid red';
  if (addressMatch) addProjectAddress.style.border = '2px solid red';

  // Only block submission if BOTH match โ€” a duplicate project
  if (nameMatch && addressMatch) {
    toggleNotification(`Project already exists.`, '#f44336', true);
    return false;
  }
  return true;
}

How it's structured

Frontend
HTML5 / CSS3 Vanilla JavaScript Modular JS files Fetch API (relative paths) Dynamic DOM rendering
โ†“
Backend API
ASP.NET Core Web API C# Controllers JWT Middleware BCrypt Password Hashing RESTful Endpoints
โ†“
Data Layer
Microsoft SQL Server Entity Framework Core EF Core Migrations Relational Data Model LINQ Queries
โ†“
Deployment
Plesk Obsidian Hosted SQL Server Relative API Routing

What's next

The IMS is the core of a larger vision โ€” a complete digital operating system for construction companies. These are the planned additions:

JWT Authentication + Role-Based Access
Login, signup, email verification, project-specific permissions
Dynamic Inventory Management
Materials, equipment, workers โ€” all generated from the database
Project Update History
Full audit trail of all project changes with timestamps
Expense Report Integration
Daily field expenses feed into the company database for tracking
Vendor Analytics
Track pricing over time, compare vendor quality and cost
LAN Messaging System
Internal company comms over Wi-Fi โ€” no internet required
Worker Ratings & Discovery
Rate workers, find skilled labour near the site
Offline Caching
Access critical data even when the site loses connectivity