for adding comments to tickets
This commit is contained in:
parent
a8fe0add5c
commit
2a711584cd
91
app/controllers/CommentController.php
Normal file
91
app/controllers/CommentController.php
Normal 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
30
public/js/ticket_view.js
Normal 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')
|
||||||
|
});
|
||||||
|
|
||||||
119
public/style.css
119
public/style.css
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
41
ui/views/comments/view.html
Normal file
41
ui/views/comments/view.html
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user