diff --git a/agent/post/ticket.php b/agent/post/ticket.php index a8d9b85a7..85513ed6f 100644 --- a/agent/post/ticket.php +++ b/agent/post/ticket.php @@ -3062,7 +3062,7 @@ if (isset($_POST['edit_ticket_schedule'])) { /// Create iCal event - $cal_str = createiCalStr($schedule, $cal_subject, $cal_description, $cal_location); + $cal_str = createiCalStr($schedule, $cal_subject, $cal_description, $cal_location, getTicketCalendarUid($ticket_id)); // Notify the agent of the scheduled work $data[] = [ @@ -3183,7 +3183,6 @@ if (isset($_GET['cancel_ticket_schedule'])) { $ticket_number = intval($row['ticket_number']); $ticket_subject = escapeSql($row['ticket_subject']); $ticket_schedule = escapeSql($row['ticket_schedule']); - $ticket_cal_str = escapeSql($row['ticket_cal_str']); // Don't Enforce Client Access if Ticket doesn't have an assigned client if ($client_id) { @@ -3203,9 +3202,6 @@ if (isset($_GET['cancel_ticket_schedule'])) { $config_ticket_from_name = escapeSql($config_ticket_from_name); $session_company_name = escapeSql($session_company_name); - //Create iCal event - $cal_str = createiCalStrCancel($ticket_cal_str); - //Send emails $sql = mysqli_query($mysqli, "SELECT client_name, contact_email, contact_name, ticket_client_id, ticket_details, ticket_number, @@ -3229,6 +3225,10 @@ if (isset($_GET['cancel_ticket_schedule'])) { $user_name = escapeSql($row['user_name']); $user_email = escapeSql($row['user_email']); + //Create the iCal cancellation - same UID and subject as the original invite + $cal_subject = $ticket_number . ": " . $client_name . " - " . $ticket_subject; + $cal_str = createiCalStrCancel($ticket_schedule, $cal_subject, getTicketCalendarUid($ticket_id)); + // Notify the agent of the cancellation $data[] = [ // User Email diff --git a/functions/app.php b/functions/app.php index c285e976b..df45126dc 100644 --- a/functions/app.php +++ b/functions/app.php @@ -415,7 +415,16 @@ function addToMailQueue($data) { return true; } -function createiCalStr($datetime, $title, $description, $location) { +function getTicketCalendarUid($ticket_id) { + // An invite and its later cancellation MUST carry the same UID or the + // recipient's calendar client cannot match them up. Derive it from the + // ticket so it is stable across both, rather than from the current time. + $ticket_id = intval($ticket_id); + $host = $_SERVER['SERVER_NAME'] ?? 'itflow'; + return "ticket-$ticket_id@$host"; +} + +function createiCalStr($datetime, $title, $description, $location, $uid = null) { require_once "../libs/zapcal/zapcallib.php"; // Create the iCal object @@ -431,8 +440,11 @@ function createiCalStr($datetime, $title, $description, $location) { // Todo: adjust this for actual duration $event->addNode(new ZCiCalDataNode("DTEND:" . ZCiCal::fromSqlDateTime($datetime))); $event->addNode(new ZCiCalDataNode("DTSTAMP:" . ZCiCal::fromSqlDateTime())); - $uid = date('Y-m-d-H-i-s') . "@" . $_SERVER['SERVER_NAME']; + if (empty($uid)) { + $uid = date('Y-m-d-H-i-s') . "@" . ($_SERVER['SERVER_NAME'] ?? 'itflow'); + } $event->addNode(new ZCiCalDataNode("UID:" . $uid)); + $event->addNode(new ZCiCalDataNode("SEQUENCE:0")); $event->addNode(new ZCiCalDataNode("LOCATION:" . $location)); $event->addNode(new ZCiCalDataNode("DESCRIPTION:" . $description)); // Todo: add organizer details @@ -442,31 +454,27 @@ function createiCalStr($datetime, $title, $description, $location) { return $cal_event->export(); } -function createiCalStrCancel($originaliCalStr) { +function createiCalStrCancel($datetime, $title, $uid) { require_once "../libs/zapcal/zapcallib.php"; - // Import the original iCal string - $cal_event = new ZCiCal($originaliCalStr); + // Build the cancellation fresh. There is no stored copy of the original + // invite to reopen - the match is made by UID, not by the body. + $cal_event = new ZCiCal(); - // Iterate through the iCalendar object to find VEVENT nodes - foreach($cal_event->tree->child as $node) { - if($node->getName() == "VEVENT") { - // Check if STATUS node exists, update it, or add a new one - $statusFound = false; - foreach($node->data as $key => $value) { - if($key == "STATUS") { - $value->setValue("CANCELLED"); - $statusFound = true; - break; // Exit the loop once the STATUS is updated - } - } - // If STATUS node is not found, add a new STATUS node - if (!$statusFound) { - $node->addNode(new ZCiCalDataNode("STATUS:CANCELLED")); - } - } + // METHOD belongs on the VCALENDAR, not on the VEVENT + $cal_event->tree->data['METHOD'] = new ZCiCalDataNode("METHOD:CANCEL"); + + $event = new ZCiCalNode("VEVENT", $cal_event->curnode); + $event->addNode(new ZCiCalDataNode("UID:" . $uid)); + $event->addNode(new ZCiCalDataNode("SUMMARY:" . $title)); + if (!empty($datetime)) { + $event->addNode(new ZCiCalDataNode("DTSTART:" . ZCiCal::fromSqlDateTime($datetime))); + $event->addNode(new ZCiCalDataNode("DTEND:" . ZCiCal::fromSqlDateTime($datetime))); } + $event->addNode(new ZCiCalDataNode("DTSTAMP:" . ZCiCal::fromSqlDateTime())); + // Must outrank the invite's SEQUENCE:0 or clients ignore the cancellation + $event->addNode(new ZCiCalDataNode("SEQUENCE:1")); + $event->addNode(new ZCiCalDataNode("STATUS:CANCELLED")); - // Return the modified iCal string return $cal_event->export(); }