Your IP : 216.73.217.117


Current Path : /var/www/cesa.co.za/php/includes/
Upload File :
Current File : /var/www/cesa.co.za/php/includes/MarketingAttendees.php

<?php

/*
 * Contact otto@igotafrica.com for details
 */

include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/BaseTableClass.php');
include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/MarketingPayments.php');
include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/Mailer.php');
include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/sqlFunctions.php');
include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/ErrorLog.php');

/**
 * Control the  MarketingAttendees table
 *
 * @author Otto Saayman
 */
class MarketingAttendees extends BaseTableClass
{

    public function findById($attendeeId)
    {
        //        echo "SELECT * FROM MarketingAttendees "
        //        . "INNER JOIN MarketingEvents ON MarketingAttendees.EventID = MarketingEvents.EventID "
        //        . "WHERE MarketingAttendees.EventID = $eventId <br />";
        return $this->sqlf->getRow("SELECT * FROM MarketingAttendees "
            . "INNER JOIN MarketingEvents ON MarketingAttendees.EventID = MarketingEvents.EventID "
            . "WHERE MarketingAttendees.AttendeeID = $attendeeId");
    }

    public function findByEventId($eventId)
    {
        //        echo "SELECT * FROM MarketingAttendees "
        //        . "INNER JOIN MarketingEvents ON MarketingAttendees.EventID = MarketingEvents.EventID "
        //        . "WHERE MarketingAttendees.EventID = $eventId <br />";
        return $this->sqlf->getAll("SELECT * FROM MarketingAttendees "
            . "INNER JOIN MarketingEvents ON MarketingAttendees.EventID = MarketingEvents.EventID "
            . "WHERE MarketingAttendees.EventID = $eventId");
    }

    public function findAllIds()
    {
        return $this->sqlf->getAll("SELECT AttendeeID FROM MarketingAttendees "
            . "WHERE EmailAddress IS NOT NULL AND EmailAddress != '' AND EmailAddress NOT LIKE 'no e-mail address%' "
            . "AND Cancelled = 0");
    }

    public function chooseAttendedPaid($requestVars)
    {

        $attendeeIds = array();

        if (isset($requestVars['AttendeeIDs']) && strlen($requestVars['AttendeeIDs']) > 0) {
            $attendeeIds = json_decode($requestVars['AttendeeIDs'], true);
        }

        if ((isset($requestVars['markAllAsAttendedPaid']) && $requestVars['markAllAsAttendedPaid'] > 0) || (isset($requestVars['markSelectedAsAttendedPaid']) && $requestVars['markSelectedAsAttendedPaid'] > 0)) {
            $this->markAllAsAttendedPaid($requestVars['EventID'], true, true, $attendeeIds);
        } elseif ((isset($requestVars['markAllAsAttended']) && $requestVars['markAllAsAttended'] > 0) || (isset($requestVars['markSelectedAsAttended']) && $requestVars['markSelectedAsAttended'] > 0)) {
            $this->markAllAsAttendedPaid($requestVars['EventID'], true, false, $attendeeIds);
        } elseif ((isset($requestVars['markAllAsPaid']) && $requestVars['markAllAsPaid'] > 0) || (isset($requestVars['markSelectedAsPaid']) && $requestVars['markSelectedAsPaid'] > 0)) {
            $this->markAllAsAttendedPaid($requestVars['EventID'], false, true, $attendeeIds);
        } elseif ((isset($requestVars['markAllAsNotAttendedPaid']) && $requestVars['markAllAsNotAttendedPaid'] > 0) || (isset($requestVars['markSelectedAsNotAttendedPaid']) && $requestVars['markSelectedAsNotAttendedPaid'] > 0)) {
            $this->markAllAsNotAttendedPaid($requestVars['EventID'], true, true, $attendeeIds);
        } elseif ((isset($requestVars['markAllAsNotAttended']) && $requestVars['markAllAsNotAttended'] > 0) || (isset($requestVars['markSelectedAsNotAttended']) && $requestVars['markSelectedAsNotAttended'] > 0)) {
            $this->markAllAsNotAttendedPaid($requestVars['EventID'], true, false, $attendeeIds);
        } elseif ((isset($requestVars['markAllAsNotPaid']) && $requestVars['markAllAsNotPaid'] > 0) || (isset($requestVars['markSelectedAsNotPaid']) && $requestVars['markSelectedAsNotPaid'] > 0)) {
            $this->markAllAsNotAttendedPaid($requestVars['EventID'], false, true, $attendeeIds);
        }
    }

