Compare commits

...

13 Commits

25 changed files with 909 additions and 3 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
/lib/
/downloads/
/app/.env.cfg
/public/tmp/

View File

@ -8,3 +8,8 @@ Used to keep track of ongoing projects/tasks, allow to view and search historic
- plesk ext composer --application -register -domain desk.tinylink.uk -path desk.tinylink.uk/tp_servicedesk
~ https://www.plesk.com/kb/support/how-to-change-in-the-php-composer-extension-the-path-of-the-composer-json-file/
## Milestones
- Database created locally
- .gitignore added
- added AuthController - login and logout process

View File

@ -0,0 +1,54 @@
<?php
class AuthController {
public function showLoginForm($f3){
// store session errors or messages, then clear
$f3->set('error', $f3->get('SESSION.login_error'));
$f3->clear('SESSION.login_error');
// this can be in our controller base
$f3->set('content', '../ui/views/login.html');
echo \Template::instance()->render('../ui/templates/layout.html');
$f3->clear('error');
}
public function login($f3){
$username = $f3->get('POST.username');
$password = $f3->get('POST.password');
$db = $f3->get('DB');
// query for user
$result = $db->exec(
'SELECT id, username, password, role FROM users WHERE username =? LIMIT 1', $username
);
// verifiy password
if($result){
$user = $result[0]; // first row
if(password_verify($password, $user['password'])){
// valid
$f3->set('SESSION.user', [
'id'=> $user['id'],
'username' => $user['username']
]);
$f3->reroute('/dashboard');
} else {
$f3->set('SESSION.login_error', 'Invalid password');
}
} else {
// if here, login failed.
$f3->set('SESSION.login_error', 'Invalid username');
}
$f3->reroute('/login');
}
public function logout($f3){
$f3->clear('SESSION');
$f3->reroute('/');
}
}

View File

@ -0,0 +1,9 @@
<?php
class DashboardController {
function index($f3){
$f3->set('content', '../ui/views/dashboard.html');
echo \Template::instance()->render('../ui/templates/layout.html');
}
}

View File

@ -0,0 +1,15 @@
<?php
class HomeController {
public function display($f3){
// $db = $f3->get('DB');
// echo \Template::instance()->render('../ui/views/home.html');
echo \Template::instance()->render('../ui/templates/layout.html');
// Query
// View
}
// ...
}

View File

@ -0,0 +1,131 @@
<?php
class KBController {
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');
}
}
public function index($f3){
$this->check_access($f3);
$db = $f3->get('DB');
$search_term = $f3->get('GET.search');
$tag_param = $f3->get('GET.tag');
// base query
$sql = 'SELECT a.* FROM kb a';
$args = [];
if($tag_param){
$sql .= '
JOIN kb_tags AS at ON a.id = at.article_id
JOIN tags t ON at.tag_id = t.id
WHERE t.name = ?
';
$args[] = $tag_param;
if($search_term){
$sql .= ' AND a.title LIKE ?';
$args[] = '%' . $search_term . '%';
}
} else if ($search_term){
$sql .= ' WHERE a.title LIKE ?';
$args[] = '%' . $search_term . '%';
}
$sql .= ' ORDER BY a.created_at DESC';
$articles = $db->exec($sql, $args);
// render
$f3->set('articles', $articles);
$f3->set('content', '../ui/views/kb/index.html');
echo \Template::instance()->render('../ui/templates/layout.html');
$f3->clear('SESSION.error');
}
/**
* Form to create new article
*/
public function createForm($f3){
$this->check_access($f3);
$db = $f3->get('DB');
$all_tags = $db->exec('SELECT * FROM tags ORDER BY name ASC');
$f3->set('all_tags', $all_tags);
// render
$f3->set('content', '../ui/views/kb/create.html');
echo \Template::instance()->render('../ui/templates/layout.html');
$f3->clear('SESSION.error');
}
// handle POST
public function create($f3){
$this->check_access($f3);
$title = $f3->get('POST.title');
$content = $f3->get('POST.content');
$created_by = $f3->get('SESSION.user.id');
$db = $f3->get('DB');
// insert
$db->exec(
'INSERT INTO kb (title, content, created_by, updated_by, created_at, updated_at)
VALUES (?,?,?,?, NOW(), NOW())',
[$title, $content, $created_by]
);
$article_id = $db->lastInsertId();
// TODO: tags
$f3->reroute('/kb');
}
// view a single
public function view($f3){
$this->check_access($f3);
$article_id = $f3->get('PARAMS.id');
$db = $f3->get('DB');
$articles = $db->exec(
'SELECT a.*, u.username AS created_by_name
FROM kb AS a
LEFT JOIN users AS u ON a.created_by = u.id
WHERE a.id = ? LIMIT 1',
[$article_id]
);
if(!$articles){
$f3->set('SESSION.error', 'Article not found');
$f3->reroute('/kb');
}
$article = $articles[0];
$f3->set('article', $article);
// TODO: tags
$tags = $db->exec(
'SELECT t.* FROM tags AS t
JOIN kb_tags AS at ON t.id = at.tag_id
WHERE at.article_id = ?',
[$article_id]
);
// render
$f3->set('content', '../ui/views/kb/view.html');
echo \Template::instance()->render('../ui/templates/layout.html');
$f3->clear('SESSION.error');
}
}

