Spaces:
Sleeping
Sleeping
| # 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 | |
| ```http | |
| POST /api/v1/tickets/{ticket_id}/comments | |
| ``` | |
| **Request Body:** | |
| ```json | |
| { | |
| "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` | |
| ```json | |
| { | |
| "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 | |
| ```http | |
| PUT /api/v1/comments/{comment_id} | |
| ``` | |
| **Authorization:** Comment author only | |
| **Request Body:** | |
| ```json | |
| { | |
| "comment_text": "Updated: Customer now prefers afternoon visits" | |
| } | |
| ``` | |
| **Response:** `200 OK` | |
| ```json | |
| { | |
| "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 | |
| ```http | |
| DELETE /api/v1/comments/{comment_id} | |
| ``` | |
| **Authorization:** Comment author or Project Manager | |
| **Response:** `200 OK` | |
| ```json | |
| { | |
| "success": true, | |
| "message": "Comment deleted successfully", | |
| "comment_id": "uuid" | |
| } | |
| ``` | |
| --- | |
| ### 4. List Comments | |
| ```http | |
| 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` | |
| ```json | |
| { | |
| "comments": [...], | |
| "total": 25, | |
| "page": 1, | |
| "page_size": 50, | |
| "pages": 1 | |
| } | |
| ``` | |
| --- | |
| ### 5. Get Comment Replies | |
| ```http | |
| GET /api/v1/comments/{comment_id}/replies | |
| ``` | |
| **Response:** `200 OK` | |
| ```json | |
| [ | |
| { | |
| "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 | |
| ```json | |
| { | |
| "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 | |
| ```json | |
| { | |
| "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 | |
| ```json | |
| { | |
| "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:** | |
| ```json | |
| { | |
| "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) | |
| ```json | |
| { | |
| "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) | |
| ```json | |
| { | |
| "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_edited` → `true` | |
| - `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 | |
| ```json | |
| { | |
| "detail": "Ticket not found" | |
| } | |
| ``` | |
| ### 403 Forbidden | |
| ```json | |
| { | |
| "detail": "You can only edit your own comments" | |
| } | |
| ``` | |
| ### 400 Bad Request | |
| ```json | |
| { | |
| "detail": "One or more mentioned users not found" | |
| } | |
| ``` | |
| --- | |
| ## Complete Workflow Example | |
| ### Field Agent Posts Issue with Photo | |
| ```typescript | |
| // 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 | |
| ```typescript | |
| // 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) | |
| ```typescript | |
| // 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) | |
| ```typescript | |
| // 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 | |
| ```sql | |
| 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 | |