<?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
 */

/**
 * The UI for a points profile.
 *
 * @param  MEMBER $member_id_of The ID of the member who is being viewed
 * @param  ?MEMBER $member_id_viewing The ID of the member who is doing the viewing (null: current member)
 * @return Tempcode The UI
 */
function points_profile(int $member_id_of, ?int $member_id_viewing) : object
{
    require_code('points');
    require_code('points_escrow');
    require_css('points');
    require_lang('points');

    require_javascript('checking');
    require_javascript('points');

    $aggregate_rows = [
        'received' => [],
        'sent' => [],
        'spent' => [],
    ];
    $aggregate_totals = [
        'received' => 0,
        'sent' => 0,
        'spent' => 0,
        'total' => 0,
    ];
    $additional_fields = [];

    // Get info about viewing/giving user
    if (!is_guest($member_id_viewing)) {
        $viewer_gift_points_balance = gift_points_balance($member_id_viewing);
        $viewer_points_balance = points_balance($member_id_viewing);
    } else {
        $viewer_gift_points_balance = 0;
        $viewer_points_balance = 0;
    }

    // Get info about viewed user
    $username = $GLOBALS['FORUM_DRIVER']->get_username($member_id_of, true, USERNAME_GUEST_AS_DEFAULT | USERNAME_DEFAULT_ERROR);
    $profile_url = $GLOBALS['FORUM_DRIVER']->member_profile_url($member_id_of, true);

    // Get additional points info
    $points_spent = points_spent($member_id_of);
    $points_balance = points_balance($member_id_of);
    $points_sent = points_sent($member_id_of);
    $gift_points_balance = gift_points_balance($member_id_of);
    $escrow_details = points_get_escrow($member_id_of, $member_id_viewing);
    $points_lifetime = points_lifetime($member_id_of);

    if (get_option('enable_poll_point_weighting', true) === '1') {
        require_code('cns_polls_action2');
        $_voting_power = float_to_raw_string(cns_points_to_voting_power($points_balance), 10);
        $voting_power = float_format(cns_points_to_voting_power($points_balance), 3);
    } else {
        $_voting_power = '';
        $voting_power = '';
    }

    // Get transaction tables
    $received_table = new Tempcode();
    $sent_table = new Tempcode();
    $spent_table = new Tempcode();
    if (($member_id_viewing == $member_id_of) || (has_privilege($member_id_viewing, 'view_points_ledger'))) {
        $received_table = points_get_transactions_screen('recipient', $member_id_of, $member_id_viewing, true, false);
        $sent_table = points_get_transactions_screen('sender', $member_id_of, $member_id_viewing, false, true);
        $spent_table = points_get_transactions_screen('debit', $member_id_of, $member_id_viewing, false, false);
    }

    // Run points hooks for low-impact transactions / aggregate tables
    $hook_obs = find_all_hook_obs('systems', 'points', 'Hook_points_');
    foreach ($hook_obs as $name => $hook_ob) {
        $type_subtype = explode('__', $name);
        $where = ' AND (' . db_string_equal_to('t_type', $type_subtype[0]);
        if (array_key_exists(1, $type_subtype)) {
            $where .= ' AND ' . db_string_equal_to('t_subtype', $type_subtype[1]);
        }
        $where .= ')';
        $_array = $hook_ob->points_profile($member_id_of, $member_id_viewing);
        if ($_array !== null) {
            $data = points_ledger_calculate(LEDGER_TYPE__ALL, $member_id_of, null, $where);
            foreach ($data as $type => $data) {
                list($count, $points, $gift_points) = $data;

                // Ignore aggregate types with no count / points
                if (($count <= 0) && ($points == 0) && ($gift_points == 0)) {
                    continue;
                }

                $total = intval($points + $gift_points);

                $aggregate_rows[$type][] = [
                    $_array['label'],
                    $count,
                    $total
                ];

                $aggregate_totals[$type] += $total;
                $aggregate_totals['total'] += $total;
            }
        }
    }

    require_code('templates_results_table');

    // Generate aggregate tables
    $aggregate_tables = [];
    foreach ($aggregate_rows as $type => $rows) {
        $max_rows = count($rows);

        // No aggregate rows? No table.
        if ($max_rows <= 0) {
            $aggregate_tables[$type] = new Tempcode();
            continue;
        }

        // Not allowed to view ledgers? No table!
        if ((!has_privilege($member_id_viewing, 'view_points_ledger')) && ($member_id_viewing != $member_id_of)) {
            $aggregate_tables[$type] = new Tempcode();
            continue;
        }

        $start = get_param_integer('a_start_' . $type, 0);
        $max = get_param_integer('a_max_' . $type, intval(get_option('point_logs_per_page')));
        $sortables = ['count' => do_lang('TRANSACTIONS'), 'points' => do_lang('POINTS')];
        $test = explode(' ', get_param_string('a_sort_' . $type, 'points DESC', INPUT_FILTER_GET_COMPLEX));
        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');
        }

        $out = new Tempcode();
        $transactions_header = protect_from_escaping(do_template('HELP_ICON_PHRASE', [
            '_GUID' => 'c2a42ade2b3d79e5c0e5e2cf6280d4c8',
            'LABEL' => do_lang_tempcode('TRANSACTIONS'),
            'TOOLTIP' => do_lang_tempcode('DESCRIPTION_TRANSACTIONS_AGGREGATE'),
        ]));
        $header_row = results_header_row([do_lang('TYPE'), $transactions_header, do_lang('POINTS')], $sortables, 'a_sort_' . $type, $sortable . ' ' . $sort_order, 'ibcaf8b021e3939bfce1dce9ff8ed63a', null, 'tab--points');

        // Sort the results
        if ($sortable == 'count') {
            sort_maps_by($rows, (($sort_order == 'ASC') ? '1' : '!1'));
        } elseif ($sortable == 'points') {
            sort_maps_by($rows, (($sort_order == 'ASC') ? '2' : '!2'));
        }

        foreach ($rows as $row) {
            list($label, $count, $points) = $row;
            $out->attach(results_entry([$label, integer_format($count), integer_format($points)], true));
        }

        $aggregate_tables[$type] = results_table(do_lang_tempcode('POINTS_AGGREGATE_ROWS'), $start, 'a_start_' . $type, $max, 'a_max_' . $type, $max_rows, $header_row, $out, $sortables, $sortable, $sort_order, 'a_sort_' . $type, null, [], null, 8, 'ibcaf8b021e3939bfce1dce9ff8ed63a', false, 'tab--points');
    }

    // Show send / modify points form
    $trans_type = get_param_string('trans_type', '');
    $send_amount = get_param_integer('send_amount', null);
    $send_reason = get_param_string('send_reason', '');
    if (is_guest($member_id_viewing)) {
        $send_template = do_lang_tempcode('POINTS_MUST_LOGIN');
    } else {
        $enough_ok = ((has_privilege($member_id_viewing, 'moderate_points')) || ($viewer_gift_points_balance > 0) || ($viewer_points_balance > 0));
        $give_ok = (($member_id_viewing != $member_id_of) || (has_privilege($member_id_viewing, 'send_points_to_self')));
        if (($enough_ok) && ($give_ok)) {
            // Show how many points are available also
            $send_url = build_url(['page' => 'points', 'type' => 'transact', 'id' => $member_id_of], get_module_zone('points'));
            $send_template = do_template('POINTS_SEND', [
                '_GUID' => 'fa1749d5a803d86b1efbcfde2ad81702',
                'SEND_URL' => $send_url,
                'MEMBER' => strval($member_id_of),
                'DEFAULT__TRANS_TYPE' => $trans_type,
                'DEFAULT__SEND_AMOUNT' => ($send_amount !== null) ? strval($send_amount) : '',
                'DEFAULT__SEND_REASON' => $send_reason,
                '_VIEWER_GIFT_POINTS_BALANCE' => strval($viewer_gift_points_balance),
                'VIEWER_GIFT_POINTS_BALANCE' => integer_format($viewer_gift_points_balance),
                '_VIEWER_POINTS_BALANCE' => strval($viewer_points_balance),
                'VIEWER_POINTS_BALANCE' => integer_format($viewer_points_balance),
            ]);
        } else {
            $send_template = do_lang_tempcode('PE_LACKING_POINTS');
        }
        if (!$give_ok) {
            $send_template = new Tempcode();
        }
        if ((!has_privilege($member_id_of, 'use_points')) || (!has_privilege($member_id_viewing, 'send_points'))) {
            $send_template = new Tempcode();
        }
    }

    // Show escrow box
    $viewer_gift_points_balance = gift_points_balance($member_id_viewing);
    $viewer_points_balance = points_balance($member_id_viewing);
    $viewer_balance = ($viewer_gift_points_balance + $viewer_points_balance);
    if (is_guest($member_id_viewing)) {
        $escrow_template = do_lang_tempcode('ESCROW_MUST_LOGIN');
    } elseif (!has_privilege($member_id_viewing, 'use_points_escrow')) {
        $escrow_template = do_lang_tempcode('ESCROW_NOT_ALLOWED_YOU');
    } elseif ($member_id_viewing == $member_id_of) {
        $escrow_template = do_lang_tempcode('ESCROW_NO_SELF');
    } elseif (is_guest($member_id_of)) {
        $escrow_template = do_lang_tempcode('ESCROW_NO_GUEST');
    } elseif (!has_privilege($member_id_of, 'use_points')) {
        $escrow_template = do_lang_tempcode('ESCROW_MEMBER_CANNOT_USE_POINTS', escape_html($username));
    } elseif ($viewer_balance <= 0) {
        $escrow_template = do_lang_tempcode('ESCROW_LACKING_POINTS');
    } else {
        $escrow_url = build_url(['page' => 'points', 'type' => 'escrow', 'id' => $member_id_of], get_module_zone('points'));

        $form = new Tempcode();
        $form->attach(do_template('BUTTON_SCREEN', ['_GUID' => '97024f2bf9014f82367cefc8c9b6dcd9', 'IMMEDIATE' => false, 'URL' => $escrow_url, 'TITLE' => do_lang_tempcode('ESCROW_TO'), 'IMG' => 'buttons/proceed', 'HIDDEN' => new Tempcode()]));

        $escrow_template = do_template('ESCROW_TRANSACTIONS', [
            '_GUID' => 'ac97ee94388e4db2b8753273694cb2a1',
            'MEMBER' => strval($member_id_of),
            '_VIEWER_POINTS_BALANCE' => strval($viewer_points_balance),
            'VIEWER_POINTS_BALANCE' => integer_format($viewer_points_balance),
            '_VIEWER_GIFT_POINTS_BALANCE' => strval($viewer_gift_points_balance),
            'VIEWER_GIFT_POINTS_BALANCE' => integer_format($viewer_gift_points_balance),
            'GIFT_POINTS_ENABLED' => (get_option('enable_gift_points') == '1'),
            'FORM' => $form,
        ]);
    }

    // Ledger export button
    $export_points_ledger = new Tempcode();
    if (($member_id_of == $member_id_viewing) || (has_privilege($member_id_viewing, 'view_points_ledger'))) {
        $export_url = build_url(['page' => 'points', 'type' => 'export', 'id' => $member_id_of], get_module_zone('points'));
        $export_points_ledger->attach(do_template('BUTTON_SCREEN', ['_GUID' => '1c2ebca6837be4d6f5e495d2e43765b2', 'IMMEDIATE' => false, 'URL' => $export_url, 'TITLE' => do_lang_tempcode('EXPORT'), 'IMG' => 'admin/export_spreadsheet', 'HIDDEN' => new Tempcode()]));
    }

    return do_template('POINTS_PROFILE', array_merge(
        [
            '_GUID' => 'f91208ef0f9a1e1a8633ce307a778a8d',

            'MEMBER' => strval($member_id_of),
            'PROFILE_URL' => $profile_url,
            'USERNAME' => $username,

            '_POINTS_LIFETIME' => strval($points_lifetime),
            'POINTS_LIFETIME' => integer_format($points_lifetime),
            '_POINTS_BALANCE' => strval($points_balance),
            'POINTS_BALANCE' => integer_format($points_balance),
            '_GIFT_POINTS_BALANCE' => strval($gift_points_balance),
            'GIFT_POINTS_BALANCE' => integer_format($gift_points_balance),
            '_VOTING_POWER' => $_voting_power,
            'VOTING_POWER' => $voting_power,
            '_POINTS_RECEIVED_AGGREGATE' => strval($aggregate_totals['received']),
            'POINTS_RECEIVED_AGGREGATE' => integer_format($aggregate_totals['received']),
            '_POINTS_RECEIVED' => strval($points_lifetime - $aggregate_totals['received']),
            'POINTS_RECEIVED' => integer_format($points_lifetime - $aggregate_totals['received']),
            '_POINTS_SENT_AGGREGATE' => strval($aggregate_totals['sent']),
            'POINTS_SENT_AGGREGATE' => integer_format($aggregate_totals['sent']),
            '_POINTS_SENT' => strval($points_sent - $aggregate_totals['sent']),
            'POINTS_SENT' => integer_format($points_sent - $aggregate_totals['sent']),
            '_POINTS_SPENT_AGGREGATE' => strval($aggregate_totals['spent']),
            'POINTS_SPENT_AGGREGATE' => integer_format($aggregate_totals['spent']),
            '_POINTS_SPENT' => strval($points_spent - $aggregate_totals['spent']),
            'POINTS_SPENT' => integer_format($points_spent - $aggregate_totals['spent']),

            'RECEIVED_TABLE_AGGREGATE' => $aggregate_tables['received'],
            'RECEIVED_TABLE' => $received_table,
            'SENT_TABLE_AGGREGATE' => $aggregate_tables['sent'],
            'SENT_TABLE' => $sent_table,
            'SPENT_TABLE_AGGREGATE' => $aggregate_tables['spent'],
            'SPENT_TABLE' => $spent_table,
            'ESCROW_DETAILS' => $escrow_details,

            'GIVE' => $send_template,
            'ESCROW' => $escrow_template,

            'EXPORT_POINTS_LEDGER' => $export_points_ledger,
        ],
        $additional_fields
    ));
}