View File

@ -0,0 +1,161 @@
<?php
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
public function index($f3){
$this->check_access($f3);
$db = $f3->get('DB');
// retrieve tickets
$tickets = $db->exec('SELECT * FROM tickets ORDER BY created_at DESC');
// pass data to template
$f3->set('tickets', $tickets);
// render
$f3->set('content', '../ui/views/ticket/index.html');
echo \Template::instance()->render('../ui/templates/layout.html');
$f3->clear('SESSION.error');
}
// view a single ticket
public function view($f3){
$this->check_access($f3);
$ticket_id = $f3->get('PARAMS.id');
$db = $f3->get('DB');
$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);
// render
$f3->set('content', '../ui/views/ticket/view.html');
echo \Template::instance()->render('../ui/templates/layout.html');
}
// show create form
public function createForm($f3){
$this->check_access($f3);
$f3->set('content', '../ui/views/ticket/create.html');
echo \Template::instance()->render('../ui/templates/layout.html');
}
// handle POST
public function create($f3){
$this->check_access($f3);
$title = $f3->get('POST.title');
$description = $f3->get('POST.description');
$priority = $f3->get('POST.priority'); // eg - low, medium, high
$status = $f3->get('POST.status'); // eg - new, in_progress
$created_by = $f3->get('SESSION.user.id'); // current logged in user
$db = $f3->get('DB');
$db->exec(
'INSERT
INTO tickets (title, description, priority, status, created_by, created_at, updated_at)
VALUES (?,?,?,?,?,NOW(), NOW())',
[$title, $description, $priority, $status, $created_by]
);
$f3->reroute('/tickets');
}
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']) && $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
public function editForm($f3){
$this->check_access($f3);
$ticket_id = $f3->get('PARAMS.id');
$db = $f3->get('DB');
$ticket = $this->get_ticket_check_edit_permission($f3);
$f3->set('ticket', $ticket);
$f3->set('ticket', $ticket);
$f3->set('content', '../ui/views/ticket/edit.html');
echo \Template::instance()->render('../ui/templates/layout.html');
}
// process edit POST TODO: if assigned or admin
public function update($f3){
$this->check_access($f3);
$ticket = $this->get_ticket_check_edit_permission($f3);
$ticket_id = $ticket['id'];
$db = $f3->get('DB');
// get updated fields from post
$title = $f3->get('POST.title');
$description = $f3->get('POST.description');
$priority = $f3->get('POST.priority'); // eg - low, medium, high
$status = $f3->get('POST.status'); // eg - new, in_progress
$updated_by = $f3->get('SESSION.user.id'); // current logged in user
// TODO: if you want to update assignment, should be added here.
$db->exec(
'UPDATE tickets
SET title=?, description=?, priority=?, status=?, updated_by=?, updated_at=?
WHERE id=?',
[$title, $description, $priority, $status, $updated_by, 'NOW()', $ticket_id]
);
$f3->reroute('/ticket/' . $ticket_id);
}
}