    public function markAllAsAttendedPaid($eventId, $attended = true, $paid = true, $attendeeIds = array())
    {

        global $sqlf;

        $eventAttendees = $this->findByEventId($eventId);
        $marketingPaymentObject = new MarketingPayments($sqlf);

        foreach ($eventAttendees as $eventAttendee) {

            if (count($attendeeIds) > 0 && !in_array($eventAttendee['AttendeeID'], $attendeeIds)) {
                continue;
            }

            //            echo 'found attendee! <br />';

            if ($attended) {

                $eventAttendee['Attended'] = 'y';

                $this->sqlf->autoExecute('MarketingAttendees', $eventAttendee, 'UPDATE', 'AttendeeID = ' . $eventAttendee['AttendeeID']);
            }

            if ($paid) {
                $marketingPaymentObject->markPaidByEventAttendeeId($eventAttendee['AttendeeID'], $eventAttendee['CostPerPerson'], $eventAttendee['EventID']);
            }
        }
    }

    public function markAllAsNotAttendedPaid($eventId, $attended = true, $paid = true, $attendeeIds = array())
    {

        global $sqlf;

        $eventAttendees = $this->findByEventId($eventId);
        $marketingPaymentObject = new MarketingPayments($sqlf);

        foreach ($eventAttendees as $eventAttendee) {

            if (count($attendeeIds) > 0 && !in_array($eventAttendee['AttendeeID'], $attendeeIds)) {
                continue;
            }

            //            echo 'found attendee! <br />';

            if ($attended) {

                $eventAttendee['Attended'] = 'n';

                // var_dump($eventAttendee);
                // exit;

                $this->sqlf->autoExecute('MarketingAttendees', $eventAttendee, 'UPDATE', 'AttendeeID = ' . $eventAttendee['AttendeeID']);
            }

            if ($paid) {
                $marketingPaymentObject->markNotPaidByEventAttendeeId($eventAttendee['AttendeeID'], $eventAttendee['CostPerPerson'], $eventAttendee['EventID']);
            }
        }
    }

    public function getICSAttachmentFile($marketingAttendee, $options = array())
    {

        if (!is_array($marketingAttendee) || count($marketingAttendee) <= 0) {
            return null;
        }

        $fullFilePath = sys_get_temp_dir() . '/' . $marketingAttendee['AttendeeID'] . '.ics';

        file_put_contents($fullFilePath, Mailer::getICalText($marketingAttendee, $marketingAttendee, $options));

        return $fullFilePath;
    }

    const FIELD_RESEND_INVITES = 'ResendInvites';
    const FIELD_CALENDAR_CHANGE_MESSAGE = 'CalendarInviteChangeMessage';
    const MAIL_SUBJECT_MAX_LENGTH = 255;
    const CALENDAR_RESEND_BATCH_SIZE = 5;
    const SESSION_CALENDAR_RESEND_PENDING = 'MarketingEventCalendarResendPending';

    const IDENTITY_TYPE_SA_ID = 'SA ID';
    const IDENTITY_TYPE_PASSPORT = 'Passport';
    const FIELD_IDENTITY_DOCUMENT_TYPE = 'IdentityDocumentType';

    public function normalizeIdentityDocumentType($value)
    {
        $value = trim((string) $value);
        if (strcasecmp($value, self::IDENTITY_TYPE_PASSPORT) === 0 || strcasecmp($value, 'Passport') === 0) {
            return self::IDENTITY_TYPE_PASSPORT;
        }

        return self::IDENTITY_TYPE_SA_ID;
    }

    public function identityNumberFieldLabel($identityDocumentType)
    {
        if ($this->normalizeIdentityDocumentType($identityDocumentType) === self::IDENTITY_TYPE_PASSPORT) {
            return 'Passport number';
        }

        return 'South African ID number';
    }

    public function isValidSouthAfricanIdNumber($value)
    {
        $value = preg_replace('/\s+/', '', trim((string) $value));
        if (!preg_match('/^\d{13}$/', $value)) {
            return false;
        }

        $val1 = 0;
        $val2 = 0;
        for ($i = 0; $i < 13; $i += 2) {
            $val1 += (int) $value[$i];
        }
        for ($i = 1; $i < 12; $i += 2) {
            $tmp = (int) $value[$i] * 2;
            if ($tmp >= 10) {
                $tmp = ($tmp - 10) + 1;
            }
            $val2 += $tmp;
        }

        return (($val1 + $val2) % 10) === 0;
    }

    public function isValidPassportNumber($value)
    {
        $value = preg_replace('/\s+/', '', trim((string) $value));
        if ($value === '') {
            return false;
        }

        return (bool) preg_match('/^[A-Za-z0-9]{6,20}$/', $value);
    }

