I want to make ostickets show notification whenever a new ticket is created and dynamically update the tickets without refreshing... I already tried a solution but didn't work...
tickets.php:
<?php
/*************************************************************************
    tickets.php

    Handles all tickets related actions.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/

require('staff.inc.php');
require_once(INCLUDE_DIR.'class.ticket.php');
require_once(INCLUDE_DIR.'class.dept.php');
require_once(INCLUDE_DIR.'class.filter.php');
require_once(INCLUDE_DIR.'class.canned.php');
require_once(INCLUDE_DIR.'class.json.php');
require_once(INCLUDE_DIR.'class.dynamic_forms.php');
require_once(INCLUDE_DIR.'class.export.php');       // For paper sizes



// Fetch ticket queues organized by root and sub-queues
$queues = CustomQueue::getHierarchicalQueues($thisstaff);

$page='';
$ticket = $user = null; //clean start.
$redirect = false;
//LOCKDOWN...See if the id provided is actually valid and if the user has access.
if(isset($_REQUEST['id']) || isset($_REQUEST['number'])) {
    if($_REQUEST['id'] && !($ticket=Ticket::lookup($_REQUEST['id'])))
         $errors['err']=sprintf(__('%s: Unknown or invalid ID.'), __('ticket'));
    elseif($_REQUEST['number'] && !($ticket=Ticket::lookup(array('number' => $_REQUEST['number']))))
         $errors['err']=sprintf(__('%s: Unknown or invalid number.'), __('ticket'));
     elseif(!$ticket || !$ticket->checkStaffPerm($thisstaff)) {
         $errors['err']=__('Access denied. Contact admin if you believe this is in error');
         $ticket=null; //Clear ticket obj.
     }
}

if (!$ticket) {
    // Display a ticket queue. Decide the contents
    $queue_id = null;

    // Search for user
    if (isset($_REQUEST['uid']))
        $user = User::lookup($_REQUEST['uid']);

    if (isset($_REQUEST['email']))
        $user = User::lookupByEmail($_REQUEST['email']);

    if ($user
            && $_GET['a'] !== 'open'
    ) {
        $criteria = [
            ['user__emails__address', 'equal', $user->getDefaultEmailAddress()],
            ['user_id', 'equal', $user->id],
        ];
        if ($S = $_GET['status'])
            // The actual state is tracked by the key
            $criteria[] = ['status__state', 'includes', [$S => $S]];
        $_SESSION['advsearch']['uid'] = $criteria;
        $queue_id = "adhoc,uid";
    }
    // Search for organization tickets
    elseif (isset($_GET['orgid'])
        && ($org = Organization::lookup($_GET['orgid']))
    ) {
        $criteria = [
            ['user__org__name', 'equal', $org->name],
            ['user__org_id', 'equal', $org->id],
        ];
        if ($S = $_GET['status'])
            $criteria[] = ['status__state', 'includes', [$S => $S]];
        $_SESSION['advsearch']['orgid'] = $criteria;
        $queue_id = "adhoc,orgid";
    }
    // Basic search (click on 🔍 )
    elseif (isset($_GET['a']) && $_GET['a'] === 'search'
        && ($_GET['query'])
    ) {
        $wc = mb_str_wc($_GET['query']);
        if ($wc < 4) {
            $key = substr(md5($_GET['query']), -10);
            if ($_GET['search-type'] == 'typeahead'
                    || Validator::is_emailish($_GET['query'])) {
                // Use a faster index
                $criteria = [
                    'user__emails__address',
                    Validator::is_valid_email($_GET['query']) ? 'equal' : 'contains',
                    $_GET['query']
                ];
            } elseif (Validator::is_numeric($_GET['query'])) {
                $criteria = ['number', 'contains', $_GET['query']];
            } else {
                $criteria = [':keywords', null, $_GET['query']];
            }
            $_SESSION['advsearch'][$key] = [$criteria];
            $queue_id = "adhoc,{$key}";
        } else {
            $errors['err'] = sprintf(
                    __('Search term cannot have more than %d keywords'), 4);
        }
    }

    $queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TICKET);
    $queue_id = $queue_id ?: @$_GET['queue'] ?: $_SESSION[$queue_key]
        ?? $thisstaff->getDefaultTicketQueueId() ?: $cfg->getDefaultTicketQueueId();

    // Recover advanced search, if requested
    if (isset($_SESSION['advsearch'])
        && strpos($queue_id, 'adhoc') === 0
    ) {
        list(,$key) = explode(',', $queue_id, 2);
        // For queue=queue, use the most recent search
        if (!$key) {
            reset($_SESSION['advsearch']);
            $key = key($_SESSION['advsearch']);
        }

        $queue = AdhocSearch::load($key);
    }

    if ((int) $queue_id && !isset($queue))
        $queue = SavedQueue::lookup($queue_id);

    if (!$queue && ($qid=$cfg->getDefaultTicketQueueId()))
        $queue = SavedQueue::lookup($qid);

    if (!$queue && $queues)
        list($queue,) = $queues[0];

    if ($queue) {
        // Set the queue_id for navigation to turn a top-level item bold
        $_REQUEST['queue'] = $queue->getId();
        // Make the current queue sticky
         $_SESSION[$queue_key] = $queue->getId();
    }
}

// Configure form for file uploads
$response_form = new SimpleForm(array(
    'attachments' => new FileUploadField(array('id'=>'attach',
        'name'=>'attach:response',
        'configuration' => array('extensions'=>'')))
));
$note_form = new SimpleForm(array(
    'attachments' => new FileUploadField(array('id'=>'attach',
        'name'=>'attach:note',
        'configuration' => array('extensions'=>'')))
));

//At this stage we know the access status. we can process the post.
if($_POST && !$errors):

    if($ticket && $ticket->getId()) {
        //More coffee please.
        $errors=array();
        $lock = $ticket->getLock(); //Ticket lock if any
        $role = $ticket->getRole($thisstaff);
        $dept = $ticket->getDept();
        $isManager = $dept->isManager($thisstaff); //Check if Agent is Manager
        switch(strtolower($_POST['a'])):
        case 'reply':
            if (!$role || !$role->hasPerm(Ticket::PERM_REPLY)) {
                $errors['err'] = __('Action denied. Contact admin for access');
            } else {
                $vars = $_POST;
                $vars['files'] = $response_form->getField('attachments')->getFiles();
                $vars['response'] = ThreadEntryBody::clean($vars['response']);
                if(!$vars['response'])
                    $errors['response']=__('Response required');

                if ($cfg->isTicketLockEnabled()) {
                    if (!$lock) {
                        $errors['err'] = sprintf('%s %s', __('This action requires a lock.'), __('Please try again!'));
                    }
                    // Use locks to avoid double replies
                    elseif ($lock->getStaffId()!=$thisstaff->getId()) {
                        $errors['err'] = __('Action Denied. Ticket is locked by someone else!');
                    }
                    // Attempt to renew the lock if possible
                    elseif (($lock->isExpired() && !$lock->renew())
                        ||($lock->getCode() != $_POST['lockCode'])
                    ) {
                        $errors['err'] = sprintf('%s %s', __('Your lock has expired.'), __('Please try again!'));
                    }
                }

                //Make sure the email is not banned
                if(!$errors['err'] && Banlist::isBanned($ticket->getEmail()))
                    $errors['err']=__('Email is in banlist. Must be removed to reply.');
            }

            $alert =  strcasecmp('none', $_POST['reply-to']);
            if (!$errors) {
                // Add new collaborators (if any)
                $_errors = array();
                if (isset($vars['ccs']) && count($vars['ccs']))
                    $ticket->addCollaborators($vars['ccs'], array(), $_errors);
                // set status of collaborators
                if ($collabs = $ticket->getCollaborators()) {
                    foreach ($collabs as $collaborator) {
                        $cid = $collaborator->getUserId();
                        // Enable collaborators if they were reselected
                        if (!$collaborator->isActive() && ($vars['ccs'] && in_array($cid, $vars['ccs'])))
                            $collaborator->setFlag(Collaborator::FLAG_ACTIVE, true);
                        // Disable collaborators if they were unchecked
                        elseif ($collaborator->isActive() && (!$vars['ccs'] || !in_array($cid, $vars['ccs'])))
                            $collaborator->setFlag(Collaborator::FLAG_ACTIVE, false);
                        $collaborator->save();
                    }
                }
            }
            if (!$errors && ($response=$ticket->postReply($vars, $errors,
                            $alert))) {
                $msg = sprintf(__('%s: Reply posted successfully'),
                        sprintf(__('Ticket #%s'),
                            sprintf('<a href="tickets.php?id=%d"><b>%s</b></a>',
                                $ticket->getId(), $ticket->getNumber()))
                        );

                // Clear attachment list
                $response_form->setSource(array());
                $response_form->getField('attachments')->reset();

                // Remove staff's locks
                $ticket->releaseLock($thisstaff->getId());

                // Cleanup response draft for this user
                Draft::deleteForNamespace(
                    'ticket.response.' . $ticket->getId(),
                    $thisstaff->getId());

                if ($ticket->isClosed())
                    $ticket = null;

                $redirect = 'tickets.php';
                if ($ticket && $thisstaff->getReplyRedirect() == 'Ticket')
                    $redirect = 'tickets.php?id='.$ticket->getId();

            } elseif (!$errors['err']) {
                $errors['err']=sprintf('%s %s',
                    __('Unable to post the reply.'),
                    __('Correct any errors below and try again.'));
            }
            break;
        case 'postnote': /* Post Internal Note */
            $vars = $_POST;
            $vars['files'] = $note_form->getField('attachments')->getFiles();
            $vars['note'] = ThreadEntryBody::clean($vars['note']);

            if ($cfg->isTicketLockEnabled()) {
                if (!$lock) {
                    $errors['err'] = sprintf('%s %s', __('This action requires a lock.'), __('Please try again!'));
                }
                // Use locks to avoid double replies
                elseif ($lock->getStaffId()!=$thisstaff->getId()) {
                    $errors['err'] = __('Action Denied. Ticket is locked by someone else!');
                }
                elseif ($lock->getCode() != $_POST['lockCode']) {
                    $errors['err'] = sprintf('%s %s', __('Your lock has expired.'), __('Please try again!'));
                }
            }

            $wasOpen = ($ticket->isOpen());
            if(($note=$ticket->postNote($vars, $errors, $thisstaff))) {

                $msg = sprintf(__('%s: %s posted successfully'),
                        sprintf(__('Ticket #%s'),
                            sprintf('<a href="tickets.php?id=%d"><b>%s</b></a>',
                                $ticket->getId(), $ticket->getNumber())),
                        __('Internal note')
                        );
                // Clear attachment list
                $note_form->setSource(array());
                $note_form->getField('attachments')->reset();

                // Remove staff's locks
                $ticket->releaseLock($thisstaff->getId());

                if($wasOpen && $ticket->isClosed())
                    $ticket = null; //Going back to main listing.
                else
                    // Ticket is still open -- clear draft for the note
                    Draft::deleteForNamespace('ticket.note.'.$ticket->getId(),
                        $thisstaff->getId());

                 $redirect = 'tickets.php';
                 if ($ticket)
                     $redirect ='tickets.php?id='.$ticket->getId();

            } else {

                if(!$errors['err'])
                    $errors['err'] = __('Unable to post internal note - missing or invalid data.');

                $errors['postnote'] = sprintf('%s %s',
                    __('Unable to post the note.'),
                    __('Correct any errors below and try again.'));
            }
            break;
        case 'edit':
        case 'update':
            if(!$ticket || !$role->hasPerm(Ticket::PERM_EDIT))
                $errors['err']=__('Permission Denied. You are not allowed to edit tickets');
            elseif($ticket->update($_POST,$errors)) {
                $msg=__('Ticket updated successfully');
                $redirect = 'tickets.php?id='.$ticket->getId();
                $_REQUEST['a'] = null; //Clear edit action - going back to view.
                //Check to make sure the staff STILL has access post-update (e.g dept change).
                if(!$ticket->checkStaffPerm($thisstaff))
                    $ticket=null;
            } elseif(!$errors['err']) {
                $errors['err']=sprintf('%s %s',
                    sprintf(__('Unable to update %s.'), __('this ticket')),
                    __('Correct any errors below and try again.')
                );
            }
            break;
        case 'process':
            switch(strtolower($_POST['do'])):
                case 'claim':
                    if(!$role->hasPerm(Ticket::PERM_EDIT)) {
                        $errors['err'] = __('Permission Denied. You are not allowed to assign/claim tickets.');
                    } elseif(!$ticket->isOpen()) {
                        $errors['err'] = __('Only open tickets can be assigned');
                    } elseif($ticket->isAssigned()) {
                        $errors['err'] = sprintf(__('Ticket is already assigned to %s'),$ticket->getAssigned());
                    } elseif ($ticket->claim()) {
                        $msg = __('Ticket is now assigned to you!');
                    } else {
                        $errors['err'] = sprintf('%s %s', __('Problems assigning the ticket.'), __('Please try again!'));
                    }
                    break;
                case 'overdue':
                    if(!$dept || !$isManager) {
                        $errors['err']=__('Permission Denied. You are not allowed to flag tickets overdue');
                    } elseif($ticket->markOverdue()) {
                        $msg=sprintf(__('Ticket flagged as overdue by %s'),$thisstaff->getName());
                        $ticket->logActivity(__('Ticket Marked Overdue'),$msg);
                    } else {
                        $errors['err']=sprintf('%s %s', __('Problems marking the the ticket overdue.'), __('Please try again!'));
                    }
                    break;
                case 'banemail':
                    if (!$thisstaff->hasPerm(Email::PERM_BANLIST)) {
                        $errors['err']=__('Permission Denied. You are not allowed to ban emails');
                    } elseif(BanList::includes($ticket->getEmail())) {
                        $errors['err']=__('Email already in banlist');
                    } elseif(Banlist::add($ticket->getEmail(),$thisstaff->getName())) {
                        $msg=sprintf(__('Email %s added to banlist'),$ticket->getEmail());
                    } else {
                        $errors['err']=__('Unable to add the email to banlist');
                    }
                    break;
                case 'unbanemail':
                    if (!$thisstaff->hasPerm(Email::PERM_BANLIST)) {
                        $errors['err'] = __('Permission Denied. You are not allowed to remove emails from banlist.');
                    } elseif(Banlist::remove($ticket->getEmail())) {
                        $msg = __('Email removed from banlist');
                    } elseif(!BanList::includes($ticket->getEmail())) {
                        $warn = __('Email is not in the banlist');
                    } else {
                        $errors['err']=sprintf('%s %s', __('Unable to remove the email from banlist.'), __('Please try again!'));
                    }
                    break;
                case 'changeuser':
                    if (!$role->hasPerm(Ticket::PERM_EDIT)) {
                        $errors['err']=__('Permission Denied. You are not allowed to edit tickets');
                    } elseif (!$_POST['user_id'] || !($user=User::lookup($_POST['user_id']))) {
                        $errors['err'] = __('Unknown user selected');
                    } elseif ($ticket->changeOwner($user)) {
                        $msg = sprintf(__('Ticket ownership changed to %s'),
                            Format::htmlchars($user->getName()));
                    } else {
                        $errors['err'] = sprintf('%s %s', __('Unable to change ticket ownership.'), __('Please try again!'));
                    }
                    break;
                case 'addcc':
                    if (!$role->hasPerm(Ticket::PERM_EDIT)) {
                        $errors['err']=__('Permission Denied. You are not allowed to add collaborators');
                    } elseif (!$_POST['user_id'] || !($user=User::lookup($_POST['user_id']))) {
                        $errors['err'] = __('Unknown user selected');
                  } elseif ($c2 = $ticket->addCollaborator($user, array(), $errors)) {
                        $c2->setFlag(Collaborator::FLAG_CC, true);
                        $c2->save();
                        $msg = sprintf(__('Collaborator %s added'),
                            Format::htmlchars($user->getName()));
                    }
                    else {
                      $errors['err'] = sprintf('%s %s', __('Unable to add collaborator.'), __('Please try again!'));
                    }
                    break;
                default:
                    $errors['err']=__('You must select action to perform');
            endswitch;
            break;
        default:
            $errors['err']=__('Unknown action');
        endswitch;
    }elseif($_POST['a']) {

        switch($_POST['a']) {
            case 'open':
                $ticket=null;
                if (!$thisstaff ||
                        !$thisstaff->hasPerm(Ticket::PERM_CREATE, false)) {
                     $errors['err'] = sprintf('%s %s',
                             sprintf(__('You do not have permission %s'),
                                 __('to create tickets')),
                             __('Contact admin for such access'));
                } else {
                    $vars = $_POST;

                    if ($vars['uid'] && !($user=User::lookup($vars['uid'])))
                        $vars['uid'] = 0;

                    $vars['files'] = $response_form->getField('attachments')->getFiles();

                    if(($ticket=Ticket::open($vars, $errors))) {
                        $msg=__('Ticket created successfully');
                        $redirect = 'tickets.php?id='.$ticket->getId();
                        $_REQUEST['a']=null;
                        if (!$ticket->checkStaffPerm($thisstaff) || $ticket->isClosed())
                            $ticket=null;
                        Draft::deleteForNamespace('ticket.staff%', $thisstaff->getId());
                        // Drop files from the response attachments widget
                        $response_form->setSource(array());
                        $response_form->getField('attachments')->reset();
                        $_SESSION[':form-data'] = null;
                        // Regenerate Session ID
                        $thisstaff->regenerateSession();
                    } elseif(!$errors['err']) {
                        // ensure that we retain the tid if ticket is created from thread
                        if ($_SESSION[':form-data']['ticketId'] || $_SESSION[':form-data']['taskId'])
                            $_GET['tid'] = $_SESSION[':form-data']['ticketId'] ?: $_SESSION[':form-data']['taskId'];

                        $errors['err']=sprintf('%s %s',
                            __('Unable to create the ticket.'),
                            __('Correct any errors below and try again.'));
                    }
                }
                break;
        }
    }
    if(!$errors)
        $thisstaff ->resetStats(); //We'll need to reflect any changes just made!
