add_action( 'add_meta_boxes', $this, 'add_meta_boxes' ); $loader->add_action( 'save_post_academiccrew_member', $this, 'save_meta', 10, 2 ); $loader->add_filter( 'manage_academiccrew_member_posts_columns', $this, 'register_columns' ); $loader->add_filter( 'manage_edit-academiccrew_member_sortable_columns', $this, 'sortable_columns' ); $loader->add_action( 'manage_academiccrew_member_posts_custom_column', $this, 'render_column', 10, 2 ); $loader->add_action( 'pre_get_posts', $this, 'sort_by_column' ); $loader->add_action( 'admin_post_acc_generate_member_page', $this, 'handle_generate_page' ); $loader->add_action( 'wp_ajax_acc_sync_member_pubs', $this, 'ajax_sync_member' ); $loader->add_action( 'wp_ajax_acc_sync_member_status', $this, 'ajax_sync_member_status' ); $loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_member_scripts' ); $loader->add_action( 'wp_ajax_acc_get_research_areas', $this, 'ajax_get_research_areas' ); $loader->add_action( 'wp_ajax_acc_search_publications', $this, 'ajax_search_publications' ); $loader->add_action( 'edit_form_after_title', $this, 'render_quick_profile_bar' ); $loader->add_action( 'admin_notices', $this, 'show_user_created_notice' ); } public function enqueue_member_scripts( string $hook ): void { global $post; if ( ! in_array( $hook, [ 'post.php', 'post-new.php' ], true ) ) return; if ( ! $post || $post->post_type !== 'academiccrew_member' ) return; wp_enqueue_media(); } public function render_quick_profile_bar( \WP_Post $post ): void { if ( 'academiccrew_member' !== $post->post_type ) return; $first_name = get_post_meta( $post->ID, '_acc_first_name', true ); $last_name = get_post_meta( $post->ID, '_acc_last_name', true ); $role = get_post_meta( $post->ID, '_acc_member_role', true ); $role_rank = (int) ( get_post_meta( $post->ID, '_acc_role_rank', true ) ?: 0 ); $roles = RoleRegistry::all(); ?>

prefix . 'academiccrew_publications' ); $like = '%' . $wpdb->esc_like( $q ) . '%'; if ( $member_id ) { $rel = esc_sql( $wpdb->prefix . 'academiccrew_member_publications' ); $results = $wpdb->get_results( $wpdb->prepare( "SELECT p.id, p.title, p.year, p.doi FROM {$table} p INNER JOIN {$rel} mp ON mp.publication_id = p.id AND mp.member_id = %d WHERE p.title LIKE %s ORDER BY p.year DESC LIMIT 15", $member_id, $like ) ); } else { $results = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, year, doi FROM {$table} WHERE title LIKE %s ORDER BY year DESC LIMIT 15", $like ) ); } wp_send_json_success( $results ?: [] ); } public function ajax_get_research_areas(): void { if ( ! current_user_can( 'edit_posts' ) || ! check_ajax_referer( 'acc_member_meta', '_wpnonce', false ) ) { wp_send_json_error(); } $q = strtolower( sanitize_text_field( wp_unslash( $_GET['q'] ?? '' ) ) ); $members = get_posts( [ 'post_type' => 'academiccrew_member', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ] ); $areas = []; foreach ( $members as $mid ) { $raw = get_post_meta( (int) $mid, '_acc_research_areas', true ); $list = is_array( $raw ) ? $raw : []; foreach ( $list as $a ) { $a = ucwords( trim( (string) $a ) ); if ( $a ) $areas[ $a ] = true; } } $areas = array_keys( $areas ); if ( $q ) { $areas = array_values( array_filter( $areas, fn( $a ) => str_contains( strtolower( $a ), $q ) ) ); } sort( $areas ); wp_send_json_success( $areas ); } // ------------------------------------------------------------------------- // Meta boxes // ------------------------------------------------------------------------- public function add_meta_boxes(): void { $cpt = 'academiccrew_member'; add_meta_box( 'acc_member_profile', __( 'Member Profile', 'academiccrew' ), [ $this, 'render_profile_box' ], $cpt, 'normal', 'high' ); add_meta_box( 'acc_member_contact', __( 'Contact Information', 'academiccrew' ), [ $this, 'render_contact_box' ], $cpt, 'normal', 'default' ); add_meta_box( 'acc_member_institutional', __( 'Institutional Links', 'academiccrew' ), [ $this, 'render_institutional_box' ], $cpt, 'normal', 'default' ); add_meta_box( 'acc_member_research_ids', __( 'Research Identifiers', 'academiccrew' ), [ $this, 'render_research_ids_box' ], $cpt, 'normal', 'default' ); add_meta_box( 'acc_member_social', __( 'Social & Web', 'academiccrew' ), [ $this, 'render_social_box' ], $cpt, 'normal', 'default' ); add_meta_box( 'acc_member_stats', __( 'Publication Statistics', 'academiccrew' ), [ $this, 'render_stats_box' ], $cpt, 'normal', 'low' ); add_meta_box( 'acc_member_linked_page', __( 'Profile Page', 'academiccrew' ), [ $this, 'render_linked_page_box' ], $cpt, 'normal', 'low' ); add_meta_box( 'acc_member_service_roles', __( 'Service Roles', 'academiccrew' ), [ $this, 'render_service_roles_box' ], $cpt, 'normal', 'low' ); add_meta_box( 'acc_member_awards', __( 'Awards & Distinctions', 'academiccrew' ), [ $this, 'render_awards_box' ], $cpt, 'normal', 'low' ); add_meta_box( 'acc_member_featured_pubs', __( 'Featured Publications', 'academiccrew' ), [ $this, 'render_featured_pubs_box' ], $cpt, 'normal', 'low' ); } // ------------------------------------------------------------------------- // Render: Member Profile // ------------------------------------------------------------------------- public function render_profile_box( \WP_Post $post ): void { wp_nonce_field( 'acc_member_meta_save', '_acc_member_nonce' ); $department = get_post_meta( $post->ID, '_acc_department', true ); $affiliation = get_post_meta( $post->ID, '_acc_affiliation', true ); $bio = get_post_meta( $post->ID, '_acc_bio', true ); $avatar_url = get_post_meta( $post->ID, '_acc_avatar_url', true ); $raw_areas = get_post_meta( $post->ID, '_acc_research_areas', true ); $research_areas = implode( ', ', (array) ( maybe_unserialize( $raw_areas ) ?: [] ) ); ?>

'acc_bio', 'media_buttons' => false, 'textarea_rows' => 6, 'teeny' => true, 'quicktags' => [ 'buttons' => 'strong,em,link,ul,ol,li' ], ] ); ?>
<?php esc_attr_e( 'Avatar preview', 'academiccrew' ); ?>