    public function validateRegistrationIdentityNumber($identityDocumentType, $idNumber, $attendeeNumber = 0)
    {
        $attendeeNumber = (int) $attendeeNumber;
        $prefix = $attendeeNumber > 0 ? 'Attendee ' . $attendeeNumber . "'s " : '';
        $type = $this->normalizeIdentityDocumentType($identityDocumentType);
        $idNumber = trim((string) $idNumber);

        if ($idNumber === '') {
            return $prefix . $this->identityNumberFieldLabel($type) . ' is required.';
        }

        if ($type === self::IDENTITY_TYPE_PASSPORT) {
            if (!$this->isValidPassportNumber($idNumber)) {
                return $prefix . 'Passport number is not valid. Please enter 6 to 20 letters and numbers.';
            }

            return null;
        }

        if (!$this->isValidSouthAfricanIdNumber($idNumber)) {
            return $prefix . 'South African ID number is not valid. Please enter a valid 13-digit ID number.';
        }

        return null;
    }

    public function extractDateOfBirthFromSouthAfricanId($idNumber)
    {
        $idNumber = preg_replace('/\s+/', '', trim((string) $idNumber));
        if (!$this->isValidSouthAfricanIdNumber($idNumber)) {
            return null;
        }

        $year = (int) substr($idNumber, 0, 2);
        $month = (int) substr($idNumber, 2, 2);
        $day = (int) substr($idNumber, 4, 2);
        $fullYear = 2000 + $year;
        if ($fullYear > (int) date('Y')) {
            $fullYear -= 100;
        }

        if (!checkdate($month, $day, $fullYear)) {
            return null;
        }

        return sprintf('%04d-%02d-%02d', $fullYear, $month, $day);
    }

    public function normalizeRegistrationDateOfBirth($value)
    {
        $value = trim((string) $value);
        if ($value === '') {
            return '';
        }

        $dt = \DateTime::createFromFormat('Y-m-d', $value);
        if ($dt instanceof \DateTime && $dt->format('Y-m-d') === $value) {
            return $value;
        }

        foreach (array('d/m/Y', 'd-m-Y', 'Y/m/d') as $format) {
            $dt = \DateTime::createFromFormat($format, $value);
            if ($dt instanceof \DateTime && $dt->format($format) === $value) {
                return $dt->format('Y-m-d');
            }
        }

        $timestamp = strtotime($value);
        if ($timestamp === false) {
            return null;
        }

        return date('Y-m-d', $timestamp);
    }

    public function validateRegistrationDateOfBirth($dateOfBirth, $attendeeNumber = 0)
    {
        $attendeeNumber = (int) $attendeeNumber;
        $prefix = $attendeeNumber > 0 ? 'Attendee ' . $attendeeNumber . "'s " : '';
        $normalized = $this->normalizeRegistrationDateOfBirth($dateOfBirth);

        if ($normalized === '' || $normalized === null) {
            return $prefix . 'date of birth is required. Please enter a valid date (YYYY-MM-DD).';
        }

        if ($normalized > date('Y-m-d')) {
            return $prefix . 'date of birth cannot be in the future.';
        }

        $minYear = (int) date('Y') - 120;
        if ((int) substr($normalized, 0, 4) < $minYear) {
            return $prefix . 'date of birth is not valid.';
        }

        return null;
    }

    public function formatRegistrationDateOfBirthForDisplay($value)
    {
        $normalized = $this->normalizeRegistrationDateOfBirth($value);
        if ($normalized === '' || $normalized === null) {
            return '';
        }

        return $normalized;
    }

    /**
     * SQL literal for MarketingAttendees.DateOfBirth (DATE column).
     * Empty / invalid values become NULL — never '' (MySQL rejects '' for DATE).
     *
     * @param string|null $value
     * @return string NULL or quoted Y-m-d
     */
    public function sqlDateOfBirthLiteral($value)
    {
        $normalized = $this->normalizeRegistrationDateOfBirth($value);
        if ($normalized === '' || $normalized === null) {
            return 'NULL';
        }

        return "'" . addslashes($normalized) . "'";
    }

    /**
     * Resolve DOB for registration insert: posted value, else SA ID extraction.
     *
     * @param string $postedDob
     * @param string $identityDocumentType
     * @param string $idNumber
     * @return string Y-m-d or ''
     */
    public function resolveRegistrationDateOfBirthForSave($postedDob, $identityDocumentType, $idNumber)
    {
        $dob = $this->formatRegistrationDateOfBirthForDisplay($postedDob);
        if ($dob !== '') {
            return $dob;
        }
        if ($this->normalizeIdentityDocumentType($identityDocumentType) === self::IDENTITY_TYPE_SA_ID) {
            $extracted = $this->extractDateOfBirthFromSouthAfricanId($idNumber);
            if (is_string($extracted) && $extracted !== '') {
                return $extracted;
            }
        }
        return '';
    }

