'Principal Investigator', 'co-pi' => 'Co-Principal Investigator', 'researcher' => 'Researcher', 'phd-student' => 'PhD Student', 'collaborator' => 'Collaborator', ]; public function register( Loader $loader ): void { $loader->add_action( 'add_meta_boxes', $this, 'add_meta_boxes' ); $loader->add_action( 'save_post_acc_collaboration', $this, 'save_meta', 10, 2 ); $loader->add_filter( 'manage_acc_collaboration_posts_columns', $this, 'register_columns' ); $loader->add_action( 'manage_acc_collaboration_posts_custom_column', $this, 'render_column', 10, 2 ); } // ------------------------------------------------------------------------- // Meta boxes // ------------------------------------------------------------------------- public function add_meta_boxes(): void { add_meta_box( 'acc_project_details', __( 'Project Details', 'academiccrew' ), [ $this, 'render_details_box' ], 'acc_collaboration', 'normal', 'high' ); add_meta_box( 'acc_project_members', __( 'AcademicCrew Members Involved', 'academiccrew' ), [ $this, 'render_members_box' ], 'acc_collaboration', 'normal', 'default' ); add_meta_box( 'acc_project_linked_page', __( 'Profile Page', 'academiccrew' ), [ $this, 'render_linked_page_box' ], 'acc_collaboration', 'normal', 'high' ); } // ------------------------------------------------------------------------- // Render: Project Details // ------------------------------------------------------------------------- public function render_details_box( \WP_Post $post ): void { wp_nonce_field( 'acc_project_meta_save', '_acc_project_nonce' ); $start_date = get_post_meta( $post->ID, '_acc_start_date', true ); $end_date = get_post_meta( $post->ID, '_acc_end_date', true ); $website = get_post_meta( $post->ID, '_acc_website', true ); $keywords_raw = get_post_meta( $post->ID, '_acc_keywords', true ); $keywords = is_array( $keywords_raw ) ? $keywords_raw : []; $kw_value = esc_attr( implode( ', ', $keywords ) ); $project_type = get_post_meta( $post->ID, '_acc_project_type', true ); $featured = get_post_meta( $post->ID, '_acc_featured', true ); $featured_order = get_post_meta( $post->ID, '_acc_featured_order', true ); $short_desc = get_post_meta( $post->ID, '_acc_short_desc', true ); $funding_source = get_post_meta( $post->ID, '_acc_funding_source', true ); $contact_email = get_post_meta( $post->ID, '_acc_contact_email', true ); $partners_raw = get_post_meta( $post->ID, '_acc_partners', true ); $partners_val = is_array( $partners_raw ) ? implode( "\n", $partners_raw ) : ''; ?>

Roles & Classes page.', 'academiccrew' ), [ 'a' => [ 'href' => [] ] ] ), esc_url( admin_url( 'admin.php?page=academiccrew-taxonomies' ) ) ); ?>

saved_member_roles( $post->ID ); $members = get_posts( [ 'post_type' => 'academiccrew_member', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'fields' => 'ids', ] ); if ( ! $members ) { echo '

' . esc_html__( 'No members found. Add members first.', 'academiccrew' ) . '

'; return; } wp_add_inline_style( 'acc-admin', <<<'CSS' #acc_members_table { border-collapse: collapse; width: 100%; } #acc_members_table th { text-align: left; padding: 6px 10px; font-size: 12px; color: #646970; border-bottom: 1px solid #dcdcde; } #acc_members_table td { padding: 5px 10px; border-bottom: 1px solid #f0f0f1; vertical-align: middle; } #acc_members_table tr.acc-mrow--off td { color: #a7aaad; } #acc_members_table tr.acc-mrow--off select { opacity: .4; pointer-events: none; } #acc_members_table select { max-width: 220px; } CSS ); ?>

post_type ) { return; } if ( isset( $_POST['acc_short_desc'] ) ) { update_post_meta( $post_id, '_acc_short_desc', sanitize_textarea_field( wp_unslash( $_POST['acc_short_desc'] ) ) ); } foreach ( [ 'acc_start_date' => '_acc_start_date', 'acc_end_date' => '_acc_end_date' ] as $input => $meta_key ) { if ( isset( $_POST[ $input ] ) ) { update_post_meta( $post_id, $meta_key, sanitize_text_field( wp_unslash( $_POST[ $input ] ) ) ); } } if ( isset( $_POST['acc_website'] ) ) { update_post_meta( $post_id, '_acc_website', esc_url_raw( wp_unslash( $_POST['acc_website'] ) ) ); } $raw_kw = sanitize_text_field( wp_unslash( $_POST['acc_keywords'] ?? '' ) ); $keywords = array_values( array_filter( array_map( 'trim', explode( ',', $raw_kw ) ) ) ); update_post_meta( $post_id, '_acc_keywords', $keywords ); $type = sanitize_key( wp_unslash( $_POST['acc_project_type'] ?? '' ) ); $allowed_types = array_merge( ProjectClassRegistry::keys(), [ '' ] ); update_post_meta( $post_id, '_acc_project_type', in_array( $type, $allowed_types, true ) ? $type : '' ); update_post_meta( $post_id, '_acc_featured', isset( $_POST['acc_featured'] ) ? '1' : '' ); if ( isset( $_POST['acc_funding_source'] ) ) { update_post_meta( $post_id, '_acc_funding_source', sanitize_text_field( wp_unslash( $_POST['acc_funding_source'] ) ) ); } if ( isset( $_POST['acc_contact_email'] ) ) { update_post_meta( $post_id, '_acc_contact_email', sanitize_email( wp_unslash( $_POST['acc_contact_email'] ) ) ); } if ( isset( $_POST['acc_partners'] ) ) { $lines = explode( "\n", wp_unslash( $_POST['acc_partners'] ) ); $partners = array_values( array_filter( array_map( 'sanitize_text_field', $lines ) ) ); update_post_meta( $post_id, '_acc_partners', $partners ); } if ( isset( $_POST['acc_featured_order'] ) ) { $fo = (int) $_POST['acc_featured_order']; update_post_meta( $post_id, '_acc_featured_order', (string) ( $fo >= 1 && $fo <= 999 ? $fo : 999 ) ); } // Linked members with roles — new format: [['member_id' => X, 'role' => Y], ...]. $checked_ids = array_map( 'intval', (array) ( $_POST['acc_linked_member_ids'] ?? [] ) ); $posted_roles = (array) ( $_POST['acc_linked_member_roles'] ?? [] ); $allowed_roles = array_keys( self::MEMBER_ROLES ); $result = []; foreach ( $checked_ids as $mid ) { if ( $mid <= 0 || 'academiccrew_member' !== get_post_type( $mid ) ) { continue; } $role = sanitize_key( $posted_roles[ $mid ] ?? 'collaborator' ); if ( ! in_array( $role, $allowed_roles, true ) ) { $role = 'collaborator'; } $result[] = [ 'member_id' => $mid, 'role' => $role ]; } update_post_meta( $post_id, '_acc_linked_members', $result ); } // ------------------------------------------------------------------------- // Render: Profile Page (side box) // ------------------------------------------------------------------------- public function render_linked_page_box( \WP_Post $post ): void { $page_id = (int) get_post_meta( $post->ID, '_academiccrew_linked_page_id', true ); if ( ! $page_id || ! get_post( $page_id ) ) { echo '

