set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one $purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]); $purifier = new HTMLPurifier($purifier_config); $company = billingCompany(); $company_phone = (string) ($company['phone_display'] ?? ''); $company_tel = phoneDialNumber($company['company_phone'] ?? '', $company['company_phone_country_code'] ?? ''); $company_website = trim((string) ($company['company_website'] ?? '')); $support_email = (string) ticketEmailFrom()['email']; // The company's logo, or its initials and name, as the client portal's top bar shows them - so the page is plainly yours $brand_html = $company['logo_url'] !== '' ? '' . escapeHtml($session_company_name) . '' : '' . '' . escapeHtml($session_company_name) . ''; $found = ticketByGuestKey($_GET['ticket_id'] ?? 0, $_GET['url_key'] ?? ''); $ticket_id = $found ? intval($found['ticket_id']) : 0; $ticket = $ticket_id ? mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT " . portalTicketColumnsSql() . ", tickets.ticket_details, tickets.ticket_feedback, tickets.ticket_url_key, tickets.ticket_client_id, tickets.ticket_assigned_to, tickets.ticket_schedule, tickets.ticket_onsite, tickets.ticket_first_response_at, tickets.ticket_response_due_at, user_name, user_avatar, category_name, contact_name, contact_photo, asset_name, asset_type, location_name, location_address, location_city, location_state, location_zip FROM tickets LEFT JOIN users ON ticket_assigned_to = user_id LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id LEFT JOIN categories ON ticket_category = category_id LEFT JOIN contacts ON contact_id = tickets.ticket_contact_id LEFT JOIN assets ON asset_id = tickets.ticket_asset_id AND asset_client_id = tickets.ticket_client_id LEFT JOIN locations ON location_id = IF(tickets.ticket_location_id > 0, tickets.ticket_location_id, contacts.contact_location_id) AND location_client_id = tickets.ticket_client_id WHERE tickets.ticket_id = $ticket_id LIMIT 1")) : null; if (!$ticket) { ?>
"This ticket link isn't working", 'filtered' => false, 'icon' => 'fa-life-ring', 'text' => 'It may have been cut short when it was copied. Open it again from the email' . ($company_phone !== '' ? ', or call us on ' . escapeHtml($company_phone) : '') . '.']) ?>
Mail), with the number in the subject so it lands on // this ticket - and in the portal, for a ticket that belongs to a client, when the portal is on $reply_mailto = $config_ticket_email_parse === 1 && filter_var($support_email, FILTER_VALIDATE_EMAIL) ? "mailto:$support_email?subject=" . rawurlencode("Re: [$config_ticket_prefix" . intval($ticket['ticket_number']) . '] - ' . $ticket['ticket_subject']) : ''; $portal_url = $config_client_portal_enable === 1 && $client_id ? "/client/ticket.php?id=$ticket_id" : ''; // Tasks - how far along, and the ones waiting on the client's approval (approved from the emailed link, not here) $tasks = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(*) AS total, COALESCE(SUM(task_completed_at IS NOT NULL), 0) AS done FROM tasks WHERE task_ticket_id = $ticket_id")); $task_count = intval($tasks['total']); $task_done = intval($tasks['done']); $approvals = []; if ($open) { $sql = mysqli_query($mysqli, "SELECT task_name, approval_type FROM tasks JOIN task_approvals ON task_id = approval_task_id WHERE task_ticket_id = $ticket_id AND task_completed_at IS NULL AND approval_scope = 'client' AND approval_status = 'pending' ORDER BY task_order ASC, task_id ASC"); while ($row = mysqli_fetch_assoc($sql)) { $approvals[] = $row; } } // Attachments, by the reply they came with (0 for the ticket itself) - never an internal note's, nor the email it was parsed from $attachments = []; $sql = mysqli_query($mysqli, "SELECT ticket_attachment_id, ticket_attachment_name, ticket_attachment_reply_id FROM ticket_attachments LEFT JOIN ticket_replies ON ticket_reply_id = ticket_attachment_reply_id WHERE ticket_attachment_ticket_id = $ticket_id AND ticket_attachment_name != 'Original-parsed-email.eml' AND (ticket_attachment_reply_id IS NULL OR (ticket_reply_type != 'Internal' AND ticket_reply_archived_at IS NULL)) ORDER BY ticket_attachment_id ASC"); while ($row = mysqli_fetch_assoc($sql)) { $attachments[intval($row['ticket_attachment_reply_id'])][] = $row; } // The conversation, oldest first $replies = mysqli_query($mysqli, "SELECT ticket_reply_id, ticket_reply, ticket_reply_type, ticket_reply_created_at, contact_name, contact_photo, user_id, user_name, user_avatar FROM ticket_replies LEFT JOIN users ON ticket_reply_type != 'Client' AND ticket_reply_by = user_id LEFT JOIN contacts ON ticket_reply_type = 'Client' AND ticket_reply_by = contact_id WHERE ticket_reply_ticket_id = $ticket_id AND ticket_reply_archived_at IS NULL AND ticket_reply_type != 'Internal' ORDER BY ticket_reply_id ASC"); /** One message - the client's side on the right, yours on the left, each with its files. */ function guestTicketMessageHtml($name, $avatar_url, $when, $body_html, array $files, $client_side, $file_query) { $html = '
' . portalAvatarHtml($name, $avatar_url, 'lg') . '
' . '
' . escapeHtml($name) . '' . (!$client_side ? '' . escapeHtml($GLOBALS['session_company_name']) . '' : '') . '' . portalDateTime($when) . '
' . '
' . $body_html; if ($files) { $html .= '
'; foreach ($files as $file) { $href = 'guest_ticket_attachment.php?' . $file_query . '&attachment_id=' . intval($file['ticket_attachment_id']); $html .= '' . '' . escapeHtml($file['ticket_attachment_name']) . '' . ''; } $html .= '
'; } return $html . '
'; } // Where it has got to: received, in progress, resolved, closed $steps = ['Received', 'In progress', 'Resolved', 'Closed']; $at = $closed ? 3 : ($resolved ? 2 : ($state['key'] === 'new' ? 0 : 1)); // When we will answer - the SLA's response time, until an agent first replies, while it is still ahead $reply_by = $open && !in_array($state['key'], ['you', 'hold'], true) && empty($ticket['ticket_first_response_at']) && !empty($ticket['ticket_response_due_at']) && strtotime($ticket['ticket_response_due_at']) > time() ? $ticket['ticket_response_due_at'] : ''; // Work booked from today on, while the ticket is open $booked = $open && !empty($ticket['ticket_schedule']) && strtotime($ticket['ticket_schedule']) >= strtotime('today'); $booked_where = ''; if ($booked && !empty($ticket['ticket_onsite'])) { $address = formatAddress($ticket['location_address'] ?? '', $ticket['location_city'] ?? '', $ticket['location_state'] ?? '', $ticket['location_zip'] ?? '', '', ', '); $booked_where = trim(implode(' - ', array_filter([(string) ($ticket['location_name'] ?? ''), $address], 'strlen'))); } // A resolved ticket closes by itself after Admin > Ticket's hours, as the resolved email says $autoclose_hours = intval($config_ticket_autoclose_hours); $autoclose_text = $autoclose_hours > 0 ? ($autoclose_hours % 24 === 0 ? ($autoclose_hours / 24) . ' day' . ($autoclose_hours === 24 ? '' : 's') : "$autoclose_hours hours") : ''; $requester = (string) ($ticket['contact_name'] ?? ''); $requester_photo = $ticket['contact_photo'] && $client_id ? "/uploads/clients/$client_id/" . $ticket['contact_photo'] : ''; $agent_name = (string) ($ticket['user_name'] ?? ''); // Urgent and High: the phone, up front - as the new ticket email says $urgent = $open && $company_phone !== '' && in_array($ticket['ticket_priority'], ['Urgent', 'High'], true); ?>
Client portal