83
app/model/BulmaForm.php Normal file
View File

@ -0,0 +1,83 @@
<?php
// this isn't the way to do it, but nevermind!
class BulmaForm {
public static function horizontal_field_input($label = "%label%", $name = "%name%", $value=""){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label>%label%</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" name="%name%" value="%value%">
</div>
</div>
</div>
</div>
';
$string = str_replace('%label%', $label, $string);
$string = str_replace('%name%', $name, $string);
$string = str_replace('%value%', $value, $string);
return $string;
}
public static function horizontal_field_textarea($label = "%label%", $name = "%name%", $value=""){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label>%label%</label>
</div>
<div class="field-body">
<div class="field">
<div class="control">
<textarea class="textarea" type="text" name="%name%">%value%</textarea>
</div>
</div>
</div>
</div>
';
$string = str_replace('%label%', $label, $string);
$string = str_replace('%name%', $name, $string);
$string = str_replace('%value%', $value, $string);
return $string;
}
public static function horizontal_field_select($label="%label%", $name="%name%", $options=[], $selected=0){
$string = '
<div class="field is-horizontal">
<div class="field-label is-normal">
<label>%label%</label>
</div>
<div class="field-body">
<div class="field">
<div class="select">
<select name="%name%">
%options%
</select>
</div>
</div>
</div>
</div>
';
$string = str_replace('%label%', $label, $string);
$string = str_replace('%name%', $name, $string);
$opts_str = ''; $i=0;
foreach($options as $v){
$opts_str .= '<option'.($i==$selected ? ' selected="selected" ' : '').'>'.$v.'</option>';
$i++;
}
$string = str_replace('%options%', $opts_str, $string);
return $string;
}
}

View File

@ -5,6 +5,7 @@
"vendor-dir": "lib"
},
"require": {
"bcosca/fatfree-core": "^3.9"
"bcosca/fatfree-core": "^3.9",
"erusev/parsedown": "^1.7"
}
}

9
public/.htaccess Normal file
View File

@ -0,0 +1,9 @@
RewriteEngine On
RewriteRule ^(app|dict|ns|tmp)\/|\.ini$ - [R=404]
RewriteCond %{REQUEST_FILENAME} !-l
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .* index.php [L,QSA]
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization},L]

62
public/index.php Normal file
View File

