Compare commits

...

15 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
21 changed files with 1238 additions and 27 deletions

View File

@ -1,2 +1,68 @@
$(document).ready ->
console.log('Hello, world!')
$('.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)

View File

@ -1,29 +1,58 @@
$primary-color: yellow
$primary-color-highlight: darken($primary-color, 10%)
$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: 60px
$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: #212121
color: #eee
font-size: 2.75rem
background: $primary-color
color: white
font-size: 3rem
font-weight: bold
box-shadow: $box-shadow-1
z-index: 100
.nav-left
float: left
@ -34,16 +63,107 @@ body
li
display: inline-block
margin-top: 6px
margin-top: 12px
margin-left: 15px
.nav-link a
color: $primary-color
transition: all 230ms ease-in-out
color: white
&:hover
color: $primary-color-highlight
color: #eee
#main-wrapper
margin-top: 25px
background: white
#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

@ -22,6 +22,7 @@
"slim/twig-view": "^3.3",
"hassankhan/config": "^3.0",
"illuminate/database": "^9.40",
"robmorgan/phinx": "^0.13.1"
"robmorgan/phinx": "^0.13.1",
"league/commonmark": "^2.3"
}
}

462
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c1bc6d458a5bde9f693cfe4ca5d4f820",
"content-hash": "f97fc3dd5b9c0a911f5aa8037accb13f",
"packages": [
{
"name": "cakephp/core",
@ -236,6 +236,81 @@
},
"time": "2022-10-10T18:01:10+00:00"
},
{
"name": "dflydev/dot-access-data",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "f41715465d65213d644d3141a6a93081be5d3549"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549",
"reference": "f41715465d65213d644d3141a6a93081be5d3549",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2"
},
"time": "2022-10-27T11:44:00+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.0.6",
@ -890,6 +965,194 @@
},
"time": "2022-09-08T13:45:54+00:00"
},
{
"name": "league/commonmark",
"version": "2.3.7",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "a36bd2be4f5387c0f3a8792a0d76b7d68865abbf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/a36bd2be4f5387c0f3a8792a0d76b7d68865abbf",
"reference": "a36bd2be4f5387c0f3a8792a0d76b7d68865abbf",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.30.0",
"commonmark/commonmark.js": "0.30.0",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.4-dev"
}
},
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2022-11-03T17:29:46+00:00"
},
{
"name": "league/config",
"version": "v1.1.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
"reference": "a9d39eeeb6cc49d10a6e6c36f22c4c1f4a767f3e",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.90",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"time": "2021-08-14T12:15:32+00:00"
},
{
"name": "nesbot/carbon",
"version": "2.63.0",
@ -992,6 +1255,153 @@
],
"time": "2022-10-30T18:34:28+00:00"
},
{
"name": "nette/schema",
"version": "v1.2.3",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f",
"reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f",
"shasum": ""
},
"require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0",
"php": ">=7.1 <8.3"
},
"require-dev": {
"nette/tester": "^2.3 || ^2.4",
"phpstan/phpstan-nette": "^1.0",
"tracy/tracy": "^2.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.3"
},
"time": "2022-10-13T01:24:26+00:00"
},
{
"name": "nette/utils",
"version": "v3.2.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/02a54c4c872b99e4ec05c4aec54b5a06eb0f6368",
"reference": "02a54c4c872b99e4ec05c4aec54b5a06eb0f6368",
"shasum": ""
},
"require": {
"php": ">=7.2 <8.3"
},
"conflict": {
"nette/di": "<3.0.6"
},
"require-dev": {
"nette/tester": "~2.0",
"phpstan/phpstan": "^1.0",
"tracy/tracy": "^2.3"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()",
"ext-xml": "to use Strings::length() etc. when mbstring is not available"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v3.2.8"
},
"time": "2022-09-12T23:36:20+00:00"
},
{
"name": "nikic/fast-route",
"version": "v1.3.0",
@ -1263,6 +1673,56 @@
},
"time": "2021-11-05T16:50:12+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "psr/http-factory",
"version": "1.0.1",

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();
}
}

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

@ -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

@ -4,14 +4,29 @@ 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 getCreate(Request $request, Response $response): Response {
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/create.twig');
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 {
@ -21,6 +36,7 @@ class TicketController extends Controller {
$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();
@ -31,4 +47,68 @@ class TicketController extends Controller {
->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");
}
}

View File

@ -3,6 +3,7 @@
namespace BitGoblin\Goliath\Models;
use Illuminate\Database\Eloquent\Model;
use League\CommonMark\CommonMarkConverter;
class Ticket extends Model {
@ -13,4 +14,29 @@ class Ticket extends Model {
'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

@ -7,6 +7,27 @@ 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');
// /ticket/create GET route - allows a user to fill out a form to create a ticket
// /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

@ -16,12 +16,19 @@
<th>Title</th>
<th>Severity</th>
<th>Due date</th>
<th>Actions</th>
</thead>
<tbody>
{% for ticket in tickets %}
<td>{{ ticket.title }}</td>
<td>{{ ticket.severity }}</td>
<td>{{ ticket.due_at }}</td>
<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>

View File

@ -16,13 +16,14 @@
<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('ticket.create') }}">Create</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="container">
<div id="main-wrapper" class="card container">
{% block content %}{% endblock %}
</div>
</body>

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 %}

View File

@ -11,17 +11,25 @@
<div class="row">
<div class="columns twelve">
<form action="/ticket/create" method="POST" class="u-full-width">
<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="columns three">
<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="three columns">
<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>
@ -34,7 +42,7 @@
<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" type="submit" value="Submit">
<input class="button-primary u-full-width" type="submit" value="Submit">
</form>
</div>
</div>

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 %}