ID, '_acc_email', true ); $phone = get_post_meta( $post->ID, '_acc_phone', true ); $office = get_post_meta( $post->ID, '_acc_office', true ); ?>
ID, '_acc_iris_profile_url', true ); $iris_person_id = get_post_meta( $post->ID, '_acc_iris_person_id', true ); ?>

ID, '_acc_orcid_id', true ); $scopus = get_post_meta( $post->ID, '_acc_scopus_id', true ); $scholar = get_post_meta( $post->ID, '_acc_scholar_id', true ); ?>
ID, '_acc_linkedin_url', true ); $github = get_post_meta( $post->ID, '_acc_github_url', true ); $twitter = get_post_meta( $post->ID, '_acc_twitter_url', true ); $website = get_post_meta( $post->ID, '_acc_personal_website', true ); ?>
get_linked_page_id( $post->ID ); if ( ! $page_id ) { $generate_url = wp_nonce_url( admin_url( 'admin-post.php?action=acc_generate_member_page&member_id=' . $post->ID ), 'acc_gen_member_' . $post->ID ); printf( '

%s

%s

', esc_html__( 'No profile page found. Publish the member and click Generate, or just re-save.', 'academiccrew' ), esc_url( $generate_url ), esc_html__( 'Generate profile page', 'academiccrew' ) ); return; } $page_url = get_permalink( $page_id ); $edit_url = get_edit_post_link( $page_id ); ?>


render_page_help( __( 'The profile header (photo, role, contacts, research areas, publication stats) is built automatically from the fields on this screen.', 'academiccrew' ), __( 'To add custom content to the public profile page (extended bio, research highlights, news, etc.), write it in the editor at the top of this screen — it is synced to the public page automatically on every save.', 'academiccrew' ) ); ?> ID, '_acc_pub_count', true ); $citation_count = get_post_meta( $post->ID, '_acc_citation_count', true ); $h_index = get_post_meta( $post->ID, '_acc_h_index', true ); $last_sync = get_post_meta( $post->ID, '_academiccrew_last_sync', true ); $mid = (int) $post->ID; $nonce = wp_create_nonce( 'acc_sync_member_' . $mid ); ?>
ID ); ?>

