Compare commits
8 Commits
67a35cdf3e
...
0bc558d4d7
| Author | SHA1 | Date | |
|---|---|---|---|
| 0bc558d4d7 | |||
| 916657e09c | |||
| 04f6f50f28 | |||
| 742e508e8a | |||
| acb428f4ee | |||
| 4a421564c2 | |||
| 2d6cd5e48d | |||
| 165d4eabe5 |
@ -6,6 +6,6 @@ class HomeController extends \BaseController
|
|||||||
{
|
{
|
||||||
public function index($f3)
|
public function index($f3)
|
||||||
{
|
{
|
||||||
$this->renderView('/ui/views/admin/index.html');
|
$this->renderView('views/admin/index.html');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,8 +2,13 @@
|
|||||||
|
|
||||||
namespace Admin;
|
namespace Admin;
|
||||||
|
|
||||||
|
use CheckCSRF;
|
||||||
|
|
||||||
class TicketOptionsController extends \BaseController
|
class TicketOptionsController extends \BaseController
|
||||||
{
|
{
|
||||||
|
|
||||||
|
use CheckCSRF;
|
||||||
|
|
||||||
public function listPriorities()
|
public function listPriorities()
|
||||||
{
|
{
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
@ -12,7 +17,7 @@ class TicketOptionsController extends \BaseController
|
|||||||
$model = new \TicketPriority($this->getDB());
|
$model = new \TicketPriority($this->getDB());
|
||||||
$priorities = $model->findAll();
|
$priorities = $model->findAll();
|
||||||
|
|
||||||
$this->renderView('/ui/views/admin/priorities/index.html', [
|
$this->renderView('views/admin/priorities/index.html', [
|
||||||
'priorities' => $priorities
|
'priorities' => $priorities
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -21,13 +26,15 @@ class TicketOptionsController extends \BaseController
|
|||||||
{
|
{
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
$this->requireAdmin(); // Added admin check
|
$this->requireAdmin(); // Added admin check
|
||||||
$this->renderView('/ui/views/admin/priorities/create.html');
|
$this->renderView('views/admin/priorities/create.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createPriority()
|
public function createPriority($f3)
|
||||||
{
|
{
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
$this->requireAdmin(); // Added admin check
|
$this->requireAdmin(); // Added admin check
|
||||||
|
$this->checkCSRF($f3, '/admin/priority/create');
|
||||||
|
|
||||||
$p = new \TicketPriority($this->getDB());
|
$p = new \TicketPriority($this->getDB());
|
||||||
$p->name = $this->f3->get('POST.name');
|
$p->name = $this->f3->get('POST.name');
|
||||||
$p->sort_order = $this->f3->get('POST.sort_order');
|
$p->sort_order = $this->f3->get('POST.sort_order');
|
||||||
@ -51,7 +58,7 @@ class TicketOptionsController extends \BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->renderView('/ui/views/admin/priorities/edit.html', [
|
$this->renderView('views/admin/priorities/edit.html', [
|
||||||
'priority' => $priority
|
'priority' => $priority
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -60,6 +67,7 @@ class TicketOptionsController extends \BaseController
|
|||||||
{
|
{
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
$this->requireAdmin();
|
$this->requireAdmin();
|
||||||
|
$this->checkCSRF($f3, '/admin/priority/', $params['id'] . '/edit');
|
||||||
$priorityId = $params['id'];
|
$priorityId = $params['id'];
|
||||||
|
|
||||||
$model = new \TicketPriority($this->getDB());
|
$model = new \TicketPriority($this->getDB());
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class AttachmentController {
|
class AttachmentController {
|
||||||
|
|
||||||
use RequiresAuth;
|
use RequiresAuth, CheckCSRF;
|
||||||
|
|
||||||
// list attachments
|
// list attachments
|
||||||
public function index($f3){
|
public function index($f3){
|
||||||
@ -25,14 +25,15 @@ class AttachmentController {
|
|||||||
$f3->set('ticket_id', $ticket_id);
|
$f3->set('ticket_id', $ticket_id);
|
||||||
$f3->set('attachments', $attachments);
|
$f3->set('attachments', $attachments);
|
||||||
|
|
||||||
$f3->set('content', '../ui/views/attachment/index.html');
|
$f3->set('content', 'views/attachment/index.html');
|
||||||
// echo \Template::instance()->render('../ui/templates/layout.html');
|
// echo \Template::instance()->render('templates/layout.html');
|
||||||
echo \Template::instance()->render($f3->get('content'));
|
echo \Template::instance()->render($f3->get('content'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle file upload
|
// handle file upload
|
||||||
public function upload($f3){
|
public function upload($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
$this->checkCSRF($f3, '/ticket/'.$f3->get('PARAMS.id')); // not ideal for AJAX
|
||||||
|
|
||||||
$ticket_id = (int) $f3->get('PARAMS.id');
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
$uploaded_by = $f3->get('SESSION.user.id');
|
$uploaded_by = $f3->get('SESSION.user.id');
|
||||||
@ -80,6 +81,9 @@ class AttachmentController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
$f3->reroute('/ticket/'.$ticket_id.'');
|
$f3->reroute('/ticket/'.$ticket_id.'');
|
||||||
|
|
||||||
|
// ideal ajax response:
|
||||||
|
// $f3->json(['success' => true, 'message' => 'file upload success.', 'filename' => $original_name, 'version' => $new_version]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// download attachment
|
// download attachment
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class AuthController {
|
class AuthController {
|
||||||
|
|
||||||
|
use CheckCSRF;
|
||||||
|
|
||||||
public function showLoginForm($f3){
|
public function showLoginForm($f3){
|
||||||
|
|
||||||
@ -10,12 +11,15 @@ class AuthController {
|
|||||||
$f3->clear('SESSION.login_error');
|
$f3->clear('SESSION.login_error');
|
||||||
|
|
||||||
// this can be in our controller base
|
// this can be in our controller base
|
||||||
$f3->set('content', '../ui/views/login.html');
|
$f3->set('content', 'views/login.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
$f3->clear('error');
|
$f3->clear('error');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function login($f3){
|
public function login($f3){
|
||||||
|
// CSRF
|
||||||
|
$this->checkCSRF($f3, '/login');
|
||||||
|
|
||||||
$username = $f3->get('POST.username');
|
$username = $f3->get('POST.username');
|
||||||
$password = $f3->get('POST.password');
|
$password = $f3->get('POST.password');
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,7 @@ abstract class BaseController
|
|||||||
$this->f3->set('content', $viewPath);
|
$this->f3->set('content', $viewPath);
|
||||||
|
|
||||||
// render tempalte
|
// render tempalte
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
|
|
||||||
// clear SESSION.error
|
// clear SESSION.error
|
||||||
$this->f3->clear('SESSION.error');
|
$this->f3->clear('SESSION.error');
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
class CommentController {
|
class CommentController {
|
||||||
|
|
||||||
|
use CheckCSRF;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new comment to a ticket.
|
* Add a new comment to a ticket.
|
||||||
* Expects POST data: comment (text)
|
* Expects POST data: comment (text)
|
||||||
@ -13,6 +15,8 @@ class CommentController {
|
|||||||
$f3->reroute('/login');
|
$f3->reroute('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->checkCSRF($f3, '/ticket/' . $f3->get('PARAMS.id'));
|
||||||
|
|
||||||
$ticket_id = (int) $f3->get('PARAMS.id');
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
$comment_text = $f3->get('POST.comment');
|
$comment_text = $f3->get('POST.comment');
|
||||||
$current_user_id = $f3->get('SESSION.user.id');
|
$current_user_id = $f3->get('SESSION.user.id');
|
||||||
@ -86,6 +90,6 @@ class CommentController {
|
|||||||
$comments = $results;
|
$comments = $results;
|
||||||
$f3->set('comments', $comments);
|
$f3->set('comments', $comments);
|
||||||
|
|
||||||
echo \Template::instance()->render('../ui/views/comments/view.html');
|
echo \Template::instance()->render('views/comments/view.html');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,6 @@ class DashboardController extends BaseController {
|
|||||||
|
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
|
|
||||||
$this->renderView('/ui/views/dashboard.html');
|
$this->renderView('views/dashboard.html');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,7 +4,7 @@ class HomeController extends BaseController {
|
|||||||
|
|
||||||
public function display($f3){
|
public function display($f3){
|
||||||
|
|
||||||
$this->renderView('/ui/views/home.html');
|
$this->renderView('views/home.html');
|
||||||
|
|
||||||
}
|
}
|
||||||
// ...
|
// ...
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class KBController implements CRUD {
|
class KBController implements CRUD {
|
||||||
|
|
||||||
use RequiresAuth;
|
use RequiresAuth, CheckCSRF;
|
||||||
|
|
||||||
public function index($f3){
|
public function index($f3){
|
||||||
|
|
||||||
@ -39,8 +39,8 @@ class KBController implements CRUD {
|
|||||||
|
|
||||||
// render
|
// render
|
||||||
$f3->set('articles', $articles);
|
$f3->set('articles', $articles);
|
||||||
$f3->set('content', '../ui/views/kb/index.html');
|
$f3->set('content', 'views/kb/index.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
$f3->clear('SESSION.error');
|
$f3->clear('SESSION.error');
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -56,8 +56,8 @@ class KBController implements CRUD {
|
|||||||
$f3->set('all_tags', $all_tags);
|
$f3->set('all_tags', $all_tags);
|
||||||
|
|
||||||
// render
|
// render
|
||||||
$f3->set('content', '../ui/views/kb/create.html');
|
$f3->set('content', 'views/kb/create.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
$f3->clear('SESSION.error');
|
$f3->clear('SESSION.error');
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -66,6 +66,8 @@ class KBController implements CRUD {
|
|||||||
public function create($f3){
|
public function create($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$this->checkCSRF($f3, '/kb/create');
|
||||||
|
|
||||||
$title = $f3->get('POST.title');
|
$title = $f3->get('POST.title');
|
||||||
$content = $f3->get('POST.content');
|
$content = $f3->get('POST.content');
|
||||||
$created_by = $f3->get('SESSION.user.id');
|
$created_by = $f3->get('SESSION.user.id');
|
||||||
@ -120,8 +122,8 @@ class KBController implements CRUD {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// render
|
// render
|
||||||
$f3->set('content', '../ui/views/kb/view.html');
|
$f3->set('content', 'views/kb/view.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
$f3->clear('SESSION.error');
|
$f3->clear('SESSION.error');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,8 +152,8 @@ class KBController implements CRUD {
|
|||||||
|
|
||||||
// render
|
// render
|
||||||
$f3->set('js', 'kb_edit.js');
|
$f3->set('js', 'kb_edit.js');
|
||||||
$f3->set('content', '../ui/views/kb/edit.html');
|
$f3->set('content', 'views/kb/edit.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
$f3->clear('SESSION.error');
|
$f3->clear('SESSION.error');
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -160,7 +162,10 @@ class KBController implements CRUD {
|
|||||||
* Handle POST to edit existing article
|
* Handle POST to edit existing article
|
||||||
*/
|
*/
|
||||||
public function update($f3){
|
public function update($f3){
|
||||||
|
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
$this->checkCSRF($f3, '/kb/' . $f3->get('PARAMS.id') . '/edit');
|
||||||
|
|
||||||
$article_id = $f3->get('PARAMS.id');
|
$article_id = $f3->get('PARAMS.id');
|
||||||
$db = $f3->get('DB');
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
|||||||
@ -16,8 +16,8 @@
|
|||||||
$f3->set('projects', $projects);
|
$f3->set('projects', $projects);
|
||||||
|
|
||||||
|
|
||||||
$f3->set('content', '../ui/views/project/index.html');
|
$f3->set('content', 'views/project/index.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
|
|
||||||
$f3->clear('SESSION.error');
|
$f3->clear('SESSION.error');
|
||||||
}
|
}
|
||||||
@ -25,8 +25,8 @@
|
|||||||
// create a new project
|
// create a new project
|
||||||
public function createForm($f3){
|
public function createForm($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
$f3->set('content', '../ui/views/project/create.html');
|
$f3->set('content', 'views/project/create.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,16 +47,16 @@
|
|||||||
$project = $result[0];
|
$project = $result[0];
|
||||||
$f3->set('project', $project);
|
$f3->set('project', $project);
|
||||||
|
|
||||||
$f3->set('content', '../ui/views/project/view.html');
|
$f3->set('content', 'views/project/view.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
// update project details
|
// update project details
|
||||||
public function editForm($f3){
|
public function editForm($f3){
|
||||||
|
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
$f3->set('content', '../ui/views/project/edit.html');
|
$f3->set('content', 'views/project/edit.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class TagController implements CRUD {
|
class TagController implements CRUD {
|
||||||
|
|
||||||
use RequiresAuth;
|
use RequiresAuth, CheckCSRF;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all tags
|
* List all tags
|
||||||
@ -14,19 +14,20 @@ class TagController implements CRUD {
|
|||||||
$tags = $db->exec('SELECT * FROM tags ORDER BY name ASC');
|
$tags = $db->exec('SELECT * FROM tags ORDER BY name ASC');
|
||||||
$f3->set('tags', $tags);
|
$f3->set('tags', $tags);
|
||||||
|
|
||||||
$f3->set('content', '../ui/views/tag/index.html');
|
$f3->set('content', 'views/tag/index.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createForm($f3){
|
public function createForm($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
|
||||||
$f3->set('content', '../ui/views/tag/create.html');
|
$f3->set('content', 'views/tag/create.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create($f3){
|
public function create($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
$this->checkCSRF($f3, '/tag/create');
|
||||||
|
|
||||||
$name = $f3->get('POST.name');
|
$name = $f3->get('POST.name');
|
||||||
$color = $f3->get('POST.color');
|
$color = $f3->get('POST.color');
|
||||||
|
|||||||
@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
class ThemeController
|
class ThemeController
|
||||||
{
|
{
|
||||||
|
use CheckCSRF;
|
||||||
|
|
||||||
function toggle($f3)
|
function toggle($f3)
|
||||||
{
|
{
|
||||||
|
$this->checkCSRF($f3, $f3->get('HEADERS.Referer') ?: '/');
|
||||||
|
|
||||||
$current = $f3->get('SESSION.theme') ?: 'light';
|
$current = $f3->get('SESSION.theme') ?: 'light';
|
||||||
$new_theme = ($current === 'light') ? 'dark' : 'light';
|
$new_theme = ($current === 'light') ? 'dark' : 'light';
|
||||||
$f3->set('SESSION.theme', $new_theme);
|
$f3->set('SESSION.theme', $new_theme);
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class TicketController extends BaseController implements CRUD {
|
class TicketController extends BaseController implements CRUD {
|
||||||
|
|
||||||
use RequiresAuth;
|
use RequiresAuth, CheckCSRF;
|
||||||
|
|
||||||
// list all tickts
|
// list all tickts
|
||||||
public function index($f3){
|
public function index($f3){
|
||||||
@ -21,7 +21,7 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// render
|
// render
|
||||||
$this->renderView('../ui/views/ticket/index.html',
|
$this->renderView('views/ticket/index.html',
|
||||||
['tickets' => $tickets]
|
['tickets' => $tickets]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -37,14 +37,35 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
$ticket_mapper = new Ticket($this->getDB());
|
$ticket_mapper = new Ticket($this->getDB());
|
||||||
$ticket = $ticket_mapper->findById($ticket_id);
|
$ticket = $ticket_mapper->findById($ticket_id);
|
||||||
|
|
||||||
|
if(!$ticket){
|
||||||
|
$this->f3->set('SESSION.error', 'Ticket not found');
|
||||||
|
$this->f3->reroute('/tickets');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assigned_user = $ticket->getAssignedUser();
|
||||||
|
$ticket_history = $ticket->getHistory();
|
||||||
|
|
||||||
|
$map_statuses = array_column((new TicketStatus($this->getDB()))->findAll(), 'name', 'id');
|
||||||
|
$map_priorities = array_column((new TicketStatus($this->getDB()))->findAll(), 'name', 'id');
|
||||||
|
$map_users = array_column($this->getDB()->exec('SELECT id, display_name FROM users'), 'display_name', 'id');
|
||||||
|
|
||||||
|
|
||||||
// render
|
// render
|
||||||
$this->renderView('../ui/views/ticket/view.html', [
|
$this->renderView('views/ticket/view.html', [
|
||||||
'ticket' => $ticket,
|
'ticket' => $ticket,
|
||||||
|
'assigned_user' => $assigned_user,
|
||||||
'attachments' => $ticket->attachments(),
|
'attachments' => $ticket->attachments(),
|
||||||
'comments' => $ticket->comments(),
|
'comments' => $ticket->comments(),
|
||||||
'parent_tickets' => $ticket->getParentTickets(),
|
'parent_tickets' => $ticket->getParentTickets(),
|
||||||
'child_tickets' => $ticket->getChildTickets(),
|
'child_tickets' => $ticket->getChildTickets(),
|
||||||
'ticket_meta' => $ticket->getMetaAssoc()
|
'ticket_meta' => $ticket->getMetaAssoc(),
|
||||||
|
'ticket_history' => $ticket_history,
|
||||||
|
'map' => [
|
||||||
|
'statuses' => $map_statuses,
|
||||||
|
'priorities' => $map_priorities,
|
||||||
|
'users' => $map_users
|
||||||
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -56,10 +77,19 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
$priorities = (new TicketPriority($db))->findAll();
|
$priorities = (new TicketPriority($db))->findAll();
|
||||||
$statuses = (new TicketStatus($db))->findAll();
|
$statuses = (new TicketStatus($db))->findAll();
|
||||||
|
|
||||||
|
$all_tags_model = new \Tag($this->getDB());
|
||||||
|
$all_tags = $all_tags_model->find([], ['order' => 'name ASC']); // get all tags
|
||||||
|
|
||||||
|
// TODO: this needs moving into a model?
|
||||||
|
$users = $this->getDB()->exec('SELECT id, username, display_name FROM users ORDER BY display_name ASC');
|
||||||
|
$users = array_merge([['id'=>'-1', 'display_name'=>'--']], $users);
|
||||||
|
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
$this->renderView('../ui/views/ticket/create.html',[
|
$this->renderView('views/ticket/create.html',[
|
||||||
'priorities' => $priorities,
|
'priorities' => $priorities,
|
||||||
'statuses' => $statuses
|
'statuses' => $statuses,
|
||||||
|
'users' => $users,
|
||||||
|
'all_tags' => $all_tags
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +98,7 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
public function create($f3){
|
public function create($f3){
|
||||||
|
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
|
$this->checkCSRF($f3, '/ticket/create');
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => $this->f3->get('POST.title'),
|
'title' => $this->f3->get('POST.title'),
|
||||||
@ -75,18 +106,29 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
'description' => $this->f3->get('POST.description'),
|
'description' => $this->f3->get('POST.description'),
|
||||||
'priority_id' => $this->f3->get('POST.priority_id'),
|
'priority_id' => $this->f3->get('POST.priority_id'),
|
||||||
'status_id' => $this->f3->get('POST.status_id'),
|
'status_id' => $this->f3->get('POST.status_id'),
|
||||||
'created_by' => $this->f3->get('SESSION.user.id')
|
'created_by' => $this->f3->get('SESSION.user.id'),
|
||||||
|
'assigned_to' => $this->f3->get('POST.assigned_to') == '-1' ? null : $this->f3->get('POST.assigned_to')
|
||||||
];
|
];
|
||||||
|
|
||||||
$ticket_mapper = new Ticket($this->getDB());
|
$ticket_mapper = new Ticket($this->getDB());
|
||||||
$new_ticket_id = $ticket_mapper->createTicket($data);
|
$new_ticket_id = $ticket_mapper->createTicket($data);
|
||||||
|
|
||||||
// custom field
|
// custom field
|
||||||
$meta_keys = $this->f3->get('POST.meta_key');
|
// $meta_keys = $this->f3->get('POST.meta_key');
|
||||||
$meta_values = $this->f3->get('POST.meta_value');
|
// $meta_values = $this->f3->get('POST.meta_value');
|
||||||
$meta_assoc = $ticket_mapper->assocMetaFromKeyValue($meta_keys, $meta_values);
|
// $meta_assoc = $ticket_mapper->assocMetaFromKeyValue($meta_keys, $meta_values);
|
||||||
$ticket_mapper->setCustomFields($meta_assoc);
|
// $ticket_mapper->setCustomFields($meta_assoc);
|
||||||
|
|
||||||
|
$new_ticket = $ticket_mapper->findById($new_ticket_id);
|
||||||
|
if($new_ticket){
|
||||||
|
// TAG handling for create
|
||||||
|
$posted_tags = $this->f3->get('POST.tags');
|
||||||
|
if(!empty($posted_tags) && is_array($posted_tags)){
|
||||||
|
$new_ticket->setTags($posted_tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->f3->set('SESSION.message', 'Ticket #' . $new_ticket_id . ' created successfully.');
|
||||||
$this->f3->reroute('/ticket/' . $new_ticket_id);
|
$this->f3->reroute('/ticket/' . $new_ticket_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,12 +154,29 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
// dropdowns
|
// dropdowns
|
||||||
$priorities = (new TicketPriority($this->getDB()))->findAll();
|
$priorities = (new TicketPriority($this->getDB()))->findAll();
|
||||||
$statuses = (new TicketStatus($this->getDB()))->findAll();
|
$statuses = (new TicketStatus($this->getDB()))->findAll();
|
||||||
|
$all_tags_model = new \Tag($this->getDB());
|
||||||
|
$all_tags = $all_tags_model->find([], ['order' => 'name ASC']);
|
||||||
|
|
||||||
$this->renderView('../ui/views/ticket/edit.html',[
|
// TODO: this needs moving into a model?
|
||||||
|
$users = $this->getDB()->exec('SELECT id, username, display_name FROM users ORDER BY display_name ASC');
|
||||||
|
$users = array_merge([['id'=>'-1', 'display_name'=>'--']], $users);
|
||||||
|
|
||||||
|
// paradox - empty($ticket->tags) was returning true, when there were items present
|
||||||
|
$current_ticket_tag_ids = [];
|
||||||
|
if(count($ticket->tags) > 0){
|
||||||
|
foreach($ticket->tags as $current_tag_data){
|
||||||
|
$current_ticket_tag_ids[] = $current_tag_data['id'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renderView('views/ticket/edit.html',[
|
||||||
'ticket' => $ticket,
|
'ticket' => $ticket,
|
||||||
'ticket_meta' => $ticket->getMeta(),
|
'ticket_meta' => $ticket->getMeta(),
|
||||||
'priorities' => $priorities,
|
'priorities' => $priorities,
|
||||||
'statuses' => $statuses
|
'statuses' => $statuses,
|
||||||
|
'users' => $users,
|
||||||
|
'all_tags' => $all_tags,
|
||||||
|
'current_ticket_tag_ids' => $current_ticket_tag_ids
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -128,38 +187,49 @@ class TicketController extends BaseController implements CRUD {
|
|||||||
{
|
{
|
||||||
|
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
|
$this->checkCSRF($f3, '/ticket/create');
|
||||||
|
|
||||||
$ticket_id = $this->f3->get('PARAMS.id');
|
$ticket_id = $f3->get('PARAMS.id');
|
||||||
$ticket_mapper = new Ticket($this->getDB());
|
$ticket_mapper = new Ticket($this->getDB());
|
||||||
$ticket = $ticket_mapper->findById($ticket_id);
|
$ticket = $ticket_mapper->findById($ticket_id);
|
||||||
|
|
||||||
if(!$ticket){
|
if(!$ticket){
|
||||||
$this->f3->set('SESSION.error', 'Ticket not found.');
|
$f3->set('SESSION.error', 'Ticket not found.');
|
||||||
$this->f3->reroute('/tickets');
|
$f3->reroute('/tickets');
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => $this->f3->get('POST.title'),
|
'title' => $f3->get('POST.title'),
|
||||||
'created_at' => $this->f3->get('POST.created_at'),
|
'created_at' => $f3->get('POST.created_at'),
|
||||||
'description' => $this->f3->get('POST.description'),
|
'description' => $f3->get('POST.description'),
|
||||||
'priority_id' => $this->f3->get('POST.priority_id'),
|
'priority_id' => $f3->get('POST.priority_id'),
|
||||||
'status_id' => $this->f3->get('POST.status_id'),
|
'status_id' => $f3->get('POST.status_id'),
|
||||||
'updated_by' => $this->f3->get('SESSION.user.id')
|
'updated_by' => $f3->get('SESSION.user.id') ,
|
||||||
|
'assigned_to' => $f3->get('POST.assigned_to') == -1 ? null : $f3->get('POST.assigned_to')
|
||||||
];
|
];
|
||||||
$ticket->updateTicket($data);
|
$ticket->updateTicket($data);
|
||||||
|
|
||||||
// deal with meta data / custom fields
|
// deal with meta data / custom fields
|
||||||
$meta_keys = $this->f3->get('POST.meta_key');
|
// $meta_keys = $this->f3->get('POST.meta_key');
|
||||||
$meta_values = $this->f3->get('POST.meta_value');
|
// $meta_values = $this->f3->get('POST.meta_value');
|
||||||
$meta_assoc = $ticket->assocMetaFromKeyValue($meta_keys, $meta_values);
|
// $meta_assoc = $ticket->assocMetaFromKeyValue($meta_keys, $meta_values);
|
||||||
$ticket->setCustomFields($meta_assoc);
|
// $ticket->setCustomFields($meta_assoc);
|
||||||
|
|
||||||
|
$posted_tags = $f3->get('POST.tags');
|
||||||
|
if(is_array($posted_tags)){
|
||||||
|
$ticket->setTags($posted_tags);
|
||||||
|
} elseif (empty($posted_tags)){
|
||||||
|
$ticket->setTags([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$f3->set('SESSION.message', 'Ticket #' . $ticket_id . ' updated successfully.') ;
|
||||||
$f3->reroute('/ticket/' . $ticket_id);
|
$f3->reroute('/ticket/' . $ticket_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// subtask
|
// subtask
|
||||||
public function addSubtask($f3){
|
public function addSubtask($f3){
|
||||||
$this->requireLogin();
|
$this->requireLogin();
|
||||||
|
$this->checkCSRF($f3, '/ticket/create');
|
||||||
|
|
||||||
$parent_id = (int) $f3->get('PARAMS.id');
|
$parent_id = (int) $f3->get('PARAMS.id');
|
||||||
$child_id = (int) $f3->get('POST.child_ticket_id');
|
$child_id = (int) $f3->get('POST.child_ticket_id');
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class UserController implements CRUD {
|
class UserController implements CRUD {
|
||||||
|
|
||||||
use RequiresAuth;
|
use RequiresAuth, CheckCSRF;
|
||||||
|
|
||||||
// list all users (admin only)
|
// list all users (admin only)
|
||||||
|
|
||||||
@ -19,8 +19,8 @@ class UserController implements CRUD {
|
|||||||
);
|
);
|
||||||
$f3->set('users', $users);
|
$f3->set('users', $users);
|
||||||
|
|
||||||
$f3->set('content', '../ui/views/user/index.html');
|
$f3->set('content', 'views/user/index.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function editForm($f3){
|
public function editForm($f3){
|
||||||
@ -37,13 +37,14 @@ class UserController implements CRUD {
|
|||||||
$f3->reroute('/users');
|
$f3->reroute('/users');
|
||||||
}
|
}
|
||||||
$f3->set('edit_user', $rows[0]);
|
$f3->set('edit_user', $rows[0]);
|
||||||
$f3->set('content', '../ui/views/user/edit.html');
|
$f3->set('content', 'views/user/edit.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('templates/layout.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update($f3){
|
public function update($f3){
|
||||||
|
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
$this->checkCSRF($f3, '/user/' . $f3->get('PARAMS.id') . '/edit');
|
||||||
|
|
||||||
$user_id = (int) $f3->get('PARAMS.id');
|
$user_id = (int) $f3->get('PARAMS.id');
|
||||||
$new_username = $f3->get('POST.username');
|
$new_username = $f3->get('POST.username');
|
||||||
|
|||||||
26
app/debug.php
Normal file
26
app/debug.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function debug_print($obj)
|
||||||
|
{
|
||||||
|
printf('<pre>%s</pre>', print_r($obj, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function debug_var_dump($obj)
|
||||||
|
{
|
||||||
|
printf('<pre>%s</pre>', var_dump_str($obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
function var_dump_str()
|
||||||
|
{
|
||||||
|
$argc = func_num_args();
|
||||||
|
$argv = func_get_args();
|
||||||
|
|
||||||
|
if ($argc > 0) {
|
||||||
|
ob_start();
|
||||||
|
call_user_func_array('var_dump', $argv);
|
||||||
|
$result = ob_get_contents();
|
||||||
|
ob_end_clean();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
@ -14,7 +14,11 @@ class BulmaFormHelper extends \Prefab {
|
|||||||
static public function render($node) {
|
static public function render($node) {
|
||||||
|
|
||||||
$attr = $node['@attrib'] ?? [];
|
$attr = $node['@attrib'] ?? [];
|
||||||
$type = strtoupper($attr['type']) ?? null;
|
if(isset($attr['type'])){
|
||||||
|
$type = strtoupper($attr['type']);
|
||||||
|
} else {
|
||||||
|
$type = null;
|
||||||
|
}
|
||||||
|
|
||||||
// all *
|
// all *
|
||||||
$label = $attr['label'] ?? '';
|
$label = $attr['label'] ?? '';
|
||||||
|
|||||||
36
app/extensions/CSRFHelper.php
Normal file
36
app/extensions/CSRFHelper.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class CSRFHelper {
|
||||||
|
|
||||||
|
const TOKEN_NAME = 'csrf_token';
|
||||||
|
|
||||||
|
public static function token():string {
|
||||||
|
$f3 = \Base::instance();
|
||||||
|
if(!$f3->exists('SESSION.' . self::TOKEN_NAME)) {
|
||||||
|
$token = bin2hex(random_bytes(32));
|
||||||
|
$f3->set('SESSION.' . self::TOKEN_NAME, $token);
|
||||||
|
}
|
||||||
|
return $f3->get('SESSION.' . self::TOKEN_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function verify(?string $submitted_token): bool {
|
||||||
|
$f3 = \Base::instance();
|
||||||
|
$session_token = $f3->get('SESSION.' . self::TOKEN_NAME);
|
||||||
|
|
||||||
|
if(empty($submitted_token) || empty($session_token)){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(hash_equals($session_token, $submitted_token)){
|
||||||
|
$f3->clear('SESSION.' . self::TOKEN_NAME);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function field(): string {
|
||||||
|
return '<input type="hidden" name="'.self::TOKEN_NAME.'" value="'.self::token().'">';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -31,7 +31,29 @@ class IconsHelper extends \Prefab {
|
|||||||
|
|
||||||
static public function icons($node){
|
static public function icons($node){
|
||||||
|
|
||||||
|
// debug_print($node);
|
||||||
|
|
||||||
|
$required = ['type', 'path'];
|
||||||
|
$check = self::checkAttributes($node, $required);
|
||||||
|
if(!is_null($check)){
|
||||||
|
return sprintf('<div class="notification is-danger is-light">%s</div>', $check);
|
||||||
|
}
|
||||||
|
|
||||||
$attr = $node['@attrib'];
|
$attr = $node['@attrib'];
|
||||||
|
$type = $attr['type'];
|
||||||
|
$path = $attr['path'];
|
||||||
|
$selected = $attr['selected'];
|
||||||
|
|
||||||
|
switch($attr['type']){
|
||||||
|
case 'status-selector':
|
||||||
|
// $selected = Base::instance()->get('GET.status') ?: null;
|
||||||
|
return '<?php echo \IconsHelper::instance()->renderStatusSelector(
|
||||||
|
Base::instance()->get("GET.status") ?: null, "'.$path.'"); ?>';
|
||||||
|
return self::renderStatusSelector($selected, $path);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '<div class="notification">unknown icon selector type</div>';
|
||||||
|
}
|
||||||
|
|
||||||
$tpl = Template::instance();
|
$tpl = Template::instance();
|
||||||
$f3 = Base::instance();
|
$f3 = Base::instance();
|
||||||
@ -43,6 +65,42 @@ class IconsHelper extends \Prefab {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function checkAttributes($node, array $required){
|
||||||
|
if(isset($node['@attrib'])){
|
||||||
|
$attr = $node['@attrib'];
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
foreach($required as $key){
|
||||||
|
if(empty($attr[$key])){
|
||||||
|
$errors[] = "Error: '$key' is missing.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($errors) ? null : implode(" ", $errors);
|
||||||
|
}
|
||||||
|
return "Error: '@attrib' is missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function renderStatusSelector($current_status, $path)
|
||||||
|
{
|
||||||
|
$output = '<div class="block"><div class="field has-addons">';
|
||||||
|
|
||||||
|
foreach (self::$status_icons as $k => $icon) {
|
||||||
|
$active = ($current_status == $k);
|
||||||
|
$url = $path . ($active ? '' : '/?status=' . $k);
|
||||||
|
$class = 'button' . ($active ? ' is-inverted' : '');
|
||||||
|
|
||||||
|
$output .= '<p class="control">';
|
||||||
|
$output .= '<a href="' . $url . '" class="' . $class . '">';
|
||||||
|
$output .= '<span class="icon is-small"><i class="fas fa-' . $icon[0] . '"></i></span>';
|
||||||
|
$output .= '<span>' . self::$status_names[$k] . '</span>';
|
||||||
|
$output .= '</a>';
|
||||||
|
$output .= '</p>';
|
||||||
|
}
|
||||||
|
$output .= '</div></div>';
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
static function do_the_switch($type, $value){
|
static function do_the_switch($type, $value){
|
||||||
|
|
||||||
if($value !== null) {
|
if($value !== null) {
|
||||||
|
|||||||
@ -63,6 +63,7 @@ class Ticket extends \DB\SQL\Mapper {
|
|||||||
|
|
||||||
public function getTagsForTickets(array $tickets)
|
public function getTagsForTickets(array $tickets)
|
||||||
{
|
{
|
||||||
|
if(empty($tickets)) return [];
|
||||||
$tag_mapper = new Tag($this->db, 'ticket');
|
$tag_mapper = new Tag($this->db, 'ticket');
|
||||||
$tickets = $tag_mapper->getTagsFor($tickets);
|
$tickets = $tag_mapper->getTagsFor($tickets);
|
||||||
|
|
||||||
@ -74,8 +75,34 @@ class Ticket extends \DB\SQL\Mapper {
|
|||||||
$this->status_name = 'SELECT name FROM ticket_statuses WHERE tickets.status_id = ticket_statuses.id';
|
$this->status_name = 'SELECT name FROM ticket_statuses WHERE tickets.status_id = ticket_statuses.id';
|
||||||
$this->priority_name = 'SELECT name FROM ticket_priorities WHERE tickets.priority_id = ticket_priorities.id';
|
$this->priority_name = 'SELECT name FROM ticket_priorities WHERE tickets.priority_id = ticket_priorities.id';
|
||||||
$this->load(['id = ?', $id]);
|
$this->load(['id = ?', $id]);
|
||||||
$this->tags = (new Tag($this->db,'ticket'))->getTagsForID($id, 'ticket_id');
|
if($this->dry()){
|
||||||
return $this->dry() ? null : $this;
|
return null;
|
||||||
|
}
|
||||||
|
$tag_model = new Tag($this->db, 'ticket');
|
||||||
|
$this->tags = $tag_model->getTagsForID($this->id, 'ticket_id');
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTags(array $tags_ids):void {
|
||||||
|
if($this->dry() || !$this->id){
|
||||||
|
// can't set tags for a ticket that hasn't been saved or loaded
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove existing tags - TODO: shouldn't this be in the tag model?
|
||||||
|
$this->db->exec('DELETE FROM ticket_tags WHERE ticket_id = ?', [$this->id]);
|
||||||
|
|
||||||
|
if(!empty($tags_ids)){
|
||||||
|
$sql_insert_tag = 'INSERT INTO ticket_tags (ticket_id, tag_id) VALUES (?,?)';
|
||||||
|
foreach($tags_ids as $tag_id){
|
||||||
|
if(filter_var($tag_id, FILTER_VALIDATE_INT)){
|
||||||
|
$this->db->exec($sql_insert_tag, [$this->id, (int)$tag_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// refresh tags
|
||||||
|
$tag_model = new Tag($this->db, 'ticket');
|
||||||
|
$this->tags = $tag_model->getTagsForID($this->id, 'ticket_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createTicket(array $data): int
|
public function createTicket(array $data): int
|
||||||
@ -93,18 +120,30 @@ class Ticket extends \DB\SQL\Mapper {
|
|||||||
$this->updated_at = date('Y-m-d H:i:s');
|
$this->updated_at = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
|
$this->logCreate();
|
||||||
return (int)$this->id;
|
return (int)$this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateTicket(array $data): void
|
public function updateTicket(array $data): void
|
||||||
{
|
{
|
||||||
|
|
||||||
|
$this->logDiff($data);
|
||||||
|
|
||||||
if(isset($data['title'])){ $this->title = $data['title']; }
|
if(isset($data['title'])){ $this->title = $data['title']; }
|
||||||
if(isset($data['description'])) { $this->description = $data['description']; }
|
if(isset($data['description'])) { $this->description = $data['description']; }
|
||||||
if(isset($data['priority_id'])) { $this->priority_id = $data['priority_id']; }
|
if(isset($data['priority_id'])) { $this->priority_id = $data['priority_id']; }
|
||||||
if(isset($data['status_id'])) { $this->status_id = $data['status_id']; }
|
if(isset($data['status_id'])) { $this->status_id = $data['status_id']; }
|
||||||
if(isset($data['updated_by'])) { $this->updated_by = $data['updated_by']; }
|
if(isset($data['updated_by'])) { $this->updated_by = $data['updated_by']; }
|
||||||
|
if(isset($data['assigned_to'])) {
|
||||||
|
if($data['assigned_to'] == '-1'){
|
||||||
|
$this->assigned_to = null;
|
||||||
|
} else {
|
||||||
|
$this->assigned_to = $data['assigned_to'];
|
||||||
|
}
|
||||||
|
}
|
||||||
$this->created_at = ($data['created_at'] == '' ? date('Y-m-d H:i:s') : $data['created_at']) ?? date('Y-m-d H:i:s');
|
$this->created_at = ($data['created_at'] == '' ? date('Y-m-d H:i:s') : $data['created_at']) ?? date('Y-m-d H:i:s');
|
||||||
$this->updated_at = date('Y-m-d H:i:s');
|
$this->updated_at = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
$this->save();
|
$this->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,4 +259,93 @@ class Ticket extends \DB\SQL\Mapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAssignedUser()
|
||||||
|
{
|
||||||
|
if(!$this->assigned_to){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = '
|
||||||
|
SELECT id, username, display_name
|
||||||
|
FROM users
|
||||||
|
WHERE id =?
|
||||||
|
';
|
||||||
|
$user = $this->db->exec($sql, [$this->assigned_to]);
|
||||||
|
return $user[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a change to the ticket history.
|
||||||
|
* @param int $user_id - the ID of the user making the change.
|
||||||
|
* @param string $field_changed - the name of the field that was changed.
|
||||||
|
* @param string|null $old_value - the old value
|
||||||
|
* @param string|null $new_value - the new value
|
||||||
|
*/
|
||||||
|
public function logHistory(int $user_id, string $field_changed,
|
||||||
|
$old_value = null, $new_value = null,
|
||||||
|
$changed_at = null): void
|
||||||
|
{
|
||||||
|
if($this->dry() || !$this->id) return;
|
||||||
|
|
||||||
|
$history = new \DB\SQL\Mapper($this->db, 'ticket_history');
|
||||||
|
$history->ticket_id = $this->id;
|
||||||
|
$history->user_id = $user_id;
|
||||||
|
$history->field_changed = $field_changed;
|
||||||
|
$history->old_value = $old_value === null ? null : (string)$old_value;
|
||||||
|
$history->new_value = $new_value === null ? null : (string)$new_value;
|
||||||
|
if($changed_at == null){
|
||||||
|
$history->changed_at = date('Y-m-d H:i:s');
|
||||||
|
} else {
|
||||||
|
$history->changed_at = $changed_at;
|
||||||
|
}
|
||||||
|
$history->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHistory(): array {
|
||||||
|
if($this->dry() || !$this->id) return[];
|
||||||
|
|
||||||
|
$sql = 'SELECT th.*, u.display_name as user_display_name
|
||||||
|
FROM ticket_history th
|
||||||
|
JOIN users u ON th.user_id = u.id
|
||||||
|
WHERE th.ticket_id = ?
|
||||||
|
ORDER BY th.changed_at DESC';
|
||||||
|
return $this->db->exec($sql, [$this->id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called from create
|
||||||
|
*/
|
||||||
|
public function logCreate(){
|
||||||
|
$changed_at = date('Y-m-d H:i:s');
|
||||||
|
$user_making_change = \Base::instance()->get('SESSION.user.id');
|
||||||
|
$this->logHistory($user_making_change, 'ticket_created', null, $this->title, $changed_at);
|
||||||
|
if($this->status_id) $this->logHistory($user_making_change, 'status_id', null, $this->status_id, $changed_at);
|
||||||
|
if($this->priority_id) $this->logHistory($user_making_change, 'priority_id', null, $this->priority_id, $changed_at);
|
||||||
|
if($this->assigned_to) $this->logHistory($user_making_change, 'assigned_to', null, $this->assigned_to, $changed_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* called from update
|
||||||
|
*/
|
||||||
|
public function logDiff($data){
|
||||||
|
$user_making_change = \Base::instance()->get('SESSION.user.id');
|
||||||
|
$changed_at = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$checks = ['title', 'description', 'priority_id', 'status_id', 'assigned_to', 'updated_by'];
|
||||||
|
|
||||||
|
// loop instead
|
||||||
|
foreach($checks as $check){
|
||||||
|
if(isset($data[$check]) && $this->$check != $data[$check]){
|
||||||
|
if($check == 'description'){
|
||||||
|
$old_hash = hash('sha256', $this->$check);
|
||||||
|
$new_hash = hash('sha256', $data[$check]);
|
||||||
|
$this->logHistory($user_making_change, $check, $old_hash, $new_hash, $changed_at);
|
||||||
|
} else {
|
||||||
|
$this->logHistory($user_making_change, $check, $this->$check, $data[$check], $changed_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
13
app/traits/CheckCSRF.php
Normal file
13
app/traits/CheckCSRF.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
trait CheckCSRF {
|
||||||
|
|
||||||
|
public function checkCSRF($f3, $reroute){
|
||||||
|
if(!\CSRFHelper::verify($f3->get('POST.' . \CSRFHelper::TOKEN_NAME))){
|
||||||
|
$f3->set('SESSION.error', 'CSRF token validation failed.');
|
||||||
|
$f3->reroute($reroute);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -5,15 +5,15 @@ This issue list defines the work required to bring the `tp_servicedesk` Fat-Free
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🧩 Ticketing System
|
## 🧩 Ticketing System
|
||||||
- [ ] Display assigned user in ticket view
|
- [x] Display assigned user in ticket view
|
||||||
- [ ] Add user assignment capability in ticket create/edit forms
|
- [x] Add user assignment capability in ticket create/edit forms
|
||||||
- [ ] Implement ticket filtering (by status, assignee, project)
|
- [ ] Implement ticket filtering (by status, assignee, project)
|
||||||
- [ ] Improve UI feedback for ticket soft-delete
|
- [ ] Improve UI feedback for ticket soft-delete
|
||||||
- [ ] Ensure metadata, attachments, and comments update properly on edit
|
- [ ] Ensure metadata, attachments, and comments update properly on edit
|
||||||
- [ ] Add tag support for tickets (UI and DB updates)
|
- [ ] Add tag support for tickets (UI and DB updates)
|
||||||
- [ ] Implement ticket history for status/priority changes
|
- [ ] Implement ticket history for status/priority changes
|
||||||
- [ ] Add comment thread display on ticket edit view
|
- [ ] Add comment thread display on ticket edit view
|
||||||
- [ ] Update "new ticket" template to match the edit form
|
- [x] Update "new ticket" template to match the edit form
|
||||||
- [ ] Enable linking tickets using markdown shortcodes (e.g. `#ticket-id` autocomplete)
|
- [ ] Enable linking tickets using markdown shortcodes (e.g. `#ticket-id` autocomplete)
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -65,8 +65,8 @@ This issue list defines the work required to bring the `tp_servicedesk` Fat-Free
|
|||||||
- [ ] Create fake/test data seeder
|
- [ ] Create fake/test data seeder
|
||||||
- [ ] Add DB integrity checks for tickets, users, and project FK links
|
- [ ] Add DB integrity checks for tickets, users, and project FK links
|
||||||
- [ ] Manually test:
|
- [ ] Manually test:
|
||||||
- [ ] Login/logout
|
- [x] Login/logout
|
||||||
- [ ] Ticket create/edit/delete
|
- [x] Ticket create/edit/delete
|
||||||
- [ ] KB CRUD
|
- [ ] KB CRUD
|
||||||
- [ ] Attachment upload/download
|
- [ ] Attachment upload/download
|
||||||
- [ ] Audit all routes and controllers for authentication and access checks
|
- [ ] Audit all routes and controllers for authentication and access checks
|
||||||
@ -97,7 +97,7 @@ This issue list defines the work required to bring the `tp_servicedesk` Fat-Free
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 🔐 Security & Session
|
## 🔐 Security & Session
|
||||||
- [ ] Add CSRF protection for all POST forms
|
- [x] Add CSRF protection for all POST forms
|
||||||
- [ ] Enforce permission checks on:
|
- [ ] Enforce permission checks on:
|
||||||
- [ ] Ticket edits
|
- [ ] Ticket edits
|
||||||
- [ ] Comment deletion
|
- [ ] Comment deletion
|
||||||
98
app/ui/parts/clipboard.html
Normal file
98
app/ui/parts/clipboard.html
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<div class="block">
|
||||||
|
<input type="hidden" id="csrf-token-clipboard" value="{{ \CSRFHelper::token() }}">
|
||||||
|
<style>
|
||||||
|
#upload-area {
|
||||||
|
height: 250px;
|
||||||
|
border: 2px dashed #aaa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #555;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload-area.hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview {
|
||||||
|
max-width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="upload-area" contenteditable="true">
|
||||||
|
Paste or drag an image here
|
||||||
|
</div>
|
||||||
|
<img id="preview" alt="image preview" hidden>
|
||||||
|
<p id="status"></p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const area = document.getElementById('upload-area');
|
||||||
|
const preview = document.getElementById('preview');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const csrfTokenClipboard = document.getElementById('csrf-token-clipboard').value;
|
||||||
|
|
||||||
|
async function uploadImage(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
preview.src = reader.result;
|
||||||
|
preview.hidden = false;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('attachment', file);
|
||||||
|
formData.append('csrf_token', csrfTokenClipboard);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/ticket/{{@ticket->id}}/attachments/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
if(res.ok){
|
||||||
|
const responseData = await res.json(); // assuming json returned
|
||||||
|
statusMsg.textContent = responseData || 'Upload success, no message';
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
const errorData = await res.json();
|
||||||
|
statusMsg.textContent = 'Upload failed: ' + (errorData.error || res.statusText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = 'upload failed.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// paste
|
||||||
|
area.addEventListener('paste', (e) => {
|
||||||
|
for (let item of e.clipboardData.items) {
|
||||||
|
if (item.type.startsWith('image/')) {
|
||||||
|
uploadImage(item.getAsFile());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// drag and drop
|
||||||
|
area.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
area.classList.add('hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
area.addEventListener('dragLeave', () => {
|
||||||
|
area.classList.remove('hover');
|
||||||
|
});
|
||||||
|
|
||||||
|
area.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
area.classList.remove('hover');
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
for (let file of files) {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
uploadImage(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@ -55,7 +55,8 @@
|
|||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<form id="theme-toggle-form" method="post" action="/toggle-theme" style="display:inline">
|
<form id="theme-toggle-form" method="POST" action="/toggle-theme" style="display:inline">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
<button type="submit" id="theme-toggle-button" class="button is-small" aria-label="Toggle Theme">
|
<button type="submit" id="theme-toggle-button" class="button is-small" aria-label="Toggle Theme">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-circle-half-stroke" id="theme-icon"></i>
|
<i class="fas fa-circle-half-stroke" id="theme-icon"></i>
|
||||||
@ -92,7 +93,7 @@
|
|||||||
|
|
||||||
<!-- Main Content Area -->
|
<!-- Main Content Area -->
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<include href="ui/session/error.html">
|
<include href="session/error.html">
|
||||||
</div>
|
</div>
|
||||||
<main class="section" id="page">
|
<main class="section" id="page">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
6
app/ui/views/admin/priorities/create.html
Normal file
6
app/ui/views/admin/priorities/create.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<h1 class="title">Create Ticket Priority</h1>
|
||||||
|
<p>TODO:</p>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
</form>
|
||||||
@ -1,5 +1,4 @@
|
|||||||
<div class="box">
|
<div class="block">
|
||||||
<div class="content">
|
|
||||||
<h4 class="title is-4">Attachments</h4>
|
<h4 class="title is-4">Attachments</h4>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<check if="isset( {{@attachments }})">
|
<check if="isset( {{@attachments }})">
|
||||||
@ -38,6 +37,7 @@
|
|||||||
</check>
|
</check>
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<form action="/ticket/{{@PARAMS.id}}/attachments/upload" method="POST" enctype="multipart/form-data">
|
<form action="/ticket/{{@PARAMS.id}}/attachments/upload" method="POST" enctype="multipart/form-data">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control has-icons-left"><!-- is-expanded -->
|
<div class="control has-icons-left"><!-- is-expanded -->
|
||||||
<input class="input" type="file" name="attachment" required>
|
<input class="input" type="file" name="attachment" required>
|
||||||
@ -53,4 +53,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
42
app/ui/views/comments/view.html
Normal file
42
app/ui/views/comments/view.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<hr>
|
||||||
|
<div class="block" id="comments">
|
||||||
|
<h3 class="title is-3">Comments</h3>
|
||||||
|
<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 class="block">
|
||||||
|
<form action="/ticket/{{@PARAMS.id}}/comment" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
<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 is-clearfix">
|
||||||
|
<button class="button is-primary is-pulled-right" type="submit">Submit Comment</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
3
app/ui/views/dashboard.html
Normal file
3
app/ui/views/dashboard.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<h1 class="title">Dashboard</h1>
|
||||||
|
|
||||||
|
<parsedown href="issues.md"></parsedown>
|
||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/kb/create" method="POST">
|
<form action="/kb/create" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
||||||
|
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<form action="/kb/{{@article.id}}/update" method="POST">
|
<form action="/kb/{{@article.id}}/update" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@article.title}}"></bulma>
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@article.title}}"></bulma>
|
||||||
|
|
||||||
@ -7,7 +7,7 @@
|
|||||||
</check>
|
</check>
|
||||||
|
|
||||||
<form action="/login" method="POST">
|
<form action="/login" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control has-icons-left has-icons-right">
|
<p class="control has-icons-left has-icons-right">
|
||||||
<input name="username" class="input" type="text" placeholder="Username">
|
<input name="username" class="input" type="text" placeholder="Username">
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/tag/create" method="POST">
|
<form action="/tag/create" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Name</label>
|
<label class="label">Name</label>
|
||||||
83
app/ui/views/ticket/create.html
Normal file
83
app/ui/views/ticket/create.html
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<!-- Ticket - View -->
|
||||||
|
<!-- made to look more in line with view-->
|
||||||
|
<form action="/ticket/create" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
<div class="is-flex">
|
||||||
|
<div class="is-flex-grow-1">
|
||||||
|
<bulma type="FIELD_INPUT" name="title" value="" class="mr-3"></bulma>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-secondary" href="/tickets">Cancel</a>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-primary" type="submit">Create Ticket</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="block">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-two-thirds">
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<bulma type="FIELD_INPUT" name="created_at" value="" label="Created At:"></bulma>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active">
|
||||||
|
<a data-tab="write"><span class="icon is-small">
|
||||||
|
<i class="fas fa-pen" aria-hidden="true"></i></span><span>Write</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a data-tab="preview"><span class="icon is-small">
|
||||||
|
<i class="fas fa-magnifying-glass"
|
||||||
|
aria-hidden="true"></i></span><span>Preview</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-write" class="tab-content block">
|
||||||
|
<bulma type="FIELD_TEXTAREA" name="description" value="" rows="20"></bulma>
|
||||||
|
</div>
|
||||||
|
<div id="tab-preview" class="tab-content content">
|
||||||
|
<div id="preview-output"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id" options="{{@priorities}}"
|
||||||
|
option_value="id" option_name="name" selected="0"></bulma>
|
||||||
|
|
||||||
|
<bulma type="FIELD_SELECT" label="Status:" name="status_id" options="{{@statuses}}" option_value="id"
|
||||||
|
option_name="name" selected="0"></bulma>
|
||||||
|
|
||||||
|
<bulma type="FIELD_SELECT" label="Assigned User:" name="assigned_to" options="{{@users}}"
|
||||||
|
option_value="id" option_name="display_name" selected="0"></bulma>
|
||||||
|
|
||||||
|
<!-- field select multiple (TODO: move this to a custom bulma tag)-->
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Tags</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select is-multiple" style="width:100%">
|
||||||
|
<select name="tags[]" multiple size="5" style="width:100%">
|
||||||
|
<repeat group="{{ @all_tags }}" value="{{@tag_option}}">
|
||||||
|
<option value="{{ @tag_option.id}}">{{ @tag_option.name }}</option>
|
||||||
|
</repeat>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="help">Hold Ctrl to select multiple.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
173
app/ui/views/ticket/edit.html
Normal file
173
app/ui/views/ticket/edit.html
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<!-- Ticket - Edit -->
|
||||||
|
<!-- made to look more in line with view-->
|
||||||
|
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
<div class="is-flex">
|
||||||
|
<div class="is-flex-grow-1">
|
||||||
|
<bulma type="FIELD_INPUT" name="title" value="{{@ticket.title}}" class="mr-3"></bulma>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-secondary" href="/ticket/{{ @ticket.id }}">Cancel</a>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-primary" type="submit">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="block">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-two-thirds">
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<bulma type="FIELD_INPUT" label="Created At:" name="created_at" value="{{@ticket.created_at}}">
|
||||||
|
</bulma>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active">
|
||||||
|
<a data-tab="write"><span class="icon is-small">
|
||||||
|
<i class="fas fa-pen" aria-hidden="true"></i></span><span>Write</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a data-tab="preview"><span class="icon is-small">
|
||||||
|
<i class="fas fa-magnifying-glass"
|
||||||
|
aria-hidden="true"></i></span><span>Preview</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-write" class="tab-content block">
|
||||||
|
<bulma type="FIELD_TEXTAREA" name="description" value="{{@ticket.description}}" rows="20"></bulma>
|
||||||
|
</div>
|
||||||
|
<div id="tab-preview" class="tab-content content">
|
||||||
|
<div id="preview-output"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="block">
|
||||||
|
<!-- priority and status -->
|
||||||
|
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id" options="{{@priorities}}"
|
||||||
|
option_value="id" option_name="name" selected="{{@ticket.priority_id}}"></bulma>
|
||||||
|
|
||||||
|
<bulma type="FIELD_SELECT" label="Status:" name="status_id" options="{{@statuses}}"
|
||||||
|
option_value="id" option_name="name" selected="{{@ticket.status_id}}"></bulma>
|
||||||
|
|
||||||
|
<bulma type="FIELD_SELECT" label="Assigned User:" name="assigned_to" options="{{@users}}"
|
||||||
|
option_value="id" option_name="display_name" selected="{{@ticket.assigned_to}}"></bulma>
|
||||||
|
|
||||||
|
<!-- tags - need to implement a bulma-->
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Tags</label>
|
||||||
|
<div class="control">
|
||||||
|
<div class="select is-multiple" style="width:100%;">
|
||||||
|
<select name="tags[]" id="tags" multiple size="8" style="width:100%;">
|
||||||
|
<repeat group="{{ @all_tags }}" value="{{ @tag_option }}">
|
||||||
|
<option value="{{ @tag_option.id}}" {{ in_array(@tag_option.id,
|
||||||
|
@current_ticket_tag_ids) ? 'selected' : '' }}>
|
||||||
|
{{ @tag_option.name }}
|
||||||
|
</option>
|
||||||
|
</repeat>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="help">Hold Ctrl to select multiple tags.</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- additional data -->
|
||||||
|
<div class="block">
|
||||||
|
</div>
|
||||||
|
<!-- meta data -->
|
||||||
|
<div class="block">
|
||||||
|
<table class="table is-bordered is-fullwidth">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="has-width-100">Property</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
|
||||||
|
<check if="{{ @key !== 'description'}}">
|
||||||
|
<tr>
|
||||||
|
<td>{{@key}}</td>
|
||||||
|
<td>{{@value}}</td>
|
||||||
|
</tr>
|
||||||
|
</check>
|
||||||
|
</repeat>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- form to add child ticket relationships -->
|
||||||
|
<div class="box">
|
||||||
|
<h4 class="title is-4">Linked Tickets</h4>
|
||||||
|
<!-- parent -->
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
<check if="{{ @parent_tickets }}">
|
||||||
|
<div class="block">
|
||||||
|
<h4 class="title">Parent Tickets</h4>
|
||||||
|
<ul>
|
||||||
|
<repeat group="{{ @parent_tickets }}" value="{{ @p }}">
|
||||||
|
<li><a href="/ticket/{{ @p.id }}">{{ @p.title }}</a></li>
|
||||||
|
</repeat>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</check>
|
||||||
|
<!-- child tickets -->
|
||||||
|
<check if="{{ @child_tickets }}">
|
||||||
|
<div class="block">
|
||||||
|
<h4 class="title">Child Tickets</h4>
|
||||||
|
<ul>
|
||||||
|
<repeat group="{{ @child_tickets }}" value="{{ @c }}">
|
||||||
|
<li><a href="/ticket/{{ @c.id }}">{{ @c.title }}</a></li>
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<form action="/ticket/{{ @ticket.id }}/add-subtask" method="POST">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Add existing ticket as child ticket (ID):</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="number" placeholder="Child Ticket ID" required
|
||||||
|
name="child_ticket_id">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link" type="submit">Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
*/ ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
<exclude>
|
||||||
|
<include href="views/attachment/index.html"></include>
|
||||||
|
<include href="views/comments/view.html"></include>
|
||||||
|
</exclude>
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div class="block" id="attachments"></div>
|
||||||
|
<div class="block" id="comments"></div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
options="statuses" option_value="id" option_name="name"
|
options="statuses" option_value="id" option_name="name"
|
||||||
selected="{{@ticket.status_id}}"></bulma>
|
selected="{{@ticket.status_id}}"></bulma>
|
||||||
|
|
||||||
<include href="/ui/parts/clipboard.html"></include>
|
<include href="parts/clipboard.html"></include>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-5">Custom Fields</h3>
|
<h3 class="title is-5">Custom Fields</h3>
|
||||||
@ -20,33 +20,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="field has-addons">
|
<icons type="status-selector" selected="{{ @GET.status ?: null }}" path="tickets"></icons>
|
||||||
<!-- TODO: move this into a template -->
|
|
||||||
<repeat group="{{ IconsHelper::$status_icons}}" key="{{ @k }}" value="{{ @icon }}">
|
|
||||||
<p class="control">
|
|
||||||
<a href="{{ @PATH }}/?status={{ @k }}" class="button">
|
|
||||||
<span class="icon is-small"><i class="fas fa-{{ @icon[0] }}"></i></span>
|
|
||||||
<span>{{ IconsHelper::$status_names[@k] }}</span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</repeat>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div id="ticket_list">
|
<div id="ticket_list">
|
||||||
<repeat group="{{@tickets}}" value="{{@ticket}}">
|
<repeat group="{{@tickets}}" value="{{@ticket}}">
|
||||||
<include href="/ui/partials/ticket_item.html"></include>
|
<include href="partials/ticket_item.html"></include>
|
||||||
</repeat>
|
</repeat>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
<exclude>
|
||||||
/*
|
|
||||||
<div id="ticket_list">
|
<div id="ticket_list">
|
||||||
<repeat group="{{@tickets}}" value="{{@ticket}}">
|
<repeat group="{{@tickets}}" value="{{@ticket}}">
|
||||||
<include href="/ui/views/ticket/index_row.html"></include>
|
<include href="views/ticket/index_row.html"></include>
|
||||||
</repeat>
|
</repeat>
|
||||||
</div>
|
</div>
|
||||||
*/
|
</exclude>
|
||||||
?>
|
|
||||||
176
app/ui/views/ticket/view.html
Normal file
176
app/ui/views/ticket/view.html
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<!-- Ticket - View -->
|
||||||
|
<div class="is-flex">
|
||||||
|
<h1 class="title is-flex-grow-1">{{ @ticket.title }}</h1>
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<p class="control"><a class="button" href="/ticket/{{ @ticket.id}}/edit">edit ticket</a></p>
|
||||||
|
<p class="control"><a class="button is-primary" href="/ticket/create">new ticket</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-two-thirds">
|
||||||
|
<!-- content -->
|
||||||
|
<div class="block">
|
||||||
|
<p>{{ @ticket.created_at }}</p>
|
||||||
|
<div class="content">
|
||||||
|
<parsedown>{{ @ticket.description | raw }}</parsedown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<include href="views/attachment/index.html">
|
||||||
|
<include href="views/comments/view.html">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<!-- meta data -->
|
||||||
|
<div class="block">
|
||||||
|
<h6 class="title is-6">Tags
|
||||||
|
<span class="icon"><i class="fas fa-cog"></i></span></h6>
|
||||||
|
<!-- tags -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="tags">
|
||||||
|
<repeat group="{{ @ticket.tags }}" value="{{@tag}}">
|
||||||
|
<span class="tag is-{{@tag.color}}">{{@tag.name}}</span>
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="block">
|
||||||
|
<h6 class="title is-6">Projects
|
||||||
|
<span class="icon"><i class="fas fa-cog"></i></span></h6>
|
||||||
|
<hr>
|
||||||
|
<h6 class="title is-6">Assigned User:</h6>
|
||||||
|
<check if="{{ @assigned_user }}">
|
||||||
|
<div class="block">
|
||||||
|
<p>{{ @assigned_user.display_name ?: @assigned_user.username }}</p>
|
||||||
|
</div>
|
||||||
|
</check>
|
||||||
|
<h6 class="title is-6">Participants</h6>
|
||||||
|
<h6 class="title is-6">Time Tracker?</h6>
|
||||||
|
<h6 class="title is-6">Dependencies</h6>
|
||||||
|
<p>Reference:</p>
|
||||||
|
<a href="#" class="button"><span class="icon">
|
||||||
|
<i class="fas fa-trash"></i></span><span>Delete</span></a>
|
||||||
|
</div>
|
||||||
|
<!-- form to add child ticket relationships -->
|
||||||
|
<div class="box">
|
||||||
|
<h3>Linked Tickets</h3>
|
||||||
|
<!-- parent -->
|
||||||
|
<check if="{{ @parent_tickets }}">
|
||||||
|
<div class="block">
|
||||||
|
<h4 class="title">Parent Tickets</h4>
|
||||||
|
<ul>
|
||||||
|
<repeat group="{{ @parent_tickets }}" value="{{ @p }}">
|
||||||
|
<li><a href="/ticket/{{ @p.id }}">{{ @p.title }}</a></li>
|
||||||
|
</repeat>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</check>
|
||||||
|
<!-- child tickets -->
|
||||||
|
<check if="{{ @child_tickets }}">
|
||||||
|
<div class="block">
|
||||||
|
<h4 class="title">Child Tickets</h4>
|
||||||
|
<ul>
|
||||||
|
<repeat group="{{ @child_tickets }}" value="{{ @c }}">
|
||||||
|
<li><a href="/ticket/{{ @c.id }}">{{ @c.title }}</a></li>
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
|
</check>
|
||||||
|
<form action="/ticket/{{ @ticket.id }}/add-subtask" method="POST">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Add existing ticket as child ticket (ID):</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="number" placeholder="Child Ticket ID" required
|
||||||
|
name="child_ticket_id">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link" type="submit">Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<div class="block" id="ticket-history">
|
||||||
|
<h4 class="title is-4">Ticket History</h4>
|
||||||
|
<check if="{{ !empty(@ticket_history) }}">
|
||||||
|
<div class="list is-hoverable">
|
||||||
|
<repeat group="{{ @ticket_history }}" value="{{ @entry }}">
|
||||||
|
<div class="list-item">
|
||||||
|
<div class="list-item-content">
|
||||||
|
<div class="list-item-title">
|
||||||
|
<small class="has-text-grey">
|
||||||
|
{{ date('Y-m-d H:i:s', strtotime(@entry.changed_at)) }} by {{ @entry.user_display_name }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-description">
|
||||||
|
<!-- creation -->
|
||||||
|
<check if="{{ @entry.field_changed == 'ticket_created' }}">
|
||||||
|
Ticket created: {{ @entry.new_value}}
|
||||||
|
</check>
|
||||||
|
<!-- status -->
|
||||||
|
<check if="{{ @entry.field_changed == 'status_id' }}">
|
||||||
|
Status changed
|
||||||
|
<check if="{{ @entry.old_value !== null }}">
|
||||||
|
from <strong>{{ @map['status'][@entry.old_value] ?? 'Unknown' }}</strong>
|
||||||
|
</check>
|
||||||
|
to <strong>{{ @map['status'][@entry.new_value] ?? 'Unknown' }}</strong>
|
||||||
|
</check>
|
||||||
|
<!-- priority -->
|
||||||
|
<check if="{{ @entry.field_changed == 'priority_id' }}">
|
||||||
|
Priority changed
|
||||||
|
<check if="{{ @entry.old_value !== null }}">
|
||||||
|
from <strong>{{ @map['priorities'][@entry.old_value] ?? 'Unknown'}}</strong>
|
||||||
|
</check>
|
||||||
|
to <strong>{{ @map['priorities'][@entry.new_value] ?? 'Unknown' }}</strong>
|
||||||
|
</check>
|
||||||
|
<!-- assignment -->
|
||||||
|
<check if="{{ @entry.field_changed == 'assigned_to' }}">
|
||||||
|
Assignment changed
|
||||||
|
<check if="{{ @entry.old_value !== null && @entry.old_value != 0 }}">
|
||||||
|
from <strong>{{ @map['users'][@entry.old_value] ?? 'Unassigned' }}</strong>
|
||||||
|
</check>
|
||||||
|
<check if="{{ @entry.old_value === null || @entry.old_value == 0}}">
|
||||||
|
from <strong>Unassigned</strong>
|
||||||
|
</check>
|
||||||
|
to
|
||||||
|
<check if="{{ @entry.new_value !== null && @entry.new_value != 0}}">
|
||||||
|
<strong>{{ @map['users'][@entry.new_value] ?? 'Unassigned' }}</strong>
|
||||||
|
</check>
|
||||||
|
<check if="{{ @entry.new_value === null || @entry.new_value == 0}}">
|
||||||
|
<strong>Unassigned</strong>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
<check if="{{ @entry.field_changed == 'title'}}">
|
||||||
|
Title changed from "{{ @entry.old_value}}" to "{{ @entry.new_value}}".
|
||||||
|
</check>
|
||||||
|
<check if="{{ @entry.field_changed == 'description'}}">
|
||||||
|
Description updated old sha256({{@entry.old_value}}).
|
||||||
|
</check>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
|
<false>
|
||||||
|
<p>No history entries for this ticket.</p>
|
||||||
|
</false>
|
||||||
|
</check>
|
||||||
|
<!--
|
||||||
|
<div class="block" id="attachments"></div>
|
||||||
|
<div class="block" id="comments"></div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</div>
|
||||||
@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
<form method="POST" action="/user/{{@edit_user.id}}/update">
|
<form method="POST" action="/user/{{@edit_user.id}}/update">
|
||||||
|
{{ \CSRFHelper::field() | raw }}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Username</label>
|
<label class="label">Username</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require '../app/debug.php';
|
||||||
|
|
||||||
require '../lib/autoload.php';
|
require '../lib/autoload.php';
|
||||||
|
|
||||||
$f3 = \Base::instance();
|
$f3 = \Base::instance();
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
<div class="block">
|
|
||||||
<style>
|
|
||||||
#upload-area {
|
|
||||||
height: 250px;
|
|
||||||
border: 2px dashed #aaa;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 10px;
|
|
||||||
color: #555;
|
|
||||||
font-family: sans-serif;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#upload-area.hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
#preview {
|
|
||||||
max-width:100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div id="upload-area" contenteditable="true">
|
|
||||||
Paste or drag an image here
|
|
||||||
</div>
|
|
||||||
<img id="preview" alt="image preview" hidden>
|
|
||||||
<p id="status"></p>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const area = document.getElementById('upload-area');
|
|
||||||
const preview = document.getElementById('preview');
|
|
||||||
const status = document.getElementById('status');
|
|
||||||
|
|
||||||
async function uploadImage(file){
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
preview.src = reader.result;
|
|
||||||
preview.hidden = false;
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('attachment', file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch('/ticket/{{@ticket->id}}/attachments/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
const text = await res.text();
|
|
||||||
status.textContent = text;
|
|
||||||
} catch (e) {
|
|
||||||
status.textContent = 'upload failed.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// paste
|
|
||||||
area.addEventListener('paste', (e) => {
|
|
||||||
for(let item of e.clipboardData.items){
|
|
||||||
if(item.type.startsWith('image/')){
|
|
||||||
uploadImage(item.getAsFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// drag and drop
|
|
||||||
area.addEventListener('dragover', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
area.classList.add('hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
area.addEventListener('dragLeave', ()=> {
|
|
||||||
area.classList.remove('hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
area.addEventListener('drop', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
area.classList.remove('hover');
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
for(let file of files){
|
|
||||||
if(file.type.startsWith('image/')){
|
|
||||||
uploadImage(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
<h1 class="title">Create Ticket Priority</h1>
|
|
||||||
<p>TODO:</p>
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<hr>
|
|
||||||
<div class="box" id="comments">
|
|
||||||
<h3 class="title is-3">Comments</h3>
|
|
||||||
<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 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 is-clearfix">
|
|
||||||
<button class="button is-primary is-pulled-right" type="submit">Submit Comment</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<h1 class="title">Dashboard</h1>
|
|
||||||
|
|
||||||
<parsedown href="issues.md"></parsedown>
|
|
||||||
|
|
||||||
<p>109</p>
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
<!-- Ticket - Edit -->
|
|
||||||
<!-- made to look more in line with view-->
|
|
||||||
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
|
||||||
<div class="is-flex">
|
|
||||||
<div class="is-flex-grow-1">
|
|
||||||
<bulma type="FIELD_INPUT" name="title" value="{{@ticket.title}}" class="mr-3"></bulma>
|
|
||||||
</div>
|
|
||||||
<div class="field is-grouped">
|
|
||||||
<!-- <p class="control"><a class="button" href="/ticket/{{ @ticket.id}}/edit">edit ticket</a></p>
|
|
||||||
<p class="control"><a class="button is-primary" href="/ticket/create">new ticket</a></p>-->
|
|
||||||
<div class="control">
|
|
||||||
<a class="button is-secondary" href="/ticket/{{ @ticket.id }}">Cancel</a>
|
|
||||||
</div>
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-primary" type="submit">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<div class="block">
|
|
||||||
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-two-thirds">
|
|
||||||
|
|
||||||
<div class="block">
|
|
||||||
<bulma type="FIELD_INPUT" label="Created At:" name="created_at" value="{{@ticket.created_at}}"></bulma>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tabs">
|
|
||||||
<ul>
|
|
||||||
<li class="is-active">
|
|
||||||
<a data-tab="write"><span class="icon is-small">
|
|
||||||
<i class="fas fa-pen" aria-hidden="true"></i></span><span>Write</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a data-tab="preview"><span class="icon is-small">
|
|
||||||
<i class="fas fa-magnifying-glass" aria-hidden="true"></i></span><span>Preview</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tab-write" class="tab-content block">
|
|
||||||
<bulma type="FIELD_TEXTAREA" name="description" value="{{@ticket.description}}" rows="20"></bulma>
|
|
||||||
</div>
|
|
||||||
<div id="tab-preview" class="tab-content content">
|
|
||||||
<div id="preview-output"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<div class="block">
|
|
||||||
<!-- priority and status -->
|
|
||||||
<bulma type="FIELD_SELECT" label="Priority:" name="priority_id"
|
|
||||||
options="{{@priorities}}" option_value="id" option_name="name"
|
|
||||||
selected="{{@ticket.priority_id}}"></bulma>
|
|
||||||
|
|
||||||
<bulma type="FIELD_SELECT" label="Status:" name="status_id"
|
|
||||||
options="{{@statuses}}" option_value="id" option_name="name"
|
|
||||||
selected="{{@ticket.status_id}}"></bulma>
|
|
||||||
</div>
|
|
||||||
<!-- meta data -->
|
|
||||||
<div class="block">
|
|
||||||
<table class="table is-bordered is-fullwidth">
|
|
||||||
<thead>
|
|
||||||
<tr><th class="has-width-100">Property</th><th>Value</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
|
|
||||||
<check if="{{ @key !== 'description'}}">
|
|
||||||
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
|
|
||||||
</check>
|
|
||||||
</repeat>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!-- form to add child ticket relationships -->
|
|
||||||
<div class="box">
|
|
||||||
<h4 class="title is-4">Linked Tickets</h4>
|
|
||||||
<!-- parent -->
|
|
||||||
<?php
|
|
||||||
/*
|
|
||||||
<check if="{{ @parent_tickets }}">
|
|
||||||
<div class="block">
|
|
||||||
<h4 class="title">Parent Tickets</h4>
|
|
||||||
<ul>
|
|
||||||
<repeat group="{{ @parent_tickets }}" value="{{ @p }}">
|
|
||||||
<li><a href="/ticket/{{ @p.id }}">{{ @p.title }}</a></li>
|
|
||||||
</repeat>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</check>
|
|
||||||
<!-- child tickets -->
|
|
||||||
<check if="{{ @child_tickets }}">
|
|
||||||
<div class="block">
|
|
||||||
<h4 class="title">Child Tickets</h4>
|
|
||||||
<ul>
|
|
||||||
<repeat group="{{ @child_tickets }}" value="{{ @c }}">
|
|
||||||
<li><a href="/ticket/{{ @c.id }}">{{ @c.title }}</a></li>
|
|
||||||
</repeat>
|
|
||||||
</div>
|
|
||||||
</check>
|
|
||||||
|
|
||||||
<form action="/ticket/{{ @ticket.id }}/add-subtask" method="POST">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Add existing ticket as child ticket (ID):</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="number" placeholder="Child Ticket ID" required
|
|
||||||
name="child_ticket_id">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-link" type="submit">Link</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
*/ ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<include href="../ui/views/attachment/index.html">
|
|
||||||
<include href="../ui/views/comments/view.html">
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="block" id="attachments"></div>
|
|
||||||
<div class="block" id="comments"></div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
<!-- Ticket - View -->
|
|
||||||
<div class="is-flex">
|
|
||||||
<h1 class="title is-flex-grow-1">{{ @ticket.title }}</h1>
|
|
||||||
<div class="field is-grouped">
|
|
||||||
<p class="control"><a class="button" href="/ticket/{{ @ticket.id}}/edit">edit ticket</a></p>
|
|
||||||
<p class="control"><a class="button is-primary" href="/ticket/create">new ticket</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-two-thirds">
|
|
||||||
<div class="block">
|
|
||||||
<p>{{ @ticket.created_at }}</p>
|
|
||||||
<div class="content">
|
|
||||||
<parsedown>{{ @ticket.description | raw }}</parsedown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column">
|
|
||||||
<!-- meta data -->
|
|
||||||
<div class="block">
|
|
||||||
<div class="tags">
|
|
||||||
<repeat group="{{ @ticket.tags }}" value="{{@tag}}">
|
|
||||||
<span class="tag is-{{@tag.color}}">{{@tag.name}}</span>
|
|
||||||
</repeat>
|
|
||||||
</div>
|
|
||||||
<table class="table is-bordered is-fullwidth">
|
|
||||||
<thead>
|
|
||||||
<tr><th class="has-width-100">Property</th><th>Value</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
|
|
||||||
<check if="{{ @key !== 'description'}}">
|
|
||||||
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
|
|
||||||
</check>
|
|
||||||
</repeat>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!-- form to add child ticket relationships -->
|
|
||||||
<div class="box">
|
|
||||||
<h3>Linked Tickets</h3>
|
|
||||||
<!-- parent -->
|
|
||||||
<check if="{{ @parent_tickets }}">
|
|
||||||
<div class="block">
|
|
||||||
<h4 class="title">Parent Tickets</h4>
|
|
||||||
<ul>
|
|
||||||
<repeat group="{{ @parent_tickets }}" value="{{ @p }}">
|
|
||||||
<li><a href="/ticket/{{ @p.id }}">{{ @p.title }}</a></li>
|
|
||||||
</repeat>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</check>
|
|
||||||
<!-- child tickets -->
|
|
||||||
<check if="{{ @child_tickets }}">
|
|
||||||
<div class="block">
|
|
||||||
<h4 class="title">Child Tickets</h4>
|
|
||||||
<ul>
|
|
||||||
<repeat group="{{ @child_tickets }}" value="{{ @c }}">
|
|
||||||
<li><a href="/ticket/{{ @c.id }}">{{ @c.title }}</a></li>
|
|
||||||
</repeat>
|
|
||||||
</div>
|
|
||||||
</check>
|
|
||||||
<form action="/ticket/{{ @ticket.id }}/add-subtask" method="POST">
|
|
||||||
<div class="field">
|
|
||||||
<label class="label">Add existing ticket as child ticket (ID):</label>
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" type="number" placeholder="Child Ticket ID" required
|
|
||||||
name="child_ticket_id">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<button class="button is-link" type="submit">Link</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<include href="../ui/views/attachment/index.html">
|
|
||||||
<include href="../ui/views/comments/view.html">
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<div class="block" id="attachments"></div>
|
|
||||||
<div class="block" id="comments"></div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user