'Journal Article', 'conference-paper' => 'Conference Paper', 'book-chapter' => 'Book Chapter', 'preprint' => 'Preprint', 'book' => 'Book', 'thesis' => 'Thesis', 'report' => 'Report', 'other' => 'Other', ]; public function register( Loader $loader ): void { $loader->add_action( 'admin_menu', $this, 'add_menu_page' ); $loader->add_action( 'admin_post_acc_save_publication', $this, 'handle_save' ); $loader->add_action( 'admin_post_acc_delete_pub', $this, 'handle_delete' ); $loader->add_action( 'admin_post_acc_merge_publications', $this, 'handle_merge' ); } private function get_linked_member_ids( int $pub_id ): array { if ( ! $pub_id ) return []; global $wpdb; return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT member_id FROM {$wpdb->prefix}academiccrew_member_publications WHERE publication_id = %d", $pub_id ) ) ); } private function refresh_member_stats( int $member_id ): void { MemberStats::refresh( $member_id ); } public function add_menu_page(): void { add_submenu_page( AdminMenu::PARENT_SLUG, __( 'Publications', 'academiccrew' ), __( 'Publications', 'academiccrew' ), 'edit_posts', self::MENU_SLUG, [ $this, 'render_page' ] ); } public function render_page(): void { $action = sanitize_key( $_GET['action'] ?? 'list' ); if ( $action === 'merge' ) { $this->render_merge_view(); } elseif ( $action === 'edit' || $action === 'new' ) { $this->render_edit_form(); } else { $this->render_list(); } } // ------------------------------------------------------------------------- // List view // ------------------------------------------------------------------------- private function render_list(): void { global $wpdb; $search = sanitize_text_field( wp_unslash( $_GET['s'] ?? '' ) ); $type = sanitize_key( $_GET['type'] ?? '' ); $paged = max( 1, (int) ( $_GET['paged'] ?? 1 ) ); $offset = ( $paged - 1 ) * self::PER_PAGE; $table = esc_sql( $wpdb->prefix . 'academiccrew_publications' ); // Sorting — column whitelist prevents SQL injection. $allowed_orderby = [ 'id', 'title', 'year', 'pub_type', 'venue', 'citations', 'source_api', 'is_featured' ]; $orderby = in_array( $_GET['orderby'] ?? '', $allowed_orderby, true ) ? sanitize_key( $_GET['orderby'] ) : ''; $order = strtoupper( $_GET['order'] ?? '' ) === 'ASC' ? 'ASC' : 'DESC'; $order_sql = esc_sql( $orderby ? "`{$orderby}` {$order}, id DESC" : 'year DESC, id DESC' ); // Featured / tag filters — only exposed when featured pubs exist. $filter_featured = ! empty( $_GET['featured'] ); $filter_tag = (int) ( $_GET['tag_id'] ?? 0 ); $has_featured = (bool) $wpdb->get_var( "SELECT EXISTS(SELECT 1 FROM {$table} WHERE is_featured = 1)" ); // phpcs:ignore $all_filter_tags = $has_featured ? ( $wpdb->get_results( // phpcs:ignore "SELECT id, name, color FROM {$wpdb->prefix}academiccrew_pub_tags ORDER BY name ASC", ARRAY_A ) ?: [] ) : []; // Build WHERE dynamically $conditions = []; $params = []; if ( $search ) { $conditions[] = 'title LIKE %s'; $params[] = '%' . $wpdb->esc_like( $search ) . '%'; } if ( $type ) { $conditions[] = 'pub_type = %s'; $params[] = $type; } if ( $filter_featured ) { $conditions[] = 'is_featured = 1'; } if ( $filter_tag && $has_featured ) { $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tag_relations' ); $conditions[] = "id IN (SELECT publication_id FROM {$rel_table} WHERE tag_id = %d)"; // phpcs:ignore $params[] = $filter_tag; } $where_sql = esc_sql( $conditions ? ( 'WHERE ' . implode( ' AND ', $conditions ) ) : '' ); if ( $params ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} {$where_sql}", ...$params ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, year, pub_type, venue, citations, source_api, authors_json, is_featured FROM {$table} {$where_sql} ORDER BY {$order_sql} LIMIT %d OFFSET %d", ...[ ...$params, self::PER_PAGE, $offset ] ), ARRAY_A ); // phpcs:enable } else { $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore "SELECT id, title, year, pub_type, venue, citations, source_api, authors_json, is_featured FROM {$table} ORDER BY {$order_sql} LIMIT %d OFFSET %d", self::PER_PAGE, $offset ), ARRAY_A ); } $total_pages = max( 1, (int) ceil( $total / self::PER_PAGE ) ); $base_url = admin_url( 'admin.php?page=' . self::MENU_SLUG ); $notice = $this->get_notice(); // Closure that renders a sortable . Pass $style for width attributes. $th = function ( string $col, string $label, string $style = '' ) use ( $orderby, $order, $base_url, $search, $type, $filter_featured, $filter_tag ): string { $is_current = ( $orderby === $col ); $next_order = ( $is_current && $order === 'ASC' ) ? 'DESC' : 'ASC'; $url = add_query_arg( array_filter( [ 'orderby' => $col, 'order' => $next_order, 's' => $search, 'type' => $type, 'featured' => $filter_featured ? '1' : '', 'tag_id' => $filter_tag ?: '', ] ), $base_url ); $arrow = $is_current ? ( $order === 'ASC' ? ' ▲' : ' ▼' ) : ''; $style_attr = $style ? ' style="' . esc_attr( $style ) . '"' : ''; return '' . '' . esc_html( $label ) . '' . $arrow . '' . ''; }; // Batch-fetch member names as fallback for publications with empty authors_json $pub_ids_in_page = array_column( $rows ?: [], 'id' ); $member_name_map = []; if ( ! empty( $pub_ids_in_page ) ) { $placeholders = implode( ',', array_fill( 0, count( $pub_ids_in_page ), '%d' ) ); $mrows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore "SELECT mp.publication_id, p.post_title FROM {$wpdb->prefix}academiccrew_member_publications mp INNER JOIN {$wpdb->posts} p ON mp.member_id = p.ID AND p.post_status = 'publish' WHERE mp.publication_id IN ({$placeholders})", // phpcs:ignore ...$pub_ids_in_page ), ARRAY_A ); foreach ( $mrows as $mr ) { $member_name_map[ (int) $mr['publication_id'] ][] = $mr['post_title']; } } // Batch-fetch tag assignments for publications on this page. $tag_map = []; // [pub_id => [[name, color], ...]] if ( ! empty( $pub_ids_in_page ) ) { $placeholders = implode( ',', array_fill( 0, count( $pub_ids_in_page ), '%d' ) ); $trows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore "SELECT r.publication_id, t.name, t.color FROM {$wpdb->prefix}academiccrew_pub_tag_relations r INNER JOIN {$wpdb->prefix}academiccrew_pub_tags t ON t.id = r.tag_id WHERE r.publication_id IN ({$placeholders}) ORDER BY t.name ASC", ...$pub_ids_in_page ), ARRAY_A ); foreach ( $trows as $tr ) { $tag_map[ (int) $tr['publication_id'] ][] = $tr; } } ?>

()


self::MENU_SLUG, 'action' => 'edit', 'id' => $pub_id ], admin_url( 'admin.php' ) ); $delete_url = wp_nonce_url( admin_url( "admin-post.php?action=acc_delete_pub&id={$pub_id}" ), 'acc_delete_pub_' . $pub_id ); $exclude_url = wp_nonce_url( admin_url( "admin-post.php?action=acc_delete_pub&id={$pub_id}&acc_exclude=1" ), 'acc_delete_pub_' . $pub_id ); $raw_authors = json_decode( $row['authors_json'] ?? '[]', true ) ?: []; $authors = array_values( array_filter( array_map( fn( $a ) => is_string( $a ) ? $a : ( $a['name'] ?? '' ), $raw_authors ) ) ); if ( empty( $authors ) ) { $authors = $member_name_map[ $pub_id ] ?? []; } $type_lbl = self::PUB_TYPES[ $row['pub_type'] ?? '' ] ?? $row['pub_type'] ?? ''; ?>

