diff --git a/app/controllers/TicketController.php b/app/controllers/TicketController.php index 1282c38..80f2973 100644 --- a/app/controllers/TicketController.php +++ b/app/controllers/TicketController.php @@ -36,7 +36,20 @@ class TicketController extends BaseController implements CRUD { $ticket_id = $f3->get('PARAMS.id'); $ticket_mapper = new Ticket($this->getDB()); $ticket = $ticket_mapper->findById($ticket_id); + + if(!$ticket){ + $this->f3->set('SESSION.error', 'Ticket not found'); + $this->f3->reroute('/tickets'); + return; + } + $assigned_user = $ticket->getAssignedUser(); + $ticket_history = $ticket->getHistory(); + + $map_statuses = array_column((new TicketStatus($this->getDB()))->findAll(), 'name', 'id'); + $map_priorities = array_column((new TicketStatus($this->getDB()))->findAll(), 'name', 'id'); + $map_users = array_column($this->getDB()->exec('SELECT id, display_name FROM users'), 'display_name', 'id'); + // render $this->renderView('views/ticket/view.html', [ @@ -46,7 +59,13 @@ class TicketController extends BaseController implements CRUD { 'comments' => $ticket->comments(), 'parent_tickets' => $ticket->getParentTickets(), 'child_tickets' => $ticket->getChildTickets(), - 'ticket_meta' => $ticket->getMetaAssoc() + 'ticket_meta' => $ticket->getMetaAssoc(), + 'ticket_history' => $ticket_history, + 'map' => [ + 'statuses' => $map_statuses, + 'priorities' => $map_priorities, + 'users' => $map_users + ] ]); } @@ -58,6 +77,9 @@ class TicketController extends BaseController implements CRUD { $priorities = (new TicketPriority($db))->findAll(); $statuses = (new TicketStatus($db))->findAll(); + $all_tags_model = new \Tag($this->getDB()); + $all_tags = $all_tags_model->find([], ['order' => 'name ASC']); // get all tags + // TODO: this needs moving into a model? $users = $this->getDB()->exec('SELECT id, username, display_name FROM users ORDER BY display_name ASC'); $users = array_merge([['id'=>'-1', 'display_name'=>'--']], $users); @@ -66,7 +88,8 @@ class TicketController extends BaseController implements CRUD { $this->renderView('views/ticket/create.html',[ 'priorities' => $priorities, 'statuses' => $statuses, - 'users' => $users + 'users' => $users, + 'all_tags' => $all_tags ]); } @@ -83,18 +106,29 @@ class TicketController extends BaseController implements CRUD { 'description' => $this->f3->get('POST.description'), 'priority_id' => $this->f3->get('POST.priority_id'), 'status_id' => $this->f3->get('POST.status_id'), - 'created_by' => $this->f3->get('SESSION.user.id') + 'created_by' => $this->f3->get('SESSION.user.id'), + 'assigned_to' => $this->f3->get('POST.assigned_to') == '-1' ? null : $this->f3->get('POST.assigned_to') ]; $ticket_mapper = new Ticket($this->getDB()); $new_ticket_id = $ticket_mapper->createTicket($data); // custom field - $meta_keys = $this->f3->get('POST.meta_key'); - $meta_values = $this->f3->get('POST.meta_value'); - $meta_assoc = $ticket_mapper->assocMetaFromKeyValue($meta_keys, $meta_values); - $ticket_mapper->setCustomFields($meta_assoc); + // $meta_keys = $this->f3->get('POST.meta_key'); + // $meta_values = $this->f3->get('POST.meta_value'); + // $meta_assoc = $ticket_mapper->assocMetaFromKeyValue($meta_keys, $meta_values); + // $ticket_mapper->setCustomFields($meta_assoc); + $new_ticket = $ticket_mapper->findById($new_ticket_id); + if($new_ticket){ + // TAG handling for create + $posted_tags = $this->f3->get('POST.tags'); + if(!empty($posted_tags) && is_array($posted_tags)){ + $new_ticket->setTags($posted_tags); + } + } + + $this->f3->set('SESSION.message', 'Ticket #' . $new_ticket_id . ' created successfully.'); $this->f3->reroute('/ticket/' . $new_ticket_id); } @@ -120,16 +154,29 @@ class TicketController extends BaseController implements CRUD { // dropdowns $priorities = (new TicketPriority($this->getDB()))->findAll(); $statuses = (new TicketStatus($this->getDB()))->findAll(); + $all_tags_model = new \Tag($this->getDB()); + $all_tags = $all_tags_model->find([], ['order' => 'name ASC']); + // TODO: this needs moving into a model? $users = $this->getDB()->exec('SELECT id, username, display_name FROM users ORDER BY display_name ASC'); $users = array_merge([['id'=>'-1', 'display_name'=>'--']], $users); + // paradox - empty($ticket->tags) was returning true, when there were items present + $current_ticket_tag_ids = []; + if(count($ticket->tags) > 0){ + foreach($ticket->tags as $current_tag_data){ + $current_ticket_tag_ids[] = $current_tag_data['id']; + } + } + $this->renderView('views/ticket/edit.html',[ 'ticket' => $ticket, 'ticket_meta' => $ticket->getMeta(), 'priorities' => $priorities, 'statuses' => $statuses, - 'users' => $users + 'users' => $users, + 'all_tags' => $all_tags, + 'current_ticket_tag_ids' => $current_ticket_tag_ids ] ); return; @@ -142,32 +189,40 @@ class TicketController extends BaseController implements CRUD { $this->requireLogin(); $this->checkCSRF($f3, '/ticket/create'); - $ticket_id = $this->f3->get('PARAMS.id'); + $ticket_id = $f3->get('PARAMS.id'); $ticket_mapper = new Ticket($this->getDB()); $ticket = $ticket_mapper->findById($ticket_id); if(!$ticket){ - $this->f3->set('SESSION.error', 'Ticket not found.'); - $this->f3->reroute('/tickets'); + $f3->set('SESSION.error', 'Ticket not found.'); + $f3->reroute('/tickets'); } $data = [ - 'title' => $this->f3->get('POST.title'), - 'created_at' => $this->f3->get('POST.created_at'), - 'description' => $this->f3->get('POST.description'), - 'priority_id' => $this->f3->get('POST.priority_id'), - 'status_id' => $this->f3->get('POST.status_id'), - 'updated_by' => $this->f3->get('SESSION.user.id') , - 'assigned_to' => $this->f3->get('POST.assigned_to') ?: null + 'title' => $f3->get('POST.title'), + 'created_at' => $f3->get('POST.created_at'), + 'description' => $f3->get('POST.description'), + 'priority_id' => $f3->get('POST.priority_id'), + 'status_id' => $f3->get('POST.status_id'), + 'updated_by' => $f3->get('SESSION.user.id') , + 'assigned_to' => $f3->get('POST.assigned_to') == -1 ? null : $f3->get('POST.assigned_to') ]; $ticket->updateTicket($data); // deal with meta data / custom fields - $meta_keys = $this->f3->get('POST.meta_key'); - $meta_values = $this->f3->get('POST.meta_value'); - $meta_assoc = $ticket->assocMetaFromKeyValue($meta_keys, $meta_values); - $ticket->setCustomFields($meta_assoc); + // $meta_keys = $this->f3->get('POST.meta_key'); + // $meta_values = $this->f3->get('POST.meta_value'); + // $meta_assoc = $ticket->assocMetaFromKeyValue($meta_keys, $meta_values); + // $ticket->setCustomFields($meta_assoc); + $posted_tags = $f3->get('POST.tags'); + if(is_array($posted_tags)){ + $ticket->setTags($posted_tags); + } elseif (empty($posted_tags)){ + $ticket->setTags([]); + } + + $f3->set('SESSION.message', 'Ticket #' . $ticket_id . ' updated successfully.') ; $f3->reroute('/ticket/' . $ticket_id); } diff --git a/app/extensions/BulmaFormHelper.php b/app/extensions/BulmaFormHelper.php index 4b8fab8..167cf65 100644 --- a/app/extensions/BulmaFormHelper.php +++ b/app/extensions/BulmaFormHelper.php @@ -14,7 +14,11 @@ class BulmaFormHelper extends \Prefab { static public function render($node) { $attr = $node['@attrib'] ?? []; - $type = strtoupper($attr['type']) ?? null; + if(isset($attr['type'])){ + $type = strtoupper($attr['type']); + } else { + $type = null; + } // all * $label = $attr['label'] ?? ''; diff --git a/app/extensions/IconsHelper.php b/app/extensions/IconsHelper.php index 00b6b72..569592f 100644 --- a/app/extensions/IconsHelper.php +++ b/app/extensions/IconsHelper.php @@ -46,8 +46,9 @@ class IconsHelper extends \Prefab { switch($attr['type']){ case 'status-selector': - $selected = Base::instance()->get('GET.status') ?: null; - return 'renderStatusSelector("'.$selected.'", "'.$path.'"); ?>'; + // $selected = Base::instance()->get('GET.status') ?: null; + return 'renderStatusSelector( + Base::instance()->get("GET.status") ?: null, "'.$path.'"); ?>'; return self::renderStatusSelector($selected, $path); default: @@ -87,7 +88,7 @@ class IconsHelper extends \Prefab { foreach (self::$status_icons as $k => $icon) { $active = ($current_status == $k); $url = $path . ($active ? '' : '/?status=' . $k); - $class = 'button' . ($active ? ' is-primary' : ''); + $class = 'button' . ($active ? ' is-inverted' : ''); $output .= '

'; $output .= ''; diff --git a/app/models/Ticket.php b/app/models/Ticket.php index 25302a0..3f1ac95 100644 --- a/app/models/Ticket.php +++ b/app/models/Ticket.php @@ -63,6 +63,7 @@ class Ticket extends \DB\SQL\Mapper { public function getTagsForTickets(array $tickets) { + if(empty($tickets)) return []; $tag_mapper = new Tag($this->db, 'ticket'); $tickets = $tag_mapper->getTagsFor($tickets); @@ -74,8 +75,34 @@ class Ticket extends \DB\SQL\Mapper { $this->status_name = 'SELECT name FROM ticket_statuses WHERE tickets.status_id = ticket_statuses.id'; $this->priority_name = 'SELECT name FROM ticket_priorities WHERE tickets.priority_id = ticket_priorities.id'; $this->load(['id = ?', $id]); - $this->tags = (new Tag($this->db,'ticket'))->getTagsForID($id, 'ticket_id'); - return $this->dry() ? null : $this; + if($this->dry()){ + return null; + } + $tag_model = new Tag($this->db, 'ticket'); + $this->tags = $tag_model->getTagsForID($this->id, 'ticket_id'); + return $this; + } + + public function setTags(array $tags_ids):void { + if($this->dry() || !$this->id){ + // can't set tags for a ticket that hasn't been saved or loaded + return; + } + + // remove existing tags - TODO: shouldn't this be in the tag model? + $this->db->exec('DELETE FROM ticket_tags WHERE ticket_id = ?', [$this->id]); + + if(!empty($tags_ids)){ + $sql_insert_tag = 'INSERT INTO ticket_tags (ticket_id, tag_id) VALUES (?,?)'; + foreach($tags_ids as $tag_id){ + if(filter_var($tag_id, FILTER_VALIDATE_INT)){ + $this->db->exec($sql_insert_tag, [$this->id, (int)$tag_id]); + } + } + } + // refresh tags + $tag_model = new Tag($this->db, 'ticket'); + $this->tags = $tag_model->getTagsForID($this->id, 'ticket_id'); } public function createTicket(array $data): int @@ -93,11 +120,15 @@ class Ticket extends \DB\SQL\Mapper { $this->updated_at = date('Y-m-d H:i:s'); $this->save(); + $this->logCreate(); return (int)$this->id; } public function updateTicket(array $data): void { + + $this->logDiff($data); + if(isset($data['title'])){ $this->title = $data['title']; } if(isset($data['description'])) { $this->description = $data['description']; } if(isset($data['priority_id'])) { $this->priority_id = $data['priority_id']; } @@ -113,8 +144,6 @@ class Ticket extends \DB\SQL\Mapper { $this->created_at = ($data['created_at'] == '' ? date('Y-m-d H:i:s') : $data['created_at']) ?? date('Y-m-d H:i:s'); $this->updated_at = date('Y-m-d H:i:s'); - // printf('

%s
', print_r($this,1)); exit(); - $this->save(); } @@ -244,5 +273,79 @@ class Ticket extends \DB\SQL\Mapper { $user = $this->db->exec($sql, [$this->assigned_to]); return $user[0]; } + + + /** + * Logs a change to the ticket history. + * @param int $user_id - the ID of the user making the change. + * @param string $field_changed - the name of the field that was changed. + * @param string|null $old_value - the old value + * @param string|null $new_value - the new value + */ + public function logHistory(int $user_id, string $field_changed, + $old_value = null, $new_value = null, + $changed_at = null): void + { + if($this->dry() || !$this->id) return; + + $history = new \DB\SQL\Mapper($this->db, 'ticket_history'); + $history->ticket_id = $this->id; + $history->user_id = $user_id; + $history->field_changed = $field_changed; + $history->old_value = $old_value === null ? null : (string)$old_value; + $history->new_value = $new_value === null ? null : (string)$new_value; + if($changed_at == null){ + $history->changed_at = date('Y-m-d H:i:s'); + } else { + $history->changed_at = $changed_at; + } + $history->save(); + } + + public function getHistory(): array { + if($this->dry() || !$this->id) return[]; + + $sql = 'SELECT th.*, u.display_name as user_display_name + FROM ticket_history th + JOIN users u ON th.user_id = u.id + WHERE th.ticket_id = ? + ORDER BY th.changed_at DESC'; + return $this->db->exec($sql, [$this->id]); + } + + /** + * called from create + */ + public function logCreate(){ + $changed_at = date('Y-m-d H:i:s'); + $user_making_change = \Base::instance()->get('SESSION.user.id'); + $this->logHistory($user_making_change, 'ticket_created', null, $this->title, $changed_at); + if($this->status_id) $this->logHistory($user_making_change, 'status_id', null, $this->status_id, $changed_at); + if($this->priority_id) $this->logHistory($user_making_change, 'priority_id', null, $this->priority_id, $changed_at); + if($this->assigned_to) $this->logHistory($user_making_change, 'assigned_to', null, $this->assigned_to, $changed_at); + } + + /** + * called from update + */ + public function logDiff($data){ + $user_making_change = \Base::instance()->get('SESSION.user.id'); + $changed_at = date('Y-m-d H:i:s'); + + $checks = ['title', 'description', 'priority_id', 'status_id', 'assigned_to', 'updated_by']; + + // loop instead + foreach($checks as $check){ + if(isset($data[$check]) && $this->$check != $data[$check]){ + if($check == 'description'){ + $old_hash = hash('sha256', $this->$check); + $new_hash = hash('sha256', $data[$check]); + $this->logHistory($user_making_change, $check, $old_hash, $new_hash, $changed_at); + } else { + $this->logHistory($user_making_change, $check, $this->$check, $data[$check], $changed_at); + } + } + } + } } \ No newline at end of file diff --git a/app/ui/views/ticket/create.html b/app/ui/views/ticket/create.html index f4d8bc9..38d3210 100644 --- a/app/ui/views/ticket/create.html +++ b/app/ui/views/ticket/create.html @@ -54,15 +54,29 @@ - + - + - + + +
+ +
+
+ +
+

Hold Ctrl to select multiple.

+
+
diff --git a/app/ui/views/ticket/edit.html b/app/ui/views/ticket/edit.html index da34047..ad7e0a7 100644 --- a/app/ui/views/ticket/edit.html +++ b/app/ui/views/ticket/edit.html @@ -2,93 +2,116 @@
{{ \CSRFHelper::field() | raw }} -
-
- -
-
-
- Cancel +
+
+
-
- -
-
-
- -
-
- -
-
- -
- +
+
+ Cancel
+
+ +
+
+
-
- -
+
+
-
+
+
+ +
+ + +
+ +
+ +
+ +
-
-
+
+
+
-
- -
-
- - - +
+
+ + - + + + + + +
+ +
+
+ +
+

Hold Ctrl to select multiple tags.

+ +
-
- -
-
- -
- - - - - - - - - - - -
PropertyValue
{{@key}} {{@value}}
-
- -
-

Linked Tickets

- - + +
+
+ +
+ + + + + + + + + + + + + + + + + +
PropertyValue
{{@key}}{{@value}}
+
+ +
+

Linked Tickets

+ +
@@ -126,19 +149,19 @@
*/ ?> +
+
-
-

- - + + - - + + @@ -147,4 +170,4 @@
--> -
+
\ No newline at end of file diff --git a/app/ui/views/ticket/index.html b/app/ui/views/ticket/index.html index 438a706..2a6a322 100644 --- a/app/ui/views/ticket/index.html +++ b/app/ui/views/ticket/index.html @@ -22,24 +22,9 @@
- 25 +
- - -
diff --git a/app/ui/views/ticket/view.html b/app/ui/views/ticket/view.html index 038a4bb..e7af297 100644 --- a/app/ui/views/ticket/view.html +++ b/app/ui/views/ticket/view.html @@ -101,6 +101,73 @@
+
+
+

Ticket History

+ +
+ +
+
+
+ + {{ date('Y-m-d H:i:s', strtotime(@entry.changed_at)) }} by {{ @entry.user_display_name }} + +
+
+ + + Ticket created: {{ @entry.new_value}} + + + + Status changed + + from {{ @map['status'][@entry.old_value] ?? 'Unknown' }} + + to {{ @map['status'][@entry.new_value] ?? 'Unknown' }} + + + + Priority changed + + from {{ @map['priorities'][@entry.old_value] ?? 'Unknown'}} + + to {{ @map['priorities'][@entry.new_value] ?? 'Unknown' }} + + + + Assignment changed + + from {{ @map['users'][@entry.old_value] ?? 'Unassigned' }} + + + from Unassigned + + to + + {{ @map['users'][@entry.new_value] ?? 'Unassigned' }} + + + Unassigned + + + + Title changed from "{{ @entry.old_value}}" to "{{ @entry.new_value}}". + + + Description updated old sha256({{@entry.old_value}}). + + +
+
+
+
+
+ +

No history entries for this ticket.

+
+