/**
 * Get the point transactions a member has had.
 *
 * @param  ID_TEXT $type The type of transactions we are looking for
 * @set sender recipient sender_recipient credit debit all
 * @param  MEMBER $member_id_of Who we are looking at transactions for
 * @param  MEMBER $member_id_viewing Who we are looking at transactions using the account of
 * @param  integer $max Maximum number of records to return
 * @param  integer $start The starting record number
 * @param  SHORT_TEXT $sortable Fields by which we want to order (blank: no ordering)
 * @param  ID_TEXT $sort_order Direction of ordering to use
 * @set ASC DESC
 * @param  ?integer $status Filter by the given ledger status (null: do not filter)
 * @param  boolean $skip_locked Whether to skip locked records
 * @param  boolean $skip_low_impact Whether to skip low-impact records (like forum posts)
 * @param  ?TIME $after_time Only return transactions after this time (null: do not filter by time)
 * @return array Duple of total rows in the database and an array of rows
 */
function points_get_transactions(string $type, int $member_id_of, int $member_id_viewing, int $max, int $start = 0, string $sortable = '', string $sort_order = 'DESC', ?int $status = null, bool $skip_locked = false, bool $skip_low_impact = true, ?int $after_time = null) : array
{
    require_code('points');

    $where = [];
    $end = '';

    switch ($type) {
        case 'sender': // transactions where the member sent points to another member
            $where = ['sending_member' => $member_id_of];

            // Ignore anonymous transactions if we do not have the privilege to trace them
            if (($member_id_of != $member_id_viewing) && !has_privilege($member_id_viewing, 'trace_anonymous_points_transactions')) {
                $where['anonymous'] = 0;
            }

            $end = ' AND receiving_member<>' . strval($GLOBALS['FORUM_DRIVER']->get_guest_id()); // Do not include debits
            break;
        case 'recipient': // Transactions where the member received points (either from other members or the system)
            $where = ['receiving_member' => $member_id_of];
            $end = '';
            break;
        case 'sender_recipient': // Transactions where the member sent points to other members or received points (either from other members or the system)
            $end = ' AND ((sending_member=' . strval($member_id_of) . ' AND receiving_member<>' . strval($GLOBALS['FORUM_DRIVER']->get_guest_id());
            $end .= ') OR (receiving_member=' . strval($member_id_of) . '))'; // We also want to include credits, so do not filter out system senders

            // Ignore anonymous transactions if we do not have the privilege to trace them
            if (($member_id_of != $member_id_viewing) && !has_privilege($member_id_viewing, 'trace_anonymous_points_transactions')) {
                $end .= ' AND anonymous<>0';
            }
            break;
        case 'credit': // Transactions where the member received points from the system
            $where = ['receiving_member' => $member_id_of, 'sending_member' => $GLOBALS['FORUM_DRIVER']->get_guest_id()];
            $end = '';
            break;
        case 'debit': // Transactions where the member sent points to the system
            $where = ['sending_member' => $member_id_of, 'receiving_member' => $GLOBALS['FORUM_DRIVER']->get_guest_id()];
            $end = '';
            break;
        case 'all': // All transactions involving the member
            $end = ' AND (sending_member=' . strval($member_id_of) . ' OR receiving_member=' . strval($member_id_of) . ')';
            break;
    }

    if ($status !== null) {
        $where['status'] = $status;
    }
    if ($skip_locked) {
        $where['locked'] = 0;
    }

    // If a transaction has a t_type__t_subtype or a t_type matching the name of a points hook, then it is a low impact transaction; filter it out in the query.
    if ($skip_low_impact) {
        $hooks = find_all_hooks('systems', 'points');
        foreach ($hooks as $hook => $place) {
            $parts = explode('__', $hook);
            $end .= ' AND NOT (' . db_string_equal_to('t_type', strval($parts[0]));
            if (array_key_exists(1, $parts)) {
                $end .= ' AND ' . db_string_equal_to('t_subtype', strval($parts[1]));
            }
            $end .= ')';
        }
    }

    if ($after_time !== null) {
        $end .= ' AND date_and_time>=' . strval($after_time);
    }

    $max_rows = $GLOBALS['SITE_DB']->query_select_value('points_ledger', 'COUNT(*)', $where, $end);

    if ($sortable != '') {
        $end .= ' ORDER BY ' . $sortable . ' ' . $sort_order;
    }
    $rows = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], $where, $end, $max, $start);

    return [$max_rows, $rows];
}

