Adia
Back to work

Help Desk Ticketing System

A fault at a retail-outlet terminal becomes a ticket — pinned to its outlet, owned by a technician, and tracked from intake to an on-site close.

My role
Full-stack · Go API, data model, auth & lifecycle
Domain
Field-service support for a retail-outlet network
Scope
Intake · assignment · status workflow · reply threads · dashboard
Stack
Go (Gin) · PostgreSQL (GORM) · React · JWT auth

A helpdesk for a network of retail outlets, where a problem at a specific terminal becomes a ticket pinned to its area, regional, and outlet — assigned to a technician, moved through explicit states, and closed after a field visit. I built the Go API, the PostgreSQL model, the auth middleware, and the ticket lifecycle behind a React console built on a Bootstrap admin template.

Ticket queue — filtered by area / regional / outlet, with status and priority tabs
Ticket queue — filtered by area / regional / outlet, with status and priority tabs

A ticket that knows where it lives.

Every ticket carries its own address — terminal, outlet, regional, area — written onto the row itself, so the queue slices at any level of the hierarchy without joining out to find where a fault physically is. Each account carries a matching scope. But that scope is handed to the browser at login and sent back as filter params the API takes at their word: a view, not a boundary.

helpdesk-detail.png
Ticket detail — the area→regional→outlet→terminal context, lifecycle timestamps, and the reply thread
Ticket detail — the area→regional→outlet→terminal context, lifecycle timestamps, and the reply thread

The trend chart is one grouped query.

Three COUNT(*) FILTER aggregates bucketed by day, straight from Postgres — the chart renders finished numbers instead of counting raw tickets in the app. The status tiles beside it are a second query, so the dashboard is two, not one.

helpdesk-dashboard.png
Dashboard — status counts and the daily activity chart
Dashboard — status counts and the daily activity chart

Status you earn — and a dropdown for the rest.

Starting and closing are buttons, not fields: each stamps the status, the actor and the time in one call, and the request carries no status to override. Reopen a ticket and it drops its close timestamp and the name on it — a closed-by that's no longer true is worse than an empty one. What's missing is the state machine. Nothing stops Finish → Process.

Every route through the same gate.

Auth isn't a decorator you remember to add — every non-public route group goes through the same two middlewares, so no endpoint ships without them. The gap is what they ask. Both check who you are; neither checks what you're allowed to do, so the roles and permissions modelled in Postgres never get read on the server. And the second one — a shared-secret signature next to the JWT — bought nothing: the build inlines that secret into the bundle, so it shipped to every browser. A second copy of the same trust, not a second layer of it.

A helpdesk stays honest when the ticket carries its own context — which terminal, which outlet, who owns it, what state it's in — so nothing depends on the person who happened to pick up the phone.

[ Stack ]
GoGinPostgreSQLGORMReactRedux-SagaJWT authREST