endif;

if ($redirect) {
    if ($msg)
        Messages::success($msg);
    Http::redirect($redirect);
}

// Clear advanced search upon request
if (isset($_GET['clear_filter']))
    unset($_SESSION['advsearch']);

//Navigation
$nav->setTabActive('tickets');
$nav->addSubNavInfo('jb-overflowmenu', 'customQ_nav');

// Start with all the top-level (container) queues
foreach ($queues as $_) {
    list($q, $children) = $_;
    if ($q->isPrivate())
        continue;
    $nav->addSubMenu(function() use ($q, $queue, $children) {
        // A queue is selected if it is the one being displayed. It is
        // "child" selected if its ID is in the path of the one selected
        $_selected = ($queue && $queue->getId() == $q->getId());
        $child_selected = $queue
            && ($queue->parent_id == $q->getId()
                || false !== strpos($queue->getPath(), "/{$q->getId()}/"));
        include STAFFINC_DIR . 'templates/queue-navigation.tmpl.php';

        return ($child_selected || $_selected);
    });
}

// Add my advanced searches
$nav->addSubMenu(function() use ($queue) {
    global $thisstaff;
    $selected = false;
    // A queue is selected if it is the one being displayed. It is
    // "child" selected if its ID is in the path of the one selected
    $child_selected = $queue instanceof SavedSearch;
    include STAFFINC_DIR . 'templates/queue-savedsearches-nav.tmpl.php';
    return ($child_selected || $selected);
});