/**
 * Show the point transactions a member has had in a table.
 *
 * @param  ID_TEXT $type The type of transactions we are looking for
 * @set sender recipient credit debit all
 * @param  MEMBER $member_id_of Who we are looking at transactions for
 * @param  MEMBER $member_id_viewing Who we are looking at transactions using the account of
 * @param  boolean $include_sender Whether to include the "Sender" column on the table
 * @param  boolean $include_recipient Whether to include the "Recipient" column on the table
 * @param  boolean $skip_low_impact Whether to skip low-impact records (like forum posts)
 * @return Tempcode The UI
 */
function points_get_transactions_screen(string $type, int $member_id_of, int $member_id_viewing, bool $include_sender = true, bool $include_recipient = true, bool $skip_low_impact = true) : object
{
    $start = get_param_integer('ledger_start_' . $type, 0);
    $max = get_param_integer('ledger_max_' . $type, intval(get_option('point_logs_per_page')));
    $sortables = ['date_and_time' => do_lang_tempcode('DATE'), 'amount' => do_lang_tempcode('AMOUNT')];
    $test = explode(' ', get_param_string('ledger_sort_' . $type, 'date_and_time DESC', INPUT_FILTER_GET_COMPLEX));
    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');
    }

    list($max_rows, $rows) = points_get_transactions($type, $member_id_of, $member_id_viewing, $max, $start, $sortable, $sort_order, null, false, $skip_low_impact);
    if ($max_rows == 0) {
        return do_template('BLOCK_NO_ENTRIES', [
            '_GUID' => 'aad1e575a032569a761aea36a5c6befb',
            'TITLE' => new Tempcode(),
            'MESSAGE' => do_lang_tempcode('NO_ENTRIES'),
        ]);
    }

    $out = new Tempcode();
    $viewing_name = $GLOBALS['FORUM_DRIVER']->get_username($member_id_of, true);

    require_code('templates_results_table');

    $header_columns = [do_lang_tempcode('DATE'), do_lang_tempcode('AMOUNT')];
    if ($include_sender) {
        $header_columns[] = do_lang_tempcode('SENDER');
    }
    if ($include_recipient) {
        $header_columns[] = do_lang_tempcode('RECIPIENT');
    }
    $header_columns[] = do_lang_tempcode('REASON');
    $header_columns[] = do_lang_tempcode('STATUS');
    if ((has_privilege($member_id_viewing, 'moderate_points')) || (has_privilege($member_id_viewing, 'amend_point_transactions'))) {
        $header_columns[] = do_lang_tempcode('ACTIONS');
    }
    $header_row = results_header_row($header_columns, $sortables, 'ledger_sort_' . $type, $sortable . ' ' . $sort_order, 'jbcaf8b021e3939bfce1dce9ff8ed63a', null, 'tab--points');

    foreach ($rows as $myrow) {
        if (($myrow['anonymous'] == 1) && ($type == 'from')) {
            continue;
        }

        // Their name
        $from_name = is_guest($myrow['sending_member']) ? do_lang('SYSTEM') : $GLOBALS['FORUM_DRIVER']->get_username($myrow['sending_member'], true);
        $to_name = is_guest($myrow['receiving_member']) ? do_lang('SYSTEM') : $GLOBALS['FORUM_DRIVER']->get_username($myrow['receiving_member'], true);
        if (($myrow['anonymous'] == 1) && (!is_guest($myrow['sending_member']))) {
            if (!has_privilege($member_id_viewing, 'trace_anonymous_points_transactions')) {
                $_from_name = do_lang_tempcode('ANON');
            } else {
                $_from_name = hyperlink(points_url($myrow['sending_member']), do_lang_tempcode('ANON'), false, false, escape_html($from_name));
            }
        } else {
            $_from_name = (is_guest($myrow['sending_member'])) ? make_string_tempcode(escape_html($from_name)) : hyperlink(points_url($myrow['sending_member']), escape_html($from_name), false, false, do_lang_tempcode('VIEW_POINTS'));
        }
        $_to_name = hyperlink(points_url($myrow['receiving_member']), escape_html($to_name), false, false, do_lang_tempcode('VIEW_POINTS'));

        $date = make_string_tempcode(escape_html(get_timezoned_date_time($myrow['date_and_time'], false)));
        $amount = $myrow['amount_gift_points'] + $myrow['amount_points'];
        $reason = get_translated_tempcode('points_ledger', $myrow, 'reason');

        if ((has_privilege($member_id_viewing, 'view_points_ledger')) && (has_actual_page_access($member_id_viewing, 'admin_points'))) {
            $_date = hyperlink(build_url(['page' => 'admin_points', 'type' => 'view', 'id' => $myrow['id']]), $date, false, true);
            $date = $_date;
        }

        $results_entry = [$date, integer_format($amount)];
        if ($include_sender) {
            $results_entry[] = $_from_name;
        }
        if ($include_recipient) {
            $results_entry[] = $_to_name;
        }
        $results_entry[] = $reason;

        if ($myrow['status'] == LEDGER_STATUS_NORMAL) {
            $status = do_lang_tempcode('LEDGER_STATUS_0');
        } elseif (($myrow['linked_ledger_id'] !== null) && (has_privilege($member_id_viewing, 'moderate_points'))) {
            $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']));
        }
        $results_entry[] = $status;
        if ((has_privilege($member_id_viewing, 'moderate_points')) || (has_privilege($member_id_viewing, 'amend_point_transactions'))) {
            $actions = new Tempcode();
            if (($myrow['locked'] == 0) && ($myrow['status'] != LEDGER_STATUS_REVERSED) && ($myrow['status'] != LEDGER_STATUS_REVERSING) && (has_privilege($member_id_viewing, 'moderate_points'))) {
                $redirect_url = points_url($member_id_of);
                $delete_url = build_url(['page' => 'points', 'type' => 'reverse', 'member_id_of' => $member_id_of, 'redirect' => protect_url_parameter($redirect_url)], get_module_zone('points'));
                $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                    '_GUID' => '3585ec7f35a1027e8584d62ffeb41e56',
                    '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,
                ]));
            }
            if ((has_privilege($member_id_viewing, 'amend_point_transactions'))) {
                $redirect_url = points_url($member_id_of);
                $edit_url = build_url(['page' => 'points', 'type' => 'amend', 'member_id_of' => $member_id_of, 'redirect' => protect_url_parameter($redirect_url)], get_module_zone('points'));
                $actions->attach(do_template('COLUMNED_TABLE_ACTION', [
                    '_GUID' => 'b7dff48f5758ee05da8fe02beed935b6',
                    '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,
                ]));
            }

            $results_entry[] = $actions;
        }
        $out->attach(results_entry($results_entry, true));
    }

    return results_table(do_lang_tempcode('_POINTS', escape_html($viewing_name)), $start, 'ledger_start_' . $type, $max, 'ledger_max_' . $type, $max_rows, $header_row, $out, $sortables, $sortable, $sort_order, 'ledger_sort_' . $type, null, [], null, 8, 'jbcaf8b021e3939bfce1dce9ff8ed63a', false, 'tab--points');
}