sync_status_payload( $mid ); ob_start(); ?> (function () { var mid = ; var nonce = ; var btn = document.getElementById('acc-sync-btn-' + mid); if (!btn) return; var label = btn.querySelector('.acc-sync-label'); var spinner = btn.querySelector('.spinner'); var result = document.getElementById('acc-sync-result-' + mid); var idleLbl = ; var busyLbl = ; var lost = ; var started = 0, timer = null, misses = 0, beforeId = 0; function call(action) { var fd = new FormData(); fd.append('action', action); fd.append('member_id', mid); fd.append('_wpnonce', nonce); return fetch(ajaxurl, { method: 'POST', body: fd, credentials: 'same-origin' }) .then(function (r) { return r.json(); }); } function elapsed() { var s = Math.round((Date.now() - started) / 1000); return ' (' + Math.floor(s / 60) + ':' + ('0' + s % 60).slice(-2) + ')'; } function show(d) { if (!d) return; if (d.pub_count !== undefined) { var pc = document.getElementById('acc-pub-count-' + mid); if (pc) pc.textContent = d.pub_count || '—'; } if (d.last_sync) { var ls = document.getElementById('acc-last-sync-' + mid); if (ls) ls.textContent = d.last_sync; } // Right after the click the job may not have written its result yet: // the previous result is not this sync's, keep waiting a little. if (d.done && beforeId && d.result && d.result.id === beforeId && Date.now() - started < 30000) { d = Object.assign({}, d, { done: false, message: busyLbl }); } if (d.done) { clearTimeout(timer); timer = null; btn.disabled = false; label.textContent = idleLbl; spinner.style.display = 'none'; result.style.color = d.failed ? '#a00' : '#3c763d'; result.textContent = d.message; } else { btn.disabled = true; label.textContent = busyLbl; spinner.style.display = 'inline-block'; result.style.color = '#646970'; result.textContent = d.message + elapsed(); timer = setTimeout(poll, 5000); } } function poll() { call('acc_sync_member_status') .then(function (resp) { misses = 0; if (resp.success) show(resp.data); }) .catch(function () { // A slow or dropped poll is not a failed sync: keep trying for a while. if (++misses < 6) { timer = setTimeout(poll, 10000); return; } btn.disabled = false; label.textContent = idleLbl; spinner.style.display = 'none'; result.style.color = '#a00'; result.textContent = lost; }); } btn.addEventListener('click', function () { started = Date.now(); misses = 0; btn.disabled = true; label.textContent = busyLbl; spinner.style.display = 'inline-block'; result.textContent = ''; call('acc_sync_member_pubs') .then(function (resp) { if (!resp.success) { btn.disabled = false; label.textContent = idleLbl; spinner.style.display = 'none'; result.style.color = '#a00'; result.textContent = (resp.data && resp.data.message) || 'Error'; return; } beforeId = resp.data.result ? resp.data.result.id : 0; resp.data.done = false; // it just started: keep polling show(resp.data); }) .catch(function () { poll(); }); }); // Opened while a sync of this member is queued or running: follow it. var initial = ; if (!initial.done) { started = Date.now(); show(initial); } })(); ID, '_acc_service_roles', true ); $roles = is_array( $raw ) ? $raw : []; $role_types = [ 'chair' => __( 'Chair', 'academiccrew' ), 'area-chair' => __( 'Area Chair', 'academiccrew' ), 'senior-pc' => __( 'Senior PC', 'academiccrew' ), 'tpc' => __( 'TPC', 'academiccrew' ), 'organiser' => __( 'Organiser', 'academiccrew' ), 'editor' => __( 'Editor', 'academiccrew' ), ]; ?>

$entry ) : // Backwards compat: old schema used role_type / event_name / year. $role_val = $entry['role'] ?? $entry['role_type'] ?? 'tpc'; $venue_val = $entry['venue'] ?? $entry['event_name'] ?? ''; $track_val = $entry['track'] ?? ''; $url_val = $entry['url'] ?? ''; $yfrom_val = $entry['year_from'] ?? $entry['year'] ?? ''; $yto_val = $entry['year_to'] ?? ''; ?>
–
(function () { 'use strict'; var nextIdx = ; var list = document.getElementById('acc_service_roles_list'); var tpl = document.getElementById('acc_sr_row_tpl'); var addBtn = document.getElementById('acc_add_service_role'); var emptyMsg = document.getElementById('acc_service_roles_empty'); function removeRow(btn) { btn.closest('.acc-sr-row').remove(); if (!list.querySelector('.acc-sr-row') && emptyMsg) { list.appendChild(emptyMsg); } } list.querySelectorAll('.acc-remove-sr').forEach(function (btn) { btn.addEventListener('click', function () { removeRow(this); }); }); addBtn.addEventListener('click', function () { if (emptyMsg && emptyMsg.parentNode === list) { list.removeChild(emptyMsg); } var html = tpl.innerHTML.replace(/__IDX__/g, nextIdx++); var tmp = document.createElement('div'); tmp.innerHTML = html; var row = tmp.firstElementChild; row.querySelector('.acc-remove-sr').addEventListener('click', function () { removeRow(this); }); list.appendChild(row); row.querySelector('input[type="text"]').focus(); }); }()); ID, '_acc_awards', true ); $awards = is_array( $raw ) ? $raw : []; $paper_types = [ 'best-paper' => __( 'Best Paper Award', 'academiccrew' ), 'best-demo' => __( 'Best Demo Paper', 'academiccrew' ), 'best-student-paper' => __( 'Best Student Paper', 'academiccrew' ), 'best-poster' => __( 'Best Poster Award', 'academiccrew' ), 'outstanding-paper' => __( 'Outstanding Paper Award', 'academiccrew' ), 'distinguished-paper' => __( 'Distinguished Paper Award', 'academiccrew' ), 'test-of-time' => __( 'Test of Time Award', 'academiccrew' ), ]; $other_types = [ 'best-dissertation' => __( 'Best PhD Dissertation', 'academiccrew' ), 'fellowship' => __( 'Fellowship', 'academiccrew' ), 'honorary-degree' => __( 'Honorary Degree', 'academiccrew' ), 'research-prize' => __( 'Research Prize', 'academiccrew' ), 'teaching-award' => __( 'Teaching Award', 'academiccrew' ), 'service-award' => __( 'Service Award', 'academiccrew' ), 'other' => __( 'Other (enter title below)', 'academiccrew' ), ]; $paper_type_keys = array_keys( $paper_types ); $search_nonce = wp_create_nonce( 'acc_member_meta' ); // Closure renders one award row; $idx may be an int or '__IDX__'. $row_html = function ( $idx, string $type_v, string $custom_v, string $paper_v, int $pub_id_v, string $venue_v, $year_v, string $coauth_v, string $url_v ) use ( $paper_types, $other_types, $paper_type_keys ) : string { $is_paper = in_array( $type_v, $paper_type_keys, true ); $is_other = ( $type_v === 'other' ); $paper_disp = $is_paper ? '' : 'none'; $custom_disp = $is_other ? '' : 'none'; $linked_disp = $pub_id_v ? '' : 'none'; ob_start(); ?>

$entry ) : echo $row_html( $idx, $entry['type'] ?? 'other', $entry['custom_title'] ?? $entry['title'] ?? '', $entry['paper_title'] ?? '', (int) ( $entry['publication_id'] ?? 0 ), $entry['venue'] ?? $entry['organization'] ?? '', $entry['year'] ?? '', $entry['coauthors'] ?? '', $entry['url'] ?? '' ); endforeach; ?>
(function () { 'use strict'; var PAPER_TYPES = ; var NONCE = ; var nextIdx = ; var list = document.getElementById('acc_awards_list'); var tpl = document.getElementById('acc_aw_row_tpl'); var addBtn = document.getElementById('acc_add_award'); var emptyMsg = document.getElementById('acc_awards_empty'); function isPaper(type) { return PAPER_TYPES.indexOf(type) !== -1; } function updateVisibility(row) { var type = row.querySelector('.acc-aw-type').value; var paperRow = row.querySelector('.acc-aw-paper-row'); var customRow = row.querySelector('.acc-aw-custom-row'); if (paperRow) paperRow.style.display = isPaper(type) ? '' : 'none'; if (customRow) customRow.style.display = (type === 'other') ? '' : 'none'; } function initPaperSearch(row) { var searchInput = row.querySelector('.acc-aw-paper-search'); var pubIdInput = row.querySelector('.acc-aw-pub-id'); var indicator = row.querySelector('.acc-aw-pub-indicator'); var clearBtn = row.querySelector('.acc-aw-clear-pub'); var dropdown = row.querySelector('.acc-aw-pub-dropdown'); var urlField = row.querySelector('.acc-aw-url'); if (!searchInput) return; var debounce; function showIndicator(show) { if (indicator) indicator.style.display = show ? 'block' : 'none'; } searchInput.addEventListener('input', function () { pubIdInput.value = ''; showIndicator(false); clearTimeout(debounce); var q = searchInput.value.trim(); if (q.length < 2) { dropdown.style.display = 'none'; return; } debounce = setTimeout(function () { fetch( ajaxurl + '?action=acc_search_publications' + '&q=' + encodeURIComponent(q) + '&_wpnonce=' + encodeURIComponent(NONCE), { credentials: 'same-origin' } ) .then(function (r) { return r.json(); }) .then(function (resp) { if (!resp.success || !resp.data.length) { dropdown.style.display = 'none'; return; } dropdown.innerHTML = ''; resp.data.forEach(function (pub) { var li = document.createElement('li'); li.style.cssText = 'padding:5px 8px;cursor:pointer;border-bottom:1px solid #eee;font-size:12px;line-height:1.35'; li.textContent = pub.title + (pub.year ? ' (' + pub.year + ')' : ''); li.addEventListener('mousedown', function (e) { e.preventDefault(); searchInput.value = pub.title; pubIdInput.value = pub.id; showIndicator(true); if (urlField && !urlField.value && pub.doi) { urlField.value = 'https://doi.org/' + pub.doi; } dropdown.style.display = 'none'; }); dropdown.appendChild(li); }); dropdown.style.display = 'block'; }) .catch(function () { dropdown.style.display = 'none'; }); }, 280); }); searchInput.addEventListener('blur', function () { setTimeout(function () { dropdown.style.display = 'none'; }, 200); }); if (clearBtn) { clearBtn.addEventListener('click', function () { pubIdInput.value = ''; showIndicator(false); }); } } function initRow(row) { row.querySelector('.acc-aw-type').addEventListener('change', function () { updateVisibility(row); }); updateVisibility(row); initPaperSearch(row); row.querySelector('.acc-remove-aw').addEventListener('click', function () { row.remove(); if (!list.querySelector('.acc-aw-row') && emptyMsg) { list.appendChild(emptyMsg); } }); } list.querySelectorAll('.acc-aw-row').forEach(initRow); addBtn.addEventListener('click', function () { if (emptyMsg && emptyMsg.parentNode === list) { list.removeChild(emptyMsg); } var html = tpl.innerHTML.replace(/__IDX__/g, nextIdx++); var tmp = document.createElement('div'); tmp.innerHTML = html; var row = tmp.firstElementChild; list.appendChild(row); initRow(row); row.querySelector('select').focus(); }); }()); ID; $nonce = wp_create_nonce( 'acc_member_meta' ); // Fetch featured pubs linked to this member directly from the publications table. $pub_table = esc_sql( $wpdb->prefix . 'academiccrew_publications' ); $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_member_publications' ); $feat_rows = $wpdb->get_results( $wpdb->prepare( "SELECT p.id, p.title, p.year FROM {$pub_table} p INNER JOIN {$rel_table} mp ON p.id = mp.publication_id WHERE mp.member_id = %d AND p.is_featured = 1 ORDER BY p.featured_order ASC, p.year DESC", $mid ), ARRAY_A ) ?: []; $ids = array_column( $feat_rows, 'id' ); $pub_map = []; foreach ( $feat_rows as $r ) { $pub_map[ (int) $r['id'] ] = $r; } // Fetch all available tags. $tags_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tags' ); $all_tags = $wpdb->get_results( "SELECT id, name, color FROM {$tags_table} ORDER BY name ASC", ARRAY_A ) ?: []; // Batch-fetch current tag assignments for all featured pubs. $pub_tag_map = []; // [pub_id => [tag_id, ...]] if ( $ids && $all_tags ) { $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_pub_tag_relations' ); $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $relations = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore "SELECT publication_id, tag_id FROM {$rel_table} WHERE publication_id IN ({$placeholders})", // phpcs:ignore ...$ids ), ARRAY_A ) ?: []; foreach ( $relations as $r ) { $pub_tag_map[ (int) $r['publication_id'] ][] = (int) $r['tag_id']; } } $tags_js = wp_json_encode( array_values( array_map( fn( $t ) => [ 'id' => (int) $t['id'], 'name' => $t['name'], 'color' => $t['color'], ], $all_tags ) ) ); $remove_label = esc_js( __( 'Remove', 'academiccrew' ) ); $tags_label = esc_js( __( 'Tags', 'academiccrew' ) ); ?>

