Skip to content

Helpdesk MVP — Ticket Submission & Agent Inbox #599

@SvenVw

Description

@SvenVw

Context

FDM needs an in-app helpdesk system to replace the current mailto:support@fdm.nl flow. This issue implements the core ticket lifecycle: users create tickets via the web UI, AI generates subject + priority, agents see and handle tickets in a shared inbox, users receive email notifications on replies.

Scope

In Scope

  • fdm-helpdesk package scaffold (monorepo workspace, tsdown build, Drizzle schema, migrations, vitest config)
  • Database schema: tickets, messages, agents, ticket_assignments, ticket_activity, ticket_views, tags, ticket_tags_map, saved_replies
  • Ticket state machine: transition validation (open → in_progress → waiting_on_customer → resolved → closed)
  • Core functions: createTicket, getTicket, updateTicketStatus, assignTicket, addMessage, closeTicket
  • AI triage (Gemini): auto-generate subject + priority from message body, with fallback
  • Ticket reference generation: nanoid-based TK-XXXXXX with collision retry
  • Agent management: register agent, list agents, basic CRUD
  • Tags & saved replies: CRUD, tag assignment to tickets, saved reply variable substitution
  • Shared inbox queries: getInbox, getMyTickets, getUnassignedTickets, searchTickets
  • Collision detection: ticket_views table with last_viewed_at, is_typing indicator
  • Outbound email notifications: Postmark transactional emails (new ticket confirmation to user, reply notification to user)
  • Customer UI (/support/*): ticket list, create ticket form, conversation view
  • Agent UI (/admin/support/*): shared inbox with tabs/filters, ticket detail with reply + internal notes + metadata sidebar
  • Navigation: sidebar entry for customers ("Ondersteuning" with badge), admin sidebar for agents ("Inbox" with unassigned count)
  • GDPR: soft-delete on messages, user deletion hook (anonymization)
  • Testing: integration tests for ticket lifecycle, unit tests for state machine + triage

Acceptance Criteria

  • fdm-helpdesk package builds and publishes in monorepo
  • Database migrations run cleanly against existing PostgreSQL with fdm-helpdesk schema
  • User can create a ticket from /support/new with a free-text message
  • AI triage generates subject + priority on ticket creation (with graceful fallback)
  • Ticket gets a unique TK-XXXXXX reference
  • User sees their tickets at /support with status indicators
  • User can reply to their own ticket at /support/$ticket_id
  • User receives an email notification when an agent replies
  • Only users registered in the agents table can access /admin/support/* routes (403 otherwise)
  • Initial agents are seeded via migration or CLI script
  • Admin sidebar "Inbox" item is only visible to agents
  • Agent can view all tickets at /admin/support (shared inbox)
  • Agent inbox supports tabs: All Open, Mine, Unassigned, Waiting on Customer
  • Agent can filter by priority and tags
  • Agent can open a ticket and see the full conversation thread
  • Agent can reply (sends email to customer) or add internal note (not visible to customer)
  • Agent can change ticket status, priority, and assign to another agent
  • Agent can add/remove tags on a ticket
  • Agent can use saved replies with variable substitution
  • Collision detection shows "X is also viewing this ticket" warning
  • Ticket state transitions are validated (invalid transitions rejected)
  • Deleting a user anonymizes their ticket data (GDPR)
  • All core functions have integration tests passing

Technical Implementation

1. Package Scaffold

fdm-helpdesk/
├── src/
│   ├── index.ts
│   ├── db/
│   │   ├── schema-helpdesk.ts
│   │   └── migrations/
│   ├── ticket.ts
│   ├── ticket-transitions.ts
│   ├── ticket-ref.ts
│   ├── conversation.ts
│   ├── inbox.ts
│   ├── agent.ts
│   ├── tag.ts
│   ├── saved-reply.ts
│   ├── triage.ts
│   ├── error.ts          (re-export from fdm-core)
│   └── id.ts             (re-export from fdm-core)
├── package.json
├── tsdown.config.ts      (mirror fdm-core, copy migrations in onSuccess)
├── tsconfig.json
├── tsconfig.build.json
├── vitest.config.ts
├── drizzle.config.ts
└── biome.json            (extends root)

2. Key Schema Tables

Core tables:

  • tickets (ticket_id, ticket_ref, requester_id, subject, status, priority, farm_id, created, updated)
  • messages (message_id, ticket_id, sender_id, sender_type, body, is_internal, deleted_at, created)
  • agents (agent_id, principal_id, display_name, role, is_active, assignment_tiers, work_days, max_tickets)
  • ticket_assignments (assignment_id, ticket_id, agent_id, assigned_by, is_primary, assigned_at, unassigned_at)
  • ticket_activity (activity_id, ticket_id, actor_id, action, old_value, new_value, metadata, created)
  • ticket_views (ticket_id, agent_id, last_viewed_at, is_typing)
  • tags, ticket_tags_map, saved_replies

3. AI Triage (Gemini)

  • Uses @google/genai with structured output schema
  • Model: gemini-3.0-flash
  • Generates: subject (max 80 chars, same language as message), priority (low/normal/high/urgent), reasoning
  • Fallback on failure: first sentence as subject, "normal" priority
  • Triage reasoning stored in ticket_activity for agent context
  • Requires GEMINI_API_KEY environment variable

4. App Routes (fdm-app)

Customer routes:

Route Purpose
support.tsx Layout with back navigation
support._index.tsx My tickets list (open/closed tabs)
support.new.tsx Create ticket form
support.$ticket_id.tsx Ticket conversation view

Agent routes:

Route Purpose
admin.support.tsx Agent inbox layout (with access guard)
admin.support._index.tsx Shared inbox (list + filters)
admin.support.$ticket_id.tsx Ticket detail (conversation + sidebar)
admin.support.settings.tsx Tags, saved replies, agent management

4a. Agent Access Control (MVP)

The full role system (better-auth admin plugin with helpdeskAgent / helpdeskAdmin roles) is delivered in #603. For the MVP, the agents table is the access gate:

  1. Guard function: requireAgent(fdm, principal_id) — queries the agents table for a row matching the current user's principal_id with is_active = true. Throws a 403 if not found.
  2. Route loaders: The admin.support.tsx layout loader calls requireAgent(). All nested agent routes inherit this guard — no duplicate checks needed.
  3. Seeding agents: Provide a seedAgent(fdm, principal_id, display_name) function that inserts a row into the agents table. During initial deployment, seed the first agent(s) via a migration or CLI script.
  4. Admin sidebar visibility: The sidebar "Inbox" item is only rendered when the user exists in the agents table (checked via a lightweight loader or root context).

This gives a clean separation: This issue gates access by agent table membership, #603upgrades to proper role-based access via the admin plugin with a UI to assign agents.

5. UI Components (new components/blocks/support/)

Component Description
ticket-list.tsx Sortable list of ticket cards with status/priority
ticket-card.tsx Single row: ref, subject, status, time, assigned
ticket-detail.tsx Full conversation thread view
message-bubble.tsx Single message with sender, time
internal-note.tsx Agent-only message (amber background)
reply-box.tsx Textarea with tabs (reply/note), send button
ticket-sidebar.tsx Metadata panel: status, priority, tags, user info, activity
priority-badge.tsx Colored dot + label
status-badge.tsx Status chip with icon
collision-banner.tsx "X is also viewing" warning bar
saved-reply-picker.tsx Dropdown inserting templates into reply box
tag-selector.tsx Autocomplete multi-select for tags
ticket-create-form.tsx Create ticket form (textarea, farm selector)

6. Email Templates

Template Trigger Content
ticket-confirmation.tsx User creates ticket "We hebben je bericht ontvangen (TK-XXXXXX)"
ticket-reply.tsx Agent replies Agent's reply with link to view in app

Uses existing BaseEmailLayout + Postmark transactional stream.

7. Navigation Changes

components/blocks/sidebar/support.tsx: Replace mailto: handler with <Link to="/support">. Add badge showing count of tickets with unread agent replies.

Admin sidebar: Add "Inbox" item with badge for unassigned ticket count (only visible to helpdeskAgent/helpdeskAdmin roles).

Testing Requirements

  • Integration tests: Full ticket lifecycle (create → assign → reply → resolve → close)
  • Unit tests: State machine transitions (valid + invalid), ticket-ref generation, triage fallback, saved reply variable substitution
  • Coverage target: ≥80% line coverage for fdm-helpdesk/src/

Dependencies

  • @google/genai (Gemini API)
  • @svenvw/fdm-core (existing: auth, DB connection, createId, handleError)
  • Postmark (existing: fdm-app/app/lib/email.server.ts)
  • Environment: GEMINI_API_KEY

Definition of Done

A user can submit a support request in-app, an agent can handle it from the inbox, and the user gets notified of the resolution — all without leaving the FDM platform.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions