kamau1's picture
feat: ticket attachements
e5f65c7

Ticket Comments API

Team collaboration system for tickets with threading, mentions, and attachments.

Overview

The ticket comments system enables team members to collaborate on tickets through:

  • Internal comments (team only) and external comments (client-visible)
  • Threading (replies to comments)
  • Mentions (tag team members)
  • Attachments (link documents and images)
  • Edit tracking (audit trail)

Comment Attachments vs Ticket Images

Comment Attachments (Informal/Conversational):

  • Quick photo sharing during discussions
  • Reference diagrams, PDFs, screenshots
  • Part of team collaboration flow
  • Can be edited/deleted with comment

Ticket Images (Formal/Evidence):

  • Official before/after photos
  • Installation proof for completion
  • Linked to progress/incident reports
  • Part of formal ticket record
  • PM verification workflow

Use comment attachments for: "Hey team, look at this issue I found"
Use ticket images for: "Official proof of installation completion"


API Endpoints

1. Create Comment

POST /api/v1/tickets/{ticket_id}/comments

Request Body:

{
  "comment_text": "Customer prefers morning visits between 9-11am",
  "is_internal": true,
  "comment_type": "note",
  "parent_comment_id": null,
  "mentioned_user_ids": [],
  "attachment_document_ids": []
}

Response: 201 Created

{
  "id": "uuid",
  "ticket_id": "uuid",
  "user_id": "uuid",
  "user_name": "John Doe",
  "comment_text": "Customer prefers morning visits between 9-11am",
  "is_internal": true,
  "comment_type": "note",
  "parent_comment_id": null,
  "mentioned_user_ids": [],
  "attachment_document_ids": [],
  "is_edited": false,
  "edited_at": null,
  "edited_by_user_id": null,
  "edited_by_user_name": null,
  "additional_metadata": {},
  "created_at": "2025-11-30T12:00:00Z",
  "updated_at": "2025-11-30T12:00:00Z",
  "reply_count": 0
}

2. Update Comment

PUT /api/v1/comments/{comment_id}

Authorization: Comment author only

Request Body:

{
  "comment_text": "Updated: Customer now prefers afternoon visits"
}

Response: 200 OK

{
  "id": "uuid",
  "comment_text": "Updated: Customer now prefers afternoon visits",
  "is_edited": true,
  "edited_at": "2025-11-30T12:05:00Z",
  "edited_by_user_id": "uuid",
  "edited_by_user_name": "John Doe",
  ...
}

3. Delete Comment

DELETE /api/v1/comments/{comment_id}

Authorization: Comment author or Project Manager

Response: 200 OK

{
  "success": true,
  "message": "Comment deleted successfully",
  "comment_id": "uuid"
}

4. List Comments

GET /api/v1/tickets/{ticket_id}/comments?page=1&page_size=50&is_internal=true&parent_only=false

Query Parameters:

  • page (int): Page number (default: 1)
  • page_size (int): Items per page (default: 50, max: 100)
  • is_internal (bool): Filter by internal/external
  • comment_type (string): Filter by type (note, issue, resolution, question, update)
  • parent_only (bool): Show only top-level comments (default: false)

Response: 200 OK

{
  "comments": [...],
  "total": 25,
  "page": 1,
  "page_size": 50,
  "pages": 1
}

5. Get Comment Replies

GET /api/v1/comments/{comment_id}/replies

Response: 200 OK

[
  {
    "id": "uuid",
    "parent_comment_id": "parent-uuid",
    "comment_text": "I'll handle this",
    "user_name": "Jane Smith",
    ...
  }
]

Comment Types

Type Description Use Case
note General note "Customer prefers morning visits"
issue Problem/blocker "Customer location is incorrect"
resolution Solution/fix "Updated customer address"
question Question for team "Should we reschedule?"
update Status update "Work 50% complete"

Threading (Replies)

Create a Reply

{
  "comment_text": "I'll handle the rescheduling",
  "parent_comment_id": "parent-comment-uuid",
  "is_internal": true,
  "comment_type": "note"
}