⠿ ()
" autocomplete="off" style="width:100%">

(function () { 'use strict'; var MID = ; var NONCE = ; var TAGS = ; var list = document.getElementById('acc-fp-list'); var emptyMsg = document.getElementById('acc-fp-empty'); var search = document.getElementById('acc-fp-search'); var ac = document.getElementById('acc-fp-ac'); var debounce; var dragSrc = null; function currentIds() { return Array.from(list.querySelectorAll('input[name="acc_featured_pub_ids[]"]')) .map(function (i) { return parseInt(i.value, 10); }); } function clearDropIndicators() { list.querySelectorAll('.acc-fp-row').forEach(function (r) { r.style.borderTop = r.style.borderBottom = ''; }); } function buildTagsHtml(pubId) { if (!TAGS.length) return ''; var html = '
'; TAGS.forEach(function (t) { html += ''; }); html += '
'; return html; } function bindRow(row) { row.querySelector('.acc-fp-remove').addEventListener('click', function () { row.remove(); if (!list.querySelector('.acc-fp-row')) emptyMsg.style.display = ''; }); 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 = ''; clearDropIndicators(); dragSrc = null; }); row.addEventListener('dragover', function (e) { e.preventDefault(); if (!dragSrc || dragSrc === row) return; clearDropIndicators(); var mid_y = row.getBoundingClientRect().top + row.offsetHeight / 2; if (e.clientY < mid_y) 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; clearDropIndicators(); var mid_y = row.getBoundingClientRect().top + row.offsetHeight / 2; if (e.clientY < mid_y) list.insertBefore(dragSrc, row); else row.after(dragSrc); }); } function makeRow(id, title, year) { var row = document.createElement('div'); row.className = 'acc-fp-row'; row.draggable = true; row.style.cssText = 'padding:6px 8px;margin-bottom:6px;background:#f9f9f9;border:1px solid #ddd;border-radius:4px'; var yearHtml = year ? ' (' + year + ')' : ''; row.innerHTML = '
' + '⠿' + '' + title.replace(/' + '' + '' + '' + '
' + buildTagsHtml(id); bindRow(row); return row; } list.querySelectorAll('.acc-fp-row').forEach(bindRow); search.addEventListener('input', function () { clearTimeout(debounce); var q = search.value.trim(); if (q.length < 2) { ac.style.display = 'none'; return; } debounce = setTimeout(function () { fetch( ajaxurl + '?action=acc_search_publications' + '&q=' + encodeURIComponent(q) + '&member_id=' + MID + '&_wpnonce=' + encodeURIComponent(NONCE), { credentials: 'same-origin' } ) .then(function (r) { return r.json(); }) .then(function (resp) { if (!resp.success || !resp.data.length) { ac.style.display = 'none'; return; } var current = currentIds(); var hits = resp.data.filter(function (p) { return current.indexOf(parseInt(p.id, 10)) === -1; }); if (!hits.length) { ac.style.display = 'none'; return; } ac.innerHTML = ''; hits.forEach(function (pub) { var item = document.createElement('div'); item.style.cssText = 'padding:6px 10px;cursor:pointer;border-bottom:1px solid #f0f0f0;font-size:12px;line-height:1.35'; item.innerHTML = pub.title.replace(/(' + pub.year + ')' : ''); item.addEventListener('mousedown', function (e) { e.preventDefault(); emptyMsg.style.display = 'none'; list.appendChild(makeRow(pub.id, pub.title, pub.year)); search.value = ''; ac.style.display = 'none'; }); ac.appendChild(item); }); ac.style.display = 'block'; }) .catch(function () { ac.style.display = 'none'; }); }, 280); }); search.addEventListener('blur', function () { setTimeout(function () { ac.style.display = 'none'; }, 200); }); }()); post_type ) { return; } $fields_text = [ 'acc_first_name' => '_acc_first_name', 'acc_last_name' => '_acc_last_name', 'acc_member_role' => '_acc_member_role', 'acc_department' => '_acc_department', 'acc_affiliation' => '_acc_affiliation', 'acc_office' => '_acc_office', 'acc_iris_person_id' => '_acc_iris_person_id', 'acc_orcid_id' => '_acc_orcid_id', 'acc_scopus_id' => '_acc_scopus_id', 'acc_scholar_id' => '_acc_scholar_id', ]; foreach ( $fields_text as $input => $meta_key ) { if ( isset( $_POST[ $input ] ) ) { update_post_meta( $post_id, $meta_key, sanitize_text_field( wp_unslash( $_POST[ $input ] ) ) ); } } // Scholar ID: accept a pasted profile URL or "ID&hl=…" and keep the bare ID. if ( isset( $_POST['acc_scholar_id'] ) ) { $scholar = sanitize_text_field( wp_unslash( $_POST['acc_scholar_id'] ) ); if ( preg_match( '/(?:[?&]user=|^)([A-Za-z0-9_-]{12})(?![A-Za-z0-9_-])/', $scholar, $m ) ) { update_post_meta( $post_id, '_acc_scholar_id', $m[1] ); } } // Bio — rich text, sanitised via wp_kses_post. if ( isset( $_POST['acc_bio'] ) ) { update_post_meta( $post_id, '_acc_bio', wp_kses_post( wp_unslash( $_POST['acc_bio'] ) ) ); } // Email. if ( isset( $_POST['acc_email'] ) ) { update_post_meta( $post_id, '_acc_email', sanitize_email( wp_unslash( $_POST['acc_email'] ) ) ); } // Phone — allow + and spaces. if ( isset( $_POST['acc_phone'] ) ) { update_post_meta( $post_id, '_acc_phone', sanitize_text_field( wp_unslash( $_POST['acc_phone'] ) ) ); } // URL fields. $fields_url = [ 'acc_avatar_url' => '_acc_avatar_url', 'acc_iris_profile_url' => '_acc_iris_profile_url', 'acc_linkedin_url' => '_acc_linkedin_url', 'acc_github_url' => '_acc_github_url', 'acc_twitter_url' => '_acc_twitter_url', 'acc_personal_website' => '_acc_personal_website', ]; foreach ( $fields_url as $input => $meta_key ) { if ( isset( $_POST[ $input ] ) ) { update_post_meta( $post_id, $meta_key, esc_url_raw( wp_unslash( $_POST[ $input ] ) ) ); } } // Research areas — comma-separated → serialized array, normalized to Title Case. if ( isset( $_POST['acc_research_areas'] ) ) { $areas = array_values( array_filter( array_map( function ( string $a ): string { return ucwords( sanitize_text_field( trim( wp_unslash( $a ) ) ) ); }, explode( ',', wp_unslash( $_POST['acc_research_areas'] ) ) ) ) ); update_post_meta( $post_id, '_acc_research_areas', $areas ); } // Rank within role — 1-999 = explicit rank; 0 or blank → default 999. $rank_input = isset( $_POST['acc_role_rank'] ) ? (int) $_POST['acc_role_rank'] : 0; $rank = ( $rank_input >= 1 && $rank_input <= 999 ) ? $rank_input : 999; update_post_meta( $post_id, '_acc_role_rank', $rank ); // Auto-provision WP user when an email is present. $saved_email = get_post_meta( $post_id, '_acc_email', true ); if ( $saved_email ) { $this->maybe_create_wp_user( $post_id, $saved_email ); } // Validate role key against registry. $role = get_post_meta( $post_id, '_acc_member_role', true ); if ( $role && ! array_key_exists( $role, RoleRegistry::all() ) ) { delete_post_meta( $post_id, '_acc_member_role' ); $role = ''; } // Sync linked page menu_order so WP nav menus reflect role+rank ordering. $page_id = (int) get_post_meta( $post_id, '_academiccrew_linked_page_id', true ); if ( $page_id && get_post( $page_id ) ) { $role_order = array_keys( RoleRegistry::all() ); $found = array_search( $role, $role_order, true ); $role_pos = $found !== false ? (int) $found : 99; $menu_order = $role_pos * 1000 + min( $rank, 999 ); wp_update_post( [ 'ID' => $page_id, 'menu_order' => $menu_order ] ); } // Featured publications — write is_featured / featured_order directly to the publications table. global $wpdb; $pub_table = esc_sql( $wpdb->prefix . 'academiccrew_publications' ); $rel_table = esc_sql( $wpdb->prefix . 'academiccrew_member_publications' ); $featured_ids = array_values( array_unique( array_filter( array_map( 'intval', (array) ( $_POST['acc_featured_pub_ids'] ?? [] ) ) ) ) ); // IDs previously featured that are linked to this member. $prev_ids = array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( "SELECT p.id FROM {$pub_table} p INNER JOIN {$rel_table} mp ON p.id = mp.publication_id WHERE mp.member_id = %d AND p.is_featured = 1", $post_id ) ) ); // Mark submitted pubs as featured with their drag-reordered position. foreach ( $featured_ids as $idx => $pub_id ) { $wpdb->update( $pub_table, [ 'is_featured' => 1, 'featured_order' => $idx + 1 ], [ 'id' => $pub_id ], [ '%d', '%d' ], [ '%d' ] ); } // Unfeature pubs that were removed from the list. foreach ( array_diff( $prev_ids, $featured_ids ) as $pub_id ) { $wpdb->update( $pub_table, [ 'is_featured' => 0, 'featured_order' => 999 ], [ 'id' => $pub_id ], [ '%d', '%d' ], [ '%d' ] ); } // Tags for each featured publication. $managed_pub_ids = array_unique( array_filter( array_map( 'intval', (array) ( $_POST['acc_pub_tags_managed'] ?? [] ) ) ) ); if ( $managed_pub_ids ) { $tag_rel = esc_sql( $wpdb->prefix . 'academiccrew_pub_tag_relations' ); $pub_tags_input = (array) ( $_POST['acc_pub_tags'] ?? [] ); foreach ( $managed_pub_ids as $pub_id ) { if ( ! in_array( $pub_id, $featured_ids, true ) ) { continue; } $wpdb->query( $wpdb->prepare( "DELETE FROM {$tag_rel} WHERE publication_id = %d", $pub_id ) ); $tag_ids = array_unique( array_filter( array_map( 'intval', (array) ( $pub_tags_input[ $pub_id ] ?? [] ) ) ) ); foreach ( $tag_ids as $tid ) { $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$tag_rel} (publication_id, tag_id) VALUES (%d, %d)", $pub_id, $tid ) ); } } } // Service roles — array of {role, venue, year_from, year_to, track, url}. $valid_roles = [ 'chair', 'area-chair', 'senior-pc', 'tpc', 'organiser', 'editor' ]; $service_roles = []; foreach ( (array) ( $_POST['acc_service_roles'] ?? [] ) as $entry ) { $role = sanitize_key( $entry['role'] ?? '' ); $venue = sanitize_text_field( wp_unslash( $entry['venue'] ?? '' ) ); $year_from = (int) ( $entry['year_from'] ?? 0 ); $year_to = (int) ( $entry['year_to'] ?? 0 ); $track = sanitize_text_field( wp_unslash( $entry['track'] ?? '' ) ); $url = esc_url_raw( wp_unslash( $entry['url'] ?? '' ) ); if ( $venue && in_array( $role, $valid_roles, true ) ) { $service_roles[] = [ 'role' => $role, 'venue' => $venue, 'year_from' => $year_from ?: '', 'year_to' => ( $year_to && $year_to !== $year_from ) ? $year_to : '', 'track' => $track, 'url' => $url, ]; } } update_post_meta( $post_id, '_acc_service_roles', $service_roles ); // Awards — richer schema {type, custom_title, paper_title, publication_id, venue, year, coauthors, url}. $valid_aw_types = [ 'best-paper', 'best-demo', 'best-student-paper', 'best-poster', 'outstanding-paper', 'distinguished-paper', 'test-of-time', 'best-dissertation', 'fellowship', 'honorary-degree', 'research-prize', 'teaching-award', 'service-award', 'other', ]; $awards = []; foreach ( (array) ( $_POST['acc_awards'] ?? [] ) as $entry ) { $type = sanitize_key( $entry['type'] ?? 'other' ); if ( ! in_array( $type, $valid_aw_types, true ) ) { $type = 'other'; } $custom_title = sanitize_text_field( wp_unslash( $entry['custom_title'] ?? '' ) ); $paper_title = sanitize_text_field( wp_unslash( $entry['paper_title'] ?? '' ) ); $publication_id = (int) ( $entry['publication_id'] ?? 0 ); $venue = sanitize_text_field( wp_unslash( $entry['venue'] ?? '' ) ); $year = (int) ( $entry['year'] ?? 0 ); $coauthors = sanitize_text_field( wp_unslash( $entry['coauthors'] ?? '' ) ); $url = esc_url_raw( wp_unslash( $entry['url'] ?? '' ) ); if ( ! $venue ) continue; if ( $type === 'other' && ! $custom_title ) continue; $awards[] = [ 'type' => $type, 'custom_title' => $custom_title, 'paper_title' => $paper_title, 'publication_id' => $publication_id, 'venue' => $venue, 'year' => $year ?: '', 'coauthors' => $coauthors, 'url' => $url, ]; } update_post_meta( $post_id, '_acc_awards', $awards ); } // ------------------------------------------------------------------------- // Admin list columns // ------------------------------------------------------------------------- public function register_columns( array $columns ): array { return [ 'cb' => $columns['cb'], 'title' => __( 'Name', 'academiccrew' ), 'acc_role' => __( 'Role', 'academiccrew' ), 'acc_dept' => __( 'Dept.', 'academiccrew' ), 'acc_orcid' => __( 'ORCID', 'academiccrew' ), 'acc_pubs' => __( 'Pubs.', 'academiccrew' ), 'acc_page' => __( 'Profile Page', 'academiccrew' ), 'acc_sync' => __( 'Last Sync', 'academiccrew' ), 'date' => $columns['date'], ]; } public function render_column( string $column, int $post_id ): void { switch ( $column ) { case 'acc_role': $key = get_post_meta( $post_id, '_acc_member_role', true ); if ( $key ) { $color = RoleRegistry::color( $key ); printf( '%s', esc_attr( $color ), esc_html( RoleRegistry::label( $key ) ) ); } break; case 'acc_dept': echo esc_html( get_post_meta( $post_id, '_acc_department', true ) ?: '—' ); break; case 'acc_orcid': $id = get_post_meta( $post_id, '_acc_orcid_id', true ); if ( $id ) { printf( '%s', esc_html( $id ) ); } else { echo '—'; } break; case 'acc_pubs': echo esc_html( get_post_meta( $post_id, '_acc_pub_count', true ) ?: '—' ); break; case 'acc_page': $page_id = $this->get_linked_page_id( $post_id ); if ( $page_id ) { printf( '%s  %s', esc_url( get_permalink( $page_id ) ), esc_attr__( 'View profile page', 'academiccrew' ), esc_html( get_permalink( $page_id ) ), esc_url( get_edit_post_link( $page_id ) ), esc_html__( 'Edit', 'academiccrew' ) ); } else { echo '' . esc_html__( 'Not created yet', 'academiccrew' ) . ''; } break; case 'acc_sync': $sync = get_post_meta( $post_id, '_academiccrew_last_sync', true ); echo $sync ? esc_html( wp_date( 'Y-m-d H:i', strtotime( $sync ) ) ) : '—'; break; } } public function sortable_columns( array $columns ): array { $columns['acc_role'] = 'acc_role'; $columns['acc_dept'] = 'acc_dept'; $columns['acc_pubs'] = 'acc_pubs'; $columns['acc_sync'] = 'acc_sync'; return $columns; } public function sort_by_column( \WP_Query $query ): void { if ( ! is_admin() || ! $query->is_main_query() ) { return; } if ( ( $query->get( 'post_type' ) ) !== 'academiccrew_member' ) { return; } $map = [ 'acc_role' => [ '_acc_member_role', 'CHAR', 'ASC' ], 'acc_dept' => [ '_acc_department', 'CHAR', 'ASC' ], 'acc_pubs' => [ '_acc_pub_count', 'NUMERIC', 'DESC' ], 'acc_sync' => [ '_academiccrew_last_sync', 'CHAR', 'DESC' ], ]; $orderby = $query->get( 'orderby' ); if ( ! isset( $map[ $orderby ] ) ) { return; } [ $meta_key, $type, $default_order ] = $map[ $orderby ]; $query->set( 'meta_key', $meta_key ); $query->set( 'orderby', 'meta_value' . ( $type === 'NUMERIC' ? '_num' : '' ) ); // Respect explicit order from the URL; fall back to the column default. if ( ! in_array( strtoupper( $query->get( 'order' ) ), [ 'ASC', 'DESC' ], true ) ) { $query->set( 'order', $default_order ); } } // ------------------------------------------------------------------------- // Linked page helpers // ------------------------------------------------------------------------- /** * Returns the linked WP profile page ID, auto-repairing a stale pointer. * Returns 0 when no live page exists. */ private function get_linked_page_id( int $member_id ): int { $page_id = (int) get_post_meta( $member_id, '_academiccrew_linked_page_id', true ); if ( $page_id && get_post( $page_id ) ) { return $page_id; } // Meta is missing or stale — try to recover by scanning for the page. $found = get_posts( [ 'post_type' => 'page', 'post_status' => [ 'publish', 'draft', 'private' ], 'posts_per_page' => 1, 'fields' => 'ids', 'meta_key' => '_academiccrew_member_id', 'meta_value' => $member_id, ] ); if ( $found ) { $recovered = (int) $found[0]; update_post_meta( $member_id, '_academiccrew_linked_page_id', $recovered ); return $recovered; } return 0; } /** * "Sync publications now": start the sync in the background and return at * once — it takes minutes, longer than browsers / proxies keep an AJAX * request open. The page then polls ajax_sync_member_status(). */ public function ajax_sync_member(): void { $member_id = $this->sync_request_member(); $how = \AcademicCrew\Sync\SyncScheduler::start_member_sync( $member_id ); wp_send_json_success( array_merge( $this->sync_status_payload( $member_id ), [ 'started' => $how ] ) ); } /** Polled by the admin while a member sync is queued or running. */ public function ajax_sync_member_status(): void { $member_id = $this->sync_request_member(); $payload = $this->sync_status_payload( $member_id ); if ( 'queued' === $payload['state'] ) { spawn_cron(); // a waiting job starts even on a site with no visitors } wp_send_json_success( $payload ); } private function sync_request_member(): int { $member_id = (int) ( $_POST['member_id'] ?? 0 ); if ( ! $member_id || ! check_ajax_referer( 'acc_sync_member_' . $member_id, '_wpnonce', false ) || ! current_user_can( 'edit_post', $member_id ) ) { wp_send_json_error( [ 'message' => __( 'Not allowed.', 'academiccrew' ) ] ); } if ( ! Freemius::is_pro() ) { wp_send_json_error( [ 'message' => __( 'Automatic publication sync is a Pro feature.', 'academiccrew' ) ] ); } return $member_id; } /** Status + a ready-to-show message (also used to render the box). */ public function sync_status_payload( int $member_id ): array { $st = \AcademicCrew\Sync\SyncScheduler::member_sync_status( $member_id ); $r = $st['result']; switch ( $st['state'] ) { case 'queued': $msg = __( 'Sync scheduled — starting in a moment…', 'academiccrew' ); break; case 'running': $msg = __( 'Syncing publications… this can take several minutes. You can leave this page: the result will show here and in the Sync Log.', 'academiccrew' ); break; case 'in_full_sync': $msg = __( 'A sync of all members is running: this member is at the front of its queue and will be updated in a few minutes.', 'academiccrew' ); break; default: if ( ! $r ) { $msg = ''; } elseif ( 'error' === $r['status'] ) { /* translators: %s: error message */ $msg = sprintf( __( 'Last sync failed: %s', 'academiccrew' ), $r['error'] ?: __( 'see the Sync Log', 'academiccrew' ) ); } else { /* translators: 1: new, 2: updated, 3: skipped publications */ $msg = sprintf( __( 'Done — %1$d new, %2$d updated, %3$d unchanged.', 'academiccrew' ), $r['new'], $r['updated'], $r['skipped'] ); } } $st['done'] = 'idle' === $st['state']; $st['failed'] = $st['done'] && $r && 'error' === $r['status']; $st['message'] = $msg; return $st; } // ------------------------------------------------------------------------- // Shared helper: linked-page help notice // ------------------------------------------------------------------------- private function render_page_help( string $auto_text, string $edit_text ): void { ?>



[] ] ); ?>

ID ); if ( ! in_array( 'academiccrew_member', (array) $existing->roles, true ) ) { $existing->add_role( 'academiccrew_member' ); } return; } $first_name = (string) get_post_meta( $member_id, '_acc_first_name', true ); $last_name = (string) get_post_meta( $member_id, '_acc_last_name', true ); $base = sanitize_user( (string) strstr( $email, '@', true ), true ) ?: sanitize_user( $email, true ); $username = $base; $counter = 1; while ( username_exists( $username ) ) { $username = $base . $counter++; } $user_id = wp_insert_user( [ 'user_login' => $username, 'user_email' => $email, 'display_name' => trim( "$first_name $last_name" ) ?: $username, 'first_name' => $first_name, 'last_name' => $last_name, 'role' => 'academiccrew_member', 'user_pass' => wp_generate_password( 24 ), ] ); if ( is_wp_error( $user_id ) ) { error_log( 'AcademicCrew: wp_insert_user failed for member ' . $member_id . ': ' . $user_id->get_error_message() ); return; } update_post_meta( $member_id, '_academiccrew_wp_user_id', $user_id ); try { wp_new_user_notification( $user_id, null, 'user' ); } catch ( \Throwable $e ) { error_log( 'AcademicCrew: wp_new_user_notification failed for user ' . $user_id . ': ' . $e->getMessage() ); } set_transient( 'acc_user_created_' . get_current_user_id(), [ 'user_id' => $user_id, 'username' => $username, 'email' => $email ], 60 ); } public function show_user_created_notice(): void { $key = 'acc_user_created_' . get_current_user_id(); $data = get_transient( $key ); if ( ! $data ) { return; } delete_transient( $key ); $edit_url = get_edit_user_link( (int) $data['user_id'] ); printf( '

%s

', wp_kses( sprintf( /* translators: 1: username, 2: email, 3: edit-user URL */ __( 'WordPress user %1$s (%2$s) created automatically and linked to this member. Edit user', 'academiccrew' ), esc_html( $data['username'] ), esc_html( $data['email'] ), esc_url( $edit_url ) ), [ 'strong' => [], 'a' => [ 'href' => [] ] ] ) ); } public function handle_generate_page(): void { $member_id = (int) ( $_REQUEST['member_id'] ?? 0 ); if ( ! $member_id || ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ?? '' ), 'acc_gen_member_' . $member_id ) || ! current_user_can( 'edit_post', $member_id ) ) { wp_die( esc_html__( 'Not allowed.', 'academiccrew' ) ); } $generator = new \AcademicCrew\PageGenerator\MemberPageGenerator(); $generator->generate_page( $member_id ); wp_safe_redirect( (string) get_edit_post_link( $member_id, 'url' ) ); exit; } }