' . esc_html__( 'The public page will be created automatically when this project is first published.', 'academiccrew' ) . '

'; $this->render_page_help( __( 'The project header (title, metadata, partners, team) is built automatically from the fields on this screen.', 'academiccrew' ), __( 'To add a project description, outputs, or related materials, write it in the editor at the top of this screen — it will be synced to the public page on first publish.', 'academiccrew' ) ); return; } $page_url = get_permalink( $page_id ); $edit_url = get_edit_post_link( $page_id ); ?>


render_page_help( __( 'The project header (title, metadata, partners, team) is built automatically from the fields on this screen.', 'academiccrew' ), __( 'To add a project description, outputs, or related materials, write it in the editor at the top of this screen — it is synced to the public page automatically on every save.', 'academiccrew' ) ); ?>



[] ] ); ?>

$columns['cb'], 'title' => __( 'Title', 'academiccrew' ), 'acc_type' => __( 'Type', 'academiccrew' ), 'acc_featured' => __( 'Featured', 'academiccrew' ), 'acc_keywords' => __( 'Keywords', 'academiccrew' ), 'acc_dates' => __( 'Period', 'academiccrew' ), 'acc_members' => __( 'Members', 'academiccrew' ), 'date' => $columns['date'], ]; } public function render_column( string $column, int $post_id ): void { switch ( $column ) { case 'acc_type': $t = get_post_meta( $post_id, '_acc_project_type', true ); echo esc_html( $t ? ProjectClassRegistry::label( $t ) : '—' ); break; case 'acc_featured': echo get_post_meta( $post_id, '_acc_featured', true ) ? '★' : '—'; break; case 'acc_keywords': $kw = get_post_meta( $post_id, '_acc_keywords', true ); $kw = is_array( $kw ) ? $kw : []; echo esc_html( $kw ? implode( ', ', array_slice( $kw, 0, 4 ) ) : '—' ); break; case 'acc_dates': $start = get_post_meta( $post_id, '_acc_start_date', true ); $end = get_post_meta( $post_id, '_acc_end_date', true ); $out = $start ? esc_html( $start ) : '?'; $out .= ' → '; $out .= $end ? esc_html( $end ) : esc_html__( 'ongoing', 'academiccrew' ); echo $out; // phpcs:ignore WordPress.Security.EscapeOutput break; case 'acc_members': $raw = (array) ( get_post_meta( $post_id, '_acc_linked_members', true ) ?: [] ); $count = count( $raw ); if ( ! $count ) { echo '—'; break; } $pi = self::extract_pi_name( $raw ); echo esc_html( $pi ?: __( 'No PI set', 'academiccrew' ) ); if ( $count > 1 ) { echo ' (+' . ( $count - 1 ) . ')'; } break; } } // ------------------------------------------------------------------------- // Shared helpers (called by shortcodes and injectors) // ------------------------------------------------------------------------- /** * Returns member_id → role map from stored meta, handling both old (flat int[]) * and new ({member_id, role}[]) formats. */ private function saved_member_roles( int $post_id ): array { $raw = get_post_meta( $post_id, '_acc_linked_members', true ); $raw = is_array( $raw ) ? $raw : []; $map = []; foreach ( $raw as $entry ) { if ( is_array( $entry ) && isset( $entry['member_id'] ) ) { $map[ (int) $entry['member_id'] ] = $entry['role'] ?? 'collaborator'; } elseif ( is_numeric( $entry ) && (int) $entry > 0 ) { $map[ (int) $entry ] = 'collaborator'; } } return $map; } /** * Finds the PI name from a raw _acc_linked_members value. * New format: looks for role='pi'. Old format (flat int[]): uses first entry. */ public static function extract_pi_name( array $raw ): string { $fallback = 0; foreach ( $raw as $entry ) { if ( is_array( $entry ) && isset( $entry['member_id'] ) ) { if ( ( $entry['role'] ?? '' ) === 'pi' ) { return (string) get_the_title( (int) $entry['member_id'] ); } } elseif ( is_numeric( $entry ) && ! $fallback ) { $fallback = (int) $entry; } } return $fallback ? (string) get_the_title( $fallback ) : ''; } }