for adding comments to tickets

This commit is contained in:
tp_dhu 2025-02-16 22:07:02 +00:00
parent a8fe0add5c
commit 2a711584cd
5 changed files with 300 additions and 18 deletions

View File

@ -0,0 +1,91 @@
<?php
class CommentController {
/**
* Add a new comment to a ticket.
* Expects POST data: comment (text)
* Route: POST /ticket/@id/comment
*/
public function create($f3){
// check logged in
if(!$f3->exists('SESSION.user')){
$f3->reroute('/login');
}
$ticket_id = (int) $f3->get('PARAMS.id');
$comment_text = $f3->get('POST.comment');
$current_user_id = $f3->get('SESSION.user.id');
if(empty($comment_text)){
$f3->set('SESSION.error', 'ticket not updated. No content');
$f3->reroute('/ticket/' . $ticket_id);
}
// insert comment
$db = $f3->get('DB');
$db->exec(
'INSERT INTO ticket_comments (ticket_id, comment, created_by, created_at)
VALUES (?, ?, ?, NOW())',
[$ticket_id, $comment_text, $current_user_id]
);
$f3->reroute('/ticket/' . $ticket_id);
}
/**
* Delete an existing comment
* Route: GET /tickey/@id/comment/@comment_id/delete
*/
public function delete($f3){
if(!$f3->exists('SESSION.user')){
$f3->reroute('/login');
}
$ticket_id = (int) $f3->get('PARAMS.id');
$comment_id = (int) $f3->get('PARAMS.comment_id');
$current_user = $f3->get('SESSION.user');
$db = $f3->get('DB');
//optional: check if user is allowed to delete comment.
// fetch who created the comment
$comment_row = $db->exec(
'SELECT created_by FROM ticket_comments WHERE id = ? AND ticket_id = ? LIMIT 1',
[$comment_id, $ticket_id]
);
if(!$comment_row){
$f3->set('SESSION.error', 'Error: Ticket comment ID not found.');
$f3->reroute('/ticket/'.$ticket_id);
}
$comment_owner = $comment_row[0]['created_by'];
// TODO: $is_admin = ()
if($current_user['id'] !== $comment_owner){
// no permission
$f3->set('SESSION.error', 'You do not have permission to delete this ticket');
$f3->reroute('/ticket/'. $ticket_id);
}
// Delete - addition, rather than delete, we set a delete flag
$db->exec('UPDATE ticket_comments SET deleted = 1 WHERE id = ?', [$comment_id]);
$f3->reroute('/ticket/' . $ticket_id);
}
// view comments
public function index($f3){
$ticket_id = (int) $f3->get('PARAMS.id');
$db = $f3->get('DB');
$results = $db->exec('
SELECT c.*, u.username AS author_name
FROM ticket_comments c
LEFT JOIN users u ON c.created_by = u.id
WHERE c.ticket_id = ?
ORDER BY c.created_at DESC',
[$ticket_id]
);
$comments = $results;
$f3->set('comments', $comments);
echo \Template::instance()->render('../ui/views/comments/view.html');
}
}

30
public/js/ticket_view.js Normal file
View File

@ -0,0 +1,30 @@
document.addEventListener('DOMContentLoaded', function(){
const ticket_id = window.location.pathname.split('/')[2];
const comments_url = `/ticket/${ticket_id}/comments`;
const attachments_url = `/ticket/${ticket_id}/attachments`;
function ajax(url, containerID){
fetch(url)
.then(response => {
if(!response.ok){
throw new Error('Network response was not ok.');
}
return response.text();
})
.then(html => {
const container_el = document.getElementById(containerID);
if(container_el){
container_el.innerHTML += html;
} else {
throw new Error('Coments container does not exist');
}
})
.catch(error => {
console.log('Error fetching comments', error);
});
}
ajax(attachments_url, 'attachments')
ajax(comments_url, 'comments')
});

View File