Load Thread

  1. List top-level comments: GET /tickets/{id}/comments?parent_only=true
  2. Load replies: GET /comments/{comment_id}/replies

Mentions

Tag Team Members

{
  "comment_text": "@john Can you check the customer location?",
  "mentioned_user_ids": ["john-uuid"],
  "is_internal": true,
  "comment_type": "question"
}

Future: Mentioned users will receive notifications


Attachments

Link Documents & Images

{
  "comment_text": "See attached site survey photos",
  "attachment_document_ids": ["doc-uuid-1", "doc-uuid-2"],
  "is_internal": true,
  "comment_type": "note"
}

Workflow:

  1. Upload image: POST /api/v1/documents/upload with entity_type=ticket and entity_id=ticket-uuid
  2. Get document ID from response
  3. Create comment with document ID in attachment_document_ids array

Response includes enriched attachment info:

{
  "id": "comment-uuid",
  "comment_text": "Found this wiring issue",
  "attachment_document_ids": ["doc-uuid-1"],
  "attachments": [
    {
      "id": "doc-uuid-1",
      "file_name": "wiring_issue.jpg",
      "file_type": "image/jpeg",
      "file_url": "https://storage.example.com/...",
      "file_size": 245678,
      "is_image": true
    }
  ]
}

Internal vs External Comments

Internal Comments (Team Only)

{
  "comment_text": "Customer is difficult, be patient",
  "is_internal": true
}
  • Visible to: Team members only
  • Use for: Internal notes, issues, coordination

External Comments (Client-Visible)

{
  "comment_text": "Installation scheduled for tomorrow 10am",
  "is_internal": false
}
  • Visible to: Team + Client
  • Use for: Customer updates, status changes

Edit Tracking

When a comment is edited:

  • is_editedtrue
  • edited_at → timestamp
  • edited_by_user_id → editor's ID
  • edited_by_user_name → editor's name

Original text is NOT preserved (consider adding version history if needed)


Authorization

Action Who Can Do It
Create comment All authenticated users
Update comment Comment author only
Delete comment Comment author OR Project Manager
View comments All authenticated users

Error Handling

404 Not Found

{
  "detail": "Ticket not found"
}

403 Forbidden

{
  "detail": "You can only edit your own comments"
}

400 Bad Request

{
  "detail": "One or more mentioned users not found"
}

Complete Workflow Example

Field Agent Posts Issue with Photo

// 1. Agent takes photo of problem
const photo = capturePhoto();

// 2. Upload photo
const formData = new FormData();
formData.append('file', photo);
formData.append('entity_type', 'ticket');
formData.append('entity_id', ticketId);
formData.append('document_type', 'comment_attachment');

const doc = await api.post('/documents/upload', formData);

// 3. Create comment with photo
await api.post(`/tickets/${ticketId}/comments`, {
  comment_text: "Found damaged cable at junction box",
  is_internal: true,
  comment_type: "issue",
  attachment_document_ids: [doc.data.id],
  mentioned_user_ids: [dispatcherId]
});

Dispatcher Responds with Reference Image

// 1. Upload reference diagram
const diagram = selectFile();
const formData = new FormData();
formData.append('file', diagram);
formData.append('entity_type', 'ticket');
formData.append('entity_id', ticketId);
formData.append('document_type', 'comment_attachment');

const doc = await api.post('/documents/upload', formData);

// 2. Reply with solution
await api.post(`/tickets/${ticketId}/comments`, {
  comment_text: "Replace with this cable type. See diagram.",
  parent_comment_id: agentCommentId,
  is_internal: true,
  comment_type: "resolution",
  attachment_document_ids: [doc.data.id]
});

Best Practices

For Field Agents

  • Use note type for general observations
  • Use issue type for problems encountered
  • Attach photos when reporting issues - visual evidence is crucial
  • Keep comments concise and actionable
  • Always mark as is_internal: true