    /**
     * @param array{submittedCount?:int,duplicateCount?:int,insertFailureCount?:int,insertFailures?:array<int,array<string,mixed>>} $context
     * @return array{heading:string,detail:string}
     */
    public function buildNoAttendeesRegisteredSummary(array $context)
    {
        $submittedCount = (int) ($context['submittedCount'] ?? 0);
        $duplicateCount = (int) ($context['duplicateCount'] ?? 0);
        $insertFailureCount = (int) ($context['insertFailureCount'] ?? 0);
        $insertFailures = is_array($context['insertFailures'] ?? null) ? $context['insertFailures'] : array();

        if ($submittedCount <= 0) {
            return array(
                'heading' => 'No attendees were registered.',
                'detail' => 'No attendee details were submitted. Please complete at least one attendee and submit the form again.',
            );
        }

        if ($duplicateCount > 0 && $insertFailureCount === 0 && $duplicateCount >= $submittedCount) {
            return array(
                'heading' => 'No new attendees were registered.',
                'detail' => 'All ' . $duplicateCount . ' attendee(s) you submitted are already registered for this event (see details below).',
            );
        }

        if ($duplicateCount > 0 && $insertFailureCount === 0) {
            return array(
                'heading' => 'No new attendees were registered.',
                'detail' => $duplicateCount . ' of the ' . $submittedCount . ' submitted attendee(s) are already registered for this event (see details below).',
            );
        }

        if ($insertFailureCount > 0 && $duplicateCount === 0) {
            $detail = 'Your registration could not be saved due to a system error. Please review the error details below, correct the problem if you can, and submit again. If the problem continues, please contact the event organisers.';
            if (count($insertFailures) === 1 && !empty($insertFailures[0]['displayError'])) {
                $detail .= ' Error: ' . $insertFailures[0]['displayError'];
            }

            return array(
                'heading' => 'No attendees were registered.',
                'detail' => $detail,
            );
        }

        if ($insertFailureCount > 0 && $duplicateCount > 0) {
            return array(
                'heading' => 'No new attendees were registered.',
                'detail' => $insertFailureCount . ' attendee(s) could not be saved and ' . $duplicateCount . ' duplicate(s) were found (see details below).',
            );
        }

        return array(
            'heading' => 'No attendees were registered.',
            'detail' => 'Please check your attendee details and submit the form again.',
        );
    }

    public function sanitizeRegistrationDatabaseErrorForDisplay($mysqlError)
    {
        $message = trim((string) $mysqlError);
        if ($message === '') {
            return 'The registration could not be saved.';
        }

        $message = preg_replace('/`[^`]+`\.`[^`]+`\.`([^`]+)`/', "'$1'", $message);
        $message = preg_replace('/`([^`]+)`/', "'$1'", $message);

        return $message;
    }

    /**
     * @param array{firstName?:string,surname?:string,email?:string} $attendeeContext
     * @return array{attendeeNumber:int,firstName:string,surname:string,email:string,displayError:string}
     */
    public function handleRegistrationInsertFailure($eventId, $attendeeNumber, array $attendeeContext, $mysqlError, $sql = '')
    {
        $attendeeNumber = (int) $attendeeNumber;
        $eventId = (int) $eventId;
        $displayError = $this->sanitizeRegistrationDatabaseErrorForDisplay($mysqlError);
        $firstName = trim((string) ($attendeeContext['firstName'] ?? ''));
        $surname = trim((string) ($attendeeContext['surname'] ?? ''));
        $email = trim((string) ($attendeeContext['email'] ?? ''));

        $logMessage = "Marketing event registration INSERT failed\n"
            . 'EventID: ' . $eventId . "\n"
            . 'Attendee slot: ' . $attendeeNumber . "\n"
            . 'Name: ' . $firstName . ' ' . $surname . "\n"
            . 'Email: ' . $email . "\n"
            . 'MySQL error: ' . trim((string) $mysqlError) . "\n"
            . 'SQL: ' . substr((string) $sql, 0, 4000);

        ErrorLog::logError($logMessage);

        if (function_exists('maill')) {
            maill(
                'cesa-marketing@igotafrica.com',
                'Marketing event registration insert failed (Event ' . $eventId . ')',
                $logMessage,
                null,
                [
                    'from-name' => 'CESA Events',
                    'from-address' => 'events@cesa.co.za',
                    'reply-to-address' => 'events@cesa.co.za',
                ]
            );
        }

        return array(
            'attendeeNumber' => $attendeeNumber,
            'firstName' => $firstName,
            'surname' => $surname,
            'email' => $email,
            'displayError' => $displayError,
        );
    }

    public function findCalendarInviteRecipientsByEventId($eventId)
    {
        $eventId = intval($eventId);
        if ($eventId <= 0) {
            return array();
        }

        return $this->sqlf->getAll(
            "SELECT MarketingAttendees.*, MarketingEvents.*
            FROM MarketingAttendees
            INNER JOIN MarketingEvents ON MarketingAttendees.EventID = MarketingEvents.EventID
            WHERE MarketingAttendees.EventID = " . $eventId . "
            AND IFNULL(MarketingAttendees.Cancelled, 0) = 0
            AND MarketingAttendees.EmailAddress IS NOT NULL
            AND MarketingAttendees.EmailAddress != ''
            AND MarketingAttendees.EmailAddress NOT LIKE 'no e-mail address%'
            ORDER BY MarketingAttendees.AttendeeID ASC"
        );
    }

