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
```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