@ -0,0 +1,62 @@
<?php
require '../lib/vendor/autoload.php';
$f3 = \Base::instance();
$f3->set('DEBUG', 3); // development debug
$f3->config('../app/.env.cfg');
$f3->set('DB', new \DB\SQL(
'mysql:host=localhost;port=3306;dbname=' . $f3->get('database.db_name'),
$f3->get('database.username'),
$f3->get('database.password')
));
new \DB\SQL\Session($f3->get('DB'));
$f3->set('SESSION.status', 'running');
// Routing and Controller Setup
// home
$f3->route('GET /', 'HomeController->display');
// auth
$f3->route('GET /login', 'AuthController->showLoginForm');
$f3->route('POST /login', 'AuthController->login');
$f3->route('GET /logout', 'AuthController->logout');
// Example protected route
$f3->route('GET /dashboard', function($f3){
if(!$f3->exists('SESSION.user')){
$f3->reroute('/login');
}
echo 'Welcome to the dashboard' . $f3->get('SESSION.username');
echo '<a href="/logout">logout</a>';
});
// tickets - CRUD (CREATE, READ, UPDATE, DELETE)
$f3->route('GET /tickets', 'TicketController->index'); // view all tickets
$f3->route('GET /ticket/@id', 'TicketController->view'); // view ticket details
$f3->route('GET /ticket/create', 'TicketController->createForm'); // show form to create
$f3->route('POST /ticket/create', 'TicketController->create'); // save
$f3->route('GET /ticket/@id/edit', 'TicketController->editForm'); // edit ticket
$f3->route('POST /ticket/@id/update', 'TicketController->update'); //
// knowledgebase
$f3->route('GET /kb', 'KBController->index');
$f3->route('GET /kb/create', 'KBController->createForm');
$f3->route('POST /kb/create', 'KBController->create');
$f3->route('GET /kb/@id', 'KBController->view');
$f3->route('GET /kb/@id/edit', 'KBController->editForm');
$f3->route('POST /kb/@id/edit', 'KBControllerKB->edit'); // should this be update - "crud"?
// tags
$f3->route('GET /tags', 'TagController->index');
$f3->route('GET /tag/create', 'Tag->createForm');
$f3->route('POST /tag/create', 'Tag->create');
// dashboard
$f3->route('GET /dashboard', 'DashboardController->index');
$f3->run();