if ($thisstaff->hasPerm(Ticket::PERM_CREATE, false)) {
    $nav->addSubMenu(array('desc'=>__('New Ticket'),
                           'title'=> __('Open a New Ticket'),
                           'href'=>'tickets.php?a=open',
                           'iconclass'=>'newTicket',
                           'id' => 'new-ticket'),
                        (isset($_REQUEST['a']) && $_REQUEST['a']=='open'));
}


$ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js?0375576"></script>');
$ost->addExtraHeader('<script type="text/javascript" src="js/thread.js?0375576"></script>');
$ost->addExtraHeader('<meta name="tip-namespace" content="tickets.queue" />',
    "$('#content').data('tipNamespace', 'tickets.queue');");

if($ticket) {
    $ost->setPageTitle(sprintf(__('Ticket #%s'),$ticket->getNumber()));
    $nav->setActiveSubMenu(-1);
    $inc = 'ticket-view.inc.php';
    if ($_REQUEST['a']=='edit'
            && $ticket->checkStaffPerm($thisstaff, Ticket::PERM_EDIT)) {
        $inc = 'ticket-edit.inc.php';
        if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId());
        // Auto add new fields to the entries
        foreach ($forms as $f) {
            $f->filterFields(function($f) { return !$f->isStorable(); });
            $f->addMissingFields();
        }
    } elseif($_REQUEST['a'] == 'print') {
        if (!extension_loaded('mbstring'))
            $errors['err'] = sprintf('%s %s',
                'mbstring',
                __('extension required to print ticket to PDF'));
        elseif (!$ticket->pdfExport($_REQUEST['psize'], $_REQUEST['notes'], $_REQUEST['events']))
            $errors['err'] = __('Unable to export the ticket to PDF for print.')
                .' '.__('Internal error occurred');
    } elseif ($_GET['a'] == 'zip' && !$ticket->zipExport($_REQUEST['notes'], $_REQUEST['tasks'])) {
        $errors['err'] = __('Unable to export the ticket to ZIP.')
            .' '.__('Internal error occurred');
    } elseif (PluginManager::auditPlugin() && $_REQUEST['a'] == 'export' && strtolower($_REQUEST['t']) == 'audits') {
      require_once(sprintf('phar:///%s/plugins/audit.phar/class.audit.php', INCLUDE_DIR));
      $show = AuditEntry::$show_view_audits;
      $filename = sprintf('%s-audits-%s.csv',
              $ticket->getNumber(), date('Ymd'));
      $tableInfo = AuditEntry::getTableInfo($ticket, true);
      if (!Export::audits('ticket', $filename, $tableInfo, $ticket, 'csv', $show))
          $errors['err'] = __('Unable to dump query results.')
              .' '.__('Internal error occurred');
    }
} else {
    $inc = 'templates/queue-tickets.tmpl.php';
    if ((isset($_REQUEST['a']) && $_REQUEST['a']=='open') &&
            $thisstaff->hasPerm(Ticket::PERM_CREATE, false)) {
        $inc = 'ticket-open.inc.php';
    } elseif ($queue) {
        // XXX: Check staff access?
        $quick_filter = @$_REQUEST['filter'];
        $tickets = $queue->getQuery(false, $quick_filter);
    }

    //set refresh rate if the user has it configured
    if(!$_POST && !isset($_REQUEST['a']) && ($min=(int)$thisstaff->getRefreshRate())) {
        $js = "+function(){ var qq = setInterval(function() { if ($.refreshTicketView === undefined) return; clearInterval(qq); $.refreshTicketView({$min}*60000); }, 200); }();";
        $ost->addExtraHeader('<script type="text/javascript">'.$js.'</script>',
            $js);
    }
}