· opened by

    $step) { ?>

We aim to reply by

  • Booked for
    Add to calendar
  • 'your technical contacts', 'billing' => 'your billing contacts'][$approval['approval_type']] ?? ($requester !== '' ? $requester : 'the ticket\'s contact'); ?>
  • Waiting for approval:
    We need a go-ahead before we do this - we have emailed a link to approve it
purify($ticket['ticket_details']), $attachments[0] ?? [], true, $query) ?>
·
purify($reply['ticket_reply']), $attachments[intval($reply['ticket_reply_id'])] ?? [], $client_reply, $query); } ?>

Our last message needs an answer before we can carry on. Reply to any of our emails about this ticket - it goes straight onto it. Screenshots and files can go with it. Sign in to the client portal to reply - screenshots and files can go with it. Give us a call on .

'We think this is fixed', 'icon' => 'fa-check', 'filtered' => false, 'text' => 'Close it if everything is working' . ($autoclose_text !== '' ? ' - or leave it, and it closes by itself after ' . $autoclose_text : '') . '. If it is not, reopen it and tell us what is still wrong.']) ?>
'How did we do?', 'text' => 'One click tells us how this one went.', 'icon' => 'fa-star', 'filtered' => false]) ?>
Closed and rated - thank you for the feedback.

Sorry this one didn't go the way it should have. Call us on and we will put it right.

Need more help with this? Replies to a closed ticket don't reach us - raise a new oneclient portal' : '' ?>' . escapeHtml($support_email) . '' : '' ?>.

Details

Ticket
Status
Priority
Category
Raised by
Working on it
Device
Tasks
of done
Opened
Closed
Resolved
Last update

Request details

Not given' ?>

Contact us

This ticket is marked . If it is stopping work right now, call us rather than waiting for a reply.
Phone
Email
Website
" target="_blank" rel="noopener noreferrer">