'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 ) : '';
?>
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 ) : '';
}
}