For Dispatchers

  • Use question type when asking agents
  • Use update type for status changes
  • Tag relevant team members with mentions
  • Share reference images/diagrams when providing solutions
  • Use external comments for customer updates

For Project Managers

  • Review external comments before posting
  • Use resolution type for solutions
  • Delete inappropriate comments if needed
  • Monitor comment activity for team collaboration
  • Verify photo attachments in issue reports

Frontend Integration

Mobile App (Field Agents)

// List comments
const comments = await api.get(`/tickets/${ticketId}/comments`, {
  params: { is_internal: true, page_size: 20 }
});

// Create comment
await api.post(`/tickets/${ticketId}/comments`, {
  comment_text: "Customer not home, will retry tomorrow",
  is_internal: true,
  comment_type: "update"
});

// Create comment with image attachment
// Step 1: Upload image
const formData = new FormData();
formData.append('file', imageFile);
formData.append('entity_type', 'ticket');
formData.append('entity_id', ticketId);
formData.append('document_type', 'comment_attachment');
formData.append('document_category', 'operational');

const uploadResponse = await api.post('/documents/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }
});

// Step 2: Create comment with attachment
await api.post(`/tickets/${ticketId}/comments`, {
  comment_text: "Found this issue with the installation",
  is_internal: true,
  comment_type: "issue",
  attachment_document_ids: [uploadResponse.data.id]
});

// Reply to comment
await api.post(`/tickets/${ticketId}/comments`, {
  comment_text: "Okay, I'll reschedule",
  parent_comment_id: parentCommentId,
  is_internal: true,
  comment_type: "note"
});

Web Dashboard (Dispatchers/PMs)

// Load threaded comments
const topLevel = await api.get(`/tickets/${ticketId}/comments`, {
  params: { parent_only: true }
});

// Load replies for each comment
for (const comment of topLevel.comments) {
  if (comment.reply_count > 0) {
    const replies = await api.get(`/comments/${comment.id}/replies`);
    comment.replies = replies;
  }
}

// Display images inline
function renderComment(comment) {
  const imageAttachments = comment.attachments.filter(a => a.is_image);
  const otherAttachments = comment.attachments.filter(a => !a.is_image);
  
  return (
    <div>
      <p>{comment.comment_text}</p>
      
      {/* Display images inline */}
      {imageAttachments.length > 0 && (
        <div className="image-gallery">
          {imageAttachments.map(img => (
            <img key={img.id} src={img.file_url} alt={img.file_name} />
          ))}
        </div>
      )}
      
      {/* Display other attachments as links */}
      {otherAttachments.length > 0 && (
        <div className="attachments">
          {otherAttachments.map(doc => (
            <a key={doc.id} href={doc.file_url} download={doc.file_name}>
              {doc.file_name}
            </a>
          ))}
        </div>
      )}
    </div>
  );
}

Database Schema

CREATE TABLE ticket_comments (
    id UUID PRIMARY KEY,
    ticket_id UUID NOT NULL REFERENCES tickets(id),
    user_id UUID REFERENCES users(id),
    comment_text TEXT NOT NULL,
    is_internal BOOLEAN DEFAULT TRUE,
    comment_type VARCHAR(50) DEFAULT 'note',
    parent_comment_id UUID REFERENCES ticket_comments(id),
    mentioned_user_ids UUID[],
    attachment_document_ids UUID[],
    is_edited BOOLEAN DEFAULT FALSE,
    edited_at TIMESTAMP WITH TIME ZONE,
    edited_by_user_id UUID REFERENCES users(id),
    additional_metadata JSONB DEFAULT '{}',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE
);

Future Enhancements

  1. Notifications

    • Notify mentioned users
    • Notify ticket assignees on external comments
    • Real-time updates via SSE
  2. Rich Text

    • Markdown support
    • Code blocks
    • Formatting
  3. Reactions

    • Like/emoji reactions
    • Quick acknowledgments
  4. Version History

    • Track all edits
    • View previous versions
    • Restore old versions
  5. Search

    • Full-text search across comments
    • Filter by author
    • Date range filtering