Compare commits
10 Commits
1f10cc7f89
...
b2764a0f58
| Author | SHA1 | Date | |
|---|---|---|---|
| b2764a0f58 | |||
| 6bbfdd6017 | |||
| be1341b8ba | |||
| d5487f5212 | |||
| 9dec80096d | |||
| 10bc56bcdf | |||
| 2a711584cd | |||
| a8fe0add5c | |||
| dcddd22a0b | |||
| 6c936208b7 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
/app/.env.cfg
|
/app/.env.cfg
|
||||||
/public/tmp/
|
/public/tmp/
|
||||||
composer.lock
|
composer.lock
|
||||||
|
/storage/
|
||||||
152
app/controllers/AttachmentController.php
Normal file
152
app/controllers/AttachmentController.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class AttachmentController {
|
||||||
|
|
||||||
|
private function check_access($f3){
|
||||||
|
if(!$f3->exists('SESSION.user')){
|
||||||
|
$f3->reroute('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// list attachments
|
||||||
|
public function index($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
// fetch attachments
|
||||||
|
|
||||||
|
$attachments = $db->exec(
|
||||||
|
'SELECT a.*, u.username
|
||||||
|
FROM attachments a
|
||||||
|
LEFT JOIN users u ON u.id = a.uploaded_by
|
||||||
|
WHERE a.ticket_id = ?
|
||||||
|
ORDER BY a.created_at DESC',
|
||||||
|
[$ticket_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
$f3->set('ticket_id', $ticket_id);
|
||||||
|
$f3->set('attachments', $attachments);
|
||||||
|
|
||||||
|
$f3->set('content', '../ui/views/attachment/index.html');
|
||||||
|
// echo \Template::instance()->render('../ui/templates/layout.html');
|
||||||
|
echo \Template::instance()->render($f3->get('content'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle file upload
|
||||||
|
public function upload($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$uploaded_by = $f3->get('SESSION.user.id');
|
||||||
|
|
||||||
|
if(!isset($_FILES['attachment']) || $_FILES['attachment']['error'] !== UPLOAD_ERR_OK){
|
||||||
|
$f3->reroute('/ticket/'.$ticket_id.'/attachments');
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_info = $_FILES['attachment'];
|
||||||
|
$original_name = $file_info['name'];
|
||||||
|
$tmp_path = $file_info['tmp_name'];
|
||||||
|
|
||||||
|
// create a unique file path
|
||||||
|
$upload_dir = '../storage/attachments/tickets/'.$ticket_id.'/';
|
||||||
|
if(!is_dir($upload_dir)){
|
||||||
|
mkdir($upload_dir, 0777, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if file exists increment version
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
$existing = $db->exec(
|
||||||
|
'SELECT * FROM attachments
|
||||||
|
WHERE ticket_id =? AND file_name = ?
|
||||||
|
ORDER BY version_number DESC
|
||||||
|
LIMIT 1',
|
||||||
|
[$ticket_id, $original_name]
|
||||||
|
);
|
||||||
|
|
||||||
|
$new_version = 1;
|
||||||
|
if($existing){
|
||||||
|
$new_version = $existing[0]['version_number'] + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$final_path = $upload_dir.$new_version.'_'.$original_name;
|
||||||
|
|
||||||
|
// move file
|
||||||
|
move_uploaded_file($tmp_path, $final_path);
|
||||||
|
|
||||||
|
// store meta data in DB
|
||||||
|
$db->exec(
|
||||||
|
'INSERT INTO attachments
|
||||||
|
(ticket_id, path, file_name, version_number, uploaded_by, created_at)
|
||||||
|
VALUES (?,?,?,?,?,NOW())',
|
||||||
|
[$ticket_id, $final_path, $original_name, $new_version, $uploaded_by]
|
||||||
|
);
|
||||||
|
|
||||||
|
$f3->reroute('/ticket/'.$ticket_id.'');
|
||||||
|
}
|
||||||
|
|
||||||
|
// download attachment
|
||||||
|
public function download($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$attachment_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
$rows = $db->exec('SELECT * FROM attachments WHERE id = ?', [$attachment_id]);
|
||||||
|
|
||||||
|
if(!$rows){
|
||||||
|
$f3->error(404, "File not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = $rows[0];
|
||||||
|
$file_path = $attachment['path'];
|
||||||
|
$file_name = $attachment['file_name'];
|
||||||
|
|
||||||
|
// validate file exists
|
||||||
|
if(!file_exists($file_path)){
|
||||||
|
$f3->error(404, "File not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// output headers for download
|
||||||
|
header('Content-Description: File Transfer');
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
header('Content-Disposition: attachment; filename="'.basename($file_name).'"');
|
||||||
|
header('Content-Length: '. filesize($file_path));
|
||||||
|
|
||||||
|
// flush headers
|
||||||
|
flush();
|
||||||
|
|
||||||
|
// read file
|
||||||
|
readfile($file_path);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete an attachment
|
||||||
|
public function delete($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$attachment_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$current_user = $f3->get('SESSION.user');
|
||||||
|
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
$rows = $db->exec('SELECT * FROM attachments WHERE id =? LIMIT 1', [$attachment_id]);
|
||||||
|
if(!$rows){
|
||||||
|
$f3->error(404, "Attachment not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = $rows[0];
|
||||||
|
// TODO: role or ownership
|
||||||
|
|
||||||
|
if(file_exists($attachment['path'])){
|
||||||
|
unlink($attachment['path']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove DB row
|
||||||
|
$db->exec('DELETE FROM attachments WHERE id =?', [$attachment_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ class AuthController {
|
|||||||
|
|
||||||
|
|
||||||
public function showLoginForm($f3){
|
public function showLoginForm($f3){
|
||||||
|
|
||||||
// store session errors or messages, then clear
|
// store session errors or messages, then clear
|
||||||
$f3->set('error', $f3->get('SESSION.login_error'));
|
$f3->set('error', $f3->get('SESSION.login_error'));
|
||||||
$f3->clear('SESSION.login_error');
|
$f3->clear('SESSION.login_error');
|
||||||
@ -21,7 +22,11 @@ class AuthController {
|
|||||||
$db = $f3->get('DB');
|
$db = $f3->get('DB');
|
||||||
// query for user
|
// query for user
|
||||||
$result = $db->exec(
|
$result = $db->exec(
|
||||||
'SELECT id, username, password, role FROM users WHERE username =? LIMIT 1', $username
|
'SELECT u.id, u.username, u.password, u.role, r.role as role_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN roles r ON r.id = u.role
|
||||||
|
WHERE username =?
|
||||||
|
LIMIT 1', $username
|
||||||
);
|
);
|
||||||
|
|
||||||
// verifiy password
|
// verifiy password
|
||||||
@ -31,7 +36,9 @@ class AuthController {
|
|||||||
// valid
|
// valid
|
||||||
$f3->set('SESSION.user', [
|
$f3->set('SESSION.user', [
|
||||||
'id'=> $user['id'],
|
'id'=> $user['id'],
|
||||||
'username' => $user['username']
|
'username' => $user['username'],
|
||||||
|
'role' => $user['role'],
|
||||||
|
'role_name' => $user['role_name']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$f3->reroute('/dashboard');
|
$f3->reroute('/dashboard');
|
||||||
|
|||||||
91
app/controllers/CommentController.php
Normal file
91
app/controllers/CommentController.php
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class CommentController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new comment to a ticket.
|
||||||
|
* Expects POST data: comment (text)
|
||||||
|
* Route: POST /ticket/@id/comment
|
||||||
|
*/
|
||||||
|
public function create($f3){
|
||||||
|
// check logged in
|
||||||
|
if(!$f3->exists('SESSION.user')){
|
||||||
|
$f3->reroute('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$comment_text = $f3->get('POST.comment');
|
||||||
|
$current_user_id = $f3->get('SESSION.user.id');
|
||||||
|
|
||||||
|
if(empty($comment_text)){
|
||||||
|
$f3->set('SESSION.error', 'ticket not updated. No content');
|
||||||
|
$f3->reroute('/ticket/' . $ticket_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert comment
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
$db->exec(
|
||||||
|
'INSERT INTO ticket_comments (ticket_id, comment, created_by, created_at)
|
||||||
|
VALUES (?, ?, ?, NOW())',
|
||||||
|
[$ticket_id, $comment_text, $current_user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
$f3->reroute('/ticket/' . $ticket_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an existing comment
|
||||||
|
* Route: GET /tickey/@id/comment/@comment_id/delete
|
||||||
|
*/
|
||||||
|
public function delete($f3){
|
||||||
|
if(!$f3->exists('SESSION.user')){
|
||||||
|
$f3->reroute('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$comment_id = (int) $f3->get('PARAMS.comment_id');
|
||||||
|
$current_user = $f3->get('SESSION.user');
|
||||||
|
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
//optional: check if user is allowed to delete comment.
|
||||||
|
// fetch who created the comment
|
||||||
|
$comment_row = $db->exec(
|
||||||
|
'SELECT created_by FROM ticket_comments WHERE id = ? AND ticket_id = ? LIMIT 1',
|
||||||
|
[$comment_id, $ticket_id]
|
||||||
|
);
|
||||||
|
if(!$comment_row){
|
||||||
|
$f3->set('SESSION.error', 'Error: Ticket comment ID not found.');
|
||||||
|
$f3->reroute('/ticket/'.$ticket_id);
|
||||||
|
}
|
||||||
|
$comment_owner = $comment_row[0]['created_by'];
|
||||||
|
// TODO: $is_admin = ()
|
||||||
|
if($current_user['id'] !== $comment_owner){
|
||||||
|
// no permission
|
||||||
|
$f3->set('SESSION.error', 'You do not have permission to delete this ticket');
|
||||||
|
$f3->reroute('/ticket/'. $ticket_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete - addition, rather than delete, we set a delete flag
|
||||||
|
$db->exec('UPDATE ticket_comments SET deleted = 1 WHERE id = ?', [$comment_id]);
|
||||||
|
$f3->reroute('/ticket/' . $ticket_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// view comments
|
||||||
|
public function index($f3){
|
||||||
|
$ticket_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
$results = $db->exec('
|
||||||
|
SELECT c.*, u.username AS author_name
|
||||||
|
FROM ticket_comments c
|
||||||
|
LEFT JOIN users u ON c.created_by = u.id
|
||||||
|
WHERE c.ticket_id = ?
|
||||||
|
ORDER BY c.created_at DESC',
|
||||||
|
[$ticket_id]
|
||||||
|
);
|
||||||
|
$comments = $results;
|
||||||
|
$f3->set('comments', $comments);
|
||||||
|
|
||||||
|
echo \Template::instance()->render('../ui/views/comments/view.html');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -154,6 +154,7 @@ class KBController {
|
|||||||
$f3->set('article_tag_ids', $article_tag_ids);
|
$f3->set('article_tag_ids', $article_tag_ids);
|
||||||
|
|
||||||
// render
|
// render
|
||||||
|
$f3->set('js', 'kb_edit.js');
|
||||||
$f3->set('content', '../ui/views/kb/edit.html');
|
$f3->set('content', '../ui/views/kb/edit.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('../ui/templates/layout.html');
|
||||||
$f3->clear('SESSION.error');
|
$f3->clear('SESSION.error');
|
||||||
|
|||||||
12
app/controllers/ParsedownPreview.php
Normal file
12
app/controllers/ParsedownPreview.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ParsedownPreview {
|
||||||
|
|
||||||
|
public function view($f3){
|
||||||
|
|
||||||
|
$preview_text = $f3->get('POST.content');
|
||||||
|
echo Parsedown::instance()->text($preview_text);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,13 +2,6 @@
|
|||||||
|
|
||||||
class TicketController {
|
class TicketController {
|
||||||
|
|
||||||
protected function check_access($f3){
|
|
||||||
if(!$f3->exists('SESSION.user')){
|
|
||||||
// $f3->set('SESSION.error', 'You don\'t have permission for this ticket.');
|
|
||||||
$f3->reroute('/login');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// list all tickts
|
// list all tickts
|
||||||
public function index($f3){
|
public function index($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
@ -35,24 +28,13 @@ class TicketController {
|
|||||||
$ticket_id = $f3->get('PARAMS.id');
|
$ticket_id = $f3->get('PARAMS.id');
|
||||||
$db = $f3->get('DB');
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
$result = $db->exec(
|
$this->get_ticket($f3, $db, $ticket_id);
|
||||||
'SELECT t.*, u.username as created_by_name
|
$this->get_child_tickets($f3, $db, $ticket_id);
|
||||||
FROM tickets t
|
$this->get_parent_tickets($f3, $db, $ticket_id);
|
||||||
LEFT JOIN users u ON t.created_by = u.id
|
$this->get_custom_fields($f3, $db, $ticket_id);
|
||||||
WHERE t.id =? LIMIT 1',
|
|
||||||
[$ticket_id]
|
|
||||||
);
|
|
||||||
|
|
||||||
if(!$result){
|
|
||||||
// no record
|
|
||||||
$f3->set('SESSION.error', 'Ticket not found.');
|
|
||||||
$f3->reroute('/tickets');
|
|
||||||
}
|
|
||||||
|
|
||||||
$ticket = $result[0];
|
|
||||||
$f3->set('ticket', $ticket);
|
|
||||||
|
|
||||||
// render
|
// render
|
||||||
|
$f3->set('js', 'ticket_view.js');
|
||||||
$f3->set('content', '../ui/views/ticket/view.html');
|
$f3->set('content', '../ui/views/ticket/view.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('../ui/templates/layout.html');
|
||||||
|
|
||||||
@ -66,6 +48,7 @@ class TicketController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle POST
|
// handle POST
|
||||||
|
// including custom forms
|
||||||
public function create($f3){
|
public function create($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
|
||||||
@ -84,39 +67,31 @@ class TicketController {
|
|||||||
[$title, $description, $priority, $status, $created_by]
|
[$title, $description, $priority, $status, $created_by]
|
||||||
);
|
);
|
||||||
|
|
||||||
$f3->reroute('/tickets');
|
$ticket_id = $db->lastInserId();
|
||||||
|
|
||||||
|
// custom fields
|
||||||
|
$meta_keys = $f3->get('POST.meta_key'); // eg ['department', 'category']
|
||||||
|
$meta_values = $f3->get('POST.meta_value');
|
||||||
|
|
||||||
|
if(is_array($meta_keys) && is_array($meta_values)){
|
||||||
|
foreach($meta_keys as $index => $key){
|
||||||
|
$val = $meta_values[$index] ?? '';
|
||||||
|
if(!empty($key) && $val !== ''){
|
||||||
|
$db->exec(
|
||||||
|
'INSERT INTO ticket_meta (ticket_id, meta_key, meta_value)
|
||||||
|
VALUES (?,?,?)',
|
||||||
|
[$ticket_id, $key, $val]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function get_ticket_check_edit_permission($f3){
|
// reroute to ticket
|
||||||
|
$f3->reroute('/ticket/'.$ticket_id);
|
||||||
$db = $f3->get('DB');
|
|
||||||
|
|
||||||
$ticket_id = $f3->get('PARAMS.id');
|
|
||||||
$result = $db->exec('SELECT * FROM tickets WHERE id = ? LIMIT 1', [$ticket_id]);
|
|
||||||
|
|
||||||
if(!$result){
|
|
||||||
$f3->set('SESSION.error', 'Ticket not found.');
|
|
||||||
$f3->reroute('/tickets');
|
|
||||||
}
|
|
||||||
|
|
||||||
$ticket = $result[0];
|
|
||||||
|
|
||||||
// TODO: refine
|
|
||||||
$current_user = $f3->get('SESSION.user');
|
|
||||||
$is_admin = (isset($current_user['role']) && $current_user['role'] == 'admin');
|
|
||||||
$is_assigned = ($ticket['assigned_to'] == $current_user['id']);
|
|
||||||
|
|
||||||
if(!$is_admin && !$is_assigned){ // should this be ||
|
|
||||||
// if not assigned and not admin, disallow edit
|
|
||||||
$f3->set('SESSION.error', 'You do not have permission to edit this ticket.');
|
|
||||||
$f3->reroute('/tickets');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $ticket;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// show edit form
|
// show edit form
|
||||||
|
// including custom forms
|
||||||
public function editForm($f3){
|
public function editForm($f3){
|
||||||
$this->check_access($f3);
|
$this->check_access($f3);
|
||||||
|
|
||||||
@ -127,6 +102,15 @@ class TicketController {
|
|||||||
$ticket = $this->get_ticket_check_edit_permission($f3);
|
$ticket = $this->get_ticket_check_edit_permission($f3);
|
||||||
$f3->set('ticket', $ticket);
|
$f3->set('ticket', $ticket);
|
||||||
|
|
||||||
|
// fetch custom fields
|
||||||
|
$meta = $db->exec(
|
||||||
|
'SELECT id, meta_key, meta_value
|
||||||
|
FROM ticket_meta
|
||||||
|
WHERE ticket_id = ?',
|
||||||
|
[$ticket_id]
|
||||||
|
);
|
||||||
|
$f3->set('ticket_meta', $meta);
|
||||||
|
|
||||||
$f3->set('ticket', $ticket);
|
$f3->set('ticket', $ticket);
|
||||||
$f3->set('content', '../ui/views/ticket/edit.html');
|
$f3->set('content', '../ui/views/ticket/edit.html');
|
||||||
echo \Template::instance()->render('../ui/templates/layout.html');
|
echo \Template::instance()->render('../ui/templates/layout.html');
|
||||||
@ -155,7 +139,163 @@ class TicketController {
|
|||||||
[$title, $description, $priority, $status, $updated_by, 'NOW()', $ticket_id]
|
[$title, $description, $priority, $status, $updated_by, 'NOW()', $ticket_id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// handle custom fields
|
||||||
|
// 1. update existing custom fields
|
||||||
|
$meta_ids = $f3->get('POST.meta_id');
|
||||||
|
$meta_keys = $f3->get('POST.meta_key');
|
||||||
|
$meta_values = $f3->get('POST.meta_value');
|
||||||
|
|
||||||
|
if(is_array($meta_ids) && is_array($meta_keys) && is_array($meta_values)){
|
||||||
|
foreach($meta_ids as $idx => $m_id){
|
||||||
|
$m_key = $meta_keys[$idx] ?? '';
|
||||||
|
$m_val = $meta_values[$idx] ?? '';
|
||||||
|
|
||||||
|
if(!empty($m_key) && $m_val !== ''){
|
||||||
|
$db->exec(
|
||||||
|
'UPDATE ticket_meta
|
||||||
|
SET meta_key = ?, meta_value=?
|
||||||
|
WHERE id = ? AND ticket_id = ?',
|
||||||
|
[$m_key, $m_val, $m_id, $ticket_id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// delete if the user cleared it
|
||||||
|
$db->exec(
|
||||||
|
'DELETE FROM ticket_meta
|
||||||
|
WHERE id =? AND ticket_id = ?',
|
||||||
|
[$m_id, $ticket_id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. insert new fields
|
||||||
|
$new_keys = $f3->get('POST.new_meta_key');
|
||||||
|
$new_values = $f3->get('POST.new_meta_value');
|
||||||
|
if(is_array($new_keys) && is_array($new_values)){
|
||||||
|
foreach($new_keys as $idx => $n_key){
|
||||||
|
$n_val = $new_values[$idx] ?? '';
|
||||||
|
if(!empty($n_key) && $n_val !== ''){
|
||||||
|
$db->exec(
|
||||||
|
'INSERT INTO ticket_meta (ticket_id, meta_key, meta_value)
|
||||||
|
VALUES (?,?,?)',
|
||||||
|
[$ticket_id, $n_key, $n_val]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$f3->reroute('/ticket/' . $ticket_id);
|
$f3->reroute('/ticket/' . $ticket_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subtask
|
||||||
|
public function addSubtask($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$parent_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$child_id = (int) $f3->get('POST.child_ticket_id');
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
// TODO check that both parent and child tickets exist
|
||||||
|
// ensure you don't link a ticket to itself, etc.
|
||||||
|
|
||||||
|
// insert or ignore
|
||||||
|
$db->exec(
|
||||||
|
'INSERT IGNORE INTO ticket_relations (parent_ticket_id, child_ticket_id)
|
||||||
|
VALUES (?, ?)',
|
||||||
|
[$parent_id, $child_id]
|
||||||
|
);
|
||||||
|
$f3->reroute('/ticket/' . $parent_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function check_access($f3){
|
||||||
|
if(!$f3->exists('SESSION.user')){
|
||||||
|
// $f3->set('SESSION.error', 'You don\'t have permission for this ticket.');
|
||||||
|
$f3->reroute('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_ticket_check_edit_permission($f3){
|
||||||
|
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
$ticket_id = $f3->get('PARAMS.id');
|
||||||
|
$result = $db->exec('SELECT * FROM tickets WHERE id = ? LIMIT 1', [$ticket_id]);
|
||||||
|
|
||||||
|
if(!$result){
|
||||||
|
$f3->set('SESSION.error', 'Ticket not found.');
|
||||||
|
$f3->reroute('/tickets');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket = $result[0];
|
||||||
|
|
||||||
|
// TODO: refine
|
||||||
|
$current_user = $f3->get('SESSION.user');
|
||||||
|
$is_admin = (isset($current_user['role_name']) && $current_user['role_name'] == 'admin');
|
||||||
|
$is_assigned = ($ticket['assigned_to'] == $current_user['id']);
|
||||||
|
|
||||||
|
if(!$is_admin && !$is_assigned){ // should this be ||
|
||||||
|
// if not assigned and not admin, disallow edit
|
||||||
|
$f3->set('SESSION.error', 'You do not have permission to edit this ticket.');
|
||||||
|
$f3->reroute('/tickets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ticket;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_ticket($f3, $db, $ticket_id){
|
||||||
|
$result = $db->exec(
|
||||||
|
'SELECT t.*, u.username as created_by_name
|
||||||
|
FROM tickets t
|
||||||
|
LEFT JOIN users u ON t.created_by = u.id
|
||||||
|
WHERE t.id =? LIMIT 1',
|
||||||
|
[$ticket_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if(!$result){
|
||||||
|
// no record
|
||||||
|
$f3->set('SESSION.error', 'Ticket not found.');
|
||||||
|
$f3->reroute('/tickets');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket = $result[0];
|
||||||
|
$f3->set('ticket', $ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_child_tickets($f3, $db, $ticket_id){
|
||||||
|
$child_tickets = $db->exec(
|
||||||
|
'SELECT c.*
|
||||||
|
FROM ticket_relations r
|
||||||
|
INNER JOIN tickets c ON r.child_ticket_id = c.id
|
||||||
|
WHERE r.parent_ticket_id = ?',
|
||||||
|
[$ticket_id]);
|
||||||
|
$f3->set('child_tickets', $child_tickets);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_parent_tickets($f3, $db, $ticket_id){
|
||||||
|
$parent_tickets = $db->exec(
|
||||||
|
'SELECT p.*
|
||||||
|
FROM ticket_relations r
|
||||||
|
INNER JOIN tickets p ON r.parent_ticket_id = p.id
|
||||||
|
WHERE r.child_ticket_id = ?',
|
||||||
|
[$ticket_id]
|
||||||
|
);
|
||||||
|
$f3->set('parent_tickets', $parent_tickets);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function get_custom_fields($f3, $db, $ticket_id){
|
||||||
|
$meta = $db->exec(
|
||||||
|
'SELECT meta_key, meta_value
|
||||||
|
FROM ticket_meta
|
||||||
|
WHERE ticket_id = ?',
|
||||||
|
[$ticket_id]
|
||||||
|
);
|
||||||
|
// convert meta rows into assoc array
|
||||||
|
$meta_array = [];
|
||||||
|
foreach($meta as $m){
|
||||||
|
$meta_array[$m['meta_key']] = $m['meta_value'];
|
||||||
|
}
|
||||||
|
$f3->set('ticket_meta', $meta_array);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
62
app/controllers/UserController.php
Normal file
62
app/controllers/UserController.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class UserController {
|
||||||
|
|
||||||
|
// list all users (admin only)
|
||||||
|
|
||||||
|
protected function check_access($f3){
|
||||||
|
$current_user = $f3->get('SESSION.user');
|
||||||
|
if(!$current_user || $current_user['role_name'] !== 'admin'){
|
||||||
|
$f3->reroute('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index($f3){
|
||||||
|
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
$users = $db->exec(
|
||||||
|
'SELECT u.*, r.role AS role_name
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN roles r ON r.id = u.role
|
||||||
|
ORDER BY id ASC'
|
||||||
|
);
|
||||||
|
$f3->set('users', $users);
|
||||||
|
|
||||||
|
$f3->set('content', '../ui/views/user/index.html');
|
||||||
|
echo \Template::instance()->render('../ui/templates/layout.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editForm($f3){
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$user_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
|
$rows = $db->exec(
|
||||||
|
'SELECt * FROM users WHERE id = ? LIMIT 1',
|
||||||
|
[$user_id]
|
||||||
|
);
|
||||||
|
if(!$rows){
|
||||||
|
$f3->reroute('/users');
|
||||||
|
}
|
||||||
|
$f3->set('edit_user', $rows[0]);
|
||||||
|
$f3->set('content', '../ui/views/user/edit.html');
|
||||||
|
echo \Template::instance()->render('../ui/templates/layout.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update($f3){
|
||||||
|
|
||||||
|
$this->check_access($f3);
|
||||||
|
|
||||||
|
$user_id = (int) $f3->get('PARAMS.id');
|
||||||
|
$new_username = $f3->get('POST.username');
|
||||||
|
// $new_role = $f3->get('POST.role_name')
|
||||||
|
$db = $f3->get('DB');
|
||||||
|
$db->exec(
|
||||||
|
'UPDATE users SET username = ? WHERE id =? LIMIT 1',
|
||||||
|
[$new_username, $user_id]);
|
||||||
|
$f3->reroute('/users');
|
||||||
|
}
|
||||||
|
}
|
||||||
108
app/extensions/BulmaFormHelper.php
Normal file
108
app/extensions/BulmaFormHelper.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class BulmaFormHelper extends \Prefab {
|
||||||
|
|
||||||
|
const H_FIELD_INPUT = 1;
|
||||||
|
const H_FIELD_TEXTAREA = 2;
|
||||||
|
const H_FIELD_SELECT = 3;
|
||||||
|
|
||||||
|
static public function render($args) {
|
||||||
|
|
||||||
|
$type = strtoupper($args['@attrib']['type']);
|
||||||
|
// all *
|
||||||
|
$label = $args['@attrib']['label'];
|
||||||
|
$name = $args['@attrib']['name'];
|
||||||
|
$value = isset($args['@attrib']['value']) ? $args['@attrib']['value'] : '';
|
||||||
|
// select
|
||||||
|
$options = isset($args['@attrib']['options']) ? $args['@attrib']['options'] : '';
|
||||||
|
$selected = isset($args['@attrib']['selected']) ? $args['@attrib']['selected'] : '';
|
||||||
|
//
|
||||||
|
$label = \Template::instance()->build($label);
|
||||||
|
$name = \Template::instance()->build($name);
|
||||||
|
$value = \Template::instance()->build($value);
|
||||||
|
|
||||||
|
if(defined("BulmaFormHelper::$type")){
|
||||||
|
|
||||||
|
$type_const = constant("BulmaFormHelper::$type");
|
||||||
|
|
||||||
|
switch( $type_const ){
|
||||||
|
case BulmaFormHelper::H_FIELD_INPUT:
|
||||||
|
return BulmaFormHelper::build_h_field_input($label, $name, $value);
|
||||||
|
break;
|
||||||
|
case BulmaFormHelper::H_FIELD_TEXTAREA:
|
||||||
|
return BulmaFormHelper::build_h_field_textarea($label, $name, $value);
|
||||||
|
break;
|
||||||
|
case BulmaFormHelper::H_FIELD_SELECT:
|
||||||
|
return BulmaFormHelper::build_h_field_select($label, $name, $options, $selected);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return '<div class="notification is-danger">Error: Bulma CSS Form TYPE ('.$type.') not defined.</div>';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return '<div class="notification is-danger">Error: Bulma CSS Form TYPE not defined.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static function build_h_field_input($label, $name, $value){
|
||||||
|
$string = '
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label">'.$label.'</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" type="text" id="'.$name.'" name="'.
|
||||||
|
$name.'" value="'.
|
||||||
|
$value.'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function build_h_field_select($label, $name, $options, $selected){
|
||||||
|
$string =
|
||||||
|
'<div class="field is-horizontal">
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label">'.$label.'</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="select">
|
||||||
|
$options
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function build_h_field_textarea($label, $name, $value){
|
||||||
|
$string = '
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label">'.$label.'</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<textarea class="textarea" id="'.$name.'" name="'.
|
||||||
|
$name.'">'.$value.'</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
';
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
\Template::instance()->extend('bulma', 'BulmaFormHelper::render');
|
||||||
20
app/extensions/ParsedownHelper.php
Normal file
20
app/extensions/ParsedownHelper.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ParsedownHelper extends \Prefab {
|
||||||
|
|
||||||
|
static public function render($args) {
|
||||||
|
$content = $args[0];
|
||||||
|
$content_token = \Template::instance()->token($content);
|
||||||
|
|
||||||
|
return '
|
||||||
|
<parsedown_rendered class="content">
|
||||||
|
<?php echo \ParsedownHelper::instance()->build('.$content_token.'); ?>
|
||||||
|
</parsedown_rendered>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function build($content){
|
||||||
|
return Parsedown::instance()->text($content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\Template::instance()->extend('parsedown', 'ParsedownHelper::render');
|
||||||
@ -8,7 +8,7 @@ class BulmaForm {
|
|||||||
$string = '
|
$string = '
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label>%label%</label>
|
<label class="label">%label%</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@ -31,7 +31,7 @@ class BulmaForm {
|
|||||||
$string = '
|
$string = '
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label>%label%</label>
|
<label class="label">%label%</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@ -54,7 +54,7 @@ class BulmaForm {
|
|||||||
$string = '
|
$string = '
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
<div class="field-label is-normal">
|
<div class="field-label is-normal">
|
||||||
<label>%label%</label>
|
<label class="label">%label%</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@ -6,13 +6,16 @@ $f3 = \Base::instance();
|
|||||||
/**
|
/**
|
||||||
* Not required yet
|
* Not required yet
|
||||||
*/
|
*/
|
||||||
// $htmlpurifier = new \HTMLPurifier();
|
$htmlpurifier = \HTMLPurifier::instance();
|
||||||
// $htmlpurifier->purify($input);
|
// $htmlpurifier->purify($input);
|
||||||
$md = \Parsedown::instance();
|
$md = \Parsedown::instance();
|
||||||
|
$md->setSafeMode(true);
|
||||||
|
|
||||||
|
|
||||||
$f3->set('DEBUG', 3); // development debug
|
|
||||||
$f3->config('../app/.env.cfg');
|
$f3->config('../app/.env.cfg');
|
||||||
|
$f3->set('DEBUG', 3); // development debug
|
||||||
|
$f3->set('CACHE', FALSE);
|
||||||
|
|
||||||
|
$f3->set('EXT', [new ParsedownHelper, new BulmaFormHelper]);
|
||||||
|
|
||||||
$f3->set('DB', new \DB\SQL(
|
$f3->set('DB', new \DB\SQL(
|
||||||
'mysql:host=localhost;port=3306;dbname=' . $f3->get('database.db_name'),
|
'mysql:host=localhost;port=3306;dbname=' . $f3->get('database.db_name'),
|
||||||
@ -49,6 +52,18 @@ $f3->route('GET /ticket/create', 'TicketController->createForm'); // show form t
|
|||||||
$f3->route('POST /ticket/create', 'TicketController->create'); // save
|
$f3->route('POST /ticket/create', 'TicketController->create'); // save
|
||||||
$f3->route('GET /ticket/@id/edit', 'TicketController->editForm'); // edit ticket
|
$f3->route('GET /ticket/@id/edit', 'TicketController->editForm'); // edit ticket
|
||||||
$f3->route('POST /ticket/@id/update', 'TicketController->update'); //
|
$f3->route('POST /ticket/@id/update', 'TicketController->update'); //
|
||||||
|
// additional routes - comments
|
||||||
|
$f3->route('POST /ticket/@id/comment', 'CommentController->create');
|
||||||
|
$f3->route('GET /ticket/@id/comment/@comment_id/delete', 'CommentController->delete');
|
||||||
|
$f3->route('GET /ticket/@id/comments', 'CommentController->index');
|
||||||
|
// route for linking a child to a parent
|
||||||
|
$f3->route('POST /ticket/@id/add-subtask', 'TicketController->addSubtask');
|
||||||
|
|
||||||
|
// attachments
|
||||||
|
$f3->route('GET /ticket/@id/attachments', 'AttachmentController->index');
|
||||||
|
$f3->route('POST /ticket/@id/attachments/upload', 'AttachmentController->upload');
|
||||||
|
$f3->route('GET /attachment/@id/download', 'AttachmentController->download');
|
||||||
|
$f3->route('GET /attachment/@id/delete', 'AttachmentController->delete');
|
||||||
|
|
||||||
// knowledgebase
|
// knowledgebase
|
||||||
$f3->route('GET /kb', 'KBController->index');
|
$f3->route('GET /kb', 'KBController->index');
|
||||||
@ -63,8 +78,16 @@ $f3->route('GET /tags', 'TagController->index');
|
|||||||
$f3->route('GET /tag/create', 'TagController->createForm');
|
$f3->route('GET /tag/create', 'TagController->createForm');
|
||||||
$f3->route('POST /tag/create', 'TagController->create');
|
$f3->route('POST /tag/create', 'TagController->create');
|
||||||
|
|
||||||
|
// parsedown preview
|
||||||
|
$f3->route('POST /parsedown/preview', 'ParsedownPreview->view');
|
||||||
|
|
||||||
// dashboard
|
// dashboard
|
||||||
$f3->route('GET /dashboard', 'DashboardController->index');
|
$f3->route('GET /dashboard', 'DashboardController->index');
|
||||||
|
|
||||||
|
|
||||||
|
// additional routes
|
||||||
|
$f3->route('GET /users', 'UserController->index');
|
||||||
|
$f3->route('GET /user/@id/edit', 'UserController->editForm');
|
||||||
|
$f3->route('POST /user/@id/update', 'UserController->update');
|
||||||
|
|
||||||
$f3->run();
|
$f3->run();
|
||||||
75
public/js/kb_edit.js
Normal file
75
public/js/kb_edit.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// switch to target tab pane
|
||||||
|
function switchTab(targetId){
|
||||||
|
var panes = document.querySelectorAll('.tab-content .tab-pane');
|
||||||
|
for (var i=0; i< panes.length; i++){
|
||||||
|
panes[i].style.display = 'none';
|
||||||
|
}
|
||||||
|
var targetPane = document.getElementById(targetId);
|
||||||
|
if(targetPane){
|
||||||
|
targetPane.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send ajax post request with content to specified url
|
||||||
|
function ajaxPost(content, url, callback){
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
xhr.onreadystatechange = function(){
|
||||||
|
if(xhr.readyState === XMLHttpRequest.DONE){
|
||||||
|
if(xhr.status === 200){
|
||||||
|
callback(xhr.responseText);
|
||||||
|
} else {
|
||||||
|
console.error("AJAX error: " + xhr.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var params = 'content=' + encodeURIComponent(content);
|
||||||
|
xhr.send(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load preview via ajax into preview element
|
||||||
|
function loadPreview(previewElement){
|
||||||
|
var sourceId = previewElement.getAttribute('data-source');
|
||||||
|
var handlerUrl = previewElement.getAttribute('data-handler');
|
||||||
|
var method = previewElement.getAttribute('data-method');
|
||||||
|
var sourceElement = document.getElementById(sourceId);
|
||||||
|
if(sourceElement){
|
||||||
|
var content = sourceElement.value;
|
||||||
|
if(method && method.toLowerCase() == 'post'){
|
||||||
|
ajaxPost(content, handlerUrl, function (response){
|
||||||
|
previewElement.innerHTML = response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialise tab links to handle tab switching
|
||||||
|
function initTabs(){
|
||||||
|
var tabLinks = document.querySelectorAll('.tabs a[data-target]');
|
||||||
|
for(var i=0; i<tabLinks.length; i++){
|
||||||
|
tabLinks[i].addEventListener('click', function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
var target = this.getAttribute('data-target');
|
||||||
|
switchTab(target);
|
||||||
|
|
||||||
|
// if the new tab has a preview element, load the preview
|
||||||
|
var pane = document.getElementById(target);
|
||||||
|
if(pane){
|
||||||
|
var previewElement = pane.querySelector('.preview');
|
||||||
|
if(previewElement){
|
||||||
|
console.log('pane has preview el')
|
||||||
|
loadPreview(previewElement);
|
||||||
|
} else {
|
||||||
|
console.log('pane doesnt have preview el');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function(){
|
||||||
|
initTabs();
|
||||||
|
});
|
||||||
30
public/js/ticket_view.js
Normal file
30
public/js/ticket_view.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function(){
|
||||||
|
const ticket_id = window.location.pathname.split('/')[2];
|
||||||
|
const comments_url = `/ticket/${ticket_id}/comments`;
|
||||||
|
const attachments_url = `/ticket/${ticket_id}/attachments`;
|
||||||
|
|
||||||
|
function ajax(url, containerID){
|
||||||
|
fetch(url)
|
||||||
|
.then(response => {
|
||||||
|
if(!response.ok){
|
||||||
|
throw new Error('Network response was not ok.');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(html => {
|
||||||
|
const container_el = document.getElementById(containerID);
|
||||||
|
if(container_el){
|
||||||
|
container_el.innerHTML += html;
|
||||||
|
} else {
|
||||||
|
throw new Error('Coments container does not exist');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log('Error fetching comments', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ajax(attachments_url, 'attachments')
|
||||||
|
ajax(comments_url, 'comments')
|
||||||
|
});
|
||||||
|
|
||||||
119
public/style.css
119
public/style.css
@ -4,3 +4,122 @@ html, body, #sidebar, #page,#base_body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#page { min-height: calc(100vh - 170px - 52px) }
|
#page { min-height: calc(100vh - 170px - 52px) }
|
||||||
|
|
||||||
|
.table th.th-icon { width: 2rem; }
|
||||||
|
|
||||||
|
/* List Component */
|
||||||
|
.list{
|
||||||
|
--be-list-color:var(--bulma-text);
|
||||||
|
--be-list-item-description-color:var(--bulma-text-50);
|
||||||
|
--be-list-item-divider-color:var(--bulma-border);
|
||||||
|
--be-list-item-hover-color:var(--bulma-scheme-main-bis);
|
||||||
|
--be-list-item-image-margin:.75em;
|
||||||
|
--be-list-item-padding:.75em;
|
||||||
|
--be-list-item-title-color:var(--bulma-text-strong);
|
||||||
|
--be-list-item-title-weight:var(--bulma-weight-semibold);
|
||||||
|
color:var(--be-list-color);
|
||||||
|
flex-direction:column;
|
||||||
|
display:flex
|
||||||
|
}
|
||||||
|
.list.has-hidden-images .list-item-image{
|
||||||
|
display:none
|
||||||
|
}
|
||||||
|
.list.has-hoverable-list-items .list-item:hover{
|
||||||
|
background-color:var(--be-list-item-hover-color)
|
||||||
|
}
|
||||||
|
.list.has-overflow-ellipsis .list-item-content{
|
||||||
|
min-inline-size:0;
|
||||||
|
max-inline-size:calc(var(--length)*1ch)
|
||||||
|
}
|
||||||
|
.list.has-overflow-ellipsis .list-item-content>*{
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
white-space:nowrap;
|
||||||
|
overflow:hidden
|
||||||
|
}
|
||||||
|
@media (hover:hover){
|
||||||
|
.list:not(.has-visible-pointer-controls) .list-item-controls{
|
||||||
|
opacity:0;
|
||||||
|
visibility:hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list .list-item{
|
||||||
|
align-items:center;
|
||||||
|
transition:background-color .125s ease-out;
|
||||||
|
display:flex;
|
||||||
|
position:relative;
|
||||||
|
/* TP: update + align top */
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
@media (hover:hover){
|
||||||
|
.list .list-item:hover .list-item-controls,.list .list-item:focus-within .list-item-controls{
|
||||||
|
opacity:initial;
|
||||||
|
visibility:initial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list .list-item:not(.box){
|
||||||
|
padding-block:var(--be-list-item-padding);
|
||||||
|
padding-inline:var(--be-list-item-padding)
|
||||||
|
}
|
||||||
|
.list .list-item:not(:last-child):not(.box){
|
||||||
|
border-block-end:1px solid var(--be-list-item-divider-color)
|
||||||
|
}
|
||||||
|
@media screen and (width<=768px){
|
||||||
|
.list:not(.has-overflow-ellipsis) .list .list-item{
|
||||||
|
flex-wrap:wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list .list-item-image{
|
||||||
|
flex-shrink:0;
|
||||||
|
margin-inline-end:var(--be-list-item-image-margin);
|
||||||
|
/* TP: update + add margin-top */
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
@media screen and (width<=768px){
|
||||||
|
.list .list-item-image{
|
||||||
|
padding-block:.5rem;
|
||||||
|
padding-inline:0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list .list-item-content{
|
||||||
|
flex-direction:column;
|
||||||
|
flex-grow:1;
|
||||||
|
display:flex
|
||||||
|
}
|
||||||
|
@media screen and (width<=768px){
|
||||||
|
.list .list-item-content{
|
||||||
|
padding-block:.5rem;
|
||||||
|
padding-inline:0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.list .list-item-title{
|
||||||
|
color:var(--be-list-item-title-color);
|
||||||
|
font-weight:var(--be-list-item-title-weight);
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
.list .list-item-description{
|
||||||
|
color:var(--be-list-item-description-color)
|
||||||
|
}
|
||||||
|
.list .list-item-controls{
|
||||||
|
flex-shrink:0;
|
||||||
|
transition:opacity .125s ease-out
|
||||||
|
}
|
||||||
|
@media screen and (width<=768px){
|
||||||
|
.list .list-item-controls{
|
||||||
|
flex-wrap:wrap;
|
||||||
|
padding-block:.5rem;
|
||||||
|
padding-inline:0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (width>=769px),print{
|
||||||
|
.list .list-item-controls{
|
||||||
|
padding-inline-start:var(--be-list-item-padding)
|
||||||
|
}
|
||||||
|
.list:not(.has-visible-pointer-controls) .list .list-item-controls{
|
||||||
|
block-size:100%;
|
||||||
|
align-items:center;
|
||||||
|
padding-block-end:var(--be-list-item-padding);
|
||||||
|
display:flex;
|
||||||
|
position:absolute;
|
||||||
|
inset-inline-end:0
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,10 +14,13 @@
|
|||||||
<link rel="stylesheet" href="/style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma-checkradio@2.1/dist/css/bulma-checkradio.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma-checkradio@2.1/dist/css/bulma-checkradio.min.css">
|
||||||
<!-- font awesome -->
|
<!-- font awesome -->
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
|
||||||
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
|
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
|
<!-- additional JS -->
|
||||||
|
<check if="{{ isset(@js) }}">
|
||||||
|
<script src="/js/{{ @js}}"></script>
|
||||||
|
</check>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -85,7 +88,9 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<p>© <?php echo date('Y'); ?> Terry Probert</p>
|
<p>©
|
||||||
|
<?php echo date('Y'); ?> Terry Probert
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@ -103,6 +108,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
48
ui/views/attachment/index.html
Normal file
48
ui/views/attachment/index.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<div class="box">
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="title">Attachments</h2>
|
||||||
|
<div class="block">
|
||||||
|
<check if="isset( {{@attachments }})">
|
||||||
|
<table class="table is-fullwidth is-narrow is-striped is-hoverable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="th-icon"></th>
|
||||||
|
<th>File Name</th>
|
||||||
|
<th>Uploaded By</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Version</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<repeat group="{{ @attachments }}" value="{{ @attach }}">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span class="icon"><i class="fas fa-file"></i></span>
|
||||||
|
</td>
|
||||||
|
<td><a href="/attachment/{{@attach.id}}/download">{{ @attach.file_name }}</a></td>
|
||||||
|
<td>{{ @attach.username }}</td>
|
||||||
|
<td>{{ @attach.created_at }}</td>
|
||||||
|
<td>{{ @attach.version_number }}</td>
|
||||||
|
</tr>
|
||||||
|
</repeat>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</check>
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title">Upload attachment</h3>
|
||||||
|
<form action="/ticket/{{@ticket_id}}/attachments/upload" method="POST" enctype="multipart/form-data">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<div class="control has-icons-left"><!-- is-expanded -->
|
||||||
|
<input class="input" type="file" name="attachment" required>
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="fas fa-file"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<button class="button" type="submit">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
41
ui/views/comments/view.html
Normal file
41
ui/views/comments/view.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<div class="box" id="comments">
|
||||||
|
<h2 class="title">Comments</h2>
|
||||||
|
<div class="block">
|
||||||
|
<form action="/ticket/{{@PARAMS.id}}/comment" method="POST">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Add comment:</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea class="textarea" name="comment" rows="4" cols="50"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<button class="button is-primary" type="submit">Submit Comment</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<check if="{{ !empty(@comments) }}">
|
||||||
|
<div class="list">
|
||||||
|
<repeat group="{{ @comments }}" value="{{ @comment}}">
|
||||||
|
<div class="list-item">
|
||||||
|
<div class="list-item-image">
|
||||||
|
<figure class="image is-48x48">
|
||||||
|
<img class="is-rounded"
|
||||||
|
src="https://placehold.co/200x200/66d1ff/FFF?text=TP">
|
||||||
|
<!-- <img class="is-rounded"
|
||||||
|
src="https://loremflickr.com/200/200/dog?{{ (int)rand()}}">-->
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-content">
|
||||||
|
<div class="list-item-title is-flex is-justify-content-space-between">
|
||||||
|
<span>{{ @comment.author_name}}</span>
|
||||||
|
<span class="has-text-weight-normal has-text-grey">{{ @comment.created_at }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-item-description">
|
||||||
|
<parsedown>{{ @comment.comment | raw }}</parsedown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
|
</check>
|
||||||
|
</div>
|
||||||
@ -4,18 +4,23 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/kb/create" method="POST">
|
<form action="/kb/create" method="POST">
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_input('Title:', 'title') }}
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
||||||
|
|
||||||
<div id="editor" class="block">
|
<div id="editor" class="block">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="is-active"><a>Write</a></li>
|
<li class="is-active"><a>Write</a></li>
|
||||||
<li class=""><a>Preivew</a></li>
|
<li class=""><a>Preview</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{{ BulmaForm::horizontal_field_textarea('Content:', 'content') }}
|
<div class="tab-content">
|
||||||
|
<bulma type="H_FIELD_TEXTAREA" label="Content:" name="content" value=""></bulma>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -3,18 +3,27 @@
|
|||||||
|
|
||||||
<form action="/kb/{{@article.id}}/update" method="POST">
|
<form action="/kb/{{@article.id}}/update" method="POST">
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_input('Title:', 'title', @article.title)}}
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@article.title}}"></bulma>
|
||||||
|
|
||||||
<div id="editor" class="block">
|
<div id="editor" class="block">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="is-active"><a>Write</a></li>
|
<li class="is-active"><a data-target="pane-editor">Write</a></li>
|
||||||
<li class=""><a>Preivew</a></li>
|
<li class=""><a data-target="pane-preview">Preview</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="tab-pane" id="pane-editor">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{{ BulmaForm::horizontal_field_textarea('Content:', 'content', @article.content) }}
|
<bulma type="H_FIELD_TEXTAREA" label="Content:" name="content" value="{{@article.content}}"></bulma>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="pane-preview">
|
||||||
|
<div class="block content">
|
||||||
|
<div class="preview" data-source="content" data-handler="/parsedown/preview" data-method="post"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,15 @@
|
|||||||
<form method="GET" action="/kb">
|
<form method="GET" action="/kb">
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
|
<check if="{{ isset(@GET.search)}}">
|
||||||
|
<true>
|
||||||
|
<input class="input" type="text" name="search" placeholder="Search by title..."
|
||||||
|
value="{{ HTMLPurifier::instance()->purify( @GET.search) }}">
|
||||||
|
</true>
|
||||||
|
<false>
|
||||||
<input class="input" type="text" name="search" placeholder="Search by title...">
|
<input class="input" type="text" name="search" placeholder="Search by title...">
|
||||||
|
</false>
|
||||||
|
</check>
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
<div class="level-right">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<h1 class="title">{{@article.title}}</h1>
|
<h1 class="title">{{@article.title}}</h1>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{ Parsedown::instance()->text(@article.content) }}
|
<parsedown>{{ @article.content | raw }}</parsedown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,37 @@
|
|||||||
|
|
||||||
<form action="/ticket/create" method="POST">
|
<form action="/ticket/create" method="POST">
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_input('Title:', 'title') }}
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
||||||
|
<bulma type="H_FIELD_TEXTAREA" label="Description:" name="description" value=""></bulma>
|
||||||
{{ BulmaForm::horizontal_field_textarea('Description:', 'description') }}
|
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
|
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
|
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
|
||||||
|
|
||||||
|
<!-- custom fields -->
|
||||||
|
<hr>
|
||||||
|
<div class="block">
|
||||||
|
<div class="field is-grouped is-grouped-right">
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Key:</label>
|
||||||
|
<input class="input" type="text" name="meta_key[]" placeholder="eg. Department">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Value:</label>
|
||||||
|
<input class="input" type="text" name="meta_value[]" placeholder="eg. Finance">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field is-grouped is-grouped-right">
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Key:</label>
|
||||||
|
<input class="input" type="text" name="meta_key[]" placeholder="eg. Category">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Value:</label>
|
||||||
|
<input class="input" type="text" name="meta_value[]" placeholder="eg. Urgent">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field is-grouped is-grouped-right">
|
<div class="field is-grouped is-grouped-right">
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<a class="button is-secondary" href="/tickets">Cancel</a>
|
<a class="button is-secondary" href="/tickets">Cancel</a>
|
||||||
|
|||||||
@ -2,14 +2,51 @@
|
|||||||
|
|
||||||
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_input('Title:', 'title', @ticket.title) }}
|
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@ticket.title}}"></bulma>
|
||||||
|
<bulma type="H_FIELD_TEXTAREA" label="Description:" name="description" value="{{@ticket.description}}"></bulma>
|
||||||
{{ BulmaForm::horizontal_field_textarea('Description:', 'description', @ticket.description) }}
|
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
|
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
|
||||||
|
|
||||||
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
|
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
|
||||||
|
|
||||||
<button class="button is-primary" type="submit">Edit Ticket</button>
|
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-5">Custom Fields</h3>
|
||||||
|
<!-- existing fields-->
|
||||||
|
<div class="block">
|
||||||
|
<repeat group="{{ @ticket_meta }}" value="{{ @m }}">
|
||||||
|
<div class="field is-grouped is-grouped-right">
|
||||||
|
<input type="hidden" name="meta_id[]" value=" {{ @m.id }}">
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Key:</label>
|
||||||
|
<input class="input" type="text" name="meta_key[]" value="{{ @m.meta_key }}"
|
||||||
|
placeholder="eg. Department">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Value:</label>
|
||||||
|
<input class="input" type="text" name="meta_value[]" value="{{ @m.meta_value }}"
|
||||||
|
placeholder="eg. Finance">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</repeat>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<!-- adding new custom meta -->
|
||||||
|
<div class="block">
|
||||||
|
<div class="field is-grouped is-grouped-right">
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Key:</label>
|
||||||
|
<input class="input" type="text" name="new_meta_key[]"
|
||||||
|
placeholder="eg. Department">
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<label class="label">Value:</label>
|
||||||
|
<input class="input" type="text" name="new_meta_value[]"
|
||||||
|
placeholder="eg. Finance">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="button is-primary" type="submit">Save Ticket</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -1,16 +1,84 @@
|
|||||||
<h1 class="title">Ticket - View</h1>
|
<!-- Ticket - View -->
|
||||||
|
<h1 class="title">{{ @ticket.title }}</h1>
|
||||||
|
<p><a href="/ticket/{{ @ticket.id}}/edit">edit ticket</a></p>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div class="box">
|
<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">
|
||||||
<table class="table is-bordered is-fullwidth">
|
<table class="table is-bordered is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th class="has-width-200">Property</th><th>Value</th></tr>
|
<tr><th class="has-width-100">Property</th><th>Value</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
|
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
|
||||||
|
<check if="{{ @key !== 'description'}}">
|
||||||
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
|
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
|
||||||
|
</check>
|
||||||
</repeat>
|
</repeat>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="block" id="attachments"></div>
|
||||||
|
<div class="block" id="comments"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
24
ui/views/user/edit.html
Normal file
24
ui/views/user/edit.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<form method="POST" action="/user/{{@edit_user.id}}/update">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Username</label>
|
||||||
|
<div class="control">
|
||||||
|
<input class="input" name="username" type="text" value="{{ @edit_user.username}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Role</label>
|
||||||
|
<div class="select">
|
||||||
|
<select class="select" name="role">
|
||||||
|
<option value="user">User</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-primary" type="submit">Edit User</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
18
ui/views/user/index.html
Normal file
18
ui/views/user/index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="block">
|
||||||
|
<h1 class="title">All Users</h1>
|
||||||
|
|
||||||
|
<table class="table table-bordered is-fullwidth">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th><th>Username</th><th>Role</th><th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<repeat group="{{ @users }}" value="{{ @u }}">
|
||||||
|
<tr>
|
||||||
|
<td>{{ @u.id }}</td>
|
||||||
|
<td>{{ @u.username }}</td>
|
||||||
|
<td>{{ @u.role_name }} ( {{ @u.role }} )</td>
|
||||||
|
</tr>
|
||||||
|
</repeat>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
Loading…
x
Reference in New Issue
Block a user