    public function getEventCalendarResendRow($eventId)
    {
        $eventId = intval($eventId);
        if ($eventId <= 0) {
            return null;
        }

        return $this->sqlf->getRow(
            'SELECT EventID, ResendInvites FROM MarketingEvents WHERE EventID = ' . $eventId . ' LIMIT 1'
        );
    }

    public function shouldResendCalendarInvitesFromEventRow($eventRow)
    {
        if (!is_array($eventRow)) {
            return false;
        }

        $value = $eventRow[self::FIELD_RESEND_INVITES] ?? 0;

        return intval($value) === 1 || $value === true || $value === '1';
    }

    public function calendarChangeMessageFromEventRow($eventRow)
    {
        if (!is_array($eventRow)) {
            return '';
        }

        if (isset($eventRow[self::FIELD_CALENDAR_CHANGE_MESSAGE])) {
            return trim((string) $eventRow[self::FIELD_CALENDAR_CHANGE_MESSAGE]);
        }

        return '';
    }

    public function clearResendInvitesFlag($eventId)
    {
        $eventId = intval($eventId);
        if ($eventId <= 0) {
            return false;
        }

        return $this->sqlf->execute(
            'UPDATE MarketingEvents SET ' . self::FIELD_RESEND_INVITES . ' = 0 WHERE EventID = ' . $eventId
        );
    }

    public function getCalendarResendRecipientCount($eventId)
    {
        return count($this->findCalendarInviteRecipientsByEventId($eventId));
    }

    public function sendCalendarInvitesBatch($eventId, $changeMessage = '', $offset = 0, $batchSize = null)
    {
        $eventId = intval($eventId);
        $offset = max(0, intval($offset));
        $batchSize = $batchSize === null ? self::CALENDAR_RESEND_BATCH_SIZE : max(1, intval($batchSize));

        $result = array(
            'eventId' => $eventId,
            'total' => 0,
            'offset' => $offset,
            'processed' => 0,
            'sent' => 0,
            'skipped' => 0,
            'failed' => 0,
            'complete' => false,
            'nextOffset' => $offset,
            'errors' => array(),
        );

        if ($eventId <= 0) {
            $result['errors'][] = 'Invalid event ID.';
            $result['complete'] = true;
            return $result;
        }

        $attendees = $this->findCalendarInviteRecipientsByEventId($eventId);
        $result['total'] = count($attendees);

        if ($result['total'] <= 0) {
            $this->clearResendInvitesFlag($eventId);
            $result['complete'] = true;
            return $result;
        }

        if ($offset >= $result['total']) {
            $result['complete'] = true;
            $result['nextOffset'] = $result['total'];
            return $result;
        }

        $batch = array_slice($attendees, $offset, $batchSize);
        foreach ($batch as $attendeeRow) {
            $sendResult = $this->sendCalendarInviteToAttendeeViaMaill($attendeeRow, $changeMessage);
            $result['processed']++;
            if ($sendResult === true) {
                $result['sent']++;
            } elseif ($sendResult === null) {
                $result['skipped']++;
            } else {
                $result['failed']++;
                if (is_string($sendResult) && $sendResult !== '') {
                    $result['errors'][] = $sendResult;
                }
            }
        }

        $result['nextOffset'] = $offset + count($batch);
        $result['complete'] = $result['nextOffset'] >= $result['total'];
        if ($result['complete'] && $result['failed'] === 0) {
            $this->clearResendInvitesFlag($eventId);
        }

        return $result;
    }

    /**
     * @return bool|null|string true sent, null skipped, string error message on failure
     */
    public function sendCalendarInviteToAttendeeViaMaill($attendeeRow, $changeMessage = '')
    {
        if (!is_array($attendeeRow) || count($attendeeRow) <= 0) {
            return null;
        }

        $recipientEmails = $this->buildCalendarInviteRecipientEmails($attendeeRow);
        if (count($recipientEmails) <= 0) {
            return null;
        }

        include_once($_SERVER['DOCUMENT_ROOT'] . '/php/includes/maill.php');

        $eventName = isset($attendeeRow['EventName']) ? $attendeeRow['EventName'] : 'CESA Event';
        $subject = $this->truncateForMailColumn('Updated: ' . $eventName, self::MAIL_SUBJECT_MAX_LENGTH);
        $body = $this->buildCalendarUpdateEmailHtml($attendeeRow, $changeMessage);
        $icsOptions = array(
            'isUpdate' => true,
            'changeMessage' => $changeMessage,
            'sequence' => time(),
        );
        $mailOptions = $this->buildDirectCalendarResendMailOptions($attendeeRow, $icsOptions);

        try {
            //TEST FIRST
            $recipientEmails = ['cesa-event-test@igotafrica.com'];

            maill(implode(',', $recipientEmails), $subject, $body, null, $mailOptions);
        } catch (\Throwable $e) {
            return 'Attendee ' . intval($attendeeRow['AttendeeID']) . ': ' . $e->getMessage();
        }

        return true;
    }