3 ) echo ' …'; ?>
| |
★
1 ) : $paginate_base = add_query_arg( array_filter( [ 's' => $search, 'type' => $type, 'featured' => $filter_featured ? '1' : '', 'tag_id' => $filter_tag ?: '', ] ), $base_url ); echo '
'; echo wp_kses_post( paginate_links( [ 'base' => add_query_arg( 'paged', '%#%', $paginate_base ), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $total_pages, 'current' => $paged, ] ) ?? '' ); echo '
'; endif; ?>
prefix . 'academiccrew_publications' ); if ( $pub_id ) { $pub = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $pub_id ), ARRAY_A ); if ( ! $pub ) { wp_die( esc_html__( 'Publication not found.', 'academiccrew' ) ); } } else { $pub = []; } $list_url = admin_url( 'admin.php?page=' . self::MENU_SLUG ); $notice = $this->get_notice(); $get = fn( string $k, string $d = '' ) => $pub[ $k ] ?? $d; // Fetch all tags + current assignment for this pub. $all_tags = $wpdb->get_results( "SELECT id, name, color FROM {$wpdb->prefix}academiccrew_pub_tags ORDER BY name ASC", ARRAY_A ) ?: []; $current_tag_ids = $pub_id ? array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT tag_id FROM {$wpdb->prefix}academiccrew_pub_tag_relations WHERE publication_id = %d", $pub_id ) ) ) : []; // Build ordered author list (unified: members + external, preserving order) $raw_auth = json_decode( $pub['authors_json'] ?? '[]', true ) ?: []; $authors_form = array_map( function ( $a ) { if ( is_string( $a ) ) return [ 'name' => $a, 'member_id' => 0 ]; return [ 'name' => (string) ( $a['name'] ?? '' ), 'member_id' => (int) ( $a['member_id'] ?? 0 ) ]; }, $raw_auth ); // Append linked members absent from authors_json (backwards compat with old synced pubs) $listed_mem_ids = array_column( $authors_form, 'member_id' ); foreach ( $this->get_linked_member_ids( $pub_id ) as $lid ) { if ( $lid > 0 && ! in_array( $lid, $listed_mem_ids, true ) ) { $mp = get_post( $lid ); if ( $mp ) $authors_form[] = [ 'name' => $mp->post_title, 'member_id' => $lid ]; } } // Members list for JS autocomplete $members_for_ac = get_posts( [ 'post_type' => 'academiccrew_member', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', ] ); $members_js = wp_json_encode( array_map( fn( $m ) => [ 'id' => $m->ID, 'name' => $m->post_title ], $members_for_ac ) ); ?>

