Compare commits

..

19 Commits

Author SHA1 Message Date
acbe69e9c7 Updated the ticket creation form to assign tickets to queues 2022-12-04 21:42:07 -05:00
80a12a86ef Added a ticket queues object to organize tickets; added ability to change a ticket's queue 2022-12-04 21:33:21 -05:00
15ee9b78a3 Refactored the ticket attribute click handlers to be more modular for adding new attribute modifications 2022-12-04 17:28:01 -05:00
ab2c87ce3a Added functionality to update the ticket's last updated time when changing the ticket status or severity 2022-12-04 17:15:17 -05:00
77c8605b49 Added ability to change the ticket severity level 2022-12-04 15:05:38 -05:00
5d5265ef38 Added ability to change the ticket status 2022-12-04 15:00:26 -05:00
f3ac8a6965 Added ticket deletion; fixed some small issues with the ticket list on the home page 2022-11-21 23:55:19 -05:00
5683c0bc8b Added ticket comments 2022-11-21 23:48:25 -05:00
4371fd0b2f More style changes 2022-11-21 22:21:38 -05:00
3ac8671742 More style changes 2022-11-21 18:48:49 -05:00
b347988937 Updated some styles on the ticket view; added markdown parsing for ticket body 2022-11-21 18:16:37 -05:00
55048bda33 Styled the ticket view page; added a section for future ticket comments 2022-11-21 18:04:24 -05:00
17c2e36bb5 Added ticket edit action 2022-11-21 13:03:51 -05:00
c6d4c3df10 Added ticket status 2022-11-20 23:42:45 -05:00
95bf9250e7 Added a ticket view route 2022-11-20 23:38:16 -05:00
830a950bf4 Added Eloquent and Phinx for handling database connections and migrations; added a Ticket model and a simple ticket/create form to create new tickets 2022-11-20 22:25:29 -05:00
41aca6215e Added CoffeeScript 2022-11-19 22:45:16 -05:00
4bf5e73179 Added some styles and a navbar 2022-11-19 22:26:16 -05:00
f4cf324280 Added grunt to project for SASS and CoffeeScript files 2022-11-19 13:58:35 -05:00
31 changed files with 6326 additions and 12 deletions

13
.gitignore vendored
View File

@ -1,5 +1,18 @@
# Composer dependencies
vendor/
# Local data
data/
# NPM dependencies (building CSS and JS)
node_modules/
# Compiled CSS and JS
public/styles/
public/js/
# SASS compilation cache
.sass-cache/
# PHP CodeSniffer cache
.phpcs-cache

65
Gruntfile.js Normal file
View File

@ -0,0 +1,65 @@
module.exports = function(grunt) {
var pkg = grunt.file.readJSON('package.json')
// Project configuration.
grunt.initConfig({
sass: {
dist: {
options: {
style: 'compressed'
},
files: [{
expand: true,
cwd: 'assets/sass',
src: ['*.sass'],
dest: 'public/styles',
ext: '.css'
}]
}
},
coffee: {
options: {
sourceMap: true,
style: 'compressed'
},
files: {
expand: true,
flatten: true,
cwd: 'assets/coffee',
src: ['*.coffee'],
dest: 'public/js',
ext: '.js'
}
},
watch: {
css: {
files: ['assets/sass/*.sass'],
tasks: ['sass'],
options: {
atBegin: true,
spawn: false
}
},
js: {
files: ['assets/coffee/*.coffee'],
tasks: ['coffee'],
options: {
atBegin: true,
spawn: false
}
}
}
});
// Load task plugins
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-contrib-coffee');
grunt.loadNpmTasks('grunt-contrib-watch');
// Default task(s).
grunt.registerTask('default', ['sass', 'coffee']);
};

View File

