# 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 (
{comment.comment_text}
{/* Display images inline */} {imageAttachments.length > 0 && (