<?php /*

 Composr
 Copyright (c) Christopher Graham, 2004-2024

 See docs/LICENSE.md for full licensing information.


 NOTE TO PROGRAMMERS:
   Do not edit this file. If you need to make changes, save your changed file to the appropriate *_custom folder
   **** If you ignore this advice, then your website upgrades (e.g. for bug fixes) will likely kill your changes ****

*/

/**
 * @license    http://opensource.org/licenses/cpal_1.0 Common Public Attribution License
 * @copyright  Christopher Graham
 * @package    points
 */

/**
 * Module page class.
 */
class Module_admin_points
{
    /**
     * Find details of the module.
     *
     * @return ?array Map of module info (null: module is disabled)
     */
    public function info() : ?array
    {
        $info = [];
        $info['author'] = 'Chris Graham';
        $info['organisation'] = 'Composr';
        $info['hacked_by'] = null;
        $info['hack_version'] = null;
        $info['version'] = 2;
        $info['locked'] = false;
        $info['min_cms_version'] = 11.0;
        $info['addon'] = 'points';
        return $info;
    }

    /**
     * Find entry-points available within this module.
     *
     * @param  boolean $check_perms Whether to check permissions
     * @param  ?MEMBER $member_id The member to check permissions as (null: current user)
     * @param  boolean $support_crosslinks Whether to allow cross links to other modules (identifiable via a full-page-link rather than a screen-name)
     * @param  boolean $be_deferential Whether to avoid any entry-point (or even return null to disable the page in the Sitemap) if we know another module, or page_group, is going to link to that entry-point. Note that "!" and "browse" entry points are automatically merged with container page nodes (likely called by page-groupings) as appropriate.
     * @return ?array A map of entry points (screen-name=>language-code/string or screen-name=>[language-code/string, icon-theme-image]) (null: disabled)
     */
    public function get_entry_points(bool $check_perms = true, ?int $member_id = null, bool $support_crosslinks = true, bool $be_deferential = false) : ?array
    {
        if (!addon_installed('points')) {
            return null;
        }

        $ret = [
            'browse' => ['POINTS_LEDGER', 'menu/adminzone/audit/points_log'],
        ];
        if (!$be_deferential) {
            $ret += [
                'export_overview' => ['EXPORT_POINTS_OVERVIEW', 'menu/social/points'],
            ];
        }
        return $ret;
    }

    public $title;