$ost->addExtraHeader('<link href="css/toastr.min.css" rel="stylesheet"/>');
$ost->addExtraHeader('<script src="js/toastr.min.js"></script>');

require_once(STAFFINC_DIR.'header.inc.php');
require_once(STAFFINC_DIR.$inc);
print $response_form->getMedia();
$ost->addExtraHeader('
<script type="text/javascript">
    $(document).ready(function() {
        // Initialize last_checked timestamp from localStorage or current time
        var lastChecked = localStorage.getItem("lastChecked") || Math.floor(Date.now() / 1000);
        
        // Function to format date (optional customization)
        function formatDate(datetime) {
            var d = new Date(datetime);
            return d.toLocaleString(); // Modify as needed
        }
        
        // Function to escape HTML to prevent XSS
        function escapeHtml(text) {
            return $("<div>").text(text).html();
        }
        
        // Function to display notification using Toastr
        function showNewTicketNotification(ticket) {
            toastr.success("Ticket #" + ticket.number + ": " + ticket.cdata__subject, "New Ticket!");
            
            // Optional: Flashing the page title
            var originalTitle = document.title;
            var blink = setInterval(function() {
                document.title = document.title === "New Ticket!" ? originalTitle : "New Ticket!";
            }, 1000);
            setTimeout(function() {
                clearInterval(blink);
                document.title = originalTitle;
            }, 10000);
        }
        
        // Function to append new ticket to the ticket table
        function appendNewTicket(ticket) {
            // Create a new table row with ticket details
            var newRow = `
                <tr id="ticket-${ticket.ticket_id}">
                    <td>
                        <a class="Icon ${ticket.source.toLowerCase()}Ticket" title="${ticket.user__default_email__address}"
                            href="tickets.php?id=${ticket.ticket_id}">${ticket.number}</a>
                    </td>
                    <td>${formatDate(ticket.created)}</td>
                    <td>${escapeHtml(ticket.status__name)}</td>
                    <td>
                        <div style="max-height: 1.2em; max-width: 320px;" class="link truncate" href="tickets.php?id=${ticket.ticket_id}">
                            ${ticket.isanswered ? "<strong>" : ""}${escapeHtml(ticket.cdata__subject)}${ticket.isanswered ? "</strong>" : ""}
                        </div>
                    </td>
                    <td><span class="truncate">${escapeHtml(ticket.dept__name)}</span></td>
                </tr>
            `;
            // Prepend the new row to the ticket table body
            $("#ticketTable tbody").prepend(newRow);

            // Optionally, highlight the new row
            $("#ticket-" + ticket.ticket_id).hide().fadeIn("slow");
        }
        
        // Polling function to fetch recent tickets
        function fetchRecentTickets() {
            $.ajax({
                url: "ajax.php/tickets/recent",
                type: "GET",
                data: { 
                    last_checked: lastChecked 
                },
                dataType: "json",
                success: function(response) {
                    if (response.tickets && response.tickets.length > 0) {
                        response.tickets.forEach(function(ticket) {
                            showNewTicketNotification(ticket);
                            appendNewTicket(ticket);
                        });
                        // Update lastChecked to the latest ticket\'s timestamp
                        lastChecked = response.last_checked;
                        // Store lastChecked in localStorage for persistence across page reloads
                        localStorage.setItem("lastChecked", lastChecked);
                    }
                },
                error: function(xhr, status, error) {
                    console.error("Error fetching recent tickets:", error);
                    // Optionally, display an error notification
                    toastr.error("Error fetching recent tickets: " + error, "Error");
                }
            });
        }
        
        // Initialize Toastr options (optional customization)
        toastr.options = {
            "closeButton": true,
            "progressBar": true,
            "positionClass": "toast-top-right",
            "timeOut": "5000"
        };

        // Initial fetch
        fetchRecentTickets();

        // Poll every 30 seconds
        setInterval(fetchRecentTickets, 5000);
    });
</script>
');

require_once(STAFFINC_DIR.'footer.inc.php');

api.tickets.php:
<?php

include_once INCLUDE_DIR.'class.api.php';
include_once INCLUDE_DIR.'class.ticket.php';

class TicketApiController extends ApiController {

    # Supported arguments -- anything else is an error. These items will be
    # inspected _after_ the fixup() method of the ApiXxxDataParser classes
    # so that all supported input formats should be supported
    function getRequestStructure($format, $data=null) {
        $supported = array(
            "alert", "autorespond", "source", "topicId",
            "attachments" => array("*" =>
                array("name", "type", "data", "encoding", "size")
            ),
            "message", "ip", "priorityId",
            "system_emails" => array(
                "*" => "*"
            ),
            "thread_entry_recipients" => array (
                "*" => array("to", "cc")
            )
        );
        # Fetch dynamic form field names for the given help topic and add
        # the names to the supported request structure
        if (isset($data['topicId'])
                && ($topic = Topic::lookup($data['topicId']))
                && ($forms = $topic->getForms())) {
            foreach ($forms as $form)
                foreach ($form->getDynamicFields() as $field)
                    $supported[] = $field->get('name');
        }

        # Ticket form fields
        # TODO: Support userId for existing user
        if(($form = TicketForm::getInstance()))
            foreach ($form->getFields() as $field)
                $supported[] = $field->get('name');

        # User form fields
        if(($form = UserForm::getInstance()))
            foreach ($form->getFields() as $field)
                $supported[] = $field->get('name');

        if(!strcasecmp($format, 'email')) {
            $supported = array_merge($supported, array('header', 'mid',
                'emailId', 'to-email-id', 'ticketId', 'reply-to', 'reply-to-name',
                'in-reply-to', 'references', 'thread-type', 'system_emails',
                'mailflags' => array('bounce', 'auto-reply', 'spam', 'viral'),
                'recipients' => array('*' => array('name', 'email', 'source'))
                ));

            $supported['attachments']['*'][] = 'cid';
        }

        return $supported;
    }
    function recent($format) {
        // Check for valid API key and permissions
        if (!($key = $this->requireApiKey()) || !$key->canViewTickets()) {
            return $this->exerr(401, __('API key not authorized'));
        }

        // Get the 'last_checked' timestamp from the request
        $last_checked = isset($_REQUEST['last_checked']) ? (int) $_REQUEST['last_checked'] : 0;

        // Fetch tickets created after 'last_checked' and visible to the staff
        global $thisstaff;
        if (!$thisstaff) {
            return $this->exerr(403, __('Access denied.'));
        }

        $tickets = Ticket::objects()
            ->filter(array('created__gt' => date('Y-m-d H:i:s', $last_checked)))
            ->filter($thisstaff->getTicketsVisibility())
            ->values(
                'ticket_id',
                'number',
                'created',
                'cdata__subject',
                'status__name',
                'dept__name',
                'user__default_email__address',
                'user__name'
            )
            ->order_by('created');

        // Convert to array
        $tickets = $tickets->toArray();

        // Prepare the response
        $response = [
            'last_checked' => time(),
            'tickets' => array_values($tickets),
        ];

        return $this->json_encode($response);
    }

    /*
     Validate data - overwrites parent's validator for additional validations.
    */
    function validate(&$data, $format, $strict=true) {
        global $ost;

        //Call parent to Validate the structure
        if(!parent::validate($data, $format, $strict) && $strict)
            $this->exerr(400, __('Unexpected or invalid data received'));

        // Use the settings on the thread entry on the ticket details
        // form to validate the attachments in the email
        $tform = TicketForm::objects()->one()->getForm();
        $messageField = $tform->getField('message');
        $fileField = $messageField->getWidget()->getAttachments();

        // Nuke attachments IF API files are not allowed.
        if (!$messageField->isAttachmentsEnabled())
            $data['attachments'] = array();

        //Validate attachments: Do error checking... soft fail - set the error and pass on the request.
        if (isset($data['attachments']) && is_array($data['attachments'])) {
            foreach($data['attachments'] as &$file) {
                if ($file['encoding'] && !strcasecmp($file['encoding'], 'base64')) {
                    if(!($file['data'] = base64_decode($file['data'], true)))
                        $file['error'] = sprintf(__('%s: Poorly encoded base64 data'),
                            Format::htmlchars($file['name']));
                }
                // Validate and save immediately
                try {
                    $F = $fileField->uploadAttachment($file);
                    $file['id'] = $F->getId();
                }
                catch (FileUploadError $ex) {
                    $name = $file['name'];
                    $file = array();
                    $file['error'] = Format::htmlchars($name) . ': ' . $ex->getMessage();
                }
            }
            unset($file);
        }

        return true;
    }


    function create($format) {

        if (!($key=$this->requireApiKey()) || !$key->canCreateTickets())
            return $this->exerr(401, __('API key not authorized'));

        $ticket = null;
        if (!strcasecmp($format, 'email')) {
            // Process remotely piped emails - could be a reply...etc.
            $ticket = $this->processEmailRequest();
        } else {
            // Get and Parse request body data for the format
            $ticket = $this->createTicket($this->getRequest($format));
        }



        if ($ticket)
            $this->response(201, $ticket->getNumber());
        else
            $this->exerr(500, _S("unknown error"));

    }

    /* private helper functions */

    function createTicket($data, $source = 'API') {

        # Pull off some meta-data
        $alert       = (bool) (isset($data['alert'])       ? $data['alert']       : true);
        $autorespond = (bool) (isset($data['autorespond']) ? $data['autorespond'] : true);

        // Assign default value to source if not defined, or defined as NULL
        $data['source'] ??= $source;

        // Create the ticket with the data (attempt to anyway)
        $errors = array();
        if (($ticket = Ticket::create($data, $errors, $data['source'],
                        $autorespond, $alert)) &&  !$errors)
            return $ticket;

        // Ticket create failed Bigly - got errors?
        $title = null;
        // Got errors?
        if (count($errors)) {
            // Ticket denied? Say so loudly so it can standout from generic
            // validation errors
            if (isset($errors['errno']) && $errors['errno'] == 403) {
                $title = _S('Ticket denied');
                $error = sprintf("%s: %s\n\n%s",
                        $title, $data['email'], $errors['err']);
            } else {
                // unpack the errors
                $error = Format::array_implode("\n", "\n", $errors);
            }
        } else {
            // unknown reason - default
            $error = _S('unknown error');
        }

        $error = sprintf('%s :%s',
                _S('Unable to create new ticket'), $error);
        return $this->exerr($errors['errno'] ?: 500, $error, $title);
    }

    function processEmailRequest() {
        return $this->processEmail();
    }

    function processEmail($data=false, array $defaults = []) {

        try {
            if (!$data)
                $data = $this->getEmailRequest();
            elseif (!is_array($data))
                $data = $this->parseEmail($data);
        } catch (Exception $ex)  {
            throw new EmailParseError($ex->getMessage());
        }

        $data = array_merge($defaults, $data);
        $seen = false;
        if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen))
            && ($message = $entry->postEmail($data))
        ) {
            if ($message instanceof ThreadEntry) {
                return $message->getThread()->getObject();
            }
            else if ($seen) {
                // Email has been processed previously
                return $entry->getThread()->getObject();
            }
        }

        // Allow continuation of thread without initial message or note
        elseif (($thread = Thread::lookupByEmailHeaders($data))
            && ($message = $thread->postEmail($data))
        ) {
            return $thread->getObject();
        }

        // All emails which do not appear to be part of an existing thread
        // will always create new "Tickets". All other objects will need to
        // be created via the web interface or the API
        try {
            return $this->createTicket($data, 'Email');
        } catch (TicketApiError $err) {
            // Check if the ticket was denied by a filter or banlist
            if ($err->isDenied() && $data['mid']) {
                // We need to log the Message-Id (mid) so we don't
                // process the same email again in subsequent fetches
                $entry = new ThreadEntry();
                $entry->logEmailHeaders(0, $data['mid']);
                // throw TicketDenied exception so the caller can handle it
                // accordingly
                throw new TicketDenied($err->getMessage());
            } else {
                // otherwise rethrow this bad baby as it is!
                throw $err;
            }
        }
    }
}