/**
 * The UI/actualiser to reverse a point transaction. Should be called from a module.
 *
 * @param  AUTO_LINK $id The ID of the transaction being reversed
 * @param  BINARY $confirm Whether the reversal was confirmed already
 * @param  Tempcode $title The title of the page
 * @return ?Tempcode The confirmation screen (null: $confirm was 1 and the reversal was successful)
 */
function transaction_reverse_screen(int $id, int $confirm, object $title) : ?object
{
    if (!has_privilege(get_member(), 'moderate_points')) {
        access_denied("I_ERROR");
    }

    $rows = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], ['id' => $id], '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
    }
    $myrow = $rows[0];
    $amount_points = $myrow['amount_points'];
    $amount_gift_points = $myrow['amount_gift_points'];
    $sending_member = $myrow['sending_member'];
    $receiving_member = $myrow['receiving_member'];

    if ($confirm == 0) {
        $_sending_member = (is_guest($sending_member)) ? do_lang('SYSTEM') : $GLOBALS['FORUM_DRIVER']->get_username($sending_member);
        $_receiving_member = (is_guest($receiving_member)) ? do_lang('SYSTEM') : $GLOBALS['FORUM_DRIVER']->get_username($receiving_member);
        $preview = do_lang_tempcode('ARE_YOU_SURE_REVERSE', escape_html(integer_format($amount_points)), escape_html(integer_format($amount_gift_points)), [escape_html($_sending_member), escape_html($_receiving_member)]);
        return do_template('CONFIRM_SCREEN', [
            '_GUID' => '74f1a21b64dc67a9953a5823462bab38',
            'TITLE' => $title,
            'PREVIEW' => $preview,
            'URL' => get_self_url(false, false, ['confirm' => 1]),
            'FIELDS' => build_keep_post_fields(),
        ]);
    }

    require_code('points2');
    $reversed_data = points_transaction_reverse($id);
    if ($reversed_data[1]) {
        attach_message(do_lang_tempcode('ESCROW_ALSO_CANCELLED'));
    }

    return null;
}

