fullstack
Gold Store Management
Internal admin for a gold retailer: sales, stock, staff targets and the profit-and-loss report.
- Role
- Full Stack Developer
- Date
- January 2026
Technologies
- php
- laravel
- mysql
- blade
- vite
- bootstrap
- jquery
- chart.js
- datatables
Problem
A gold retailer’s stock is its capital, and it moves fast. Sales get split between staff (one invoice can be shared by up to three people), so commission has to follow the split rather than the till. Targets are set per person per month, and at month end somebody has to assemble a profit and loss statement out of revenue, operating costs, production costs, salaries and bonuses. Done in spreadsheets, that reconciliation is exactly where the errors live.
The system already existed when the work started, and most of it turned out to be making it safe rather than extending it: about a quarter of the commits describe a fix, a refactor or an adjustment. Authorization across the four roles was the first thing to fall over: menus pointed at pages a role could not open, an access rule sat in a controller constructor where it blocked the people it was meant to allow instead of the ones it was meant to stop, and the sidebar listed modules for staff who had no business seeing them. The expense transactions followed: a total price that turned into NaN whenever the quantity field received anything non-numeric, form validation thin enough that the value reached the backend anyway, and an edit modal that failed to load the transaction it was supposed to be editing.
Stock was the serious one. A single order number can be shared between several users and split evenly among them, and the original update returned stock by always adding the previous quantity back instead of the difference, so the count drifted in whichever direction the arithmetic happened to favour, over a number that is the shop’s capital. Two users adjusting the same product at the same moment could race each other into it as well. Reporting had its own version of the same problem: the sales target was filtered by a month written into the code, monthly and yearly reports did not exist, and the dashboard could not separate one user’s numbers from another’s, all of which then met a change in requirements, when per-branch data and currency formatting arrived and had to be applied across nearly every report template at once.
Solution
Nothing is public: every page needs a login, and four roles (admin, manager, accountant, staff) decide which pages exist for you at all. That rule started out scattered and ended up in one place: an explicit per-role mapping for the menu, the restrictions written next to the routes they protect instead of inside controllers, and the user module hidden from staff rather than shown to them filtered. Sales are recorded as invoice numbers that can be split across up to three staff, each share getting its own sub-number, so commission follows the work; stock is now updated by the difference between old and new, with single-owner and shared orders handled as two explicit paths. The whole update runs inside a database transaction that locks the rows it touches and throws when there is not enough stock to cover the change: an exception rather than a redirect, so a failed update cannot leave half of itself behind. The form still checks stock through an endpoint before it is submitted, which keeps that failure rare, but the invariant now holds on the server regardless of what the client sends: a quantity field full of letters no longer turns the total into NaN.
Reporting grew from a hardcoded month into a layer: target attainment computed from the transactions instead of typed in, monthly and yearly exports to both PDF and Excel, reports split per user and per staff role, branch information in the heading with inactive branches excluded, and currency formatting throughout. Deletion became one pattern instead of a habit: a flag on every table, queries that respect it, and an explicit restore action, so data can be taken out of circulation without being lost. Product codes are generated rather than typed, so the catalogue stays consistent no matter how fast data is entered.
The profit and loss view is computed from the same sales, cost and salary records the rest of the app writes, so the numbers reconcile by construction instead of by care. Each role gets a dashboard comparing transactions against target, which is the only view most staff ever need. The refactor that ran alongside all of this is not finished: a foreign-key migration still carries a date years in the future, and one relationship still names its table with a hyphen where the schema uses an underscore. Both are the kind of leftover the framework would have prevented: validation belongs in form requests, and deletion belongs in the framework’s own trait rather than a column maintained by hand.
Contact
Interested in working together?
Tell me what you are building and what you need. A short note is enough.