Compare commits
No commits in common. "b2764a0f5840513fafa57a59abc3dbfb049e5252" and "1f10cc7f89c165fb3ee4c4ae620a59b20bda3b50" have entirely different histories.
b2764a0f58
...
1f10cc7f89
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,5 +2,4 @@
|
|||||||
/downloads/
|
/downloads/
|
||||||
/app/.env.cfg
|
/app/.env.cfg
|
||||||
/public/tmp/
|
/public/tmp/
|
||||||
composer.lock
|
composer.lock
|
||||||
/storage/
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
<?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,7 +4,6 @@ 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');
|
||||||
@ -22,11 +21,7 @@ class AuthController {
|
|||||||
$db = $f3->get('DB');
|
$db = $f3->get('DB');
|
||||||
// query for user
|
// query for user
|
||||||
$result = $db->exec(
|
$result = $db->exec(
|
||||||
'SELECT u.id, u.username, u.password, u.role, r.role as role_name
|
'SELECT id, username, password, role FROM users WHERE username =? LIMIT 1', $username
|
||||||
FROM users u
|
|
||||||
LEFT JOIN roles r ON r.id = u.role
|
|
||||||
WHERE username =?
|
|
||||||
LIMIT 1', $username
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// verifiy password
|
// verifiy password
|
||||||
@ -36,9 +31,7 @@ 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');
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
<?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,7 +154,6 @@ 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');
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class ParsedownPreview {
|
|
||||||
|
|
||||||
public function view($f3){
|
|
||||||
|
|
||||||
$preview_text = $f3->get('POST.content');
|
|
||||||
echo Parsedown::instance()->text($preview_text);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
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);
|
||||||
@ -28,13 +35,24 @@ class TicketController {
|
|||||||
$ticket_id = $f3->get('PARAMS.id');
|
$ticket_id = $f3->get('PARAMS.id');
|
||||||
$db = $f3->get('DB');
|
$db = $f3->get('DB');
|
||||||
|
|
||||||
$this->get_ticket($f3, $db, $ticket_id);
|
$result = $db->exec(
|
||||||
$this->get_child_tickets($f3, $db, $ticket_id);
|
'SELECT t.*, u.username as created_by_name
|
||||||
$this->get_parent_tickets($f3, $db, $ticket_id);
|
FROM tickets t
|
||||||
$this->get_custom_fields($f3, $db, $ticket_id);
|
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);
|
||||||
|
|
||||||
// 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');
|
||||||
|
|
||||||
@ -48,7 +66,6 @@ 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);
|
||||||
|
|
||||||
@ -67,31 +84,39 @@ class TicketController {
|
|||||||
[$title, $description, $priority, $status, $created_by]
|
[$title, $description, $priority, $status, $created_by]
|
||||||
);
|
);
|
||||||
|
|
||||||
$ticket_id = $db->lastInserId();
|
$f3->reroute('/tickets');
|
||||||
|
}
|
||||||
|
|
||||||
// custom fields
|
protected function get_ticket_check_edit_permission($f3){
|
||||||
$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)){
|
$db = $f3->get('DB');
|
||||||
foreach($meta_keys as $index => $key){
|
|
||||||
$val = $meta_values[$index] ?? '';
|
$ticket_id = $f3->get('PARAMS.id');
|
||||||
if(!empty($key) && $val !== ''){
|
$result = $db->exec('SELECT * FROM tickets WHERE id = ? LIMIT 1', [$ticket_id]);
|
||||||
$db->exec(
|
|
||||||
'INSERT INTO ticket_meta (ticket_id, meta_key, meta_value)
|
if(!$result){
|
||||||
VALUES (?,?,?)',
|
$f3->set('SESSION.error', 'Ticket not found.');
|
||||||
[$ticket_id, $key, $val]
|
$f3->reroute('/tickets');
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reroute to ticket
|
$ticket = $result[0];
|
||||||
$f3->reroute('/ticket/'.$ticket_id);
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
@ -102,15 +127,6 @@ 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');
|
||||||
@ -139,163 +155,7 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,62 +0,0 @@
|
|||||||
<?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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
<?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');
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
<?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 class="label">%label%</label>
|
<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 class="label">%label%</label>
|
<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 class="label">%label%</label>
|
<label>%label%</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@ -6,16 +6,13 @@ $f3 = \Base::instance();
|
|||||||
/**
|
/**
|
||||||
* Not required yet
|
* Not required yet
|
||||||
*/
|
*/
|
||||||
$htmlpurifier = \HTMLPurifier::instance();
|
// $htmlpurifier = new \HTMLPurifier();
|
||||||
// $htmlpurifier->purify($input);
|
// $htmlpurifier->purify($input);
|
||||||
$md = \Parsedown::instance();
|
$md = \Parsedown::instance();
|
||||||
$md->setSafeMode(true);
|
|
||||||
|
|
||||||
$f3->config('../app/.env.cfg');
|
|
||||||
$f3->set('DEBUG', 3); // development debug
|
$f3->set('DEBUG', 3); // development debug
|
||||||
$f3->set('CACHE', FALSE);
|
$f3->config('../app/.env.cfg');
|
||||||
|
|
||||||
$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'),
|
||||||
@ -52,18 +49,6 @@ $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');
|
||||||
@ -78,16 +63,8 @@ $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();
|
||||||
@ -1,75 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
});
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
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')
|
|
||||||
});
|
|
||||||
|
|
||||||
121
public/style.css
121
public/style.css
@ -3,123 +3,4 @@ html, body, #sidebar, #page,#base_body {
|
|||||||
min-height: 100%
|
min-height: 100%
|
||||||
}
|
}
|
||||||
|
|
||||||
#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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,19 +8,16 @@
|
|||||||
<!-- bulma.io-->
|
<!-- bulma.io-->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
|
||||||
<!-- bulma helpers -->
|
<!-- bulma helpers -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma-helpers/0.4.3/css/bulma-helpers.min.css"
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma-helpers/0.4.3/css/bulma-helpers.min.css"
|
||||||
integrity="sha512-U6ELnUi7oqVEjkLmFw5r5UR5LEtvpImS/jUykBKneVhD0lxZxfJZ3k3pe003ktrtNZYungd9u3Urp2X09wKwXg=="
|
integrity="sha512-U6ELnUi7oqVEjkLmFw5r5UR5LEtvpImS/jUykBKneVhD0lxZxfJZ3k3pe003ktrtNZYungd9u3Urp2X09wKwXg=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<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" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
<link rel="stylesheet"
|
||||||
integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg=="
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"
|
||||||
|
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>
|
||||||
@ -88,27 +85,24 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<p>©
|
<p>© <?php echo date('Y'); ?> Terry Probert</p>
|
||||||
<?php echo date('Y'); ?> Terry Probert
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- JavaScript for Bulma navbar burger (mobile) -->
|
<!-- JavaScript for Bulma navbar burger (mobile) -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||||
if (burgers.length > 0) {
|
if (burgers.length > 0) {
|
||||||
burgers.forEach(el => {
|
burgers.forEach(el => {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
const target = document.getElementById(el.dataset.target);
|
const target = document.getElementById(el.dataset.target);
|
||||||
el.classList.toggle('is-active');
|
el.classList.toggle('is-active');
|
||||||
target.classList.toggle('is-active');
|
target.classList.toggle('is-active');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
</script>
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,48 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<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,23 +4,18 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<form action="/kb/create" method="POST">
|
<form action="/kb/create" method="POST">
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
{{ BulmaForm::horizontal_field_input('Title:', 'title') }}
|
||||||
|
|
||||||
<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>Preview</a></li>
|
<li class=""><a>Preivew</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class="tab-content">
|
{{ BulmaForm::horizontal_field_textarea('Content:', 'content') }}
|
||||||
<bulma type="H_FIELD_TEXTAREA" label="Content:" name="content" value=""></bulma>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -3,27 +3,18 @@
|
|||||||
|
|
||||||
<form action="/kb/{{@article.id}}/update" method="POST">
|
<form action="/kb/{{@article.id}}/update" method="POST">
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@article.title}}"></bulma>
|
{{ BulmaForm::horizontal_field_input('Title:', 'title', @article.title)}}
|
||||||
|
|
||||||
<div id="editor" class="block">
|
<div id="editor" class="block">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="is-active"><a data-target="pane-editor">Write</a></li>
|
<li class="is-active"><a>Write</a></li>
|
||||||
<li class=""><a data-target="pane-preview">Preview</a></li>
|
<li class=""><a>Preivew</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content">
|
<div class="block">
|
||||||
<div class="tab-pane" id="pane-editor">
|
{{ BulmaForm::horizontal_field_textarea('Content:', 'content', @article.content) }}
|
||||||
<div class="block">
|
|
||||||
<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,15 +13,7 @@
|
|||||||
<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)}}">
|
<input class="input" type="text" name="search" placeholder="Search by title...">
|
||||||
<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...">
|
|
||||||
</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>{{ @article.content | raw }}</parsedown>
|
{{ Parsedown::instance()->text(@article.content) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,36 +3,13 @@
|
|||||||
|
|
||||||
<form action="/ticket/create" method="POST">
|
<form action="/ticket/create" method="POST">
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value=""></bulma>
|
{{ BulmaForm::horizontal_field_input('Title:', 'title') }}
|
||||||
<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'])}}
|
|
||||||
|
|
||||||
<!-- custom fields -->
|
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
|
||||||
<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">
|
||||||
|
|||||||
@ -2,51 +2,14 @@
|
|||||||
|
|
||||||
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
|
||||||
|
|
||||||
<bulma type="H_FIELD_INPUT" label="Title:" name="title" value="{{@ticket.title}}"></bulma>
|
{{ BulmaForm::horizontal_field_input('Title:', 'title', @ticket.title) }}
|
||||||
<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,84 +1,16 @@
|
|||||||
<!-- Ticket - View -->
|
<h1 class="title">Ticket - View</h1>
|
||||||
<h1 class="title">{{ @ticket.title }}</h1>
|
|
||||||
<p><a href="/ticket/{{ @ticket.id}}/edit">edit ticket</a></p>
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<div class="content">
|
<div class="box">
|
||||||
|
|
||||||
<div class="columns">
|
<table class="table is-bordered is-fullwidth">
|
||||||
<div class="column is-two-thirds">
|
<thead>
|
||||||
<div class="block">
|
<tr><th class="has-width-200">Property</th><th>Value</th></tr>
|
||||||
<p>{{ @ticket.created_at }}</p>
|
</thead>
|
||||||
<div class="content">
|
<tbody>
|
||||||
<parsedown>{{ @ticket.description | raw }}</parsedown>
|
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
|
||||||
</div>
|
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
|
||||||
</div>
|
</repeat>
|
||||||
</div>
|
</tbody>
|
||||||
|
</table>
|
||||||
<div class="column">
|
|
||||||
<!-- 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">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="block" id="attachments"></div>
|
|
||||||
<div class="block" id="comments"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
<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>
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<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