1
public/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="300.000000pt" height="300.000000pt" viewBox="0 0 300.000000 300.000000" preserveAspectRatio="xMidYMid meet"> <g transform="translate(0.000000,300.000000) scale(0.100000,-0.100000)" fill="#03045e" stroke="none"> <path d="M465 2639 c-88 -12 -236 -51 -312 -83 l-58 -24 0 -1054 c0 -880 2 -1053 14 -1050 177 37 258 45 461 46 187 1 230 -2 328 -22 145 -29 257 -69 414 -148 70 -35 130 -64 133 -64 3 0 5 470 5 1044 l0 1044 -26 31 c-14 17 -55 50 -92 74 -256 170 -569 244 -867 206z m397 -313 c85 -17 147 -37 241 -79 l67 -30 0 -318 c0 -176 -2 -319 -5 -319 -4 0 -46 14 -95 31 l-88 31 -4 97 c-3 90 -5 101 -32 135 -16 22 -50 48 -80 62 -33 16 -43 24 -29 24 12 0 37 15 56 33 46 43 56 98 29 161 -26 59 -66 80 -139 74 -77 -7 -114 -45 -121 -122 -6 -68 21 -119 74 -138 l34 -12 -51 -24 c-76 -36 -103 -84 -108 -189 -3 -82 -3 -83 -30 -84 -14 0 -65 -10 -114 -23 -48 -12 -90 -20 -92 -17 -3 2 -5 156 -5 342 l0 339 23 5 c148 36 343 45 469 21z m26 -990 c99 -22 209 -59 285 -97 34 -17 64 -33 66 -35 1 -1 -7 -20 -19 -42 l-22 -40 -86 40 c-244 113 -543 132 -803 52 -12 -3 -19 6 -27 38 -6 24 -8 46 -4 49 8 8 123 35 202 49 78 13 322 5 408 -14z m-5 -281 c110 -23 321 -100 355 -130 2 -2 -6 -22 -18 -44 l-20 -41 -48 24 c-74 37 -213 83 -302 100 -154 30 -381 17 -536 -29 -18 -6 -23 -1 -32 37 -6 23 -8 46 -4 50 7 7 175 43 242 52 67 8 285 -3 363 -19z m-33 -286 c88 -13 219 -54 313 -96 42 -20 77 -38 77 -41 0 -11 -44 -82 -49 -79 -105 58 -268 111 -397 129 l-94 12 0 41 c0 23 3 44 6 48 6 6 30 3 144 -14z m-334 -23 c3 -19 4 -40 2 -47 -2 -7 -42 -20 -89 -29 -46 -10 -96 -21 -110 -25 -23 -6 -27 -3 -37 33 -7 22 -12 42 -12 45 0 7 62 25 145 43 92 19 93 18 101 -20z"/> <path d="M2180 2634 c-218 -35 -517 -171 -605 -275 l-25 -31 0 -1044 c0 -574 2 -1044 5 -1044 3 0 63 29 133 64 157 79 269 119 414 148 98 20 141 23 328 22 203 -1 284 -9 461 -46 12 -3 14 170 17 1049 l2 1052 -47 21 c-188 83 -472 118 -683 84z m118 -246 l3 -48 -34 0 c-92 0 -268 -47 -393 -105 l-72 -33 -22 40 c-12 22 -20 41 -19 42 9 8 110 55 159 74 81 31 165 53 255 68 118 18 119 18 123 -38z m352 15 c36 -9 68 -19 72 -22 11 -10 -13 -93 -26 -88 -10 4 -81 19 -193 43 -24 5 -29 26 -16 72 5 22 9 23 52 17 25 -4 75 -14 111 -22z m-180 -253 c93 -10 231 -38 250 -50 9 -6 9 -17 -1 -49 -7 -22 -13 -41 -15 -41 -1 0 -42 10 -91 22 -75 19 -114 23 -263 22 -156 0 -186 -3 -272 -27 -53 -14 -136 -44 -184 -66 -48 -23 -88 -41 -90 -41 -2 0 -13 18 -24 39 l-19 39 22 14 c47 31 197 88 279 108 155 37 274 45 408 30z m-15 -280 c94 -8 259 -41 272 -54 3 -2 -1 -23 -8 -46 -11 -38 -15 -41 -38 -36 -14 3 -55 13 -91 21 -103 25 -298 30 -411 11 -100 -16 -239 -61 -321 -102 -26 -13 -50 -24 -52 -24 -3 0 -15 18 -26 40 l-20 40 57 29 c129 64 282 108 428 121 44 4 85 8 90 8 6 1 60 -3 120 -8z m130 -398 l40 -7 3 -340 c1 -187 0 -345 -2 -351 -3 -8 -22 -7 -68 2 -168 36 -398 27 -581 -22 -51 -13 -104 -27 -120 -30 l-27 -7 2 328 3 328 95 34 c103 37 190 60 285 73 65 10 305 4 370 -8z"/> <path d="M2174 1248 c-15 -29 -27 -54 -29 -56 -1 -1 -30 -7 -64 -13 l-63 -11 47 -48 47 -47 -11 -58 c-6 -32 -11 -63 -11 -68 0 -4 25 6 56 24 l57 32 59 -33 c51 -28 59 -30 55 -14 -3 11 -9 41 -13 67 -6 48 -6 49 41 97 l48 48 -49 8 c-77 11 -89 19 -113 73 -12 28 -24 51 -26 51 -3 0 -17 -24 -31 -52z"/> </g> </svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

6
public/style.css Normal file
View File

@ -0,0 +1,6 @@
html, body {padding:0; margin:0;}
html, body, #sidebar, #page,#base_body {
min-height: 100%
}
#page { min-height: calc(100vh - 170px - 52px) }

6
public/test.php Normal file
View File

@ -0,0 +1,6 @@
<?php
$password = "pass!local";
echo password_hash($password, PASSWORD_DEFAULT);