//Local email piping controller - no API key required!
class PipeApiController extends TicketApiController {

    // Overwrite grandparent's (ApiController) response method.
    function response($code, $resp) {

        // It's important to use postfix exit codes for local piping instead
        // of HTTP's so the piping script can process them accordingly
        switch($code) {
            case 201: //Success
                $exitcode = 0;
                break;
            case 400:
                $exitcode = 66;
                break;
            case 401: /* permission denied */
            case 403:
                $exitcode = 77;
                break;
            case 415:
            case 416:
            case 417:
            case 501:
                $exitcode = 65;
                break;
            case 503:
                $exitcode = 69;
                break;
            case 500: //Server error.
            default: //Temp (unknown) failure - retry
                $exitcode = 75;
        }
        //We're simply exiting - MTA will take care of the rest based on exit code!
        exit($exitcode);
    }

    static function process($sapi=null) {
        $pipe = new PipeApiController($sapi);
        if (($ticket=$pipe->processEmail()))
           return $pipe->response(201,
                   is_object($ticket) ? $ticket->getNumber() : $ticket);

        return $pipe->exerr(416, __('Request failed - retry again!'));
    }

    static function local() {
        return self::process('cli');
    }
}

class TicketApiError extends Exception {

    // Check if exception is because of denial
    public function isDenied() {
        return ($this->getCode() === 403);
    }
}

class TicketDenied extends Exception {}
class EmailParseError extends Exception {}

?>