Chapter 29 — Help Desk¶
1. Purpose¶
The Help Desk is Bizwiz's internal ticketing system. When a member of staff — a branch cashier, a warehouse clerk, a route supervisor — hits something broken, confusing, or missing in the ERP, they raise a ticket here. The ticket then flows to an internal support team (in practice, IT / the engineering group) who triage it, assign it to a person, work it, and close it out. The presence of a dedicated "Development" status is the tell: this is not a customer-care queue, it is effectively the internal engineering / support backlog — a ticket that reaches "Development" has been accepted as work for the dev team to build or fix.
A ticket carries a subject, a free-text description, a priority, a category (e.g. "Sales", "Stock", "Reports" — configured elsewhere), the module of the ERP it concerns, and an optional file attachment (screenshot, export). From creation onward every state change, assignment, and reply is recorded as an append-only history, so you can always see who did what and when.
Technically this module is thin and self-contained. A single controller — TicketController (app/Http/Controllers/Admin/TicketController.php) — backs the entire lifecycle. It is wired under the help-desk. route group in routes/web.php:4829-4835. The permission tree is defined in app/Permissions/HelpDesk.php under the root model help-desk (permission help-desk___view) with a child model help-desk-tickets. The navigation is resources/views/admin/includes/sidebar_includes/help_desk.blade.php.
The data lives in five tables created in July 2024 (tickets, ticket_statuses, ticket_assignees, ticket_responses, ticket_categories) plus support_teams. Statuses are not a column on the ticket — they are event rows in a separate ticket_statuses table, and the current status is always "the latest row." Same for assignees.
2. Users & roles¶
Two informal populations use the Help Desk:
- Requesters — any staff member with
help-desk-tickets___addcan raise a ticket and see their own via "My Tickets." - Support / agents — members of the Support Team (
support_teamstable). These are the people a ticket can be assigned to, who respond to it, and who move it through Development → Completed → Closed.
Role 1 (super-admin) bypasses every permission check throughout (role_id == 1).
Permissions come from app/Permissions/HelpDesk.php:
| Permission key (flattened) | Gate in controller | Controls |
|---|---|---|
help-desk___view |
sidebar visibility | shows the Help Desk menu (help_desk.blade.php:1,18) |
help-desk-tickets___add |
can('add', 'help-desk-tickets') |
create ticket (TicketController.php:235,258) |
help-desk-tickets___my-tickets |
can('my-tickets', ...) |
"My Tickets" list (:146) |
help-desk-tickets___status-tickets |
can('status-tickets', ...) |
status-filtered lists Open/Development/Completed/Closed (:47) |
help-desk-tickets___show |
can('show', ...) |
open a ticket detail (:351) |
help-desk-tickets___assign |
can('assign', ...) |
assign to a support user (:397) |
help-desk-tickets___respond |
can('respond', ...) |
post a reply (:435) |
help-desk-tickets___update-status |
can('update-status', ...) |
change the status (:489) |
Note the sidebar only surfaces three of these (add, my-tickets, status-tickets); show, assign, respond, update-status are contextual actions on the detail page, not menu items.
Visibility scoping. On the main list, a non-admin without the view___help-desk-tickets permission only sees tickets they created or are currently assigned to (TicketController.php:88-95). "My Tickets" hard-filters to created_by = current user regardless (:164). So an ordinary requester sees their own tickets; a support agent sees theirs plus anything routed to them; an admin sees everything.
3. Processes¶
Raising a ticket. The requester fills subject / message / priority / category / module (+ optional attachment). On save, the system pulls a sequential ticket number from the WaNumerSeriesCode series for module TICKET, formats a human code like TKT-00042, stores the attachment to public/uploads/Tickets, writes the tickets row, and immediately writes an Open status row. Every Support-Team member flagged get_notifications = 1 is then notified by mail and SMS.
Working a ticket. From the detail page a support agent can (a) assign it to a user, (b) respond with a message, and (c) change status. Each of these appends a row to its respective history table and fires a notification to the ticket creator.
sequenceDiagram
actor Req as Requester (staff)
participant C as TicketController
participant DB as tickets / statuses
actor Sup as Support agent
Req->>C: store() subject, priority, category, attachment
C->>DB: create Ticket + status "Open"
C-->>Sup: notify SupportTeam (mail + SMS)
Sup->>C: assign(assignee)
C->>DB: TicketAssignee row (latest = current)
C-->>Sup: TicketAssignNotification (mail+SMS)
Sup->>C: status("Development")
C->>DB: TicketStatus row
C-->>Req: TicketStatusUpdateNotification
Sup->>C: respond(message)
C->>DB: TicketResponse row
C-->>Req: TicketResponseNotification
The state machine. Status is enforced client-side in the detail view (ticket_view.blade.php:349-363): the dropdown only offers the legal next status given the current one. There is no server-side transition guard — status() writes whatever value it is given — so the allowed graph below is the intended flow, not a hard constraint.
stateDiagram-v2
[*] --> Open: ticket created
Open --> Development: accepted by dev/support
Development --> Completed: work done
Completed --> Closed: signed off
Closed --> ReOpen: reopened
ReOpen --> Development: back into dev
note right of Open : "Open" and "Re-Open"\nboth lead to Development
Concretely, the UI permits: Open → Development, Re-Open → Development, Development → Completed, Completed → Closed, Closed → Re-Open. Notably there is no direct Open → Closed; even trivial tickets are meant to pass through Development and Completed. The six sidebar lists (New / My / Open / Development / Completed / Closed) are just this same table filtered by the latest status.
Assignment mechanics. assign() (:395) inserts a TicketAssignee row; "current assignee" is simply the latest such row (Ticket::current_assignee = hasOne(...)->latest()). Re-assigning just adds another row. respond() (:433) has a nice fallback: if no assignee exists yet, the responder auto-assigns the ticket to themselves before writing the response, so replying implicitly takes ownership. Responses support an internal-note flag (ticket_responses.note, "only agents can see this") though the create-response path in the controller does not currently set it — see Open Questions.
Status mechanics. status() (:487) inserts a TicketStatus row and notifies the creator. Current status = latest ticket_statuses row, computed with a MAX(id) correlated subquery in the list queries (:97-103). Rows in "My Tickets" are colour-coded: Open → red, Development → blue, Completed → green (:212-222).
4. Tables touched & key data¶
All schemas from database/migrations/2024_07_*:
| Table | Key columns | Notes |
|---|---|---|
tickets |
code, subject, message, priority, branch_id, module, attachment, category_id, created_by |
The ticket itself. branch_id = creator's restaurant_id. category_id added later (2024_07_26_165927). |
ticket_statuses |
ticket_id, status (string), created_by |
Append-only status history. No FK/enum — status is a free string ("Open", "Development", "Completed", "Closed", "Re-Open"). |
ticket_assignees |
ticket_id, assignee_id, created_by |
Append-only assignment history; latest = current owner. |
ticket_responses |
ticket_id, assignee_id, message, note (bool), created_by |
Threaded replies. note=1 = internal, agent-only (per migration comment). |
ticket_categories |
title, created_by |
Free-text category list (e.g. Sales, Stock). |
support_teams |
user_id, get_notifications (bool), created_by |
Roster of agents; get_notifications=1 = paged on new tickets. |
Design pattern worth flagging: the module uses event-sourced status and assignment rather than mutable columns. There is no status column on tickets; the current state is derived by "latest row wins." This gives free audit history but means every list query carries a correlated subquery. All models (Ticket, TicketStatus, TicketAssignee, TicketResponse) extend BaseModel with $guarded = [] (mass-assignment open).
The ticket code is drawn from the shared wa_numer_series_codes table under module TICKET (TicketController.php:279-283) — the same numbering-series mechanism used across Bizwiz for document numbers.
5. Interactions with other modules¶
- CRM (Chapter 28) — DIFFERENT system, do not conflate. See §6 for the crisp table-level distinction. In short: Help Desk =
tickets(internal staff issues); CRM =case_tickets(customer-facing cases). They share zero tables and zero controllers. - System Admin (Chapter 27) — configuration lives there. The two config surfaces the Help Desk depends on are not in the Help Desk menu; they sit under the main admin route group:
Route::resource('ticket-category', TicketCategoryController::class)andRoute::resource('support-team', HelpDeskSupportController::class)(routes/web.php:5009-5011). So Ticket Categories and the Support Team roster are configured as System-Admin objects and consumed here. - Communication Centre (Chapter 30) — notifications go out over mail + SMS. Every Help Desk notification (
TicketCreationAdminNotification,TicketAssignNotification,TicketResponseNotification,TicketStatusUpdateNotification) declaresvia() = ['mail', InfoSky::class]— Laravelmailplus the InfoSky SMS channel (app/Notifications/Channels/InfoSky.php, backed byInfoSkySmsService). New tickets page the support team; assignment / response / status changes notify the ticket creator. - Branches / Users.
branch_idlinks a ticket to the creator'sRestaurant(branch);created_by/assignee_idlink toUser.
The "bulk-sms" navigation reference — VERDICT: copy-paste artifact. The sidebar (help_desk.blade.php:19-23) sets $active_class when $model is in ['tickets', 'bulk-sms-test-message', 'bulk-sms-message-log']. This is a stray artifact, not a real integration:
$active_classis computed but never used anywhere in the file (the<li>items each compute their ownactiveinline).- No Help Desk route, controller, view, or model references bulk SMS.
grep bulk-sms resources/views/admin/help_desk/returns nothing. - The two model names belong to the Communication Centre's Bulk SMS screens.
The block was almost certainly cloned from the Bulk-SMS sidebar partial and left in. It has no functional effect on the Help Desk. (Note: Help Desk does send SMS, but via the InfoSky notification channel above — that is a genuine, separate mechanism unrelated to this dead $active_class.)
6. Alternatives & variants¶
Bizwiz contains three parallel ticketing systems, which is the single most important thing to keep straight:
| System | Table(s) | Model | Controller | Audience |
|---|---|---|---|---|
| Help Desk (this chapter) | tickets, ticket_statuses, ticket_assignees, ticket_responses |
App\Models\Ticket |
TicketController |
Internal staff → IT/dev |
| CRM (Ch 28) | case_tickets, case_ticket_comments, case_ticket_assignments, case_ticket_categories |
App\Models\CaseTicket |
CaseTicketController |
Customer-facing cases |
| Supplier ticketing | supplier-case tables | App\Models\SupplierCaseTicketing (routes) |
SupplierCaseTicketingController |
Supplier disputes |
| Weighbridge tickets | weighbridge_tickets |
WeighbridgeTicket |
WeighbridgeTicketController |
Weighbridge ops (unrelated) |
Help Desk Ticket vs CRM CaseTicket — crisp distinction: they are entirely separate tables (tickets vs case_tickets), separate models, separate controllers, separate route groups (help-desk.* vs cases.*), and even separate category tables (ticket_categories vs case_ticket_categories). The Help Desk is the internal engineering/support backlog with a hard-coded Open→Development→Completed→Closed flow; CRM Cases is the customer/route case-management system with its own escalation matrix. They do not share data.
No SLA. There is no SLA / due-date / resolution-time logic anywhere in the Help Desk path — no timers, no EscalateTicketJob, no overdue reports. (Those exist, but for CRM case tickets: app/Jobs/EscalateTicketJob.php, GenerateOverdueTicketsJob.php, ResolutionTimeOverdueCaseTicketsController — Ch 28, not here.) Help Desk priority is a plain string field with no automated consequence.
7. Open questions¶
Code-proven observations (no ambiguity):
- Statuses are free strings with no server-side transition guard; the legal flow is enforced only by the blade dropdown (ticket_view.blade.php:349-363). A crafted POST to help-desk.tickets.status could set any status value (TicketController.php:504-509).
- The self-notification on creation is commented out (TicketController.php:326-327) — the requester is not notified their own ticket was created; only the support team is.
- The ticket_responses.note (internal-note) flag exists in schema but respond() never sets it (:465-470), so all responses are visible to the requester in the current code path. The feature is scaffolded but unwired.
Inference / needs runtime confirmation (NOT proven from static reading):
- Whether Support-Team membership is the only thing that makes someone an "agent," or whether the help-desk-tickets permissions are also granted to those users, is a configuration question resolved in the permissions DB, not in code.
- Whether SMS actually delivers depends on each notifiable User having a phone_number — InfoSky::send() silently no-ops if $notifiable->phone_number is unset. Coverage in practice is unknown from static analysis.
- The module field on a ticket is a required free-text/select input (store() validates module required) but its allowed values / picklist source were not located in the controller; likely a static list in create.blade.php (not read here).
- Attachment is single-file only (tickets.attachment is one string column); multi-attachment is not supported.
8. Source references¶
- Navigation:
resources/views/admin/includes/sidebar_includes/help_desk.blade.php:1-75(bulk-sms artifact at:19-23). - Routes:
routes/web.php:4829-4835(help-desk group);routes/web.php:5009-5011(ticket-category + support-team config under System Admin). - Controller:
app/Http/Controllers/Admin/TicketController.php—index():45,my_tickets():144,create():233,store():256(code series:279, Open status:301, support notify:307-324),show():349,assign():395,respond():433,status():487. - Permissions:
app/Permissions/HelpDesk.php:1-49. - Models:
app/Models/Ticket.php(relations,current_status/current_assignee=latest()),TicketStatus.php,TicketAssignee.php,TicketResponse.php,SupportTeam.php. - Migrations:
database/migrations/2024_07_19_103809_create_tickets_table.php,..._104141_create_ticket_statuses_table.php,..._104350_create_ticket_assignees_table.php,..._104528_create_ticket_responses_table.php,2024_07_26_165424_create_ticket_categories_table.php,2024_07_26_165927_add_columns_to_tickets_table.php(category_id),2024_07_27_134525_create_support_teams_table.php. - State machine UI:
resources/views/admin/help_desk/ticket_view.blade.php:349-363; row colouring:212-222. - Notifications (mail + SMS):
app/Notifications/HelpDesk/TicketCreationAdminNotification.php(via():26-32),TicketAssignNotification.php,TicketResponseNotification.php,TicketStatusUpdateNotification.php; SMS channelapp/Notifications/Channels/InfoSky.php. - CRM contrast:
app/Models/CaseTicket.php,app/Http/Controllers/Admin/CaseTicketController.php,database/migrations/2025_08_11_150157_create_case_tickets_table.php.
The Help Desk is a compact, self-contained internal ticketing queue: staff raise it, the support team routes it through a hard-coded Open → Development → Completed → Closed flow, and every step pages people over mail and SMS — cleanly separate from the customer-facing CRM cases it is easily confused with.