
Employee Portal Modules
Two Laravel modules built during an internship, replacing paper job order and purchase order routing with tracked approval workflows inside an existing staff portal.
- Engagement
- Internship
- Type
- Internal Tool
- Role
- Backend and Full-stack Developer
Demo
Archived
Code
Private
Tech Stack
Both modules ship inside an organization's internal staff portal. The source and the deployment are theirs, not mine, so there is no repository or demo to link, and the screenshots contain live operational data I have not published. What follows is the design and the reasoning, which are mine to describe.
The Brief
The organization already ran an internal employee portal. Two of its processes had never made it in: requesting work to be done, and requesting things to be bought. Both ran on printed forms carried between desks for signatures.
That has a specific failure mode, and it is not the paperwork. It is that a request in transit is invisible. Nobody could answer where a request had stalled, who was holding it, or whether it had been approved last week and forgotten, without walking the building and asking. Requesters chased. Approvers were chased.
I built both modules during my internship, as additions to the existing portal rather than as standalone applications. That constraint set most of the technical decisions before I made any of them.
Job Order Module
Staff raise a request for work to be done: the task, the location, the urgency, the justification. It enters a queue rather than a person's inbox.
Assigned personnel pick requests up, move them through the workflow, and close them out. Every status change is recorded against the request with who made it and when, which is what turns "where is my request" from a question you ask a person into one you ask the system.
The modelling decision worth naming is that status lives on the request as a history of transitions, not as a single overwritten field. Knowing a request is approved answers less than knowing how long it sat pending first. The second is the thing the organization was missing, and it is only available if you never overwrite the first.
Purchase Order Module
Same shape, different rules. A requester submits items, quantities, suppliers, pricing, and a justification. Approvers review, then approve or reject with a reason attached.
Purchasing differs from job orders in a way that matters: it carries money and it carries a paper trail obligation. A rejected job order is a task nobody does. A rejected purchase order is a decision somebody may have to account for later, so a rejection reason is required rather than optional, and approval history is retained rather than superseded.
Validation also has to be stricter, because the cost of a badly filled purchase request is not a correction, it is an order placed for the wrong thing.
Key Decisions
Blade instead of a frontend framework
The portal was already a server-rendered Laravel application. Introducing React for two modules would have meant a build step, a second styling system, and an authentication seam between the new pages and every existing one, in exchange for interactivity these forms do not need.
The cost: every interaction is a round trip. Filtering a request list reloads the page. That is genuinely worse than a client-rendered table would be, and it would be the wrong call if these screens were used continuously rather than a few times a day.
SQLite as the database
This is the decision I would push back on if I were making it now, so it is worth being straight about.
SQLite was the path of least resistance for internal deployment: no server to provision, no credentials to manage, a database that backs up by copying a file. For the concurrency these modules see, where a handful of people submit and approve requests across a working day, it holds up.
The cost: SQLite serializes writes. Two approvals landing at the same instant means one waits, and under real contention that becomes lock timeouts rather than queuing. It also gives up the things you want as this grows: no concurrent write scaling, weaker typing, and a migration to Postgres later that gets more expensive the longer it is deferred. It was the right call for the deadline and the wrong shape for the destination.
Status transitions as records
Both modules treat a status change as an event to append rather than a field to overwrite. This is what makes the system answer the question the organization actually had, which was never "what state is this in" but "why has this taken so long".
The cost: more rows, and every read of "current status" is a query against history rather than a column lookup. Worth it here, and I would keep this one.
Laravel's built-in authentication and validation
Both modules use the framework's authentication, request validation, and MVC structure rather than hand-rolled equivalents. On a fixed internship timeline, the code I did not write is the code that did not need reviewing, and it kept both modules recognizable to whoever maintains the portal after me.
What I'd Do Differently
The two modules are more similar than their code admits. Both are a request, a set of approval stages, a status history, and a notification. I built them one after the other and wrote that shape twice rather than extracting it, so a fix to the workflow logic in one module does not reach the other. A shared workflow layer with per-module rules would have been the same amount of work and considerably less to maintain.
I also never instrumented the thing the project existed to improve. The whole justification was that nobody could see where requests were stalling, and the status history now holds exactly the data needed to measure that. Nothing surfaces it. A dashboard showing average time in each stage would have turned the modules from a replacement for paper into an argument for having replaced it.
The last one is that I optimized for shipping inside the internship rather than for whoever inherits it. SQLite is the clearest example: a decision that was correct on my timeline and inconvenient on theirs.