←

⠿

(function(){ var MEMBERS = ; var wrap = document.getElementById('acc-authors-wrap'); var dragSrc = null; function esc(s){ return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function clearDrop(){ wrap.querySelectorAll('.acc-author-row').forEach(function(r){ r.style.borderTop = ''; r.style.borderBottom = ''; }); } function makeRow(name, mid){ mid = mid || 0; var d = document.createElement('div'); d.className = 'acc-author-row'; d.draggable = true; d.style.cssText = 'display:flex;gap:6px;margin-bottom:6px;align-items:center'; d.innerHTML = '⠿' + '
' + '' + '' + '' + '
' + '' + ''; bind(d); return d; } function bind(row){ var inp = row.querySelector('.acc-author-input'); var hid = row.querySelector('input[name="pub_author_member_id[]"]'); var ac = row.querySelector('.acc-author-ac'); var badge = row.querySelector('.acc-member-badge'); inp.addEventListener('input', function(){ hid.value = 0; badge.style.display = 'none'; var q = this.value.toLowerCase().trim(); if(!q){ ac.style.display='none'; return; } var hits = MEMBERS.filter(function(m){ return m.name.toLowerCase().indexOf(q) !== -1; }); if(!hits.length){ ac.style.display='none'; return; } ac.innerHTML = hits.map(function(m){ return '
'+esc(m.name)+'
'; }).join(''); ac.style.display = 'block'; }); ac.addEventListener('mousedown', function(e){ var item = e.target.closest('.acc-ac-item'); if(!item) return; e.preventDefault(); inp.value = item.dataset.name; hid.value = item.dataset.id; badge.style.display = ''; ac.style.display = 'none'; }); inp.addEventListener('blur', function(){ setTimeout(function(){ ac.style.display='none'; }, 150); }); row.querySelector('.acc-author-remove').addEventListener('click', function(){ row.remove(); }); // Drag & drop reordering row.addEventListener('dragstart', function(e){ dragSrc = row; e.dataTransfer.effectAllowed = 'move'; setTimeout(function(){ row.style.opacity = '0.4'; }, 0); }); row.addEventListener('dragend', function(){ row.style.opacity = ''; clearDrop(); dragSrc = null; }); row.addEventListener('dragover', function(e){ e.preventDefault(); if(!dragSrc || dragSrc === row) return; clearDrop(); var rect = row.getBoundingClientRect(); if(e.clientY < rect.top + rect.height / 2){ row.style.borderTop = '2px solid #0073aa'; } else { row.style.borderBottom = '2px solid #0073aa'; } }); row.addEventListener('dragleave', function(){ row.style.borderTop = ''; row.style.borderBottom = ''; }); row.addEventListener('drop', function(e){ e.preventDefault(); if(!dragSrc || dragSrc === row) return; clearDrop(); var rect = row.getBoundingClientRect(); if(e.clientY < rect.top + rect.height / 2){ wrap.insertBefore(dragSrc, row); } else { row.after(dragSrc); } }); } wrap.querySelectorAll('.acc-author-row').forEach(bind); document.getElementById('acc-add-author').addEventListener('click', function(){ var r = makeRow('', 0); wrap.appendChild(r); r.querySelector('.acc-author-input').focus(); }); })();

Add tags first.', 'academiccrew' ), [ 'a' => [ 'href' => [] ] ] ), esc_url( admin_url( 'admin.php?page=academiccrew-pub-tags' ) ) ); ?>

__( 'Source', 'academiccrew' ), 'orcid_work_id' => 'ORCID work ID', 'scholar_id' => 'Scholar ID', 'iris_id' => 'IRIS ID', 'created_at' => __( 'Created', 'academiccrew' ), 'updated_at' => __( 'Updated', 'academiccrew' ), ]; $has_meta = array_filter( array_map( fn( $k ) => $pub[ $k ] ?? '', array_keys( $meta_fields ) ) ); if ( $has_meta ) : ?> $label ) : $val = $pub[ $field ] ?? ''; if ( ! $val ) continue; ?>
prefix . 'academiccrew_publications' ); // Build ordered author list from parallel arrays $author_names = (array) ( $_POST['pub_author_name'] ?? [] ); $author_mem_ids = (array) ( $_POST['pub_author_member_id'] ?? [] ); $authors_list = []; $new_member_ids = []; foreach ( $author_names as $i => $raw_name ) { $name = sanitize_text_field( wp_unslash( $raw_name ) ); if ( ! $name ) continue; $mid = (int) ( $author_mem_ids[ $i ] ?? 0 ); $entry = [ 'name' => $name ]; if ( $mid > 0 ) { $entry['member_id'] = $mid; $new_member_ids[] = $mid; } $authors_list[] = $entry; } $authors_json = wp_json_encode( $authors_list ); $year = ( $_POST['pub_year'] ?? '' ) !== '' ? (int) wp_unslash( $_POST['pub_year'] ) : null; $citations = ( $_POST['pub_citations'] ?? '' ) !== '' ? (int) wp_unslash( $_POST['pub_citations'] ) : null; // Keywords — comma-separated input → JSON array $kw_raw = sanitize_text_field( wp_unslash( $_POST['pub_keywords'] ?? '' ) ); $kw_arr = $kw_raw ? array_values( array_filter( array_map( 'trim', explode( ',', $kw_raw ) ) ) ) : []; $keywords_json = $kw_arr ? wp_json_encode( $kw_arr ) : null; $data = [ 'title' => sanitize_text_field( wp_unslash( $_POST['pub_title'] ?? '' ) ), 'abstract' => sanitize_textarea_field( wp_unslash( $_POST['pub_abstract'] ?? '' ) ), 'venue' => sanitize_text_field( wp_unslash( $_POST['pub_venue'] ?? '' ) ) ?: null, 'pub_type' => sanitize_key( $_POST['pub_type'] ?? '' ) ?: null, 'doi' => sanitize_text_field( wp_unslash( $_POST['pub_doi'] ?? '' ) ) ?: null, 'pdf_url' => esc_url_raw( wp_unslash( $_POST['pub_pdf_url'] ?? '' ) ) ?: null, 'url' => esc_url_raw( wp_unslash( $_POST['pub_url'] ?? '' ) ) ?: null, 'keywords' => $keywords_json, 'authors_json' => $authors_json, 'updated_at' => current_time( 'mysql', true ), ]; // Use raw SQL SET for nullable integers to avoid wpdb turning null into 0 $set_clauses = []; foreach ( [ 'title' => '%s', 'abstract' => '%s', 'venue' => '%s', 'pub_type' => '%s', 'doi' => '%s', 'pdf_url' => '%s', 'url' => '%s', 'authors_json' => '%s', 'keywords' => '%s', 'updated_at' => '%s', ] as $col => $fmt ) { if ( $data[ $col ] === null ) { $set_clauses[] = "`{$col}` = NULL"; } else { $set_clauses[] = $wpdb->prepare( "`{$col}` = {$fmt}", $data[ $col ] ); // phpcs:ignore } } // year and citations — nullable integers $set_clauses[] = $year !== null ? $wpdb->prepare( '`year` = %d', $year ) : '`year` = NULL'; $set_clauses[] = $citations !== null ? $wpdb->prepare( '`citations` = %d', $citations ) : '`citations` = NULL'; // A count typed here is an editor's correction: syncs must not overwrite it. $old_citations = $pub_id ? $wpdb->get_var( $wpdb->prepare( "SELECT citations FROM {$table} WHERE id = %d", $pub_id ) ) : null; // phpcs:ignore if ( $citations === null ) { $set_clauses[] = '`citations_source` = NULL'; } elseif ( $old_citations === null || (int) $old_citations !== $citations ) { $set_clauses[] = "`citations_source` = 'manual'"; } // featured flags $is_featured = ! empty( $_POST['pub_is_featured'] ) ? 1 : 0; $featured_order = ( $_POST['pub_featured_order'] ?? '' ) !== '' ? max( 1, min( 999, (int) $_POST['pub_featured_order'] ) ) : 999; $set_clauses[] = $wpdb->prepare( '`is_featured` = %d', $is_featured ); $set_clauses[] = $wpdb->prepare( '`featured_order` = %d', $featured_order ); $set_sql = implode( ', ', $set_clauses ); if ( $pub_id ) { $wpdb->query( $wpdb->prepare( // phpcs:ignore "UPDATE {$table} SET {$set_sql} WHERE id = %d", // phpcs:ignore $pub_id ) ); $redirect_id = $pub_id; $msg = 'updated'; } else { $set_clauses[] = $wpdb->prepare( '`created_at` = %s', current_time( 'mysql', true ) ); $set_sql = implode( ', ', $set_clauses ); $wpdb->query( "INSERT INTO {$table} SET {$set_sql}" ); // phpcs:ignore $redirect_id = $wpdb->insert_id; $msg = 'added'; } // Sync member-publication links and refresh stats $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_member_publications' ); $prev_member_ids = $this->get_linked_member_ids( $redirect_id ); $new_member_ids = array_unique( array_filter( $new_member_ids ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$rel_table} WHERE publication_id = %d", $redirect_id ) ); foreach ( $new_member_ids as $mid ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$rel_table} (member_id, publication_id) VALUES (%d, %d)", $mid, $redirect_id ) ); } foreach ( array_unique( array_merge( $prev_member_ids, $new_member_ids ) ) as $mid ) { $this->refresh_member_stats( (int) $mid ); } // Sync tag relations — tags are only valid for featured publications. $tag_rel_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tag_relations' ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$tag_rel_table} WHERE publication_id = %d", $redirect_id ) ); if ( $is_featured ) { $submitted_tag_ids = array_unique( array_filter( array_map( 'intval', (array) ( $_POST['pub_tag_ids'] ?? [] ) ) ) ); if ( $submitted_tag_ids ) { $placeholders = implode( ',', array_fill( 0, count( $submitted_tag_ids ), '%d' ) ); $valid_tag_ids = $wpdb->get_col( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery "SELECT id FROM {$wpdb->prefix}academiccrew_pub_tags WHERE id IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared ...$submitted_tag_ids ) ); foreach ( array_map( 'intval', $valid_tag_ids ) as $tid ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$tag_rel_table} (publication_id, tag_id) VALUES (%d, %d)", $redirect_id, $tid ) ); } } } wp_safe_redirect( add_query_arg( [ 'page' => self::MENU_SLUG, 'action' => 'edit', 'id' => $redirect_id, 'acc_msg' => $msg ], admin_url( 'admin.php' ) ) ); exit; } public function handle_delete(): void { $pub_id = (int) ( $_GET['id'] ?? 0 ); $exclude = ! empty( $_GET['acc_exclude'] ); if ( ! $pub_id || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ?? '' ), 'acc_delete_pub_' . $pub_id ) || ! current_user_can( 'edit_posts' ) ) { wp_die( esc_html__( 'Not allowed.', 'academiccrew' ) ); } global $wpdb; if ( $exclude ) { $pub = $wpdb->get_row( $wpdb->prepare( "SELECT doi, orcid_work_id, scholar_id, iris_id, title, year FROM {$wpdb->prefix}academiccrew_publications WHERE id = %d", $pub_id ), ARRAY_A ); if ( $pub ) { $wpdb->insert( $wpdb->prefix . 'academiccrew_excluded_publications', [ 'doi' => $pub['doi'] ?: null, 'orcid_work_id' => $pub['orcid_work_id'] ?: null, 'scholar_id' => $pub['scholar_id'] ?: null, 'iris_id' => $pub['iris_id'] ?: null, 'title' => $pub['title'] ?: null, 'year' => $pub['year'] ? (int) $pub['year'] : null, 'excluded_by' => get_current_user_id() ?: null, ], [ '%s', '%s', '%s', '%s', '%s', '%d', '%d' ] ); } } $wpdb->delete( $wpdb->prefix . 'academiccrew_publications', [ 'id' => $pub_id ], [ '%d' ] ); $wpdb->delete( $wpdb->prefix . 'academiccrew_member_publications', [ 'publication_id' => $pub_id ], [ '%d' ] ); $msg = $exclude ? 'deleted-excluded' : 'deleted'; wp_safe_redirect( add_query_arg( 'acc_msg', $msg, admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) ); exit; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private function get_notice(): ?array { $msg = sanitize_key( $_GET['acc_msg'] ?? '' ); return match ( $msg ) { 'updated' => [ 'type' => 'success', 'message' => __( 'Publication updated.', 'academiccrew' ) ], 'added' => [ 'type' => 'success', 'message' => __( 'Publication added.', 'academiccrew' ) ], 'deleted' => [ 'type' => 'success', 'message' => __( 'Publication deleted.', 'academiccrew' ) ], 'deleted-excluded' => [ 'type' => 'success', 'message' => __( 'Publication deleted and excluded from future syncs.', 'academiccrew' ) ], 'merged' => [ 'type' => 'success', 'message' => __( 'Publications merged successfully.', 'academiccrew' ) ], 'merge-error' => [ 'type' => 'error', 'message' => __( 'Merge failed. Please check the IDs and try again.', 'academiccrew' ) ], default => null, }; } private function type_style( string $type ): string { $colors = [ 'journal-article' => 'background:#E6F1FB;color:#0C447C', 'conference-paper' => 'background:#EEEDFE;color:#3C3489', 'book-chapter' => 'background:#EAF3DE;color:#3B6D11', 'preprint' => 'background:#F1EFE8;color:#444441', 'book' => 'background:#EAF3DE;color:#3B6D11', ]; $base = 'font-size:10px;font-weight:500;padding:2px 7px;border-radius:8px;white-space:nowrap;display:inline-block'; return $base . ';' . ( $colors[ $type ] ?? 'background:#f0f0f0;color:#666' ); } // ------------------------------------------------------------------------- // Merge view // ------------------------------------------------------------------------- private function render_merge_view(): void { global $wpdb; $pub_id = (int) ( $_GET['id'] ?? 0 ); $table = esc_sql( $wpdb->prefix . 'academiccrew_publications' ); if ( ! $pub_id ) { wp_die( esc_html__( 'No publication specified.', 'academiccrew' ) ); } $pub = $wpdb->get_row( $wpdb->prepare( "SELECT id, title, year, venue, doi, source_api, scholar_id, orcid_work_id, iris_id FROM {$table} WHERE id = %d", $pub_id ), ARRAY_A ); if ( ! $pub ) { wp_die( esc_html__( 'Publication not found.', 'academiccrew' ) ); } $list_url = admin_url( 'admin.php?page=' . self::MENU_SLUG ); $search_q = sanitize_text_field( wp_unslash( $_GET['merge_search'] ?? '' ) ); $search_id = (int) ( $_GET['merge_id'] ?? 0 ); $candidates = []; if ( $search_q ) { $candidates = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, year, venue, doi, source_api FROM {$table} WHERE id != %d AND (title LIKE %s OR doi LIKE %s) ORDER BY year DESC, id DESC LIMIT 20", $pub_id, '%' . $wpdb->esc_like( $search_q ) . '%', '%' . $wpdb->esc_like( $search_q ) . '%' ), ARRAY_A ); } elseif ( $search_id > 0 ) { $candidates = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, year, venue, doi, source_api FROM {$table} WHERE id = %d AND id != %d", $search_id, $pub_id ), ARRAY_A ); } // If exactly one candidate is selected for merging, show confirmation. $confirm_id = (int) ( $_GET['confirm_with'] ?? 0 ); $confirm_pub = null; if ( $confirm_id ) { $confirm_pub = $wpdb->get_row( $wpdb->prepare( "SELECT id, title, year, venue, doi, source_api, scholar_id, orcid_work_id, iris_id FROM {$table} WHERE id = %d AND id != %d", $confirm_id, $pub_id ), ARRAY_A ); } // Suggest authoritative record: prefer the one with a DOI. $suggested_keep = $pub['doi'] ? (int) $pub['id'] : ( $confirm_pub && $confirm_pub['doi'] ? (int) $confirm_pub['id'] : (int) $pub['id'] ); ?>

←


·
DOI

# DOI
DOI
"return confirm('" . esc_js( __( 'This will permanently delete the unselected publication. Continue?', 'academiccrew' ) ) . "')" ] ); ?>

# DOI

DOI
merge_publications( $keep_id, $drop_id ); // Refresh stats for all members linked to either publication. if ( $success ) { $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_member_publications' ); $member_ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT member_id FROM {$rel_table} WHERE publication_id = %d", $keep_id ) ); foreach ( $member_ids as $mid ) { $this->refresh_member_stats( (int) $mid ); } } $msg = $success ? 'merged' : 'merge-error'; wp_safe_redirect( add_query_arg( 'acc_msg', $msg, admin_url( 'admin.php?page=' . self::MENU_SLUG ) ) ); exit; } }