    protected function buildDirectCalendarResendMailOptions($attendeeRow, array $icsOptions)
    {
        return array(
            'isHTML' => true,
            'content-type' => 'text/html; charset=UTF-8',
            'from-address' => 'bonolo@cesa.co.za',
            'from-name' => 'CESA Events',
            'reply-to-address' => 'events@cesa.co.za',
            'i-cal' => Mailer::getICalText($attendeeRow, $attendeeRow, $icsOptions),
        );
    }

    public function clearStaleCalendarResendSessionOnEditPageLoad()
    {
        if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') {
            return;
        }

        unset($_SESSION[self::SESSION_CALENDAR_RESEND_PENDING]);
    }

    public function prepareCalendarResendSession($eventId, $changeMessage = '')
    {
        $eventId = intval($eventId);
        if ($eventId <= 0) {
            return false;
        }

        $_SESSION[self::SESSION_CALENDAR_RESEND_PENDING] = array(
            'eventId' => $eventId,
            'changeMessage' => trim((string) $changeMessage),
        );

        return true;
    }

    public function consumeCalendarResendSession($eventId)
    {
        $eventId = intval($eventId);
        if ($eventId <= 0 || !isset($_SESSION[self::SESSION_CALENDAR_RESEND_PENDING])) {
            return null;
        }

        $pending = $_SESSION[self::SESSION_CALENDAR_RESEND_PENDING];
        if (!is_array($pending) || intval($pending['eventId'] ?? 0) !== $eventId) {
            return null;
        }

        unset($_SESSION[self::SESSION_CALENDAR_RESEND_PENDING]);

        return array(
            'eventId' => $eventId,
            'changeMessage' => trim((string) ($pending['changeMessage'] ?? '')),
        );
    }

    public function hasCalendarResendPendingRedirect()
    {
        return isset($_SESSION[self::SESSION_CALENDAR_RESEND_PENDING])
            && is_array($_SESSION[self::SESSION_CALENDAR_RESEND_PENDING])
            && intval($_SESSION[self::SESSION_CALENDAR_RESEND_PENDING]['eventId'] ?? 0) > 0;
    }

    public function getCalendarResendWaitPageUrl($eventId)
    {
        $eventId = intval($eventId);
        if ($eventId <= 0) {
            return '';
        }

        return '/echasl23/MarketingEventCalendarResendWait.php?EventID=' . $eventId;
    }

    public function truncateForMailColumn($text, $maxLength)
    {
        $text = (string) $text;
        $maxLength = intval($maxLength);
        if ($maxLength <= 0 || strlen($text) <= $maxLength) {
            return $text;
        }

        if ($maxLength <= 3) {
            return substr($text, 0, $maxLength);
        }

        return substr($text, 0, $maxLength - 3) . '...';
    }

    public function buildCalendarInviteRecipientEmails($attendeeRow)
    {
        $emails = array();

        if (!empty($attendeeRow['EmailAddress']) && $this->isUsableCalendarEmail($attendeeRow['EmailAddress'])) {
            $emails[] = trim($attendeeRow['EmailAddress']);
        }

        if (!empty($attendeeRow['BookingEmail']) && $this->isUsableCalendarEmail($attendeeRow['BookingEmail'])) {
            $emails[] = trim($attendeeRow['BookingEmail']);
        }

        return array_values(array_unique($emails));
    }

    public function buildCalendarInviteRecipientList($attendeeRow)
    {
        return implode(',', $this->buildCalendarInviteRecipientEmails($attendeeRow));
    }

    public function isUsableCalendarEmail($email)
    {
        $email = trim((string) $email);
        if ($email === '') {
            return false;
        }

        $lower = strtolower($email);
        if ($lower === 'not applicable' || $lower === 'n/a' || $lower === 'na') {
            return false;
        }

        if (stripos($email, 'no e-mail address') === 0) {
            return false;
        }

        return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
    }

    public function buildCalendarUpdateEmailHtml($attendeeRow, $changeMessage = '')
    {
        $firstName = isset($attendeeRow['ContactFirstName']) ? $attendeeRow['ContactFirstName'] : '';
        $lastName = isset($attendeeRow['ContactLastName']) ? $attendeeRow['ContactLastName'] : '';
        $safeName = htmlspecialchars(trim($firstName . ' ' . $lastName), ENT_QUOTES, 'UTF-8');

        $html = '<html><head><style>body, td { font-family: Arial, Verdana, sans-serif; font-size: 11pt; }</style></head><body>';
        $html .= '<p>Dear ' . $safeName . ',</p>';
        $html .= '<p><strong>This event has been updated.</strong> Please find the updated calendar invitation attached.</p>';

        $changeMessage = trim((string) $changeMessage);
        if ($changeMessage !== '') {
            $html .= '<p>' . nl2br(htmlspecialchars($changeMessage, ENT_QUOTES, 'UTF-8')) . '</p>';
        }

        $html .= $this->buildEventSummaryHtml($attendeeRow);
        $html .= '<p>With kind regards,<br>CESA Events</p>';
        $html .= '</body></html>';

        return $html;
    }

    public function buildEventSummaryHtml($eventRow)
    {
        $eventName = isset($eventRow['EventName']) ? htmlspecialchars($eventRow['EventName'], ENT_QUOTES, 'UTF-8') : '';
        $html = '<p><strong>' . $eventName . '</strong>';

        if (!empty($eventRow['StartDate'])) {
            $html .= ' will be held on ' . date('D j F Y', strtotime($eventRow['StartDate']));
            if (!empty($eventRow['EndDate']) && $eventRow['EndDate'] !== $eventRow['StartDate']) {
                $html .= ' to ' . date('D j F Y', strtotime($eventRow['EndDate']));
            }
        }

        if (!empty($eventRow['StartTime'])) {
            $html .= ' at ' . date('H:i', strtotime($eventRow['StartTime']));
            if (!empty($eventRow['EndTime'])) {
                $html .= '-' . date('H:i', strtotime($eventRow['EndTime']));
            }
        }

        if (!empty($eventRow['Venue'])) {
            if ($eventRow['Venue'] === 'MS Teams') {
                $html .= ', on ' . htmlspecialchars($eventRow['Venue'], ENT_QUOTES, 'UTF-8');
            } else {
                $html .= ', at ' . htmlspecialchars($eventRow['Venue'], ENT_QUOTES, 'UTF-8');
            }
        }

        $html .= '.</p>';
        return $html;
    }

    public function formatCalendarResendResultMessage(array $result)
    {
        $sent = isset($result['sent']) ? intval($result['sent']) : 0;
        $skipped = isset($result['skipped']) ? intval($result['skipped']) : 0;
        $failed = isset($result['failed']) ? intval($result['failed']) : 0;

        $message = 'Calendar invites sent: ' . $sent;
        if ($skipped > 0) {
            $message .= ', ' . $skipped . ' skipped';
        }
        if ($failed > 0) {
            $message .= ', ' . $failed . ' failed (ResendInvites left set)';
        }
        $message .= '.';

        if (!empty($result['errors']) && is_array($result['errors'])) {
            $message .= ' ' . implode(' ', array_slice($result['errors'], 0, 3));
        }

        return $message;
    }

    public function resolveAttendeeDateOfBirth(array $attendee)
    {
        $stored = trim((string) ($attendee['DateOfBirth'] ?? ''));
        if ($stored !== '') {
            $normalized = $this->normalizeRegistrationDateOfBirth($stored);
            if ($normalized !== '' && $normalized !== null) {
                return $normalized;
            }
        }

        $idNumber = trim((string) ($attendee['IDNumber'] ?? ''));
        if ($idNumber === '') {
            return null;
        }

        return $this->extractDateOfBirthFromSouthAfricanId($idNumber);
    }

    public function getYoungProfessionalYesNo($attendeeID)
    {

        if (empty($attendeeID)) {
            return '-';
        }

        $attendee = $this->findById($attendeeID);

        if (empty($attendee)) {
            return '-';
        }

        $dateOfBirth = $this->resolveAttendeeDateOfBirth($attendee);
        if ($dateOfBirth === null) {
            return '-';
        }

        $birthDate = DateTime::createFromFormat('Y-m-d', $dateOfBirth);
        if (!$birthDate instanceof DateTime) {
            return '-';
        }

        $today = new DateTime('today');
        $age = $today->diff($birthDate)->y;

        if ($age <= 36) {
            return 'Yes';
        }

        return 'No';
    }
}

