calculator = new AcademicYearCalculator(); } public function register( Loader $loader ): void { $loader->add_action( 'add_meta_boxes', $this, 'add_meta_boxes' ); $loader->add_action( 'save_post_academiccrew_course', $this, 'save_meta', 10, 2 ); $loader->add_filter( 'manage_academiccrew_course_posts_columns', $this, 'register_columns' ); $loader->add_action( 'manage_academiccrew_course_posts_custom_column', $this, 'render_column', 10, 2 ); $loader->add_filter( 'manage_edit-academiccrew_course_sortable_columns', $this, 'sortable_columns' ); } // ------------------------------------------------------------------------- // Meta boxes // ------------------------------------------------------------------------- public function add_meta_boxes(): void { $cpt = 'academiccrew_course'; add_meta_box( 'acc_course_details', __( 'Course Details', 'academiccrew' ), [ $this, 'render_details_box' ], $cpt, 'normal', 'high' ); add_meta_box( 'acc_course_staff', __( 'Teaching Staff', 'academiccrew' ), [ $this, 'render_staff_box' ], $cpt, 'normal', 'default' ); add_meta_box( 'acc_course_content', __( 'Course Content', 'academiccrew' ), [ $this, 'render_content_box' ], $cpt, 'normal', 'default' ); add_meta_box( 'acc_course_resources', __( 'Resources', 'academiccrew' ), [ $this, 'render_resources_box' ], $cpt, 'normal', 'low' ); add_meta_box( 'acc_course_linked_page', __( 'Profile Page', 'academiccrew' ), [ $this, 'render_linked_page_box' ], $cpt, 'normal', 'low' ); } // ------------------------------------------------------------------------- // Render: Course Details // ------------------------------------------------------------------------- public function render_details_box( \WP_Post $post ): void { wp_nonce_field( 'acc_course_meta_save', '_acc_course_nonce' ); $codes = (array) ( get_post_meta( $post->ID, '_acc_course_codes', true ) ?: [] ); // Migrate from old single-code meta if needed. if ( empty( $codes ) ) { $old_code = get_post_meta( $post->ID, '_acc_course_code', true ); if ( $old_code ) { $codes = [ $old_code ]; } } $credits = get_post_meta( $post->ID, '_acc_credits', true ); $lecture_hours = get_post_meta( $post->ID, '_acc_lecture_hours', true ); $degree = get_post_meta( $post->ID, '_acc_degree_program', true ); $language = get_post_meta( $post->ID, '_acc_language', true ) ?: 'IT'; $period = (int) ( get_post_meta( $post->ID, '_acc_period', true ) ?: 1 ); $acad_year = get_post_meta( $post->ID, '_acc_academic_year', true ); $year_override = get_post_meta( $post->ID, '_acc_year_override', true ); $level = get_post_meta( $post->ID, '_acc_level', true ); $featured = get_post_meta( $post->ID, '_acc_featured', true ); $featured_order = get_post_meta( $post->ID, '_acc_featured_order', true ); $archived = get_post_meta( $post->ID, '_acc_archived', true ); $period_count = PeriodCadence::period_count(); $years_by_period = []; for ( $p = 1; $p <= $period_count; $p++ ) { $years_by_period[ $p ] = $this->calculator->calculate( $p ); } $use_stored = $year_override || $archived; $display_year = $use_stored ? (int) $acad_year : $this->calculator->calculate( $period ); ?>
*
$c ) : ?>
←
←