@ -0,0 +1,68 @@
$(document).ready ->
$('.ticket-queue').on('click', (e) ->
handleQueueClick(e)
)
$('.ticket-severity').on('click', (e) ->
handleAttributeClick(e, 'severity')
)
$('.ticket-status').on('click', (e) ->
handleAttributeClick(e, 'status')
)
validOptions =
'severity': ['low', 'medium', 'high'],
'status': ['open', 'closed', 'parked']
handleQueueClick = (e, fail = false) ->
newQueueId = prompt('Set queue ID:', $('.ticket-queue').data('id'))
if (newQueueId != null) and (newQueueId != '')
if (true)
console.log('Setting queue ID to ' + newQueueId)
editLink = $('#ticketEditLink').attr('href') + '/queue_id'
console.log('Sending data to ' + editLink)
$.ajax({
type: "POST",
url: editLink,
data:
'queue_id': newQueueId,
dataType: 'json',
success: (result) ->
$('.ticket-queue').data('id', newQueueId)
$('.ticket-queue > span').text(result.queue_name)
updateTicketModified(result.updated_at)
console.log('Ticket updated successfully.')
})
else
console.log('Invalid queue ID entered')
handleQueueClick(e, 'Invalid queue ID entered; you must enter a number.')
handleAttributeClick = (e, attr, fail = false) ->
newValue = prompt('Set ticket ' + attr + ':', $('.ticket-' + attr + ' > span').text())
newValue = newValue.toLowerCase()
if (newValue != null) and (newValue != '')
if (newValue in validOptions[attr])
console.log('Setting ' + attr + ' to ' + newValue)
editLink = $('#ticketEditLink').attr('href') + '/' + attr
postData = {}
postData[attr] = newValue
console.log('Sending data to ' + editLink)
$.ajax({
type: "POST",
url: editLink,
data: postData,
dataType: 'json',
success: (result) ->
newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1)
$('.ticket-' + attr + ' > span').text(newValue)
updateTicketModified(result.updated_at)
console.log('Ticket updated successfully.')
})
else
console.log('Invalid ' + attr + ' entered')
handleAttributeClick(e, attr, 'Invalid ' + attr + '; valid values are ' + validOptions.toString() + '.')
updateTicketModified = (date) ->
$('.ticket-updated > span').text(date)
console.log('Ticket update time is ' + date)

169
assets/sass/darkmeyer.sass Normal file
View File

@ -0,0 +1,169 @@
$primary-color: #009688
$primary-color-highlight: lighten($primary-color, 10%)
$accent-color: #795548
$accent-color-highlight: lighten($accent-color, 10%)
$box-shadow-1: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23)
$box-shadow-2: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)
$nav-height: 75px
body
padding-top: $nav-height
padding-bottom: 50px
background: lightgrey
font-size: 16px
a
color: $accent-color
transition: all 230ms ease-in-out
&:hover
color: $accent-color-highlight
.button.button-primary,
button.button-primary,
input[type="button"].button-primary,
input[type="reset"].button-primary,
input[type="submit"].button-primary
background-color: $primary-color
border-color: $primary-color
font-size: 1.5rem
transition: all 230ms ease-in-out
&:hover
background-color: $primary-color-highlight
border-color: $primary-color-highlight
.container
max-width: 1100px
padding: 20px 30px
.card
background: white
box-shadow: $box-shadow-2
#main-nav
position: fixed
top: 0
left: 0
width: 100%
height: $nav-height
background: $primary-color
color: white
font-size: 3rem
font-weight: bold
box-shadow: $box-shadow-1
z-index: 100
.nav-left
float: left
.nav-menu
list-style: none
padding-left: 15px
li
display: inline-block
margin-top: 12px
margin-left: 15px
.nav-link a
color: white
&:hover
color: #eee
#main-wrapper
margin-top: 25px
#ticket-form,
#comment-form
textarea
max-width: 100%
height: 250px
min-height: 100px
#queue-header,
#ticket-header
margin-bottom: 15px
.queue-title,
.ticket-title
margin-bottom: 5px
.queue-created,
.queue-updated,
.ticket-created,
.ticket-updated
margin-bottom: 3px
color: #666
font-size: 1.5rem
font-style: italic
#queue-description,
#ticket-body
p:last-child
margin-bottom: 5px
.ticket-attributes
list-style: none
margin: 0
border: 1px solid #bbb
border-bottom: none
box-shadow: $box-shadow-1
font-size: 1.75rem
> li
margin: 0
padding: 10px 12px
border-bottom: 1px solid #999
.ticket-actions
padding: 0
ul
list-style: none
margin: 0
li
display: inline
margin: 0
padding: 0
&:not(:first-child)
a
border-left: 1px solid #999
a
position: relative
display: inline-block
box-sizing: border-box
width: 50%
height: 100%
padding: 10px 12px
text-align: center
&:hover
background: rgba(0, 0, 0, .1)
i
margin-right: 5px
font-size: 2rem
.ticket-queue,
.ticket-severity,
.ticket-status
transition: all 230ms ease-in-out
&:hover
background: rgba(0, 0, 0, .1)
cursor: pointer
#comment-form
textarea
height: 150px
.comments-list
list-style: none
margin: 0
.comment
&:not(:last-child)
border-bottom: 1px solid #666
p:last-child
margin-bottom: 5px