function marketingEventRowUpdatedResendCalendarInvites(&$rsold, &$rsnew)
{
    global $sqlf;

    $marketingAttendees = new MarketingAttendees($sqlf);

    $eventId = 0;
    if (is_array($rsnew) && isset($rsnew['EventID'])) {
        $eventId = intval($rsnew['EventID']);
    } elseif (is_array($rsold) && isset($rsold['EventID'])) {
        $eventId = intval($rsold['EventID']);
    }

    $shouldResend = $marketingAttendees->shouldResendCalendarInvitesFromEventRow($rsnew);

    if (!$shouldResend) {
        return;
    }

    $changeMessage = $marketingAttendees->calendarChangeMessageFromEventRow($rsnew);

    $marketingAttendees->prepareCalendarResendSession($eventId, $changeMessage);
}

function marketingEventEditPageLoadClearStaleCalendarResend()
{
    global $sqlf;

    $marketingAttendees = new MarketingAttendees($sqlf);
    $marketingAttendees->clearStaleCalendarResendSessionOnEditPageLoad();
}

function marketingEventEditPageRedirectingCalendarResend(&$url)
{
    global $sqlf;

    $marketingAttendees = new MarketingAttendees($sqlf);
    if (!$marketingAttendees->hasCalendarResendPendingRedirect()) {
        return;
    }

    $pending = $_SESSION[MarketingAttendees::SESSION_CALENDAR_RESEND_PENDING];
    $eventId = intval($pending['eventId'] ?? 0);
    if ($eventId <= 0) {
        return;
    }

    $url = $marketingAttendees->getCalendarResendWaitPageUrl($eventId);
}