94
ui/templates/layout.html Normal file
View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Desk - Work Streams</title>
<!-- bulma.io-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
<!-- bulma helpers -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma-helpers/0.4.3/css/bulma-helpers.min.css"
integrity="sha512-U6ELnUi7oqVEjkLmFw5r5UR5LEtvpImS/jUykBKneVhD0lxZxfJZ3k3pe003ktrtNZYungd9u3Urp2X09wKwXg=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- <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">
<!-- font awesome -->
<link rel="stylesheet"
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" />
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<!-- Your logo or app name -->
<img src="/logo.svg" alt="App Logo">
</a>
<!-- Burger menu for mobile -->
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="mainNavbar">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="mainNavbar" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/dashboard">Dashboard</a>
<a class="navbar-item" href="/tickets">Tickets</a>
<a class="navbar-item" href="/projects">Projects</a>
<a class="navbar-item" href="/kb">Knowledge Base</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<check if="{{ isset(@SESSION.user) }}">
<true>
<a class="button is-primary" href="/logout">Log Out</a>
</true>
<false>
<a class="button is-primary" href="/login">Log In</a>
</false>
</check>
</div>
</div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="section" id="page">
<div class="container">
<!-- Fat-Free Framework content injection -->
<include href="{{@content}}" />
</div>
</main>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p>&copy; <?php echo date('Y'); ?> Terry Probert</p>
</div>
</footer>
<!-- JavaScript for Bulma navbar burger (mobile) -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
if (burgers.length > 0) {
burgers.forEach(el => {
el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.target);
el.classList.toggle('is-active');
target.classList.toggle('is-active');
});
});
}
});
</script>
</body>
</html>

1
ui/views/dashboard.html Normal file
View File

@ -0,0 +1 @@
<h1 class="title">Dashboard</h1>

77
ui/views/home.html Normal file
View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bulma Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.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">
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<!-- Your logo or app name -->
<img src="logo.svg" alt="App Logo">
</a>
<!-- Burger menu for mobile -->
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="mainNavbar">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="mainNavbar" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/dashboard">Dashboard</a>
<a class="navbar-item" href="/tickets">Tickets</a>
<a class="navbar-item" href="/projects">Projects</a>
<a class="navbar-item" href="/knowledge">Knowledge Base</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary" href="/login">Log in</a>
</div>
</div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="section" id="page">
<div class="container">
<!-- Fat-Free Framework content injection -->
{{@content}}
</div>
</main>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p>&copy; <?php echo date('Y'); ?> Terry Probert</p>
</div>
</footer>
<!-- JavaScript for Bulma navbar burger (mobile) -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const burgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
if (burgers.length > 0) {
burgers.forEach(el => {
el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.target);
el.classList.toggle('is-active');
target.classList.toggle('is-active');
});
});
}
});
</script>
</body>
</html>

16
ui/views/kb/create.html Normal file
View File

@ -0,0 +1,16 @@
<h1 class="title">Create Knowledge Base Article</h1>
<form action="/kb/create" method="POST">
{{ BulmaForm::horizontal_field_input('Title:', 'title') }}
{{ BulmaForm::horizontal_field_textarea('Description:', 'description') }}
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
<button class="button is-primary" type="submit">Create Ticket</button>
</div>
</form>

37
ui/views/kb/index.html Normal file
View File

@ -0,0 +1,37 @@
<h1 class="title">Knowledge Base</h1>
<check if="{{isset(@SESSION.error)}}">
<div class="notification is-warning">
{{ @SESSION.error }}
</div>
</check>
<p><a href="/kb/create">create kb article</a></p>
<hr>
<table class="table is-fullwidth is-bordered">
<thead>
<tr>
<th>id</th><th>title</th><th>description</th>
<th>status</th><th>priority</th><th>created_at</th>
<th></th>
</tr>
</thead>
<tbody>
<repeat group="{{@tickets}}" value="{{@ticket}}">
<tr>
<td>{{@ticket.id}}</td>
<td>{{@ticket.title}}</td>
<td>{{@ticket.description}}</td>
<td>{{@ticket.status}}</td>
<td>{{@ticket.priority}}</td>
<td>{{@ticket.created_at}}</td>
<td>
<a href="/ticket/{{@ticket.id}}"><i class="fa fa-eye"></i></a>
<a href="/ticket/{{@ticket.id}}/edit"><i class="fa fa-edit"></i></a>
</td>
</tr>
</repeat>
</tbody>
</table>

18
ui/views/kb/view.html Normal file
View File