(function () { 'use strict'; var list = document.getElementById('acc_codes_list'); var addBtn = document.getElementById('acc_add_code'); var phText = ; function bindRemove(btn) { btn.addEventListener('click', function () { btn.closest('.acc-code-entry').remove(); }); } list.querySelectorAll('.acc-remove-code').forEach(bindRemove); addBtn.addEventListener('click', function () { var row = document.createElement('div'); row.className = 'acc-code-entry'; row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:4px'; var inp = document.createElement('input'); inp.type = 'text'; inp.name = 'acc_course_codes[]'; inp.placeholder = phText; inp.style.cssText = 'font-family:monospace;text-transform:uppercase;width:160px'; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'acc-remove-code button button-small'; btn.style.cssText = 'color:#d63638;border-color:#d63638'; btn.textContent = ; bindRemove(btn); row.appendChild(inp); row.appendChild(btn); list.appendChild(row); inp.focus(); }); }());

  calculator->format( $display_year ) ); ?>

(function () { 'use strict'; var yearByPeriod = ; var periodSel = document.getElementById('acc_period'); var overrideCb = document.getElementById('acc_year_override'); var manualSec = document.getElementById('acc_year_manual_section'); var preview = document.getElementById('acc_year_preview'); function formatAY(y) { return 'A.Y. ' + y + '/' + String(y + 1).slice(-2); } function updatePreview() { if (overrideCb.checked) return; var p = parseInt(periodSel.value, 10); preview.textContent = formatAY(yearByPeriod[p] || yearByPeriod[1]); } periodSel.addEventListener('change', updatePreview); overrideCb.addEventListener('change', function () { manualSec.style.display = this.checked ? 'block' : 'none'; if (!this.checked) updatePreview(); }); }()); ID, '_acc_instructors', true ); $instructors = is_array( $raw_instructors ) ? $raw_instructors : []; $members = get_posts( [ 'post_type' => 'academiccrew_member', 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC', 'fields' => 'ids', ] ); $instructor_roles = [ 'teacher' => __( 'Teacher', 'academiccrew' ), 'teaching-assistant' => __( 'Teaching Assistant', 'academiccrew' ), 'coordinator' => __( 'Coordinator', 'academiccrew' ), ]; ?>

$entry ) : $m_id = (int) ( $entry['member_id'] ?? 0 ); $role = $entry['role'] ?? 'teacher'; ?>
ID, '_acc_past_instructors', true ); $past = is_array( $past ) ? $past : []; if ( $past ) : ?>

(function () { 'use strict'; var nextIdx = ; var list = document.getElementById('acc_staff_list'); var tpl = document.getElementById('acc_staff_row_tpl'); var addBtn = document.getElementById('acc_add_staff'); var emptyMsg = document.getElementById('acc_staff_empty'); function removeRow(btn) { btn.closest('.acc-staff-row').remove(); if (!list.querySelector('.acc-staff-row')) { if (emptyMsg) { list.appendChild(emptyMsg); } } } list.querySelectorAll('.acc-remove-staff').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-staff').addEventListener('click', function () { removeRow(this); }); list.appendChild(row); }); }()); ID, '_acc_abstract', true ) ?: ''; $outcomes_raw = get_post_meta( $post->ID, '_acc_learning_outcomes', true ); $outcomes = is_array( $outcomes_raw ) ? $outcomes_raw : []; $topics_raw = get_post_meta( $post->ID, '_acc_topics', true ); $topics = is_array( $topics_raw ) ? $topics_raw : []; $prereq_raw = get_post_meta( $post->ID, '_acc_prerequisites', true ); $prereq = is_array( $prereq_raw ) ? $prereq_raw : []; $assessment = get_post_meta( $post->ID, '_acc_assessment', true ) ?: ''; $schedule_raw = get_post_meta( $post->ID, '_acc_schedule', true ); $schedule = is_array( $schedule_raw ) ? $schedule_raw : []; $reading_raw = get_post_meta( $post->ID, '_acc_reading_list', true ); $reading = is_array( $reading_raw ) ? $reading_raw : []; $prereq_text = $prereq['text'] ?? ''; $prereq_tags_arr = $prereq['tags'] ?? []; $prereq_tags_str = is_array( $prereq_tags_arr ) ? implode( ', ', $prereq_tags_arr ) : ''; $completeness = (int) (bool) $abstract + (int) ( ! empty( $outcomes ) ) + (int) ( ! empty( $topics ) ) + (int) ( $prereq_text || $prereq_tags_str ) + (int) (bool) $assessment + (int) ( ! empty( $schedule ) ) + (int) ( ! empty( $reading ) ); $has_content = $completeness > 0; $h4 = 'style="margin:1.25rem 0 .4rem;font-size:13px;border-top:1px solid #eee;padding-top:1rem"'; $h4_first = 'style="margin:0 0 .4rem;font-size:13px"'; $day_opts = [ 'Mon' => __( 'Monday', 'academiccrew' ), 'Tue' => __( 'Tuesday', 'academiccrew' ), 'Wed' => __( 'Wednesday', 'academiccrew' ), 'Thu' => __( 'Thursday', 'academiccrew' ), 'Fri' => __( 'Friday', 'academiccrew' ), ]; $build_day_select = function ( string $name, string $selected_val ) use ( $day_opts ): string { $opts = ''; foreach ( $day_opts as $val => $label ) { $sel = selected( $selected_val, $val, false ); $opts .= "'; } return ""; }; ?>

✓   ·   : /7 — ✓

style="margin-top:4px"> —

>

>

>

$t ) : ?>

>

>

>

$s ) : ?>
–