/**
 * The UI/actualiser to amend a point transaction.
 *
 * @param  AUTO_LINK $id The ID of the transaction record to edit
 * @param  Tempcode $title The title of the screen
 * @param  ?MEMBER $member_id_of The member belonging to the transaction being amended (null: We are using the points ledger from admin_points)
 * @param  ?SHORT_TEXT $reason The new reason to set for the transaction (null: not yet specified)
 * @param  URLPATH $redirect Where to redirect after editing the transaction
 * @return ?Tempcode The UI to edit a transaction (null: the edit was successful)
 */
function transaction_amend_screen(int $id, object $title, ?int $member_id_of = null, ?string $reason = null, string $redirect = '') : ?object
{
    if (!has_privilege(get_member(), 'amend_point_transactions')) {
        access_denied('I_ERROR');
    }

    $rows = $GLOBALS['SITE_DB']->query_select('points_ledger', ['*'], ['id' => $id], '', 1);
    if (!array_key_exists(0, $rows)) {
        warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
    }
    $myrow = $rows[0];
    $myreason = get_translated_text($myrow['reason']);

    if ($reason === null) {
        // Edit reason form
        require_code('form_templates');

        $fields = build_keep_post_fields();
        $fields->attach(form_input_line_comcode(do_lang('REASON'), do_lang('DESCRIPTION_POINTS_REASON'), 'reason', $myreason, true));

        $map = ['page' => '_SELF', 'type' => 'amend', 'redirect' => protect_url_parameter($redirect)];
        if ($member_id_of !== null) {
            $map['member_id_of'] = $member_id_of;
        }
        $url = build_url($map, '_SELF');

        return do_template('FORM_SCREEN', [
            '_GUID' => 'ce1752a0c5508a061bffbf242a13e5bd',
            'HIDDEN' => new Tempcode(),
            'TITLE' => $title,
            'FIELDS' => $fields,
            'TEXT' => '',
            'SUBMIT_ICON' => 'buttons/proceed',
            'SUBMIT_NAME' => do_lang_tempcode('PROCEED'),
            'URL' => $url,
            'JS_FUNCTION_CALLS' => [],
        ]);
    }

    // Edit reason
    $_reason = lang_remap_comcode('reason', $id, $reason);
    $GLOBALS['SITE_DB']->query_update('points_ledger', $_reason, ['id' => $id], '', 1);

    // Log it
    log_it('AMEND_POINT_TRANSACTION', strval($id), $reason);

    return null;
}
