diff --git a/app/controllers/CommentController.php b/app/controllers/CommentController.php
new file mode 100644
index 0000000..2f449b3
--- /dev/null
+++ b/app/controllers/CommentController.php
@@ -0,0 +1,91 @@
+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');
+ }
+}
\ No newline at end of file
diff --git a/public/js/ticket_view.js b/public/js/ticket_view.js
new file mode 100644
index 0000000..46c05d5
--- /dev/null
+++ b/public/js/ticket_view.js
@@ -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')
+});
+
diff --git a/public/style.css b/public/style.css
index 41b2afb..351b7f7 100644
--- a/public/style.css
+++ b/public/style.css
@@ -3,4 +3,123 @@ html, body, #sidebar, #page,#base_body {
min-height: 100%
}
-#page { min-height: calc(100vh - 170px - 52px) }
\ No newline at end of file
+#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
+ }
+}
\ No newline at end of file
diff --git a/ui/templates/layout.html b/ui/templates/layout.html
index e024bf7..d1e4475 100644
--- a/ui/templates/layout.html
+++ b/ui/templates/layout.html
@@ -8,14 +8,14 @@
-
@@ -94,20 +94,21 @@
-
-
+ }
+ });
+