>

$r ) : ?>
ID, '_acc_syllabus_url', true ); $moodle = get_post_meta( $post->ID, '_acc_moodle_url', true ); ?>
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 course is first published.', 'academiccrew' ) . '

'; $this->render_page_help( __( 'The course header (code, credits, teaching staff, links) is built automatically from the fields on this screen.', 'academiccrew' ), __( 'To add course content (slides, announcements, recordings, etc.), 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 course header (code, credits, teaching staff, links) is built automatically from the fields on this screen.', 'academiccrew' ), __( 'To add course content (slides, announcements, recordings, etc.), write it in the editor at the top of this screen — it is synced to the public page automatically on every save.', 'academiccrew' ) ); ?>



[] ] ); ?>

post_type ) { return; } // Course codes — store array; keep _acc_course_code = first entry for lookups. $raw_codes = array_map( 'wp_unslash', (array) ( $_POST['acc_course_codes'] ?? [] ) ); $codes = array_values( array_filter( array_map( fn( $c ) => strtoupper( sanitize_text_field( $c ) ), $raw_codes ) ) ); update_post_meta( $post_id, '_acc_course_codes', $codes ); update_post_meta( $post_id, '_acc_course_code', $codes[0] ?? '' ); // Credits — 1-30. $credits = max( 1, min( 30, (int) ( $_POST['acc_credits'] ?? 6 ) ) ); update_post_meta( $post_id, '_acc_credits', $credits ); // Lecture hours — 0-500. $lh = max( 0, min( 500, (int) ( $_POST['acc_lecture_hours'] ?? 0 ) ) ); update_post_meta( $post_id, '_acc_lecture_hours', $lh ?: '' ); // Degree programme. update_post_meta( $post_id, '_acc_degree_program', sanitize_text_field( wp_unslash( $_POST['acc_degree_program'] ?? '' ) ) ); // Language — IT or EN only. sanitize_key would lowercase, so use sanitize_text_field. $lang_raw = sanitize_text_field( wp_unslash( $_POST['acc_language'] ?? '' ) ); $language = in_array( $lang_raw, [ 'IT', 'EN' ], true ) ? $lang_raw : 'IT'; update_post_meta( $post_id, '_acc_language', $language ); // Period — within the currently configured cadence's range. $posted_period = (int) ( $_POST['acc_period'] ?? 1 ); $period = ( $posted_period >= 1 && $posted_period <= PeriodCadence::period_count() ) ? $posted_period : 1; update_post_meta( $post_id, '_acc_period', $period ); // Archived flag — freezes the academic year display. $archived = ! empty( $_POST['acc_archived'] ); update_post_meta( $post_id, '_acc_archived', $archived ? '1' : '' ); // Academic year — calculated or manually overridden. $year_override = ! empty( $_POST['acc_year_override'] ); update_post_meta( $post_id, '_acc_year_override', $year_override ? '1' : '' ); if ( $year_override && ! empty( $_POST['acc_academic_year_manual'] ) ) { $acad_year = (int) $_POST['acc_academic_year_manual']; } else { $acad_year = $this->calculator->calculate( $period ); } update_post_meta( $post_id, '_acc_academic_year', $acad_year ); // Instructors — validated [{member_id, role}] array; rows with no member // but a name are external instructors [{member_id:0, name, url, role}]. $valid_roles = [ 'teacher', 'teaching-assistant', 'coordinator' ]; $instructors = []; foreach ( (array) ( $_POST['acc_instructors'] ?? [] ) as $entry ) { $mid = (int) ( $entry['member_id'] ?? 0 ); $role = sanitize_key( $entry['role'] ?? 'teacher' ); if ( ! in_array( $role, $valid_roles, true ) ) { continue; } $name = sanitize_text_field( wp_unslash( $entry['name'] ?? '' ) ); if ( $mid > 0 && 'academiccrew_member' === get_post_type( $mid ) ) { $instructors[] = [ 'member_id' => $mid, 'role' => $role ]; } elseif ( $name !== '' ) { $instructors[] = [ 'member_id' => 0, 'name' => $name, 'url' => esc_url_raw( wp_unslash( $entry['url'] ?? '' ) ), 'role' => $role, ]; } } update_post_meta( $post_id, '_acc_instructors', $instructors ); // Course level. $level = sanitize_key( wp_unslash( $_POST['acc_level'] ?? '' ) ); $allowed_levels = [ 'bsc', 'msc', 'phd', 'professional', 'other', '' ]; update_post_meta( $post_id, '_acc_level', in_array( $level, $allowed_levels, true ) ? $level : '' ); // Featured flag + order (1–998 = explicit; 0/blank → 999 default). update_post_meta( $post_id, '_acc_featured', isset( $_POST['acc_featured'] ) ? '1' : '' ); $fo = (int) ( $_POST['acc_featured_order'] ?? 0 ); update_post_meta( $post_id, '_acc_featured_order', (string) ( $fo >= 1 && $fo <= 999 ? $fo : 999 ) ); // Resource URLs. update_post_meta( $post_id, '_acc_syllabus_url', esc_url_raw( wp_unslash( $_POST['acc_syllabus_url'] ?? '' ) ) ); update_post_meta( $post_id, '_acc_moodle_url', esc_url_raw( wp_unslash( $_POST['acc_moodle_url'] ?? '' ) ) ); // Optional content fields. update_post_meta( $post_id, '_acc_abstract', sanitize_textarea_field( wp_unslash( $_POST['acc_abstract'] ?? '' ) ) ); update_post_meta( $post_id, '_acc_assessment', sanitize_textarea_field( wp_unslash( $_POST['acc_assessment'] ?? '' ) ) ); $raw_outcomes = sanitize_textarea_field( wp_unslash( $_POST['acc_learning_outcomes'] ?? '' ) ); $outcomes = array_values( array_filter( array_map( 'trim', explode( "\n", str_replace( "\r", '', $raw_outcomes ) ) ) ) ); update_post_meta( $post_id, '_acc_learning_outcomes', $outcomes ); $raw_topics = (array) ( $_POST['acc_topics'] ?? [] ); $topics = []; foreach ( $raw_topics as $entry ) { $num = sanitize_text_field( wp_unslash( $entry['num'] ?? '' ) ); $name = sanitize_text_field( wp_unslash( $entry['name'] ?? '' ) ); if ( $name !== '' ) { $topics[] = [ 'num' => $num, 'name' => $name ]; } } update_post_meta( $post_id, '_acc_topics', $topics ); $prereq_text = sanitize_textarea_field( wp_unslash( $_POST['acc_prerequisites_text'] ?? '' ) ); $raw_tags = sanitize_text_field( wp_unslash( $_POST['acc_prerequisites_tags'] ?? '' ) ); $prereq_tags = array_values( array_filter( array_map( 'trim', explode( ',', $raw_tags ) ) ) ); update_post_meta( $post_id, '_acc_prerequisites', [ 'text' => $prereq_text, 'tags' => $prereq_tags, ] ); $raw_schedule = (array) ( $_POST['acc_schedule'] ?? [] ); $schedule = []; $allowed_days = [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri' ]; foreach ( $raw_schedule as $entry ) { $day = sanitize_text_field( wp_unslash( $entry['day'] ?? 'Mon' ) ); $start = sanitize_text_field( wp_unslash( $entry['start_time'] ?? '' ) ); $end = sanitize_text_field( wp_unslash( $entry['end_time'] ?? '' ) ); $room = sanitize_text_field( wp_unslash( $entry['room'] ?? '' ) ); if ( in_array( $day, $allowed_days, true ) && ( $start !== '' || $room !== '' ) ) { $schedule[] = [ 'day' => $day, 'start_time' => $start, 'end_time' => $end, 'room' => $room, ]; } } update_post_meta( $post_id, '_acc_schedule', $schedule ); $raw_reading = (array) ( $_POST['acc_reading_list'] ?? [] ); $reading = []; foreach ( $raw_reading as $entry ) { $title = sanitize_text_field( wp_unslash( $entry['title'] ?? '' ) ); $authors = sanitize_text_field( wp_unslash( $entry['authors'] ?? '' ) ); $year = sanitize_text_field( wp_unslash( $entry['year'] ?? '' ) ); if ( $title !== '' ) { $reading[] = [ 'title' => $title, 'authors' => $authors ?: null, 'year' => $year ?: null, ]; } } update_post_meta( $post_id, '_acc_reading_list', $reading ); } // ------------------------------------------------------------------------- // Admin list columns // ------------------------------------------------------------------------- public function register_columns( array $columns ): array { return [ 'cb' => $columns['cb'], 'title' => __( 'Course Name', 'academiccrew' ), 'acc_code' => __( 'Code', 'academiccrew' ), 'acc_level' => __( 'Level', 'academiccrew' ), 'acc_featured' => __( 'Featured', 'academiccrew' ), 'acc_credits' => __( 'CFU', 'academiccrew' ), 'acc_period' => __( 'Period', 'academiccrew' ), 'acc_acad_year' => __( 'A.Y.', 'academiccrew' ), 'acc_language' => __( 'Lang.', 'academiccrew' ), 'acc_teachers' => __( 'Teachers', 'academiccrew' ), 'acc_page' => __( 'Profile Page', 'academiccrew' ), 'date' => $columns['date'], ]; } public function render_column( string $column, int $post_id ): void { switch ( $column ) { case 'acc_level': $level_labels = [ 'bsc' => 'BSc', 'msc' => 'MSc', 'phd' => 'PhD', 'professional' => __( 'Professional', 'academiccrew' ), 'other' => __( 'Other', 'academiccrew' ), ]; $lv = get_post_meta( $post_id, '_acc_level', true ); echo esc_html( $lv ? ( $level_labels[ $lv ] ?? ucfirst( $lv ) ) : '—' ); break; case 'acc_featured': echo get_post_meta( $post_id, '_acc_featured', true ) ? '★' : '—'; break; case 'acc_code': $all_codes = (array) ( get_post_meta( $post_id, '_acc_course_codes', true ) ?: [] ); if ( empty( $all_codes ) ) { $primary = get_post_meta( $post_id, '_acc_course_code', true ); if ( $primary ) { $all_codes = [ $primary ]; } } if ( $all_codes ) { $parts = array_map( fn( $c ) => '' . esc_html( $c ) . '', $all_codes ); echo implode( ' ', $parts ); // phpcs:ignore WordPress.Security.EscapeOutput } break; case 'acc_credits': echo esc_html( get_post_meta( $post_id, '_acc_credits', true ) ?: '—' ); break; case 'acc_period': $p = get_post_meta( $post_id, '_acc_period', true ); echo $p ? esc_html( 'P' . $p ) : '—'; break; case 'acc_acad_year': $p_col = (int) ( get_post_meta( $post_id, '_acc_period', true ) ?: 1 ); $year_override = (bool) get_post_meta( $post_id, '_acc_year_override', true ); $col_archived = (bool) get_post_meta( $post_id, '_acc_archived', true ); $y = ( $year_override || $col_archived ) ? (int) get_post_meta( $post_id, '_acc_academic_year', true ) : $this->calculator->calculate( $p_col ); echo esc_html( $this->calculator->format( $y ) ); if ( $col_archived ) { echo ' ' . esc_html__( 'archived', 'academiccrew' ) . ''; } elseif ( $year_override ) { echo ' ' . esc_html__( '(manual)', 'academiccrew' ) . ''; } break; case 'acc_language': $lang = get_post_meta( $post_id, '_acc_language', true ); echo $lang ? esc_html( $lang ) : '—'; break; case 'acc_teachers': $instructors = (array) ( get_post_meta( $post_id, '_acc_instructors', true ) ?: [] ); $names = []; foreach ( $instructors as $e ) { if ( 'teacher' === ( $e['role'] ?? '' ) ) { $name = (int) ( $e['member_id'] ?? 0 ) ? get_the_title( (int) $e['member_id'] ) : (string) ( $e['name'] ?? '' ); if ( $name ) { $names[] = $name; } } } echo esc_html( $names ? implode( ', ', $names ) : '—' ); break; case 'acc_page': $page_id = (int) get_post_meta( $post_id, '_academiccrew_linked_page_id', true ); if ( $page_id && get_post( $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; } } public function sortable_columns( array $columns ): array { $columns['acc_code'] = 'acc_code'; $columns['acc_acad_year'] = 'acc_acad_year'; $columns['acc_period'] = 'acc_period'; return $columns; } }