@ -4,3 +4,122 @@ html, body, #sidebar, #page,#base_body {
} }
#page { min-height: calc(100vh - 170px - 52px) } #page { min-height: calc(100vh - 170px - 52px) }
.table th.th-icon { width: 2rem; }
/* List Component */
.list{
--be-list-color:var(--bulma-text);
--be-list-item-description-color:var(--bulma-text-50);
--be-list-item-divider-color:var(--bulma-border);
--be-list-item-hover-color:var(--bulma-scheme-main-bis);
--be-list-item-image-margin:.75em;
--be-list-item-padding:.75em;
--be-list-item-title-color:var(--bulma-text-strong);
--be-list-item-title-weight:var(--bulma-weight-semibold);
color:var(--be-list-color);
flex-direction:column;
display:flex
}
.list.has-hidden-images .list-item-image{
display:none
}
.list.has-hoverable-list-items .list-item:hover{
background-color:var(--be-list-item-hover-color)
}
.list.has-overflow-ellipsis .list-item-content{
min-inline-size:0;
max-inline-size:calc(var(--length)*1ch)
}
.list.has-overflow-ellipsis .list-item-content>*{
text-overflow:ellipsis;
white-space:nowrap;
overflow:hidden
}
@media (hover:hover){
.list:not(.has-visible-pointer-controls) .list-item-controls{
opacity:0;
visibility:hidden
}
}
.list .list-item{
align-items:center;
transition:background-color .125s ease-out;
display:flex;
position:relative;
/* TP: update + align top */
align-items: flex-start;
}
@media (hover:hover){
.list .list-item:hover .list-item-controls,.list .list-item:focus-within .list-item-controls{
opacity:initial;
visibility:initial
}
}
.list .list-item:not(.box){
padding-block:var(--be-list-item-padding);
padding-inline:var(--be-list-item-padding)
}
.list .list-item:not(:last-child):not(.box){
border-block-end:1px solid var(--be-list-item-divider-color)
}
@media screen and (width<=768px){
.list:not(.has-overflow-ellipsis) .list .list-item{
flex-wrap:wrap
}
}
.list .list-item-image{
flex-shrink:0;
margin-inline-end:var(--be-list-item-image-margin);
/* TP: update + add margin-top */
margin-top: 0.5rem;
}
@media screen and (width<=768px){
.list .list-item-image{
padding-block:.5rem;
padding-inline:0
}
}
.list .list-item-content{
flex-direction:column;
flex-grow:1;
display:flex
}
@media screen and (width<=768px){
.list .list-item-content{
padding-block:.5rem;
padding-inline:0
}
}
.list .list-item-title{
color:var(--be-list-item-title-color);
font-weight:var(--be-list-item-title-weight);
margin-bottom: .25rem;
}
.list .list-item-description{
color:var(--be-list-item-description-color)
}
.list .list-item-controls{
flex-shrink:0;
transition:opacity .125s ease-out
}
@media screen and (width<=768px){
.list .list-item-controls{
flex-wrap:wrap;
padding-block:.5rem;
padding-inline:0
}
}
@media screen and (width>=769px),print{
.list .list-item-controls{
padding-inline-start:var(--be-list-item-padding)
}
.list:not(.has-visible-pointer-controls) .list .list-item-controls{
block-size:100%;
align-items:center;
padding-block-end:var(--be-list-item-padding);
display:flex;
position:absolute;
inset-inline-end:0
}
}

View File

@ -94,20 +94,21 @@
</div> </div>
</footer> </footer>
<!-- JavaScript for Bulma navbar burger (mobile) --> <!-- JavaScript for Bulma navbar burger (mobile) -->
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
if (burgers.length > 0) { if (burgers.length > 0) {
burgers.forEach(el => { burgers.forEach(el => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.target); const target = document.getElementById(el.dataset.target);
el.classList.toggle('is-active'); el.classList.toggle('is-active');
target.classList.toggle('is-active'); target.classList.toggle('is-active');
});
}); });
}); }
} });
}); </script>
</script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,41 @@
<div class="box" id="comments">
<h2 class="title">Comments</h2>
<div class="block">
<form action="/ticket/{{@PARAMS.id}}/comment" method="POST">
<div class="field">
<label class="label">Add comment:</label>
<div class="control">
<textarea class="textarea" name="comment" rows="4" cols="50"></textarea>
</div>
</div>
<div class="field">
<button class="button is-primary" type="submit">Submit Comment</button>
</div>
</form>
</div>
<check if="{{ !empty(@comments) }}">
<div class="list">
<repeat group="{{ @comments }}" value="{{ @comment}}">
<div class="list-item">
<div class="list-item-image">
<figure class="image is-48x48">
<img class="is-rounded"
src="https://placehold.co/200x200/66d1ff/FFF?text=TP">
<!-- <img class="is-rounded"
src="https://loremflickr.com/200/200/dog?{{ (int)rand()}}">-->
</figure>
</div>
<div class="list-item-content">
<div class="list-item-title is-flex is-justify-content-space-between">
<span>{{ @comment.author_name}}</span>
<span class="has-text-weight-normal has-text-grey">{{ @comment.created_at }}</span>
</div>
<div class="list-item-description">
<parsedown>{{ @comment.comment | raw }}</parsedown>
</div>
</div>
</div>
</repeat>
</div>
</check>
</div>