@ -0,0 +1,18 @@
<h1 class="title">Knowledge Article</h1>
<div class="box">
<h2 class="title">{{@article.title}}</h2>
<table class="table is-bordered is-fullwidth">
<thead>
<tr><th class="has-width-200">Property</th><th>Value</th></tr>
</thead>
<tbody>
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
</repeat>
</tbody>
</table>
</div>

35
ui/views/login.html Normal file
View File

@ -0,0 +1,35 @@
<h1 class="title">Please Log In</h1>
<check if="{{ @error}}">
<div class="notification is-danger is-light">
<p style="color: red;">{{ @error }}</p>
</div>
</check>
<form action="/login" method="POST">
<div class="field">
<p class="control has-icons-left has-icons-right">
<input name="username" class="input" type="text" placeholder="Username">
<span class="icon is-small is-left">
<i class="fas fa-user"></i>
</span>
</p>
</div>
<div class="field">
<p class="control has-icons-left">
<input name="password" class="input" type="password" placeholder="Password">
<span class="icon is-small is-left">
<i class="fas fa-lock"></i>
</span>
</p>
</div>
<div class="field">
<p class="control">
<button class="button is-success">
Login
</button>
</p>
</div>
</form>

View File

@ -0,0 +1,16 @@
<h1 class="title">Create Ticket Form</h1>
<form action="/ticket/create" method="POST">
{{ BulmaForm::horizontal_field_input('Title:', 'title') }}
{{ BulmaForm::horizontal_field_textarea('Description:', 'description') }}
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
<button class="button is-primary" type="submit">Create Ticket</button>
</div>
</form>

15
ui/views/ticket/edit.html Normal file
View File

@ -0,0 +1,15 @@
<h1 class="title">Edit Ticket Form</h1>
<form action="/ticket/{{ @PARAMS.id }}/update" method="POST">
{{ BulmaForm::horizontal_field_input('Title:', 'title', @ticket.title) }}
{{ BulmaForm::horizontal_field_textarea('Description:', 'description', @ticket.description) }}
{{ BulmaForm::horizontal_field_select('Priority:', 'priority', ['Low', 'Medium', 'High'])}}
{{ BulmaForm::horizontal_field_select('Status:', 'status', ['New', 'In Progress', 'On Hold', 'Completed'])}}
<button class="button is-primary" type="submit">Edit Ticket</button>
</div>
</form>

View File

@ -0,0 +1,36 @@
<h1 class="title">View Tickets</h1>
<check if="{{isset(@SESSION.error)}}">
<div class="notification is-warning">
{{ @SESSION.error }}
</div>
</check>
<p><a href="/ticket/create">create ticket</a></p>
<hr>
<table class="table is-fullwidth is-bordered">
<thead>
<tr>
<th>id</th><th>title</th><th>description</th>
<th>status</th><th>priority</th><th>created_at</th>
<th></th>
</tr>
</thead>
<tbody>
<repeat group="{{@tickets}}" value="{{@ticket}}">
<tr>
<td>{{@ticket.id}}</td>
<td>{{@ticket.title}}</td>
<td>{{@ticket.status}}</td>
<td>{{@ticket.priority}}</td>
<td>{{@ticket.created_at}}</td>
<td>
<a href="/ticket/{{@ticket.id}}"><i class="fa fa-eye"></i></a>
<a href="/ticket/{{@ticket.id}}/edit"><i class="fa fa-edit"></i></a>
</td>
</tr>
</repeat>
</tbody>
</table>

16
ui/views/ticket/view.html Normal file
View File

@ -0,0 +1,16 @@
<h1 class="title">Ticket - View</h1>
<div class="box">
<table class="table is-bordered is-fullwidth">
<thead>
<tr><th class="has-width-200">Property</th><th>Value</th></tr>
</thead>
<tbody>
<repeat group="{{ @ticket }}" key="{{ @key }}" value="{{ @value }}">
<tr><td>{{@key}}</td> <td>{{@value}}</td></tr>
</repeat>
</tbody>
</table>
</div>