Skip to content

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___add can raise a ticket and see their own via "My Tickets."
  • Support / agents — members of the Support Team (support_teams table). 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) and Route::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) declares via() = ['mail', InfoSky::class] — Laravel mail plus the InfoSky SMS channel (app/Notifications/Channels/InfoSky.php, backed by InfoSkySmsService). New tickets page the support team; assignment / response / status changes notify the ticket creator.
  • Branches / Users. branch_id links a ticket to the creator's Restaurant (branch); created_by / assignee_id link to User.

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:

  1. $active_class is computed but never used anywhere in the file (the <li> items each compute their own active inline).
  2. No Help Desk route, controller, view, or model references bulk SMS. grep bulk-sms resources/views/admin/help_desk/ returns nothing.
  3. 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 channel app/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.