View File

@ -20,6 +20,9 @@
"slim/psr7": "^1.6",
"php-di/php-di": "^6.4",
"slim/twig-view": "^3.3",
"hassankhan/config": "^3.0"
"hassankhan/config": "^3.0",
"illuminate/database": "^9.40",
"robmorgan/phinx": "^0.13.1",
"league/commonmark": "^2.3"
}
}

2307
composer.lock generated

File diff suppressed because it is too large Load Diff

0
data/.gitkeep Normal file
View File

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddTicketsTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('tickets');
$table->addColumn('title', 'string', ['null' => false])
->addColumn('body', 'text', ['null' => false])
->addColumn('severity', 'string', ['default' => 'low'])
->addColumn('due_at', 'datetime')
->addTimestamps()
->addIndex(['title', 'body', 'due_at', 'severity'])
->create();
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddStatusToTicketsTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('tickets');
$table->addColumn('status', 'string', ['null' => false, 'default' => 'open'])
->update();
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddCommentsTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
$table = $this->table('comments');
$table->addColumn('body', 'text', ['null' => false])
->addColumn('ticket_id', 'integer', ['null' => false])
->addTimestamps()
->addForeignKey('ticket_id', 'tickets', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->addIndex(['body'])
->create();
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Phinx\Migration\AbstractMigration;
final class AddQueueTable extends AbstractMigration
{
/**
* Change Method.
*
* Write your reversible migrations using this method.
*
* More information on writing migrations is available here:
* https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
*
* Remember to call "create()" or "update()" and NOT "save()" when working
* with the Table class.
*/
public function change()
{
// Create table for Queue objects
$table = $this->table('queues');
$table->addColumn('title', 'string', ['null' => false])
->addColumn('description', 'text', ['null' => false])
->addTimestamps()
->addIndex(['title'])
->create();
// Update tickets table to have a Queue ID field
$tickets = $this->table('tickets')
->addColumn('queue_id', 'integer', ['null' => false])
->addForeignKey('queue_id', 'queues', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE'])
->update();
}
}

2892
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "goliath",
"version": "1.0.0",
"description": "Open source internal ticketing software",
"main": "index.js",
"scripts": {
"grunt": "grunt",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.metaunix.net/BitGoblin/goliath"
},
"keywords": [
"ticketing"
],
"author": "Gregory Ballantine <gballantine@bitgoblin.tech>",
"license": "BSD-2-Clause",
"devDependencies": {
"grunt": "^1.5.3",
"grunt-contrib-coffee": "^2.1.0",
"grunt-contrib-sass": "^2.0.0",
"grunt-contrib-watch": "^1.1.0"
}
}

37
phinx.php Normal file
View File

@ -0,0 +1,37 @@
<?php
return
[
'paths' => [
'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds'
],
'environments' => [
'default_migration_table' => 'phinxlog',
'default_environment' => 'development',
'production' => [
'adapter' => 'mysql',
'host' => 'localhost',
'name' => 'production_db',
'user' => 'root',
'pass' => '',
'port' => '3306',
'charset' => 'utf8',
],
'development' => [
'adapter' => 'sqlite',
'name' => './data/goliath',
'suffix' => '.db'
],
'testing' => [
'adapter' => 'mysql',
'host' => 'localhost',
'name' => 'testing_db',
'user' => 'root',
'pass' => '',
'port' => '3306',
'charset' => 'utf8',
]
],
'version_order' => 'creation'
];

View File

@ -0,0 +1,29 @@
<?php
namespace BitGoblin\Goliath\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteContext;
use BitGoblin\Goliath\Models\Comment;
class CommentController extends Controller {
public function postAdd(Request $request, Response $response): Response {
$params = (array)$request->getParsedBody();
$comment = new Comment;
$comment->body = $params['comment_body'];
$comment->ticket_id = $params['ticket_id'];
$comment->save();
// redirect the user back to the home page
$routeContext = RouteContext::fromRequest($request);
$routeParser = $routeContext->getRouteParser();
return $response
->withHeader('Location', $routeParser->urlFor('ticket.view', ['ticket_id' => $comment->ticket->id]))
->withStatus(302);
}
}

View File

@ -5,12 +5,18 @@ namespace BitGoblin\Goliath\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use BitGoblin\Goliath\Models\Ticket;
class HomeController extends Controller {
public function getIndex(Request $request, Response $response): Response {
// find tickets from the database
$tickets = Ticket::all();
$view = Twig::fromRequest($request);
return $view->render($response, 'index.twig');
return $view->render($response, 'index.twig', [
'tickets' => $tickets,
]);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace BitGoblin\Goliath\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteContext;
use Slim\Views\Twig;
use BitGoblin\Goliath\Models\Queue;
class QueueController extends Controller {
public function getView(Request $request, Response $response, array $args): Response {
$queue = Queue::where('id', $args['queue_id'])->first();
$view = Twig::fromRequest($request);
return $view->render($response, 'queue/view.twig', [
'queue' => $queue,
]);
}
public function getCreate(Request $request, Response $response): Response {
$view = Twig::fromRequest($request);
return $view->render($response, 'queue/create.twig');
}
public function postCreate(Request $request, Response $response): Response {
$params = (array)$request->getParsedBody();
$queue = new Queue;
$queue->title = $params['queue_title'];
$queue->description = $params['queue_description'];
$queue->save();
// redirect the user back to the home page
$routeContext = RouteContext::fromRequest($request);
$routeParser = $routeContext->getRouteParser();
return $response
->withHeader('Location', $routeParser->urlFor('queue.view', ['queue_id' => $queue->id]))
->withStatus(302);
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace BitGoblin\Goliath\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteContext;
use Slim\Views\Twig;
use BitGoblin\Goliath\Models\Queue;
use BitGoblin\Goliath\Models\Ticket;
class TicketController extends Controller {
public function getView(Request $request, Response $response, array $args): Response {
$ticket = Ticket::where('id', $args['ticket_id'])->first();
$view = Twig::fromRequest($request);
return $view->render($response, 'ticket/view.twig', [
'ticket' => $ticket,
]);
}
public function getCreate(Request $request, Response $response): Response {
$queues = Queue::all();
$view = Twig::fromRequest($request);
return $view->render($response, 'ticket/create.twig', [
'queues' => $queues,
]);
}
public function postCreate(Request $request, Response $response): Response {
$params = (array)$request->getParsedBody();
$ticket = new Ticket;
$ticket->title = $params['ticket_title'];
$ticket->body = $params['ticket_body'];
$ticket->severity = $params['ticket_severity'];
$ticket->queue_id = $params['ticket_queue'];
$ticket->due_at = $params['ticket_due'];
$ticket->save();
// redirect the user back to the home page
return $response
->withHeader('Location', '/')
->withStatus(302);
}
public function getEdit(Request $request, Response $response, array $args): Response {
$ticket = Ticket::where('id', $args['ticket_id'])->get();
$view = Twig::fromRequest($request);
return $view->render($response, 'ticket/edit.twig', [
'ticket' => $ticket[0],
]);
}
public function postEdit(Request $request, Response $response, array $args): Response {
$ticket = Ticket::where('id', $args['ticket_id'])->first();
$params = (array)$request->getParsedBody();
$ticket->title = $params['ticket_title'];
$ticket->body = $params['ticket_body'];
$ticket->severity = $params['ticket_severity'];
$ticket->due_at = $params['ticket_due'];
$ticket->save();
// redirect the user back to the home page
$routeContext = RouteContext::fromRequest($request);
$routeParser = $routeContext->getRouteParser();
return $response
->withHeader('Location', $routeParser->urlFor('ticket.view', ['ticket_id' => $ticket->id]))
->withStatus(302);
}
public function postEditAttr(Request $request, Response $response, array $args): Response {
// grab ticket from database
$ticket = Ticket::where('id', $args['ticket_id'])->first();
// edit the specified attribute
$params = (array)$request->getParsedBody();
$attr = $args['attr'];
$ticket->$attr = $params[$attr];
// save updated ticket
$ticket->save();
// build JSON response
$jsonResponse = [
'result' => 'success',
'updated_at' => $ticket->formatUpdatedAt(),
];
if ($attr == 'queue_id') {
$jsonResponse['queue_name'] = $ticket->queue->title;
}
// return a response
$response->getBody()->write(json_encode($jsonResponse));
return $response;
}
public function getDelete(Request $request, Response $response, array $args): Response {
$ticket = Ticket::where('id', $args['ticket_id'])->first();
$ticket->delete();
return $response
->withHeader('Location', '/')
->withStatus(302);
}
}

28
src/Models/Comment.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace BitGoblin\Goliath\Models;
use Illuminate\Database\Eloquent\Model;
use League\CommonMark\CommonMarkConverter;
class Comment extends Model {
protected $fillable = [
'body',
'ticket_id',
];
public function render(): string {
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
return $converter->convert($this->body);
}
public function ticket() {
return $this->belongsTo(Ticket::class);
}
}

36
src/Models/Queue.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace BitGoblin\Goliath\Models;
use Illuminate\Database\Eloquent\Model;
use League\CommonMark\CommonMarkConverter;
class Queue extends Model {
protected $fillable = [
'title',
'description',
];
public function tickets() {
return $this->hasMany(Ticket::class);
}
public function render(): string {
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
return $converter->convert($this->description);
}
public function formatCreatedAt(): string {
return date_format(date_create($this->created_at), "F jS\\, Y \\a\\t g:i:s a");
}
public function formatUpdatedAt(): string {
return date_format(date_create($this->updated_at), "F jS\\, Y \\a\\t g:i:s a");
}
}

42
src/Models/Ticket.php Normal file
View File

@ -0,0 +1,42 @@
<?php
namespace BitGoblin\Goliath\Models;
use Illuminate\Database\Eloquent\Model;
use League\CommonMark\CommonMarkConverter;
class Ticket extends Model {
protected $fillable = [
'title',
'body',
'severity',
'due_at',
];
public function queue() {
return $this->belongsTo(Queue::class);
}
public function comments() {
return $this->hasMany(Comment::class);
}
public function render(): string {
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
return $converter->convert($this->body);
}
public function formatCreatedAt(): string {
return date_format(date_create($this->created_at), "F jS\\, Y \\a\\t g:i:s a");
}
public function formatUpdatedAt(): string {
return date_format(date_create($this->updated_at), "F jS\\, Y \\a\\t g:i:s a");
}
}

View File

@ -18,10 +18,16 @@ $container->set('config', function () use ($config) {
return $config;
});
// Load database configuration
require_once __DIR__ . '/database.php';
// Set container to create App with on AppFactory
AppFactory::setContainer($container);
$app = AppFactory::create();
// Allow body parsing for POST parameters
$app->addBodyParsingMiddleware();
// Add Error Handling Middleware
$app->addErrorMiddleware(true, false, false);

10
src/database.php Normal file
View File

@ -0,0 +1,10 @@
<?php
$capsule = new \Illuminate\Database\Capsule\Manager;
$capsule->addConnection($config->get('database'));
$capsule->setAsGlobal();
$capsule->bootEloquent();
$container->set('db', function () use ($capsule) {
return $capsule;
});

View File

@ -6,3 +6,28 @@ use Slim\Views\Twig;
// index GET route - this page should welcome the user and direct them to the available actions
$app->get('/', '\\BitGoblin\\Goliath\\Controllers\\HomeController:getIndex')->setName('index');
// /queue/create routes - allows a user to fill out a form to create a new queue
$app->get('/queue/create', '\\BitGoblin\\Goliath\\Controllers\\QueueController:getCreate')->setName('queue.create');
$app->post('/queue/create', '\\BitGoblin\\Goliath\\Controllers\\QueueController:postCreate');
// /queue/id route - displays queue info to user
$app->get('/queue/{queue_id}', '\\BitGoblin\\Goliath\\Controllers\QueueController:getView')->setName('queue.view');
// /ticket/create routes - allows a user to fill out a form to create a ticket
$app->get('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getCreate')->setName('ticket.create');
$app->post('/ticket/create', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postCreate');
// /ticket/edit routes - update a ticket!
$app->get('/ticket/{ticket_id}/edit', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getEdit')->setName('ticket.edit');
$app->post('/ticket/{ticket_id}/edit', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postEdit')->setName('ticket.edit');
$app->post('/ticket/{ticket_id}/edit/{attr}', '\\BitGoblin\\Goliath\\Controllers\\TicketController:postEditAttr')->setName('ticket.edit.attr');
// ticket deletion route
$app->get('/ticket/{ticket_id}/delete', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getDelete')->setName('ticket.delete');
// /ticket/id route - displays ticket info to user
$app->get('/ticket/{ticket_id}', '\\BitGoblin\\Goliath\\Controllers\\TicketController:getView')->setName('ticket.view');
// add a comment to a ticket
$app->post('/comment/add', '\\BitGoblin\\Goliath\\Controllers\\CommentController:postAdd')->setName('comment.add');

View File

@ -3,5 +3,37 @@
{% block title %}Home{% endblock %}
{% block content %}
<p>This is a test.</p>
<div class="row">
<div class="columns twelve">
<h1>Welcome to Goliath!</h1>
</div>
</div>
<div class="row">
{% if tickets|length > 0 %}
<table class="columns twelve">
<thead>
<th>Title</th>
<th>Severity</th>
<th>Due date</th>
<th>Actions</th>
</thead>
<tbody>
{% for ticket in tickets %}
<tr>
<td><a href="{{ url_for('ticket.view', {ticket_id: ticket.id}) }}">{{ ticket.title }}</a></td>
<td>{{ ticket.severity }}</td>
<td>{{ ticket.due_at ? ticket.due_at : 'N/a' }}</td>
<td>
<a href="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-pen-to-square"></i></a>
<a href="{{ url_for('ticket.delete', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>There a no tickets in the database at this time.</p>
{% endif %}
</div>
{% endblock %}

View File

@ -6,9 +6,25 @@
<title>{% block title %}{% endblock %} | Goliath</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<link rel="stylesheet" href="/styles/darkmeyer.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="/js/slepe.js"></script>
</head>
<body>
{% block content %}{% endblock %}
<nav id="main-nav">
<div class="nav-left">
<ul class="nav-menu">
<li>Goliath</li>
<li class="nav-link"><a href="{{ url_for('index') }}">Home</a></li>
<li class="nav-link"><a href="{{ url_for('queue.create') }}">New Queue</a></li>
<li class="nav-link"><a href="{{ url_for('ticket.create') }}">New Ticket</a></li>
<li class="nav-link"><a href="/search">Search</a></li>
</ul>
</div>
</nav>
<div id="main-wrapper" class="card container">
{% block content %}{% endblock %}
</div>
</body>
</html>

29
views/queue/create.twig Normal file
View File

@ -0,0 +1,29 @@
{% extends 'layout.twig' %}
{% block title %}Create New Queue{% endblock %}
{% block content %}
<div class="row">
<div class="columns twelve">
<h1>Create new queue</h1>
</div>
</div>
<div class="row">
<div class="columns twelve">
<form id="queue-form" action="/queue/create" method="POST" class="u-full-width">
<div class="row">
<div class="twelve columns">
<label for="queue_title">Title</label>
<input id="queue_title" class="u-full-width" type="text" placeholder="My new queue" name="queue_title">
</div>
</div>
<label for="queue_description">Description</label>
<textarea id="queue_description" class="u-full-width" placeholder="Explain what this queue is about..." name="queue_description"></textarea>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>
{% endblock %}

44
views/queue/view.twig Normal file
View File

@ -0,0 +1,44 @@
{% extends 'layout.twig' %}
{% block title %}Ticket Queue: {{ queue.title }}{% endblock %}
{% block content %}
<!-- queue content -->
<div class="row">
<div class="twelve columns">
<div id="queue-header" class="row">
<div class="columns twelve">
<h1 class="queue-title">{{ queue.title }}</h1>
<h4 class="queue-created">Created at: <span>{{ queue.formatCreatedAt() }}</span></h4>
<h4 class="queue-updated">Last updated at: <span>{{ queue.formatUpdatedAt() }}</span></h4>
</div>
</div>
<div id="queue-description" class="row">
<div class="columns twelve">
{{ queue.render() | raw }}
</div>
</div>
</div>
</div>
<hr>
<!-- queue tickets -->
<div class="row">
<div class="twelve columns">
<ul id="queue-list" class="row">
<h3>Tickets in this queue:</h3>
{% if queue.tickets | length > 0 %}
{% for ticket in queue.tickets %}
<li>
<a href="{{ url_for('ticket.view', { ticket_id: ticket.id }) }}">{{ ticket.title }}</a>
</li>
{% endfor %}
{% else %}
<p>There are no tickets in this queue.</p>
{% endif %}
</ul>
</div>
</div>
{% endblock %}

49
views/ticket/create.twig Normal file
View File

@ -0,0 +1,49 @@
{% extends 'layout.twig' %}
{% block title %}Create new ticket{% endblock %}
{% block content %}
<div class="row">
<div class="columns twelve">
<h1>Create new ticket</h1>
</div>
</div>
<div class="row">
<div class="columns twelve">
<form id="ticket-form" action="/ticket/create" method="POST" class="u-full-width">
<div class="row">
<div class="six columns">
<label for="ticket_title">Title</label>
<input id="ticket_title" class="u-full-width" type="text" placeholder="My new ticket" name="ticket_title">
</div>
<div class="two columns">
<label for="ticket_queue">Queue</label>
<select name="ticket_queue" id="ticket_queue" class="u-full-width">
{% for q in queues %}
<option value="{{ q.id }}">{{ q.title }}</option>
{% endfor %}
</select>
</div>
<div class="two columns">
<label for="ticket_due">Due at</label>
<input id="ticket_due" class="u-full-width" type="datetime-local" name="ticket_due">
</div>
<div class="two columns">
<label for="ticket_severity">Severity level</label>
<select id="ticket_severity" class="u-full-width" name="ticket_severity">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<label for="ticket_body">Ticket body</label>
<textarea id="ticket_body" class="u-full-width" placeholder="Explain what this ticket is about..." name="ticket_body"></textarea>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>
{% endblock %}

41
views/ticket/edit.twig Normal file
View File

@ -0,0 +1,41 @@
{% extends 'layout.twig' %}
{% block title %}Editing ticket{% endblock %}
{% block content %}
<div class="row">
<div class="columns twelve">
<h1>Editing "{{ ticket.title }}"</h1>
</div>
</div>
<div class="row">
<div class="columns twelve">
<form id="ticket-form" action="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}" method="POST" class="u-full-width">
<div class="row">
<div class="six columns">
<label for="ticket_title">Title</label>
<input id="ticket_title" class="u-full-width" type="text" placeholder="My new ticket" name="ticket_title" value="{{ ticket.title }}">
</div>
<div class="columns three">
<label for="ticket_due">Due at</label>
<input id="ticket_due" class="u-full-width" type="datetime-local" name="ticket_due" value="{{ ticket.due_at }}">
</div>
<div class="three columns">
<label for="ticket_severity">Severity level</label>
<select id="ticket_severity" class="u-full-width" name="ticket_severity" value="{{ ticket.severity }}">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
<label for="ticket_body">Ticket body</label>
<textarea id="ticket_body" class="u-full-width" placeholder="Explain what this ticket is about..." name="ticket_body">{{ ticket.body }}</textarea>
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>
{% endblock %}

81
views/ticket/view.twig Normal file
View File

@ -0,0 +1,81 @@
{% extends 'layout.twig' %}
{% block title %}{{ ticket.title }}{% endblock %}
{% block content %}
<div class="row">
<!-- ticket content -->
<div class="nine columns">
<div id="ticket-header" class="row">
<div class="columns twelve">
<h1 class="ticket-title">{{ ticket.title }}</h1>
<h4 class="ticket-created">Created at: <span>{{ ticket.formatCreatedAt() }}</span></h4>
<h4 class="ticket-updated">Last updated at: <span>{{ ticket.formatUpdatedAt() }}</span></h4>
</div>
</div>
<div id="ticket-body" class="row">
<div class="columns twelve">
{{ ticket.render() | raw }}
</div>
</div>
</div>
<!-- ticket status column -->
<div class="three columns">
<ul class="ticket-attributes">
<li class="ticket-actions">
<ul>
<li><a id="ticketEditLink" href="{{ url_for('ticket.edit', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-pen-to-square"></i>Edit</a></li><li><a href="{{ url_for('ticket.delete', {ticket_id: ticket.id}) }}"><i class="fa-solid fa-trash"></i>Delete</a></li>
</ul>
</li>
<li class="ticket-queue" data-id="{{ ticket.queue.id }}">
Queue: <span>{{ ticket.queue.title }}</span>
</li>
<li class="ticket-severity">
Severity: <span>{{ ticket.severity | capitalize }}</span>
</li>
<li class="ticket-status">
Status: <span>{{ ticket.status | capitalize }}</span>
</li>
<li>
Assignee(s): N/a
</li>
</ul>
</div>
</div>
<hr>
<!-- ticket updates -->
<div class="row">
<div class="twelve columns">
<h3>Ticket comments/Updates</h3>
{% if ticket.comments | length > 0 %}
<ul class="comments-list">
{% for comment in ticket.comments %}
<li class="comment">{{ comment.render() | raw }}</li>
{% endfor %}
</ul>
{% else %}
<p>There are no comments to display at this time.</p>
{% endif %}
</div>
</div>
<hr>
<!-- add a comment form -->
<div class="row">
<div class="twelve columns">
<form id="comment-form" action="{{ url_for('comment.add') }}" method="POST" class="u-full-width">
<label for="comment_body">Add a comment:</label>
<textarea id="comment_body" class="u-full-width" placeholder="Add a comment..." name="comment_body"></textarea>
<input type="hidden" name="ticket_id" value="{{ ticket.id }}">
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>
{% endblock %}