function marketingAttendeeListOptionsLoad(&$objThis) {

    // $objThis->ListOptions->Add("registration");     
    // $objThis->ListOptions->Items["registration"]->CssStyle = "white-space: nowrap;";
    // $objThis->ListOptions->Items["registration"]->OnLeft = FALSE;  
    // $objThis->ListOptions->Items["registration"]->Header="Registration Form";
    // $objThis->ListOptions->MoveItem("registration",count($objThis->ListOptions->Items)-1);

    $objThis->ListOptions->Add("YoungProfessional");     
    $objThis->ListOptions->Items["YoungProfessional"]->CssStyle = "white-space: nowrap;";
    $objThis->ListOptions->Items["YoungProfessional"]->OnLeft = FALSE;  
    $objThis->ListOptions->Items["YoungProfessional"]->Header="Young Pro";
    $objThis->ListOptions->MoveItem("YoungProfessional", 0);
}

function marketingAttendeeListOptionsRendered(&$objThis, &$marketingAttendeesTable) {

    global $sqlf;

    $attendeeId = $marketingAttendeesTable->AttendeeID->CurrentValue;
    $marketingAttendeesService = new MarketingAttendees($sqlf);

    $objThis->ListOptions->Items["YoungProfessional"]->Body = $marketingAttendeesService->getYoungProfessionalYesNo($attendeeId);

}

function marketingCertificatesListMenu() {
    
    if (isset($_SESSION['EventID']) && strlen($_SESSION['EventID']) > 0) {
        $_SESSION[EW_SESSION_MESSAGE] .= '<br><SPAN class=phpmaker>'
            . '<A href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '"><IMG title="" border=0 alt="" src="images/exportemail.gif" width=16 height=16> E-mail All Certificates</A> '
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markAllAsAttendedPaid=1">Mark All as Attended and Paid</a> '
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markAllAsAttended=1">Mark All as Attended</a> '
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markAllAsPaid=1">Mark All as Paid</a> '
            //                . '<a href="CertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markAllAsUnpaid=1">Mark All as Unpaid</a>'
            . '<br /><br />'
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markSelectedEMail=1" class="selected-e-mail"><IMG title="" border=0 alt="" src="images/exportemail.gif" width=16 height=16> E-mail Selected Certificates</a> '
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markSelectedAsAttendedPaid=1" class="selected-attended-paid">Mark Selected as Attended and Paid</a> '
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markSelectedAsAttended=1" class="selected-attended">Mark Selected as Attended</a> '
            . '<a href="MarketingCertificateEmail.php?EventID=' . $_SESSION['EventID'] . '&markSelectedAsPaid=1" class="selected-paid">Mark Selected as Paid</a><br /><br />'
		. '<a href="MarketingCertificateEmail.php?EventID='.$_SESSION['EventID'].'&markAllAsNotAttendedPaid=1">Mark All as NOT Attended and NOT Paid</a> '
		. '<a href="CertificateEmail.php?EventID='.$_SESSION['EventID'].'&markAllAsNotAttended=1">Mark All NOT NOT Attended</a> '
		. '<a href="CertificateEmail.php?EventID='.$_SESSION['EventID'].'&markAllAsNotPaid=1">Mark All as NOT Paid</a><br /><br />' 
		. '<a href="CertificateEmail.php?EventID='.$_SESSION['EventID'].'&markSelectedAsNotAttendedPaid=1" class="selected-attended-paid">Mark Selected as NOT Attended and Paid</a> '
        . '<a href="CertificateEmail.php?EventID='.$_SESSION['EventID'].'&markSelectedAsNotAttended=1" class="selected-attended">Mark Selected as NOT Attended</a> '
        . '<a href="CertificateEmail.php?EventID='.$_SESSION['EventID'].'&markSelectedAsNotPaid=1" class="selected-paid">Mark Selected as NOT Paid</a><br /><br />'
        . '</SPAN>';
    }
}