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.
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.
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.
Try the deployed system
The IMS is deployed on Plesk with a live SQL Server database. Click below to open the real application.
Key screens walkthrough
A visual tour through the main flows โ from authentication through project management to inventory tracking.
Full system tour
A screen recording walking through every major feature โ from signup and email verification through to inventory management and audit history.
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.
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.
"One function gives me the right table setup depending on inventory type"
Factory Pattern โ a function that produces different objects based on the input type, without the caller needing to know the details
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 */ };
}
}
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.
"Get the user name first, then load the projects, and handle errors at each step"
Asynchronous programming with sequential execution โ async/await ensures each API call completes before the next begins, without blocking the browser
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();
}
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.
"If someone clicks twice, I don't want it to load twice"
Race condition prevention using a mutex flag โ a boolean lock that ensures only one async operation runs at a time
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
}
});
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.
"Each function should only do one thing"
Separation of Concerns and the Single Responsibility Principle (SRP) โ each function has one job and one reason to change
// 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
}
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.
"I can't add listeners to elements that don't exist yet, so I listen on the parent instead"
Event delegation โ attaching a single
listener to a parent element and using
event.target to determine which child was
actually clicked
// 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);
}
});
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.
"Show them which field is the problem, not just a generic error message"
Duplicate detection with field-level validation feedback โ checking uniqueness constraints client-side before making an API call
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
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: