In June 2024 I started as a frontend developer at Miracle Yard, where I had interned a few months earlier. A year before that I was working in pharmaceutical sales. My first big task was meromenu, a restaurant management system: menus, orders, tables, credit customers, inventory and reports.
It was built on Vue 2, and Vue 2 had reached its end of life in December 2023. No more security fixes, and fewer libraries supporting it every month. It had to move to Vue 3.
Not an upgrade, a rebuild
We did not try to upgrade the old code in place. A senior developer started a fresh Nuxt 3 project with Vue 3, Vuetify 3 and Pinia, and set up the foundations: login, a wrapper for API calls, form validation helpers, a shared data table and route guards. Then we moved the app over one page at a time.
My first commits were small: a date column on a report, validation on the login form. Another developer built several report pages. By July I was the only one working on it, and over the next three months I rebuilt most of the remaining app: the whole inventory module (products, units, vendors, locations, stock, purchase entries, credit and debit notes), credit customers and credit recovery, the customer pages, several reports, the take-order screen and the dashboard.
Rebuilding page by page meant I could compare each new page with the old one before moving on. It also meant every page taught me something. Two bugs taught me the most.
Bug 1: the arrow that never changed
The stock page lists every product, and clicking a row expands it to show details. In the new version the arrow on each row always pointed the same way, open or closed.
The cause was a small change in Vuetify 3. In Vuetify 2, the data table's
item slot gave you isExpanded as a true/false value. In Vuetify 3 it is a
function, and you pass it the row to ask about:
<v-icon>
{{ isExpanded ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon><v-icon>
{{ isExpanded(internalItem) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>A function is always "truthy", so the old check was always true, and there was no error to point at it. The fix was one line. Finding it took me from 30 July to 4 August, because the code looked correct.
The lesson: when a component library changes major versions, read its upgrade guide for every component you use, even the ones you think you know.
Bug 2: pages that would not paginate
Paging did not work on the inventory activity log. The URL for the table looked like this:
<TheDataTable
:url="accountAPI.activityLog(org) + '?action_by=&action=&app_name=Inventory'"
/>Our shared data table adds its own query string for paging:
getData(prop.url + '?' + serializedParam)So the final URL had two ? characters. Everything after the second one, the
offset and limit for paging, was read as part of the app_name value.
The table component already accepted filters as an object, so the fix was to
use it:
<TheDataTable
:url="accountAPI.activityLog(org)"
:filter-params="{ action_by: '', action: '', app_name: 'Inventory' }"
/>The lesson: when a shared component has a way to do something, use it, and read the component before working around it.
What I would do differently
Looking back at my commit history from that time, I see messages like "for backup", "just for record" and "initial commit: error to be solved". I was committing to save my work, not to explain it. Today I write commits that say what changed and why, so the next developer, or me six months later, can follow the history.
I would also read each library's migration guide at the start, not when something breaks. Several of my bugs came from small API changes that were already documented.
What it gave me
This project is where I learned Vue 3 for real: the Composition API, Pinia, Vuetify 3 and Nuxt 3, on a real product instead of a tutorial. It also taught me something I still use: when the code looks right but the app is wrong, check what changed underneath it, one assumption at a time.