diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index ab756e82f..4900be461 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -2398,6 +2398,40 @@ public class Controller : ControllerBase return comment; } + public class UpdateTicketCommentRequest + { + public Int64 Id { get; set; } + public String Body { get; set; } = ""; + } + + [HttpPost(nameof(UpdateTicketComment))] + public ActionResult UpdateTicketComment([FromBody] UpdateTicketCommentRequest req, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user is null || user.UserType != 2) return Unauthorized(); + + var comment = Db.TicketComments.FirstOrDefault(c => c.Id == req.Id); + if (comment is null) return NotFound(); + + if (comment.AuthorType != (Int32)CommentAuthorType.Human) return Forbid(); + if (comment.AuthorId != user.Id) return Forbid(); + + if (String.IsNullOrWhiteSpace(req.Body)) return BadRequest("Body required."); + + comment.Body = req.Body; + comment.EditedAt = DateTime.UtcNow; + if (!Db.Update(comment)) return StatusCode(500, "Failed to update comment."); + + var ticket = Db.GetTicketById(comment.TicketId); + if (ticket is not null) + { + ticket.UpdatedAt = DateTime.UtcNow; + Db.Update(ticket); + } + + return comment; + } + [HttpGet(nameof(GetTicketDetail))] public ActionResult GetTicketDetail(Int64 id, Token authToken) { diff --git a/csharp/App/Backend/DataTypes/TicketComment.cs b/csharp/App/Backend/DataTypes/TicketComment.cs index df0df19a6..6f08d97e5 100644 --- a/csharp/App/Backend/DataTypes/TicketComment.cs +++ b/csharp/App/Backend/DataTypes/TicketComment.cs @@ -13,6 +13,7 @@ public class TicketComment public Int64? AuthorId { get; set; } public String Body { get; set; } = ""; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? EditedAt { get; set; } [Ignore] public List MentionedUserIds { get; set; } = new(); } diff --git a/csharp/App/Backend/Database/Update.cs b/csharp/App/Backend/Database/Update.cs index ed5b2cf6a..fe6f66a73 100644 --- a/csharp/App/Backend/Database/Update.cs +++ b/csharp/App/Backend/Database/Update.cs @@ -72,4 +72,5 @@ public static partial class Db // Ticket system public static Boolean Update(Ticket ticket) => Update(obj: ticket); public static Boolean Update(TicketAiDiagnosis diagnosis) => Update(obj: diagnosis); + public static Boolean Update(TicketComment comment) => Update(obj: comment); } \ No newline at end of file diff --git a/typescript/frontend-marios2/src/content/dashboards/Tickets/CommentThread.tsx b/typescript/frontend-marios2/src/content/dashboards/Tickets/CommentThread.tsx index 6bc3ccb64..07e33f38f 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tickets/CommentThread.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tickets/CommentThread.tsx @@ -1,4 +1,5 @@ -import React, { useRef, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; +import { UserContext } from 'src/contexts/userContext'; import { Avatar, Box, @@ -39,8 +40,13 @@ function CommentThread({ adminUsers = [] }: CommentThreadProps) { const intl = useIntl(); + const userCtx = useContext(UserContext); + const currentUserId = userCtx?.currentUser?.id; const [body, setBody] = useState(''); const [submitting, setSubmitting] = useState(false); + const [editingId, setEditingId] = useState(null); + const [editBody, setEditBody] = useState(''); + const [savingEdit, setSavingEdit] = useState(false); const fileInputRef = useRef(null); const [selectedFiles, setSelectedFiles] = useState([]); const [uploading, setUploading] = useState(false); @@ -125,6 +131,32 @@ function CommentThread({ (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); + const startEdit = (comment: TicketComment) => { + setEditingId(comment.id); + setEditBody(comment.body); + }; + + const cancelEdit = () => { + setEditingId(null); + setEditBody(''); + }; + + const saveEdit = async (commentId: number) => { + if (!editBody.trim()) return; + setSavingEdit(true); + try { + await axiosConfig.post('/UpdateTicketComment', { + id: commentId, + body: editBody + }); + setEditingId(null); + setEditBody(''); + onCommentAdded(); + } finally { + setSavingEdit(false); + } + }; + const handleSubmit = async () => { if (!body.trim() && selectedFiles.length === 0) return; setSubmitting(true); @@ -196,6 +228,8 @@ function CommentThread({ {sorted.map((comment) => { const isAi = comment.authorType === CommentAuthorType.AiAgent; + const canEdit = !isAi && currentUserId != null && comment.authorId === currentUserId; + const isEditing = editingId === comment.id; return ( {isAi ? 'AI' : (adminUsers.find(u => u.id === comment.authorId)?.name ?? `User #${comment.authorId ?? '?'}`)} @@ -224,10 +258,59 @@ function CommentThread({ {new Date(comment.createdAt).toLocaleString()} + {comment.editedAt && ( + + + + )} + {canEdit && !isEditing && ( + + )} - - {comment.body} - + {isEditing ? ( + + setEditBody(e.target.value)} + /> + + + + + + ) : ( + + {comment.body} + + )} diff --git a/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx b/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx index e0e986e41..888dc58ee 100644 --- a/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx +++ b/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx @@ -214,6 +214,7 @@ export type TicketComment = { authorId: number | null; body: string; createdAt: string; + editedAt?: string | null; }; export type TicketAiDiagnosis = { diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json index ecb97852c..8ed453573 100644 --- a/typescript/frontend-marios2/src/lang/de.json +++ b/typescript/frontend-marios2/src/lang/de.json @@ -567,6 +567,7 @@ "noDiagnosis": "Keine KI-Diagnose verfügbar.", "comments": "Kommentare", "noComments": "Noch keine Kommentare.", + "commentEdited": "(bearbeitet {time})", "addComment": "Hinzufügen", "timeline": "Zeitverlauf", "noTimelineEvents": "Noch keine Ereignisse.", diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json index 5b5e67b91..6ae525ddb 100644 --- a/typescript/frontend-marios2/src/lang/en.json +++ b/typescript/frontend-marios2/src/lang/en.json @@ -315,6 +315,7 @@ "noDiagnosis": "No AI diagnosis available.", "comments": "Comments", "noComments": "No comments yet.", + "commentEdited": "(edited {time})", "addComment": "Add", "timeline": "Timeline", "noTimelineEvents": "No events yet.", diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json index d6cdc6f62..747f15ce5 100644 --- a/typescript/frontend-marios2/src/lang/fr.json +++ b/typescript/frontend-marios2/src/lang/fr.json @@ -567,6 +567,7 @@ "noDiagnosis": "Aucun diagnostic IA disponible.", "comments": "Commentaires", "noComments": "Aucun commentaire pour le moment.", + "commentEdited": "(modifié {time})", "addComment": "Ajouter", "timeline": "Chronologie", "noTimelineEvents": "Aucun événement pour le moment.", diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json index 59a8c11ce..326452359 100644 --- a/typescript/frontend-marios2/src/lang/it.json +++ b/typescript/frontend-marios2/src/lang/it.json @@ -567,6 +567,7 @@ "noDiagnosis": "Nessuna diagnosi IA disponibile.", "comments": "Commenti", "noComments": "Nessun commento ancora.", + "commentEdited": "(modificato {time})", "addComment": "Aggiungi", "timeline": "Cronologia", "noTimelineEvents": "Nessun evento ancora.",