    /**
     * Module pre-run function. Allows us to know metadata for <head> before we start streaming output.
     *
     * @return ?Tempcode Tempcode indicating some kind of exceptional output (null: none)
     */
    public function pre_run() : ?object
    {
        $error_msg = new Tempcode();
        if (!addon_installed__messaged('points', $error_msg)) {
            return $error_msg;
        }

        require_code('form_templates'); // Needs to run high so that the anti-click-hacking header is sent

        $type = get_param_string('type', 'browse');

        require_lang('points');

        if ($type == 'export_overview') {
            set_helper_panel_text(comcode_lang_string('DOC_EXPORT_POINTS_OVERVIEW'));

            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('POINTS_LEDGER')]]);
            breadcrumb_set_self(do_lang_tempcode('EXPORT'));

            $this->title = get_screen_title('EXPORT_POINTS_OVERVIEW');
        } elseif ($type == 'export') {
            set_helper_panel_text(comcode_lang_string('DOC_EXPORT_POINTS_LEDGER'));

            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('POINTS_LEDGER')]]);
            breadcrumb_set_self(do_lang_tempcode('EXPORT'));

            $this->title = get_screen_title('EXPORT_POINTS_LEDGER');
        } else {
            set_helper_panel_tutorial('tut_points');
        }

        if ($type == 'browse') {
            $this->title = get_screen_title('POINTS_LEDGER');
        }

        if ($type == 'view') {
            $id = get_param_integer('id');

            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('POINTS_LEDGER')]]);

            $this->title = get_screen_title('VIEW_POINT_TRANSACTION', true, [strval($id)]);
        }

        if ($type == 'reverse') {
            $id = post_param_integer('id');
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('POINTS_LEDGER')], ['_SELF:_SELF:view:' . strval($id), do_lang_tempcode('VIEW_POINT_TRANSACTION', strval($id))]]);

            $this->title = get_screen_title('REVERSE_TRANSACTION');
        }

        if ($type == 'amend') {
            $id = post_param_integer('id');
            breadcrumb_set_parents([['_SELF:_SELF:browse', do_lang_tempcode('POINTS_LEDGER')], ['_SELF:_SELF:view:' . strval($id), do_lang_tempcode('VIEW_POINT_TRANSACTION', strval($id))]]);

            $this->title = get_screen_title('AMEND_REASON');
        }

        return null;
    }

    /**
     * Execute the module.
     *
     * @return Tempcode The result of execution
     */
    public function run() : object
    {
        require_code('points');
        require_css('points');

        $type = get_param_string('type', 'browse');

        if ($type == 'export_overview') {
            return $this->export_overview();
        }
        if ($type == 'export') {
            return $this->export();
        }
        if ($type == 'reverse') {
            return $this->reverse();
        }
        if ($type == 'amend') {
            return $this->amend();
        }
        if ($type == 'browse') {
            return $this->points_ledger();
        }
        if ($type == 'view') {
            return $this->view();
        }

        return new Tempcode();
    }

    /**
     * An interface for choosing between dates.
     *
     * @param  Tempcode $title The title to display
     * @return Tempcode The result of execution
     */
    public function _get_between(object $title) : object
    {
        $fields = new Tempcode();
        require_code('global4');
        $time_start = get_site_start_time() - 60 * 60 * 24 * 365 * 5; // 5 years before site start time, so that the default is "beginning"

        $fields->attach(form_input_date(do_lang_tempcode('FROM'), '', 'from', true, false, false, $time_start, -10));
        $fields->attach(form_input_date(do_lang_tempcode('TO'), '', 'to', true, false, false, time(), -10));

        return do_template('FORM_SCREEN', [
            '_GUID' => 'e7529ab3f49792924ecdad78e1f3593c',
            'GET' => true,
            'SKIP_WEBSTANDARDS' => true,
            'TITLE' => $title,
            'FIELDS' => $fields,
            'TEXT' => '',
            'HIDDEN' => '',
            'URL' => get_self_url(false, false, [], false, true),
            'SUBMIT_ICON' => 'admin/export_spreadsheet',
            'SUBMIT_NAME' => do_lang_tempcode('EXPORT'),
        ]);
    }

    /**
     * Export a spreadsheet showing a point overview for all members.
     *
     * @return Tempcode The result of execution
     */
    public function export_overview() : object
    {
        $d = [post_param_date('from', true), post_param_date('to', true)];
        if ($d[0] === null) {
            return $this->_get_between($this->title);
        }
        list($from, $to) = $d;

        require_code('tasks');
        return call_user_func_array__long_task(do_lang('EXPORT_POINTS_OVERVIEW'), $this->title, 'export_points_overview', [$from, $to]);
    }

    /**
     * Export a spreadsheet showing the entire points ledger.
     *
     * @return Tempcode The result of execution
     */
    public function export() : object
    {
        require_code('tasks');
        return call_user_func_array__long_task(do_lang('EXPORT_POINTS_LEDGER'), $this->title, 'export_points_ledger', [null]);
    }

    /**
     * The UI to view point transactions ordered by date.
     *
     * @return Tempcode The UI
     */
    public function points_ledger() : object
    {
        if (!has_privilege(get_member(), 'view_points_ledger')) {
            access_denied('PRIVILEGE', 'view_points_ledger');
        }

        $start = get_param_integer('ledger_start', 0);
        $max = get_param_integer('ledger_max', 50);

        // Filter parameters
        $filter_username = get_param_string('filter_ledger_username', '', INPUT_FILTER_NONE);
        $filter_type = get_param_string('filter_ledger_type', 'all');
        $filter_from = post_param_date('filter_ledger_from', true);
        $filter_to = post_param_date('filter_ledger_to', true);
        $_filter_t_type = get_param_string('filter_ledger_t_type', '');
        $filter_t_type = explode(',', $_filter_t_type);

        // Build WHERE query with filters
        $where = [];
        $end = '';
        if ($filter_username != '') {
            $member_id = $GLOBALS['FORUM_DRIVER']->get_member_from_username($filter_username);
            if ($member_id !== null) {
                $end .= ' AND (sending_member=' . strval($member_id) . ' OR receiving_member=' . strval($member_id) . ')';
            } else {
                attach_message(do_lang_tempcode('_MEMBER_NO_EXIST', $filter_username));
            }
        }
        if ($filter_type !== 'all') {
            $guest_id = $GLOBALS['FORUM_DRIVER']->get_guest_id();
            switch ($filter_type) {
                case 'send':
                    $end .= ' AND receiving_member<>' . strval($guest_id) . ' AND sending_member<>' . strval($guest_id);
                    break;
                case 'credit':
                    $where['sending_member'] = $guest_id;
                    break;
                case 'debit':
                    $where['receiving_member'] = $guest_id;
                    break;
                case 'reversal':
                    $end .= ' AND (status=' . strval(LEDGER_STATUS_REVERSED) . ' OR status=' . strval(LEDGER_STATUS_REVERSING) . ')';
                    break;
                case 'refund':
                    $where['status'] = LEDGER_STATUS_REFUND;
                    break;
                default:
                    $filter_type = 'all';
            }
        }
        if ($filter_from !== null) {
            $end .= ' AND date_and_time>=' . strval($filter_from);
        }
        if ($filter_to !== null) {
            $end .= ' AND date_and_time<=' . strval($filter_to);
        }
        if ($_filter_t_type != '') {
            $end .= ' AND (';
            foreach ($filter_t_type as $key => $t_type) {
                if ($key > 0) {
                    $end .= ' OR (';
                }
                $type_subtype = explode('__', $t_type);
                $end .= db_string_equal_to('t_type', $type_subtype[0]);
                if (array_key_exists(1, $type_subtype)) {
                    $end .= ' AND ' . db_string_equal_to('t_subtype', $type_subtype[1]);
                }
                $end .= ')';
            }
        }

        // Construct the table
        $max_rows = $GLOBALS['SITE_DB']->query_select_value('points_ledger', 'COUNT(*)', $where, $end);
        $has_gift_points = $GLOBALS['SITE_DB']->query_select_value_if_there('points_ledger', 'id', $where, $end . ' AND amount_gift_points>0');

        $sortables = ['date_and_time' => do_lang_tempcode('DATE_TIME')];
        if ($has_gift_points !== null) {
            $sortables['amount_gift_points'] = do_lang_tempcode('GIFT_POINTS');
        }
        $sortables['amount_points'] = do_lang_tempcode('POINTS');
        $test = explode(' ', get_param_string('ledger_sort', 'date_and_time DESC', INPUT_FILTER_GET_COMPLEX), 2);
        if (count($test) == 1) {
            $test[1] = 'DESC';
        }
        list($sortable, $sort_order) = $test;
        if (((cms_strtoupper_ascii($sort_order) != 'ASC') && (cms_strtoupper_ascii($sort_order) != 'DESC')) || (!array_key_exists($sortable, $sortables))) {
            log_hack_attack_and_exit('ORDERBY_HACK');
        }

        $rows = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], $where, $end . ' ORDER BY ' . $sortable . ' ' . $sort_order, $max, $start);
        $result_entries = new Tempcode();

        require_code('templates_results_table');

        $map = [do_lang_tempcode('IDENTIFIER'), do_lang_tempcode('DATE_TIME')];
        if ($has_gift_points !== null) {
            $map[] = do_lang_tempcode('GIFT_POINTS');
        }
        $map = array_merge($map, [do_lang_tempcode('POINTS'), do_lang_tempcode('SENDER'), do_lang_tempcode('RECIPIENT'), do_lang_tempcode('REASON'), do_lang_tempcode('STATUS'), do_lang_tempcode('ACTIONS')]);
        $header_row = results_header_row($map, $sortables, 'ledger_sort', $sortable . ' ' . $sort_order);
        foreach ($rows as $myrow) {
            $date = get_timezoned_date_time($myrow['date_and_time'], false);
            $reason = get_translated_tempcode('points_ledger', $myrow, 'reason');
            $_date = hyperlink(build_url(['page' => '_SELF', 'type' => 'view', 'id' => $myrow['id']], '_SELF'), $date, false, true);

            if (is_guest($myrow['receiving_member'])) {
                $to = do_lang_tempcode('USER_SYSTEM');
            } else {
                $to_name = $GLOBALS['FORUM_DRIVER']->get_username($myrow['receiving_member'], false, USERNAME_DEFAULT_NULL);
                $to_url = points_url($myrow['receiving_member']);
                $to = ($to_name === null) ? do_lang_tempcode('UNKNOWN_EM') : hyperlink($to_url, $to_name, false, true);
            }
            if (is_guest($myrow['sending_member'])) {
                $from = do_lang_tempcode('USER_SYSTEM');
            } else {
                $from_name = $GLOBALS['FORUM_DRIVER']->get_username($myrow['sending_member'], false, USERNAME_DEFAULT_NULL);
                $from_url = points_url($myrow['sending_member']);
                $from = ($from_name === null) ? do_lang_tempcode('UNKNOWN_EM') : hyperlink($from_url, $from_name, false, true);

                // Mask sender if we do not have permission to trace anonymous transactions
                if (($myrow['anonymous'] == 1) && (!has_privilege(get_member(), 'trace_anonymous_points_transactions'))) {
                    $from = do_lang_tempcode('ANONYMOUS');
                }
            }

            $actions = new Tempcode();

            // Undo / reverse transaction button
            if ($myrow['locked'] == 0) {
                if (has_privilege(get_member(), 'moderate_points')) {
                    $delete_url = build_url(['page' => '_SELF', 'type' => 'reverse', 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF');
                    $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                        '_GUID' => '50b8e7e63c19f377f9aa4ae29a9b4975',
                        'NAME' => '#' . strval($myrow['id']),
                        'URL' => $delete_url,
                        'HIDDEN' => form_input_hidden('id', strval($myrow['id'])),
                        'ACTION_TITLE' => do_lang_tempcode('UNDO'),
                        'ICON' => 'buttons/undo',
                        'GET' => false,
                    ]));
                }
            }

            // Edit / amend transaction button
            if (has_privilege(get_member(), 'amend_point_transactions')) {
                $edit_url = build_url(['page' => '_SELF', 'type' => 'amend', 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF');
                $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                    '_GUID' => '9e30dbb3eef7938f417c6ab5dccc83ed',
                    'URL' => $edit_url,
                    'HIDDEN' => form_input_hidden('id', strval($myrow['id'])),
                    'NAME' => '#' . strval($myrow['id']),
                    'ACTION_TITLE' => do_lang_tempcode('AMEND'),
                    'ICON' => 'admin/edit',
                    'GET' => false,
                ]));
            }

            $map = [strval($myrow['id']), $_date];
            if ($has_gift_points > 0) {
                $map[] = integer_format($myrow['amount_gift_points']);
            }
            if ($myrow['status'] == LEDGER_STATUS_NORMAL) {
                $status = do_lang_tempcode('LEDGER_STATUS_0');
            } elseif ($myrow['linked_ledger_id'] !== null) {
                $status = do_lang_tempcode('LEDGER_STATUS_SHORT_' . strval($myrow['status']), escape_html(strval($myrow['linked_ledger_id'])));
            } else {
                $status = do_lang_tempcode('LEDGER_STATUS_SHORT_B_' . strval($myrow['status']));
            }
            $map = array_merge($map, [integer_format($myrow['amount_points']), $from, $to, $reason, $status, $actions]);

            $result_entries->attach(results_entry($map, true));
        }

        $results_table = results_table(do_lang_tempcode('POINTS_LEDGER'), $start, 'ledger_start', $max, 'ledger_max', $max_rows, $header_row, $result_entries, $sortables, $sortable, $sort_order, 'ledger_sort', paragraph(do_lang_tempcode('POINTS_LEDGER_HEAD')));

        // Export button
        $form = new Tempcode();
        if ($max_rows > 0) {
            $export_url = build_url(['page' => '_SELF', 'type' => 'export'], '_SELF');
            $form->attach(do_template('BUTTON_SCREEN', ['_GUID' => '29a25bc2a39049dab57ff6b1eeb1a413', 'IMMEDIATE' => false, 'URL' => $export_url, 'TITLE' => do_lang_tempcode('EXPORT'), 'IMG' => 'admin/export_spreadsheet', 'HIDDEN' => new Tempcode()]));
        }

        // Start building fields for the filter box
        push_field_encapsulation(FIELD_ENCAPSULATION_RAW);

        $transaction_types = new Tempcode();
        $transaction_types->attach(form_input_list_entry('all', ($filter_type == 'all'), do_lang_tempcode('ALL')));
        $transaction_types->attach(form_input_list_entry('credit', ($filter_type == 'credit'), do_lang_tempcode('CREDIT')));
        $transaction_types->attach(form_input_list_entry('debit', ($filter_type == 'debit'), do_lang_tempcode('DEBIT')));
        $transaction_types->attach(form_input_list_entry('refund', ($filter_type == 'refund'), do_lang_tempcode('REFUND')));
        $transaction_types->attach(form_input_list_entry('reversal', ($filter_type == 'reversal'), do_lang_tempcode('REVERSAL')));
        $transaction_types->attach(form_input_list_entry('send', ($filter_type == 'send'), do_lang_tempcode('SEND')));

        // List item for every aggregate content type per points hooks
        $t_types = new Tempcode();
        $hook_obs = find_all_hook_obs('systems', 'points', 'Hook_points_');

        $labels = [];
        foreach ($hook_obs as $name => $hook_ob) {
            $data = $hook_ob->points_profile(null, null);
            if ($data === null) {
                continue;
            }
            $selected = (($_filter_t_type != '') && (in_array($name, $filter_t_type)));
            $labels[] = [
                'label' => $data['label'],
                'name' => $name,
                'selected' => $selected,
            ];
        }
        sort_maps_by($labels, 'label');
        foreach ($labels as $label) {
            $t_types->attach(form_input_list_entry($label['name'], $label['selected'], $label['label']));
        }

        $filters_row_a = [
            [
                'PARAM' => 'filter_ledger_username',
                'LABEL' => do_lang_tempcode('USERNAME'),
                'FIELD' => form_input_username(do_lang_tempcode('USERNAME'), new Tempcode(), 'filter_ledger_username', $filter_username, false),
            ],
            [
                'PARAM' => 'filter_ledger_type',
                'LABEL' => do_lang_tempcode('TYPE'),
                'FIELD' => form_input_list(do_lang_tempcode('TYPE'), new Tempcode(), 'filter_ledger_type', $transaction_types, null, false, false),
            ],
            [
                'PARAM' => 'filter_ledger_t_type',
                'LABEL' => do_lang_tempcode('POINTS_AGGREGATE_ROWS'),
                'FIELD' => form_input_multi_list(do_lang_tempcode('POINTS_AGGREGATE_ROWS'), new Tempcode(), 'filter_ledger_t_type', $t_types),
            ],
        ];

        $filters_row_b = [
            [
                'PARAM' => 'filter_ledger_from',
                'LABEL' => do_lang_tempcode('FROM'),
                'FIELD' => form_input_date(do_lang_tempcode('FROM'), new Tempcode(), 'filter_ledger_from', false, false, true, $filter_from),
            ],
            [
                'PARAM' => 'filter_ledger_to',
                'LABEL' => do_lang_tempcode('TO'),
                'FIELD' => form_input_date(do_lang_tempcode('TO'), new Tempcode(), 'filter_ledger_to', false, false, true, $filter_to),
            ],
        ];

        $url = build_url(['page' => '_SELF', 'type' => 'browse'], '_SELF');

        $tpl = do_template('RESULTS_TABLE_SCREEN', [
            '_GUID' => 'e796e4efe18afa26424a3ceae282cf90',
            'TITLE' => $this->title,
            'RESULTS_TABLE' => $results_table,
            'FORM' => $form,
            'FILTERS_ROW_A' => $filters_row_a,
            'FILTERS_ROW_B' => $filters_row_b,
            'URL' => $url,
            'FILTERS_HIDDEN' => new Tempcode(),
        ]);

        pop_field_encapsulation();

        require_code('templates_internalise_screen');
        return internalise_own_screen($tpl);
    }

    /**
     * The UI/actualiser to reverse a point transaction.
     *
     * @return Tempcode The UI
     */
    public function reverse() : object
    {
        if (!has_privilege(get_member(), 'moderate_points')) {
            access_denied('PRIVILEGE', 'moderate_points');
        }

        require_code('points3');

        $id = post_param_integer('id');
        $confirm = get_param_integer('confirm', 0);

        $out = transaction_reverse_screen($id, $confirm, $this->title);

        if ($out === null) {
            // Show it worked / Refresh
            $url = get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL);
            if ($url == '') {
                $_url = build_url(['page' => '_SELF', 'type' => 'browse'], '_SELF');
                $url = $_url->evaluate();
            }
            return redirect_screen($this->title, $url, do_lang_tempcode('SUCCESS'));
        }

        return $out;
    }

    /**
     * The UI/actualiser to amend a point transaction.
     *
     * @return Tempcode The UI
     */
    public function amend() : object
    {
        if (!has_privilege(get_member(), 'amend_point_transactions')) {
            access_denied('PRIVILEGE', 'amend_point_transactions');
        }

        $id = post_param_integer('id');
        $reason = post_param_string('reason', null);
        $redirect = get_param_string('redirect', '', INPUT_FILTER_URL_INTERNAL);

        require_code('points3');
        $out = transaction_amend_screen($id, $this->title, null, $reason, $redirect);
        if ($out === null) {
            // Show it worked / Refresh
            if ($redirect == '') {
                $_redirect = build_url(['page' => '_SELF', 'type' => 'browse'], '_SELF');
                $redirect = $_redirect->evaluate();
            }
            return redirect_screen($this->title, $redirect, do_lang_tempcode('SUCCESS'));
        }

        return $out;
    }

    /**
     * The UI for a single point transaction.
     *
     * @return Tempcode The UI
     */
    public function view() : object
    {
        $member_id_viewing = get_member();

        // No guests allowed
        if (is_guest($member_id_viewing)) {
            access_denied('NOT_AS_GUEST');
        }

        if (!has_privilege($member_id_viewing, 'view_points_ledger')) {
            access_denied('PRIVILEGE', 'view_points_ledger');
        }

        $id = get_param_integer('id');

        $_row = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], ['id' => $id], '', 1);
        if ($_row === null || !array_key_exists(0, $_row)) {
            warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
        }
        $row = $_row[0];
        $reason = get_translated_tempcode('points_ledger', $row, 'reason');

        require_code('templates_map_table');

        $status = new Tempcode();
        switch ($row['status']) {
            case LEDGER_STATUS_REVERSING:
            case LEDGER_STATUS_REVERSED:
                $_row2 = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], ['id' => $row['linked_ledger_id'], 'status' => ($row['status'] == LEDGER_STATUS_REVERSING) ? LEDGER_STATUS_REVERSED : LEDGER_STATUS_REVERSING], '', 1);
                if (empty($_row2)) {
                    $status = do_lang_tempcode('LEDGER_STATUS_' . strval($row['status']) . '_UNLINKED');
                } else {
                    $row2 = $_row2[0];
                    $date = get_timezoned_date_time($row2['date_and_time'], false);
                    $_status = do_lang_tempcode('LEDGER_STATUS_' . strval($row['status']), escape_html(strval($row['linked_ledger_id'])), escape_html($date));
                    $status = hyperlink(build_url(['page' => '_SELF', 'type' => 'view', 'id' => $row['linked_ledger_id']], '_SELF'), $_status, false, true);
                }
                break;
            case LEDGER_STATUS_REFUND:
                $_row2 = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], ['id' => $row['linked_ledger_id']], '', 1);
                if (empty($_row2)) {
                    $status = do_lang_tempcode('LEDGER_STATUS_' . strval($row['status']) . '_UNLINKED');
                } else {
                    $row2 = $_row2[0];
                    $date = get_timezoned_date_time($row2['date_and_time'], false);
                    $_status = do_lang_tempcode('LEDGER_STATUS_' . strval($row['status']), escape_html(strval($row['linked_ledger_id'])), escape_html($date));
                    $status = hyperlink(build_url(['page' => '_SELF', 'type' => 'view', 'id' => $row['linked_ledger_id']], '_SELF'), $_status, false, true);
                }
                break;
            default:
                $status = do_lang_tempcode('LEDGER_STATUS_' . strval($row['status']));
        }

        $date = get_timezoned_date_time($row['date_and_time'], false);
        $from_name = is_guest($row['sending_member']) ? do_lang('SYSTEM') : $GLOBALS['FORUM_DRIVER']->get_username($row['sending_member'], true);
        $_from_name = (is_guest($row['sending_member'])) ? make_string_tempcode(escape_html($from_name)) : hyperlink(points_url($row['sending_member']), escape_html($from_name), false, false, do_lang_tempcode('VIEW_POINTS'));
        $to_name = is_guest($row['receiving_member']) ? do_lang('SYSTEM') : $GLOBALS['FORUM_DRIVER']->get_username($row['receiving_member'], true);
        $_to_name = (is_guest($row['receiving_member'])) ? make_string_tempcode(escape_html($to_name)) : hyperlink(points_url($row['receiving_member']), escape_html($to_name), false, false, do_lang_tempcode('VIEW_POINTS'));

        // Mask sender if we do not have permission to trace anonymous transactions
        if (($row['anonymous'] == 1) && (!has_privilege($member_id_viewing, 'trace_anonymous_points_transactions'))) {
            $_from_name = do_lang_tempcode('ANONYMOUS');
        }

        $buttons = new Tempcode();
        if ($row['locked'] == 0) {
            if (has_privilege(get_member(), 'moderate_points')) {
                $delete_url = build_url(['page' => '_SELF', 'type' => 'reverse', 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF');
                $buttons->attach(do_template('BUTTON_SCREEN', [
                    '_GUID' => '787c80a9dc8e4a4b9679277696da9fc9',
                    'IMMEDIATE' => true,
                    'HIDDEN' => form_input_hidden('id', strval($row['id'])),
                    'URL' => $delete_url,
                    'TITLE' => do_lang_tempcode('UNDO'),
                    'IMG' => 'buttons/undo',
                ]));
            }
            if (has_privilege(get_member(), 'amend_point_transactions')) {
                $edit_url = build_url(['page' => '_SELF', 'type' => 'edit', 'redirect' => protect_url_parameter(SELF_REDIRECT)], '_SELF');
                $buttons->attach(do_template('BUTTON_SCREEN', [
                    '_GUID' => 'a4085e1641804fa8b4ca4794ea57b3f0',
                    'IMMEDIATE' => true,
                    'HIDDEN' => form_input_hidden('id', strval($row['id'])),
                    'URL' => $edit_url,
                    'TITLE' => do_lang_tempcode('EDIT'),
                    'IMG' => 'admin/edit',
                ]));
            }
        }

        $fields = [
            'IDENTIFIER' => strval($id),
            'DATE' => $date,
            'STATUS' => $status,
            'POINTS' => integer_format($row['amount_points']),
            'GIFT_POINTS' => integer_format($row['amount_gift_points']),
            'FROM' => $_from_name,
            'TO' => $_to_name,
            'REASON' => $reason,
        ];

        $title = get_screen_title('VIEW_POINT_TRANSACTION', true, [strval($id)]);

        return map_table_screen($title, $fields, true, null, $buttons, true);
    }
}
