mirror of
https://github.com/getnamingo/registry.git
synced 2025-07-03 09:33:25 +02:00
Support system improvement
- Made it more beautiful - Added ability to close/reopen and escalate tickets - Fixed some security issues
This commit is contained in:
parent
ee48a0e67d
commit
9b19930892
4 changed files with 188 additions and 28 deletions
|
@ -24,7 +24,7 @@ class SupportController extends Controller
|
|||
$categories = $db->select("SELECT * FROM ticket_categories");
|
||||
|
||||
$category = $data['category'] ?? null;
|
||||
$subject = $data['subject'] ?? null;
|
||||
$subject = htmlspecialchars($data['subject'], ENT_QUOTES, 'UTF-8') ?? null;
|
||||
$message = $data['message'] ?? null;
|
||||
|
||||
if (!$subject) {
|
||||
|
@ -103,15 +103,22 @@ class SupportController extends Controller
|
|||
|
||||
public function viewTicket(Request $request, Response $response, $args)
|
||||
{
|
||||
$rawNumber = $args;
|
||||
$ticketNumber = filter_var($rawNumber, FILTER_VALIDATE_INT);
|
||||
$ticketNumber = filter_var($args, FILTER_VALIDATE_INT);
|
||||
$db = $this->container->get('db');
|
||||
|
||||
if ($ticketNumber === false) {
|
||||
$this->container->get('flash')->addMessage('error', 'Invalid ticket number');
|
||||
return $response->withHeader('Location', '/support')->withStatus(302);
|
||||
}
|
||||
|
||||
$result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]);
|
||||
$clid = $_SESSION["auth_roles"] != 0 ? $result['registrar_id'] : $_SESSION['auth_user_id'];
|
||||
$ticket_owner = $db->selectValue('SELECT user_id FROM support_tickets WHERE id = ?', [$ticketNumber]);
|
||||
|
||||
if ($ticket_owner != $clid && $_SESSION["auth_roles"] != 0) {
|
||||
return $response->withHeader('Location', '/support')->withStatus(302);
|
||||
}
|
||||
|
||||
$db = $this->container->get('db');
|
||||
// Get the current URI
|
||||
$uri = $request->getUri()->getPath();
|
||||
|
||||
|
@ -142,7 +149,7 @@ class SupportController extends Controller
|
|||
}
|
||||
|
||||
public function replyTicket(Request $request, Response $response)
|
||||
{
|
||||
{
|
||||
if ($request->getMethod() === 'POST') {
|
||||
// Retrieve POST data
|
||||
$data = $request->getParsedBody();
|
||||
|
@ -153,6 +160,15 @@ class SupportController extends Controller
|
|||
|
||||
$ticket_id = $data['ticket_id'] ?? null;
|
||||
$responseText = $data['responseText'] ?? null;
|
||||
|
||||
$result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]);
|
||||
$clid = $_SESSION["auth_roles"] != 0 ? $result['registrar_id'] : $_SESSION['auth_user_id'];
|
||||
$ticket_owner = $db->selectValue('SELECT user_id FROM support_tickets WHERE id = ?', [$ticket_id]);
|
||||
|
||||
if ($ticket_owner != $clid && $_SESSION["auth_roles"] != 0) {
|
||||
$this->container->get('flash')->addMessage('error', 'You do not have permission to perform this action');
|
||||
return $response->withHeader('Location', '/support')->withStatus(302);
|
||||
}
|
||||
|
||||
if (!$responseText) {
|
||||
$this->container->get('flash')->addMessage('error', 'Please enter a reply');
|
||||
|
@ -172,6 +188,17 @@ class SupportController extends Controller
|
|||
'date_created' => $crdate,
|
||||
]
|
||||
);
|
||||
|
||||
$db->update(
|
||||
'support_tickets',
|
||||
[
|
||||
'status' => 'In Progress',
|
||||
'last_updated' => $crdate
|
||||
],
|
||||
[
|
||||
'id' => $ticket_id
|
||||
]
|
||||
);
|
||||
|
||||
$link = envi('APP_URL').'/ticket/'.$ticket_id;
|
||||
$email = $db->selectValue('SELECT email FROM users WHERE id = ?', [$_SESSION['auth_user_id']]);
|
||||
|
@ -190,7 +217,7 @@ class SupportController extends Controller
|
|||
// send message
|
||||
Mail::send($mailsubject, $message, $from, $to);
|
||||
|
||||
$this->container->get('flash')->addMessage('success', 'Reply has been created successfully on ' . $crdate);
|
||||
$this->container->get('flash')->addMessage('success', 'Reply has been posted successfully on ' . $crdate);
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
} catch (Exception $e) {
|
||||
$this->container->get('flash')->addMessage('error', 'Database error: '.$e->getMessage());
|
||||
|
@ -198,6 +225,88 @@ class SupportController extends Controller
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function statusTicket(Request $request, Response $response)
|
||||
{
|
||||
if ($request->getMethod() === 'POST') {
|
||||
// Retrieve POST data
|
||||
$data = $request->getParsedBody();
|
||||
$db = $this->container->get('db');
|
||||
// Get the current URI
|
||||
$uri = $request->getUri()->getPath();
|
||||
$categories = $db->select("SELECT * FROM ticket_categories");
|
||||
|
||||
$ticket_id = $data['ticket_id'] ?? null;
|
||||
$action = $data['action'] ?? null;
|
||||
|
||||
$result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]);
|
||||
$clid = $_SESSION["auth_roles"] != 0 ? $result['registrar_id'] : $_SESSION['auth_user_id'];
|
||||
$ticket_owner = $db->selectValue('SELECT user_id FROM support_tickets WHERE id = ?', [$ticket_id]);
|
||||
|
||||
if ($ticket_owner != $clid && $_SESSION["auth_roles"] != 0) {
|
||||
$this->container->get('flash')->addMessage('error', 'You do not have permission to perform this action');
|
||||
return $response->withHeader('Location', '/support')->withStatus(302);
|
||||
}
|
||||
|
||||
if (!$action) {
|
||||
$this->container->get('flash')->addMessage('error', 'Please select an action');
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
}
|
||||
|
||||
try {
|
||||
$currentDateTime = new \DateTime();
|
||||
$update = $currentDateTime->format('Y-m-d H:i:s.v');
|
||||
|
||||
if ($action === 'close') {
|
||||
$db->update(
|
||||
'support_tickets',
|
||||
[
|
||||
'status' => 'Closed',
|
||||
'last_updated' => $update
|
||||
],
|
||||
[
|
||||
'id' => $ticket_id
|
||||
]
|
||||
);
|
||||
$this->container->get('flash')->addMessage('success', 'Ticket has been closed successfully');
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
} else if ($action === 'escalate') {
|
||||
$db->update(
|
||||
'support_tickets',
|
||||
[
|
||||
'priority' => 'High',
|
||||
'last_updated' => $update
|
||||
],
|
||||
[
|
||||
'id' => $ticket_id
|
||||
]
|
||||
);
|
||||
$this->container->get('flash')->addMessage('success', 'Ticket has been escalated successfully');
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
} else if ($action === 'reopen') {
|
||||
$db->update(
|
||||
'support_tickets',
|
||||
[
|
||||
'status' => 'In Progress',
|
||||
'last_updated' => $update
|
||||
],
|
||||
[
|
||||
'id' => $ticket_id
|
||||
]
|
||||
);
|
||||
$this->container->get('flash')->addMessage('success', 'Ticket has been escalated successfully');
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
} else {
|
||||
$this->container->get('flash')->addMessage('error', 'Incorrect action specified');
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->container->get('flash')->addMessage('error', 'Database error: '.$e->getMessage());
|
||||
return $response->withHeader('Location', '/ticket/'.$ticket_id)->withStatus(302);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function docs(Request $request, Response $response)
|
||||
{
|
||||
|
|
|
@ -25,33 +25,65 @@
|
|||
<div class="container-xl">
|
||||
<div class="col-12">
|
||||
{% include 'partials/flash.twig' %}
|
||||
<form action="/support/reply" method="post">
|
||||
<form action="/support/status" method="post">
|
||||
{{ csrf.field | raw }}
|
||||
<input type="hidden" name="ticket_id" value="{{ ticket.id }}">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">{{ __('Ticket') }} #{{ ticket.id }} - {{ ticket.subject }}</h5>
|
||||
<h3 class="card-title">{{ __('Ticket') }} #{{ ticket.id }} - {{ ticket.subject }}</h3>
|
||||
{% if ticket.status == 'Open' %}
|
||||
<span class="badge bg-success-lt">{{ ticket.status }}</span>
|
||||
<span class="status status-success">{{ ticket.status }}</span>
|
||||
{% elseif ticket.status == 'In Progress' %}
|
||||
<span class="badge bg-warning-lt">{{ ticket.status }}</span>
|
||||
<span class="status status-warning">{{ ticket.status }}</span>
|
||||
{% elseif ticket.status == 'Resolved' %}
|
||||
<span class="badge bg-info-lt">{{ ticket.status }}</span>
|
||||
<span class="status status-info">{{ ticket.status }}</span>
|
||||
{% elseif ticket.status == 'Closed' %}
|
||||
<span class="badge bg-secondary-lt">{{ ticket.status }}</span>
|
||||
<span class="status status-cyan">{{ ticket.status }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-dark-lt">{{ __('Unknown Status') }}</span>
|
||||
<span class="status status-teal">{{ __('Unknown Status') }}</span>
|
||||
{% endif %}
|
||||
<div class="card-actions">
|
||||
{% if ticket.status != 'Closed' %}
|
||||
<button type="submit" name="action" value="close" class="btn btn-ghost-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5.641 5.631a9 9 0 1 0 12.719 12.738m1.68 -2.318a9 9 0 0 0 -12.074 -12.098" /><path d="M12 17v.01" /><path d="M12 13.5a1.5 1.5 0 0 1 .394 -1.1m2.106 -1.9a2.6 2.6 0 0 0 -3.347 -3.361" /><path d="M3 3l18 18" /></svg>
|
||||
Close Ticket
|
||||
</button>
|
||||
<button type="submit" name="action" value="escalate" class="btn btn-outline-secondary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 7h-2.672a2 2 0 0 0 -1.414 .586l-8.414 8.414h-2.5a2.5 2.5 0 1 0 0 5h3.672a2 2 0 0 0 1.414 -.586l8.414 -8.414h1.5a2.5 2.5 0 1 0 0 -5z" /><path d="M6 10v-7" /><path d="M3 6l3 -3l3 3" /></svg>
|
||||
Escalate Ticket
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="submit" name="action" value="reopen" class="btn btn-outline-pink">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 12v-3a3 3 0 0 1 3 -3h13m-3 -3l3 3l-3 3" /><path d="M20 12v3a3 3 0 0 1 -3 3h-13m3 3l-3 -3l3 -3" /></svg>
|
||||
Reopen Ticket
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% if ticket.status != 'Closed' %}<form action="/support/reply" method="post">
|
||||
{{ csrf.field | raw }}
|
||||
<input type="hidden" name="ticket_id" value="{{ ticket.id }}">{% endif %}
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{ __('Ticket Details') }}</h6>
|
||||
<p><span>{{ __('Created On') }}:</span> <strong>{{ ticket.date_created }}</strong></p>
|
||||
<p><span>{{ __('Category') }}:</span> <strong>{{ category }}</strong></p>
|
||||
<p><span>{{ __('Priority') }}:</span> <strong>{{ ticket.priority }}</strong></p>
|
||||
<p><span>{{ __('Category') }}:</span> <span class="status status-indigo">{{ category }}</span></p>
|
||||
<p><span>{{ __('Priority') }}:</span>
|
||||
{% if ticket.priority == 'Low' %}
|
||||
<span class="status status-teal">{{ ticket.priority }}</span>
|
||||
{% elseif ticket.priority == 'Medium' %}
|
||||
<span class="status status-blue">{{ ticket.priority }}</span>
|
||||
{% elseif ticket.priority == 'High' %}
|
||||
<span class="status status-orange">{{ ticket.priority }}</span>
|
||||
{% elseif ticket.priority == 'Critical' %}
|
||||
<span class="status status-red">{{ ticket.priority }}</span>
|
||||
{% else %}
|
||||
<span class="status status-cyan">{{ ticket.priority }}</span>
|
||||
{% endif %}</p>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card mt-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-subtitle mb-2 text-muted">{{ __('Conversation') }}</h6>
|
||||
<h6 class="card-subtitle text-muted">{{ __('Conversation') }}</h6>
|
||||
{% for reply in replies %}
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<div class="flex-shrink-0">
|
||||
|
@ -64,7 +96,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="avatar">{{ ticket.ticket_creator|slice(0, 2) }}</span>
|
||||
</div>
|
||||
|
@ -76,11 +108,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if ticket.status != 'Closed' %}
|
||||
<div class="mt-3">
|
||||
<label for="responseText" class="form-label">{{ __('Your Response') }}</label>
|
||||
<textarea class="form-control" id="responseText" name="responseText" rows="3" required></textarea>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if ticket.status != 'Closed' %}
|
||||
<div class="card-footer">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
|
@ -88,8 +123,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% if ticket.status != 'Closed' %}</form>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -28,13 +28,13 @@
|
|||
|
||||
switch (status) {
|
||||
case 'Open':
|
||||
return createBadge(status, 'primary');
|
||||
return createBadge(status, 'success');
|
||||
case 'In Progress':
|
||||
return createBadge(status, 'warning');
|
||||
case 'Resolved':
|
||||
return createBadge(status, 'success');
|
||||
return createBadge(status, 'info');
|
||||
case 'Closed':
|
||||
return createBadge(status, 'secondary');
|
||||
return createBadge(status, 'cyan');
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -45,22 +45,33 @@
|
|||
|
||||
// Function to create an outline badge with color based on priority
|
||||
function createBadge(text, badgeClass) {
|
||||
return `<span class="badge bg-${badgeClass}-lt">${text}</span>`;
|
||||
return `<span class="status status-${badgeClass}">${text}</span>`;
|
||||
}
|
||||
|
||||
switch (priority) {
|
||||
case 'Low':
|
||||
return createBadge(priority, 'info');
|
||||
return createBadge(priority, 'teal');
|
||||
case 'Medium':
|
||||
return createBadge(priority, 'secondary');
|
||||
return createBadge(priority, 'blue');
|
||||
case 'High':
|
||||
return createBadge(priority, 'warning');
|
||||
return createBadge(priority, 'orange');
|
||||
case 'Critical':
|
||||
return createBadge(priority, 'danger');
|
||||
return createBadge(priority, 'red');
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function catFormatter(cell) {
|
||||
var category = cell.getValue();
|
||||
|
||||
// Function to create an outline badge with color based on category
|
||||
function createBadge(text, badgeClass) {
|
||||
return `<span class="status status-${badgeClass}">${text}</span>`;
|
||||
}
|
||||
|
||||
return createBadge(category, 'indigo');
|
||||
}
|
||||
|
||||
table = new Tabulator("#supportTable", {
|
||||
ajaxURL:"/api/records/support_tickets?join=ticket_categories", // Set the URL for your JSON data
|
||||
|
@ -74,10 +85,13 @@
|
|||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseStartOpen:false,
|
||||
resizableColumns:false,
|
||||
initialSort:[
|
||||
{column:"status", dir:"desc"},
|
||||
],
|
||||
columns:[
|
||||
{formatter:"responsiveCollapse", width:30, minWidth:30, hozAlign:"center", resizable:false, headerSort:false, responsive:0},
|
||||
{title:"{{ __('Subject') }}", field:"subject", width:350, minWidth:100, headerSort:true, formatter: ticketLinkFormatter, responsive:0},
|
||||
{title:"{{ __('Category') }}", field:"category_id.name", width:250, minWidth:80, headerSort:true, responsive:0},
|
||||
{title:"{{ __('Category') }}", field:"category_id.name", width:250, minWidth:80, formatter: catFormatter, headerSort:true, responsive:0},
|
||||
{title:"{{ __('Status') }}", field:"status", headerSort:true, width:250, minWidth:100, formatter: statusFormatter, responsive:2},
|
||||
{title:"{{ __('Priority') }}", field:"priority", headerSort:true, width:250, minWidth:100, formatter: priorityFormatter, responsive:2},
|
||||
{title: "{{ __('Actions') }}", formatter: actionsFormatter, headerSort: false, download:false, hozAlign: "center", responsive:0, cellClick:function(e, cell){ e.stopPropagation(); }},
|
||||
|
|
|
@ -121,6 +121,7 @@ $app->group('', function ($route) {
|
|||
$route->map(['GET', 'POST'], '/support/new', SupportController::class .':newticket')->setName('newticket');
|
||||
$route->get('/ticket/{ticket}', SupportController::class . ':viewTicket')->setName('viewTicket');
|
||||
$route->post('/support/reply', SupportController::class . ':replyTicket')->setName('replyTicket');
|
||||
$route->post('/support/status', SupportController::class . ':statusTicket')->setName('statusTicket');
|
||||
$route->get('/support/docs', SupportController::class .':docs')->setName('docs');
|
||||
$route->get('/support/media', SupportController::class .':mediakit')->setName('mediakit');
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue