Developer Guide
1. Custom Post Type for Notifications
phpCopyEditadd_action( 'init', function() {
register_post_type( 'ams_notification', [ /* … */ ] );
});
- Purpose: Stores per-user, in-dashboard messages (e.g. “Your payment is due”).
- Slug:
ams_notification
(must be ≤20 chars). - Visibility: Hidden from public site, visible in WP-Admin UI only.
Dependencies: None (core WP).
2. Progress-Step Definitions
phpCopyEdit$applicant_steps = [ 'submitted'=>'Submitted', /*…*/ ];
$staff_steps = [ 'received'=>'Received', /*…*/ ];
- Two arrays define the ordered steps for Applicants vs. Staff.
- Keys like
offer_made
orawaiting_payment
power the CSS classes. - Labels (“Offer Made”, “Awaiting Payment”) appear in the UI.
3. Mapping Gravity Flow Status → Slug
phpCopyEditfunction ams_get_application_progress_slug( $entry_id ) {
if ( ! function_exists('gravity_flow') || ! method_exists(...)) {
return 'submitted';
}
$status_name = gravity_flow()->get_workflow_status_name( $entry_id );
static $map = [ 'Offer Made'=>'offer_made', /*…*/ ];
return $map[ $status_name ] ?? 'submitted';
}
- What it does:
- Guards in case Gravity Flow isn’t active.
- Fetches the human-readable status (“Offer Made”, etc.).
- Translates that back into our internal slug (for CSS/state tracking).
Dependencies: Gravity Flow plugin.
4. [ams_dashboard]
Shortcode
phpCopyEditadd_shortcode( 'ams_dashboard', 'ams_dashboard_shortcode' );
function ams_dashboard_shortcode() { … }
- Entry point for your front-end dashboard.
- Checks
is_user_logged_in()
, then reads user’s role (staff
,applicant
, orstudent
). - Builds a role-specific sidebar menu (
<ul>
of tabs). - Calculates unread notification count by querying
ams_notification
. - Renders three main regions:
- Sidebar (tabs with badge)
- Progress panel (vertical list of steps, marking “done” vs. “current”)
- Content pane (calls a tab-specific function)
Dependencies:
- Core WP (users, shortcodes)
- Gravity Flow (for progress)
- WooCommerce (if you display orders in “Payments”)
5. Tab Callback Functions
Each ams_tab_*()
function returns HTML for that tab:
ams_tab_profile()
: showsdisplay_name
+user_email
.ams_tab_applications()
: queriesapplications
CPT, shows status viagravity_flow()->get_workflow_status_name()
.ams_tab_reviews()
: for staff, callsgravity_flow()->get_user_assignments()
.ams_tab_offers()
: listsoffer_letter
CPT posts.ams_tab_acceptances()
: listsoffer_acceptance
CPT posts.ams_tab_payments()
: useswc_get_orders()
to list this user’s WooCommerce orders.ams_tab_notifications()
: listsams_notification
posts, marks them “read”.
Dependencies:
- Gravity Flow (assignments, status names)
- Gravity Forms (entries created in CPT)
- WooCommerce (orders)
6. Gravity Forms → CPT Creation Hooks
phpCopyEditadd_action( 'gform_after_submission_1', 'sapm_save_application_entry', 10, 2 );
add_action( 'gform_after_submission_2', 'sapm_save_offer_acceptance_entry', 10, 2 );
- Form 1 → insert into
applications
CPT - Form 2 → insert into
offer_acceptance
CPT - Loop over
$entry
array, saving each numeric field asgf_field_{ID}
post meta - Store
gf_form_id
andgf_entry_id
for reference - Sideload profile photo URL into WP Media Library and set as featured image
Dependencies: Gravity Forms core, WP Media API.
7. Image Sideload Helper
phpCopyEditfunction sapm_sideload_image_as_attachment( $url, $post_id ) { … }
- Downloads a publicly-accessible URL into a temp file.
- Calls
media_handle_sideload()
to register it in the Media Library. - Returns the attachment ID.
Dependencies:
- WP Filesystem / Media / Image includes (loaded via
require_once
).
8. Inline CSS
- Injected via
<style>
in the shortcode output. - Defines flex layout, sidebar styling, progress-dot colors, responsive breakpoints.
Dependencies: None, but you can move to an external CSS file and enqueue it if you prefer.
Summary of Third-Party Dependencies
- Gravity Forms (core) – for form submissions and entry data.
- Gravity Flow – for workflow steps, statuses, assignments.
- WooCommerce – for listing orders in the “Payments” tab.
- Elementor – only to render the shortcode widget on your page.
- Dompdf (if using PDF generation elsewhere in this project).
With these notes, any new developer can see the “big picture”:
- User roles determine which tabs they see.
- Forms auto-generate CPT entries, which the dashboard queries.
- Workflow status drives the progress bar.
- Notifications are custom CPT records surfaced with a red badge.
All functionality lives within your theme’s functions.php
(or could be refactored into a dedicated plugin), and everything’s hooked into WordPress at the correct action points.
Code
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 1) Register Notification CPT under a shorter slug.
*/
add_action( 'init', function() {
register_post_type( 'ams_notification', [
'labels' => [
'name' => 'Dashboard Notifications',
'singular_name' => 'Notification',
],
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'supports' => [ 'title','editor','custom-fields' ],
] );
});
/**
* 2) Define Workflow Progress Steps and Mapping.
*/
$applicant_steps = [
'submitted' => 'Submitted',
'received' => 'Acknowledged',
'offer_made' => 'Offer Made',
'offer_accepted' => 'Offer Accepted',
'awaiting_payment' => 'Awaiting Payment',
'payment_received' => 'Payment Received',
'registration_opened' => 'Registration Opened',
];
$staff_steps = [
'received' => 'Received',
'ack_sent' => 'Acknowledged',
'reviewing' => 'Reviewing',
'creating_offer' => 'Creating Offer',
'offer_made' => 'Offer Made',
'awaiting_acceptance' => 'Awaiting Acceptance',
'offer_accepted' => 'Offer Accepted',
'payment_initiated' => 'Payment Initiated',
'awaiting_payment' => 'Awaiting Payment',
'payment_received' => 'Payment Received',
'registration_opened' => 'Registration Opened',
];
function ams_map_gf_status_to_slug( $status ) {
static $map = [
'submitted' => 'submitted',
'awaiting_author' => 'received',
'under_review' => 'reviewing',
'offer_made' => 'offer_made',
'awaiting_acceptance' => 'awaiting_acceptance',
'offer_accepted' => 'offer_accepted',
'payment_initiated' => 'payment_initiated',
'payment_received' => 'payment_received',
'complete' => 'registration_opened',
];
return $map[ $status ] ?? '';
}
/**
* Get current progress slug for a given entry ID.
* If Gravity Flow or the method is unavailable, default to 'submitted'.
*/
function ams_get_application_progress_slug( $entry_id ) {
// Bail to 'submitted' if GF Flow isn't available
if ( ! function_exists( 'gravity_flow' ) || ! method_exists( gravity_flow(), 'get_workflow_status_name' ) ) {
return 'submitted';
}
// Get the human status name (e.g. "Under Review", "Offer Made", etc.)
$status_name = gravity_flow()->get_workflow_status_name( $entry_id );
// Map the human name back to our slug keys
static $name_to_slug = [
'Submitted' => 'submitted',
'Acknowledged' => 'received',
'Reviewing' => 'reviewing',
'Creating Offer' => 'creating_offer',
'Offer Made' => 'offer_made',
'Awaiting Acceptance' => 'awaiting_acceptance',
'Offer Accepted' => 'offer_accepted',
'Payment Initiated' => 'payment_initiated',
'Awaiting Payment' => 'awaiting_payment',
'Payment Received' => 'payment_received',
'Complete' => 'registration_opened',
'Registration Opened' => 'registration_opened',
];
return $name_to_slug[ $status_name ] ?? 'submitted';
}
/**
* 3) Dashboard Shortcode
*/
add_shortcode( 'ams_dashboard', 'ams_dashboard_shortcode' );
function ams_dashboard_shortcode() {
if ( ! is_user_logged_in() ) {
return '<p>Please <a href="' . wp_login_url() . '">log in</a> to view your dashboard.</p>';
}
global $applicant_steps, $staff_steps;
$user = wp_get_current_user();
$roles = (array) $user->roles;
$is_staff = in_array( 'staff', $roles, true );
$is_applicant= in_array( 'applicant', $roles, true ) || in_array( 'student', $roles, true );
// Tabs
$tabs = $is_staff
? [ 'applications'=>'Applications', 'reviews'=>'Under Review', 'offers'=>'Offer Letters', 'acceptances'=>'Offer Acceptances', 'payments'=>'Payments', 'notifications'=>'Notifications' ]
: [ 'profile'=>'Profile', 'applications'=>'My Applications', 'offers'=>'My Offers', 'acceptances'=>'My Acceptances', 'payments'=>'Payments', 'notifications'=>'Notifications' ];
// Active
$tab = isset( $_GET['tab'] ) && isset( $tabs[ $_GET['tab'] ] )
? $_GET['tab']
: array_key_first( $tabs );
// Unread count
$unread = get_posts([
'post_type' => 'ams_notification',
'meta_key' => '_notification_user_id',
'meta_value' => $user->ID,
'meta_query' => [ ['key'=>'_notification_read','value'=>'0'] ],
'fields' => 'ids',
'posts_per_page' => -1,
]);
$unread_count = count( $unread );
$notif_label = 'Notifications' . ( $unread_count ? " <span class='ams-notif-count'>{$unread_count}</span>" : '' );
// Render
ob_start(); ?>
<div class="ams-dashboard">
<div class="ams-dashboard-sidebar"><ul>
<?php foreach ( $tabs as $key => $label ) :
$active = ( $key === $tab ) ? 'active' : '';
$disp = $key === 'notifications' ? $notif_label : $label;
?>
<li class="<?php echo esc_attr($active); ?>">
<a href="?tab=<?php echo esc_attr($key); ?>"><?php echo wp_kses_post($disp); ?></a>
</li>
<?php endforeach; ?>
</ul></div>
<?php
$steps = $is_staff ? $staff_steps : $applicant_steps;
if ( $is_staff ) {
$assign = gravity_flow()->get_user_assignments( $user->ID );
$current_slug = ! empty( $assign ) ? ams_get_application_progress_slug( $assign[0]['id'] ) : '';
} else {
$apps = get_posts([ 'post_type'=>'applications','author'=>$user->ID,'numberposts'=>1,'orderby'=>'date' ]);
$current_slug = ! empty($apps) ? ams_get_application_progress_slug( get_post_meta($apps[0]->ID,'gf_entry_id',true) ) : '';
}
?>
<div class="ams-dashboard-progress">
<h4>My Progress</h4><ul>
<?php foreach ( $steps as $slug => $label ) :
$done = array_search($slug,array_keys($steps)) < array_search($current_slug,array_keys($steps));
$cls = $done ? 'done' : ($slug === $current_slug ? 'current' : '');
?>
<li class="<?php echo esc_attr($cls); ?>">
<span class="step-label"><?php echo esc_html($label); ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="ams-dashboard-content">
<?php
switch ( $tab ) {
case 'profile': echo ams_tab_profile( $user ); break;
case 'applications': echo ams_tab_applications( $user, $is_staff ); break;
case 'reviews': echo ams_tab_reviews(); break;
case 'offers': echo ams_tab_offers( $user, $is_staff ); break;
case 'acceptances': echo ams_tab_acceptances( $user, $is_staff ); break;
case 'payments': echo ams_tab_payments( $user ); break;
case 'notifications':echo ams_tab_notifications( $user ); break;
}
?>
</div>
</div>
<!-- Inline CSS -->
<style>
.ams-dashboard { display:flex; font-family:Arial,sans-serif; color:#333; min-height:600px; }
.ams-dashboard-sidebar { width:220px; background:#f5f5f5; border-right:1px solid #ddd; }
.ams-dashboard-sidebar ul{list-style:none;padding:0;margin:0;}
.ams-dashboard-sidebar li a{display:block;padding:12px 16px;color:#333;text-decoration:none;border-bottom:1px solid #ddd;transition:.2s;}
.ams-dashboard-sidebar li.active a,.ams-dashboard-sidebar li a:hover{background:#e2e6ea;color:#000;}
.ams-notif-count{background:#d9534f;color:#fff;padding:2px 6px;border-radius:12px;font-size:12px;vertical-align:middle;margin-left:6px;}
.ams-dashboard-progress{width:200px;padding:24px 16px;background:#fafafa;border-right:1px solid #ddd;}
.ams-dashboard-progress h4{margin:0 0 12px;font-size:16px;color:#555;}
.ams-dashboard-progress ul{list-style:none;padding:0;margin:0;}
.ams-dashboard-progress li{position:relative;padding-left:28px;margin-bottom:14px;font-size:14px;}
.ams-dashboard-progress li:before{content:'';width:14px;height:14px;border:2px solid #ccc;border-radius:50%;position:absolute;left:0;top:2px;background:#fff;transition:.3s;}
.ams-dashboard-progress li.done:before{background:#28a745;border-color:#28a745;}
.ams-dashboard-progress li.current:before{background:#007bff;border-color:#007bff;}
.ams-dashboard-progress .step-label{color:#333;}
.ams-dashboard-progress li.done .step-label{color:#28a745;}
.ams-dashboard-progress li.current .step-label{color:#007bff;font-weight:bold;}
.ams-dashboard-content{flex:1;padding:24px;background:#fff;}
.ams-dashboard-content h2{margin-top:0;font-size:22px;color:#333;}
.ams-dashboard-content ul{list-style:none;padding:0;margin:0;}
.ams-dashboard-content li{margin-bottom:10px;line-height:1.4;}
.ams-dashboard-content li a{color:#007bff;text-decoration:none;}
.ams-dashboard-content li a:hover{text-decoration:underline;}
.ams-dashboard-content li.unread{background:#ffecec;}
.ams-dashboard-content li.read{color:#777;}
@media(max-width:992px){
.ams-dashboard{flex-direction:column;}
.ams-dashboard-sidebar,.ams-dashboard-progress{width:100%;border-right:none;border-bottom:1px solid #ddd;}
.ams-dashboard-content{padding:16px;}
}
</style>
<?php
return ob_get_clean();
}
/**
* 4) Tab Callbacks
*/
function ams_tab_profile( $user ) {
return "<h2>Your Profile</h2>"
. "<p><strong>Name:</strong> " . esc_html($user->display_name) . "</p>"
. "<p><strong>Email:</strong> " . esc_html($user->user_email) . "</p>";
}
function ams_tab_applications( $user, $is_staff ) {
$args = [ 'post_type'=>'applications','posts_per_page'=>-1 ];
if ( ! $is_staff ) {
$args['author'] = $user->ID;
}
$apps = get_posts( $args );
if ( ! $apps ) {
return '<p>No applications found.</p>';
}
$out = '<h2>Applications</h2><ul>';
foreach ( $apps as $app ) {
$eid = get_post_meta( $app->ID, 'gf_entry_id', true );
$status = gravity_flow()->get_workflow_status_name( $eid );
$out .= "<li><a href='" . get_permalink($app) . "'>{$app->post_title}</a> — <em>$status</em></li>";
}
$out .= '</ul>';
return $out;
}
function ams_tab_reviews() {
$assign = gravity_flow()->get_user_assignments( get_current_user_id() );
if ( ! $assign ) {
return '<p>No items pending review.</p>';
}
$out = '<h2>Under Review</h2><ul>';
foreach ( $assign as $a ) {
$app_post = get_posts([
'post_type'=>'applications',
'meta_key'=>'gf_entry_id',
'meta_value'=> $a['id'],
'numberposts'=>1
])[0] ?? null;
if ( $app_post ) {
$out .= "<li><a href='" . get_permalink($app_post) . "'>{$app_post->post_title}</a> — <em>{$a['step_name']}</em></li>";
}
}
$out .= '</ul>';
return $out;
}
function ams_tab_offers( $user, $is_staff ) {
$args = [ 'post_type'=>'offer_letter','posts_per_page'=>-1 ];
if ( ! $is_staff ) {
$args['author'] = $user->ID;
}
$offers = get_posts( $args );
if ( ! $offers ) {
return '<p>No offers found.</p>';
}
$out = '<h2>Offer Letters</h2><ul>';
foreach ( $offers as $offer ) {
$out .= "<li><a href='" . get_permalink($offer) . "'>{$offer->post_title}</a></li>";
}
$out .= '</ul>';
return $out;
}
function ams_tab_acceptances( $user, $is_staff ) {
$args = [ 'post_type'=>'offer_acceptance','posts_per_page'=>-1 ];
if ( ! $is_staff ) {
$args['author'] = $user->ID;
}
$accs = get_posts( $args );
if ( ! $accs ) {
return '<p>No acceptances found.</p>';
}
$out = '<h2>Offer Acceptances</h2><ul>';
foreach ( $accs as $acc ) {
$out .= "<li><a href='" . get_permalink($acc) . "'>{$acc->post_title}</a></li>";
}
$out .= '</ul>';
return $out;
}
function ams_tab_payments( $user ) {
$orders = wc_get_orders([ 'customer' => $user->ID, 'status'=>['processing','completed'] ]);
if ( ! $orders ) {
return '<p>No payments found.</p>';
}
$out = '<h2>Payments</h2><ul>';
foreach ( $orders as $order ) {
$out .= "<li>Order #{$order->get_id()} — {$order->get_formatted_order_total()} (" . wc_get_order_status_name($order->get_status()) . ")</li>";
}
$out .= '</ul>';
return $out;
}
function ams_tab_notifications( $user ) {
$notes = get_posts([
'post_type' => 'ams_notification',
'meta_key' => '_notification_user_id',
'meta_value' => $user->ID,
'posts_per_page' => -1,
]);
if ( ! $notes ) {
return '<p>No notifications.</p>';
}
$out = '<h2>Notifications</h2><ul>';
foreach ( $notes as $n ) {
$read = get_post_meta( $n->ID, '_notification_read', true ) ? 'read' : 'unread';
$out .= "<li class='{$read}'>{$n->post_title} — <small>" . get_the_date( '', $n ) . "</small></li>";
update_post_meta( $n->ID, '_notification_read', 1 );
}
$out .= '</ul>';
return $out;
}
/**
* 5) After-Submission Hooks: Save GF entries into CPTs
*/
add_action( 'gform_after_submission_1', 'sapm_save_application_entry', 10, 2 );
function sapm_save_application_entry( $entry, $form ) {
$title = rgar($entry,'39').' – '.rgar($entry,'7').' '.rgar($entry,'8');
$post_id = wp_insert_post([ 'post_type'=>'applications','post_title'=>wp_strip_all_tags($title),'post_status'=>'publish' ]);
if ( $post_id ) {
foreach ( $entry as $fid => $val ) {
if ( is_numeric($fid) && $val !== '' ) {
update_post_meta( $post_id, 'gf_field_'.$fid, $val );
}
}
update_post_meta( $post_id, 'gf_form_id', $form['id'] );
update_post_meta( $post_id, 'gf_entry_id', $entry['id'] );
if ( ! empty($entry['50']) ) {
if ( $aid = sapm_sideload_image_as_attachment($entry['50'],$post_id) ) {
set_post_thumbnail( $post_id, $aid );
}
}
}
}
add_action( 'gform_after_submission_2', 'sapm_save_offer_acceptance_entry', 10, 2 );
function sapm_save_offer_acceptance_entry( $entry, $form ) {
$title = rgar($entry,'application_id').' – '.rgar($entry,'20');
$post_id = wp_insert_post([ 'post_type'=>'offer_acceptance','post_title'=>wp_strip_all_tags($title),'post_status'=>'publish' ]);
if ( $post_id ) {
foreach ( $entry as $fid => $val ) {
if ( is_numeric($fid) && $val !== '' ) {
if ( is_array($val) ) {
$val = implode(', ',$val);
}
update_post_meta( $post_id, 'gf_field_'.$fid, $val );
}
}
update_post_meta( $post_id, 'gf_form_id', $form['id'] );
update_post_meta( $post_id, 'gf_entry_id', $entry['id'] );
// Copy featured image from parent Application
$apps = get_posts([ 'post_type'=>'applications','meta_key'=>'gf_entry_id','meta_value'=>rgar($entry,'application_id'),'numberposts'=>1 ]);
if ( $apps ) {
$thumb = get_post_thumbnail_id( $apps[0]->ID );
if ( $thumb ) {
set_post_thumbnail( $post_id, $thumb );
}
}
}
}
/**
* 6) Helper: Sideload external image
*/
function sapm_sideload_image_as_attachment( $url, $post_id ) {
require_once ABSPATH.'wp-admin/includes/file.php';
require_once ABSPATH.'wp-admin/includes/media.php';
require_once ABSPATH.'wp-admin/includes/image.php';
$tmp = download_url($url);
if ( is_wp_error($tmp) ) {
return false;
}
$file = [ 'name'=>basename($url), 'tmp_name'=>$tmp, 'error'=>0, 'size'=>filesize($tmp) ];
$id = media_handle_sideload( $file, $post_id );
@unlink($tmp);
return is_wp_error($id) ? false : $id;
}