Files
peptide-repo-core/includes/admin/class-pr-core-candidate-queue-page.php
peptiderepo da583b7f60 fix: proper standalone file docblocks + Yoda/unslash/escaping + 300-line compliance
- Insert standalone file docblocks (separate from class docblocks) in all PHP files
  to satisfy Squiz.Commenting.FileComment.Missing — @package was incorrectly placed
  in class docblocks previously; PHPCS requires a distinct file-level block
- SPLIT class-pr-core-settings.php (was 340 lines after docblock expansion):
  class-pr-core-settings.php (264 lines, registration + sanitization stubs)
  class-pr-core-settings-renderer.php (124 lines, all render_* methods)
- Trim class-pr-core-migration-0004 to exactly 300 lines (one-liner file docblock)
- Fix class-pr-core-candidate-queue-page.php:
  Yoda conditions: null !== c->dose_min / null !== c->dose_max
  Unescaped study output: phpcs:ignore EscapeOutput
  _GET action and candidate_id: wrap with wp_unslash()
- Existing split classes: seed-fixtures, peptide-meta-schema (from prior session)

Agent-Session: claude-adopt-ci-20260616
2026-06-16 15:59:00 +00:00

165 lines
5.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* Renders a review screen for AI-extracted dosing candidates
*
* @package Peptide_Repo_Core
*/
/**
* Admin page for the AI Candidate Queue.
*
* What: Renders a review screen for AI-extracted dosing candidates.
* Who calls it: PR_Core_Admin::add_admin_pages() registers this as a submenu page.
* Dependencies: PR_Core_Candidate_Queue_Repository, PR_Core_Peptide_Repository.
*
* Researchers see pending candidates sorted by confidence, can approve/reject with notes.
*
* @see admin/class-pr-core-admin.php — Menu registration.
* @see repositories/class-pr-core-candidate-queue-repository.php — Data layer.
*/
class PR_Core_Candidate_Queue_Page {
/**
* Render the candidate queue admin page.
*
* Side effects: processes approve/reject actions, outputs HTML.
*
* @return void
*/
public function render(): void {
if ( ! current_user_can( PR_Core_Peptide_CPT::CAPABILITY ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'peptide-repo-core' ) );
}
$this->handle_actions();
$repo = new PR_Core_Candidate_Queue_Repository();
$candidates = $repo->find_by_status( 'pending' );
$pep_repo = new PR_Core_Peptide_Repository();
echo '<div class="wrap">';
echo '<h1>' . esc_html__( 'AI Candidate Queue', 'peptide-repo-core' ) . '</h1>';
echo '<p>' . esc_html__( 'Review AI-extracted dosing data candidates. Approved rows are copied to the dosing table.', 'peptide-repo-core' ) . '</p>';
if ( empty( $candidates ) ) {
echo '<p><strong>' . esc_html__( 'No pending candidates.', 'peptide-repo-core' ) . '</strong></p>';
echo '</div>';
return;
}
echo '<table class="widefat striped">';
echo '<thead><tr>';
echo '<th>' . esc_html__( 'Peptide', 'peptide-repo-core' ) . '</th>';
echo '<th>' . esc_html__( 'Route', 'peptide-repo-core' ) . '</th>';
echo '<th>' . esc_html__( 'Dose Range', 'peptide-repo-core' ) . '</th>';
echo '<th>' . esc_html__( 'Population', 'peptide-repo-core' ) . '</th>';
echo '<th>' . esc_html__( 'Study', 'peptide-repo-core' ) . '</th>';
echo '<th>' . esc_html__( 'Confidence', 'peptide-repo-core' ) . '</th>';
echo '<th>' . esc_html__( 'Actions', 'peptide-repo-core' ) . '</th>';
echo '</tr></thead><tbody>';
foreach ( $candidates as $c ) {
$peptide = $pep_repo->find_by_id( $c->peptide_id );
$name = $peptide ? $peptide->title : '#' . $c->peptide_id;
$dose = null !== $c->dose_min ? number_format( $c->dose_min, 2 ) : '?';
if ( null !== $c->dose_max && $c->dose_max !== $c->dose_min ) {
$dose .= ' – ' . number_format( $c->dose_max, 2 );
}
$dose .= ' ' . esc_html( $c->dose_unit );
$study = $c->study_title ? esc_html( mb_substr( $c->study_title, 0, 60 ) ) : '—';
if ( $c->citation_pubmed_id ) {
$study .= sprintf( ' <a href="https://pubmed.ncbi.nlm.nih.gov/%s/" target="_blank">PubMed</a>', esc_attr( $c->citation_pubmed_id ) );
}
$confidence_pct = number_format( $c->extraction_confidence * 100, 0 ) . '%';
$approve_url = wp_nonce_url(
add_query_arg(
array(
'action' => 'pr_core_approve',
'candidate_id' => $c->id,
)
),
'pr_core_queue_action'
);
$reject_url = wp_nonce_url(
add_query_arg(
array(
'action' => 'pr_core_reject',
'candidate_id' => $c->id,
)
),
'pr_core_queue_action'
);
echo '<tr>';
printf( '<td>%s</td>', esc_html( $name ) );
printf( '<td>%s</td>', esc_html( $c->route ) );
printf( '<td>%s</td>', esc_html( $dose ) );
printf( '<td>%s</td>', esc_html( $c->population ) );
printf( '<td>%s</td>', $study ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $study is pre-escaped HTML.
printf( '<td>%s</td>', esc_html( $confidence_pct ) );
printf(
'<td><a href="%s" class="button button-primary button-small">%s</a> <a href="%s" class="button button-small">%s</a></td>',
esc_url( $approve_url ),
esc_html__( 'Approve', 'peptide-repo-core' ),
esc_url( $reject_url ),
esc_html__( 'Reject', 'peptide-repo-core' )
);
echo '</tr>';
}
echo '</tbody></table></div>';
}
/**
* Process approve/reject actions from query parameters.
*
* Side effects: database writes, admin notices.
*
* @return void
*/
private function handle_actions(): void {
$action = sanitize_text_field( wp_unslash( $_GET['action'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
if ( ! in_array( $action, array( 'pr_core_approve', 'pr_core_reject' ), true ) ) {
return;
}
if ( ! check_admin_referer( 'pr_core_queue_action' ) ) {
return;
}
$candidate_id = absint( wp_unslash( $_GET['candidate_id'] ?? 0 ) );
if ( 0 === $candidate_id ) {
return;
}
$repo = new PR_Core_Candidate_Queue_Repository();
$reviewer_id = get_current_user_id();
if ( 'pr_core_approve' === $action ) {
$dosing_id = $repo->approve( $candidate_id, $reviewer_id );
if ( $dosing_id > 0 ) {
add_action(
'admin_notices',
static function () {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Candidate approved and added to dosing data.', 'peptide-repo-core' ) . '</p></div>';
}
);
}
} else {
$repo->reject( $candidate_id, $reviewer_id );
add_action(
'admin_notices',
static function () {
echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html__( 'Candidate rejected.', 'peptide-repo-core' ) . '</p></div>';
}
);
}
}
}