setup_hooks();
}
return self::$instance;
}
/**
* Constructor is private to enforce singleton.
*/
private function __construct() {
// Intentionally left blank.
}
/**
* Setup WordPress hooks
*/
private function setup_hooks() {
add_action( ‘init’, array( $this, ‘register_post_type’ ) );
add_action( ‘init’, array( $this, ‘register_shortcodes’ ) );
// Form submission handlers (logged-in only): use admin_post action.
add_action( ‘admin_post_lvad_entry_submit’, array( $this, ‘handle_entry_submission’ ) );
// Enqueue assets for front-end chart and form.
add_action( ‘wp_enqueue_scripts’, array( $this, ‘enqueue_assets’ ) );
}
/**
* Register the Log Entry custom post type.
*
* Using a non-public CPT but with show_ui so it’s manageable in WP admin.
*/
public static function register_post_type() {
$labels = array(
‘name’ => __( ‘Log Entries’, ‘lvad-health-monitor’ ),
‘singular_name’ => __( ‘Log Entry’, ‘lvad-health-monitor’ ),
‘menu_name’ => __( ‘LVAD Log Entries’, ‘lvad-health-monitor’ ),
‘name_admin_bar’ => __( ‘Log Entry’, ‘lvad-health-monitor’ ),
‘add_new’ => __( ‘Add New’, ‘lvad-health-monitor’ ),
‘add_new_item’ => __( ‘Add New Log Entry’, ‘lvad-health-monitor’ ),
‘new_item’ => __( ‘New Log Entry’, ‘lvad-health-monitor’ ),
‘edit_item’ => __( ‘Edit Log Entry’, ‘lvad-health-monitor’ ),
‘view_item’ => __( ‘View Log Entry’, ‘lvad-health-monitor’ ),
‘all_items’ => __( ‘All Log Entries’, ‘lvad-health-monitor’ ),
‘search_items’ => __( ‘Search Log Entries’, ‘lvad-health-monitor’ ),
‘not_found’ => __( ‘No log entries found.’, ‘lvad-health-monitor’ ),
‘not_found_in_trash’ => __( ‘No log entries found in Trash.’, ‘lvad-health-monitor’ ),
);
$args = array(
‘labels’ => $labels,
‘public’ => false, // private to site (not queryable by public)
‘show_ui’ => true, // show in admin
‘show_in_menu’ => true,
‘menu_position’ => 26,
‘menu_icon’ => ‘dashicons-heart’,
‘supports’ => array( ‘title’, ‘editor’, ‘author’ ),
‘has_archive’ => false,
‘rewrite’ => false,
‘capability_type’ => ‘post’,
);
register_post_type( ‘log_entry’, $args );
}
/**
* Register shortcodes.
*/
public function register_shortcodes() {
add_shortcode( ‘lvad_entry_form’, array( $this, ‘render_entry_form_shortcode’ ) );
add_shortcode( ‘lvad_dashboard’, array( $this, ‘render_dashboard_shortcode’ ) );
}
/**
* Enqueue frontend assets including Chart.js from CDN and a small inline initializer.
*/
public function enqueue_assets() {
// Only enqueue Chart.js when necessary.
// We’ll always register so it’s available when needed by the shortcode.
wp_register_script(
‘lvad-chartjs’,
‘https://cdn.jsdelivr.net/npm/chart.js’,
array(),
‘4.4.1’, // It’s good practice to add a version number
true
);
// Optional small stylesheet for table/chart spacing
wp_register_style(
‘lvad-styles’,
false,
array(),
‘1.0.0’ // Example version
);
$custom_css = ‘
.lvad-entry-form p { margin-bottom: 1rem; }
.lvad-entry-form label { font-weight: bold; display: block; margin-bottom: 0.25rem; }
.lvad-entry-form input, .lvad-entry-form textarea { width: 100%; max-width: 400px; padding: 8px; }
.lvad-table { width:100%; border-collapse: collapse; margin-bottom: 1rem; }
.lvad-table th, .lvad-table td { border:1px solid #ddd; padding: 8px; text-align:left; font-size:14px; }
.lvad-table thead th { background-color: #f7f7f7; }
.lvad-chart-wrap { max-width:900px; margin: 2rem auto; }
‘;
wp_add_inline_style( ‘lvad-styles’, $custom_css );
wp_enqueue_style( ‘lvad-styles’ );
}
/* ———————- Entry Form ———————- */
/**
* Render the entry form shortcode output.
*
* Only visible to logged-in users. The actual form posts to admin-post.php and action ‘lvad_entry_submit’.
*
* @return string HTML
*/
public function render_entry_form_shortcode() {
if ( ! is_user_logged_in() ) {
return ‘
Please log in to enter your data.
‘;
}
// For UX: preserve previous values after redirect (if provided)
$old = array();
foreach ( array_merge( $this->meta_keys, array( ‘notes’ ) ) as $k ) {
$old[ $k ] = isset( $_REQUEST[ $k ] ) ? esc_attr( wp_unslash( $_REQUEST[ $k ] ) ) : ”;
}
$action_url = esc_url( admin_url( ‘admin-post.php’ ) );
ob_start();
?>
Weight (kg)
Pump Flow (L/min)
Power (W)
Pulsatility Index (PI)
Heart Rate (bpm)
SpO2 (%)
INR
Warfarin Dose (mg)
Notes
wp_strip_all_tags( $post_title ),
‘post_content’ => $notes,
‘post_status’ => ‘private’,
‘post_type’ => ‘log_entry’,
‘post_author’ => $current_user->ID,
);
$post_id = wp_insert_post( $post_data, true );
if ( is_wp_error( $post_id ) ) {
// Something went wrong; redirect back with failure.
$redirect = wp_get_referer() ? wp_get_referer() : home_url();
wp_safe_redirect( add_query_arg( ‘lvad_status’, ‘error’, $redirect ) );
exit;
}
// Save each meta field after sanitizing.
foreach ( $this->meta_keys as $key ) {
if ( isset( $_POST[ $key ] ) && ” !== $_POST[ $key ] ) {
// Sanitize as a floating point number.
$sanitized = floatval( wp_unslash( $_POST[ $key ] ) );
update_post_meta( $post_id, $key, $sanitized );
} else {
// If field is omitted or blank, ensure no old value persists.
delete_post_meta( $post_id, $key );
}
}
// Successful submission — redirect back with success param.
$redirect = wp_get_referer() ? wp_get_referer() : home_url();
$redirect = add_query_arg( ‘lvad_status’, ‘success’, $redirect );
wp_safe_redirect( $redirect );
exit;
}
/* ———————- Dashboard ———————- */
/**
* Render the per-user dashboard with recent entries and Chart.js visualization.
*
* Only the logged-in user who owns the data can see their entries.
*
* @return string HTML
*/
public function render_dashboard_shortcode() {
if ( ! is_user_logged_in() ) {
return ‘
Please log in to view your dashboard.
‘;
}
$current_user = wp_get_current_user();
$user_id = $current_user->ID;
// Query the 10 most recent entries by this user
$args = array(
‘post_type’ => ‘log_entry’,
‘posts_per_page’ => 10,
‘post_status’ => ‘private’, // as created earlier
‘author’ => $user_id,
‘orderby’ => ‘date’,
‘order’ => ‘DESC’,
);
$query = new WP_Query( $args );
ob_start();
if ( ! $query->have_posts() ) {
echo ‘
No log entries found. Use the form to add your first entry.
‘;
wp_reset_postdata();
return ob_get_clean();
}
// Build table header and rows
echo ‘
‘;
echo ‘‘;
echo ‘Date | ‘;
echo ‘MAP | ‘;
echo ‘Weight | ‘;
echo ‘Pump Flow | ‘;
echo ‘Power | ‘;
echo ‘PI | ‘;
echo ‘HR | ‘;
echo ‘SpO2 | ‘;
echo ‘INR | ‘;
echo ‘Warfarin Dose | ‘;
echo ‘Notes | ‘;
echo ‘
‘;
echo ‘‘;
$labels = array(); // x-axis labels (dates)
$data_weight = array(); // weight series
$data_map = array(); // map series
$data_pump_flow = array(); // pump flow series
while ( $query->have_posts() ) {
$query->the_post();
$post_id = get_the_ID();
$post_date = get_the_date( ‘Y-m-d H:i’, $post_id );
echo ‘‘;
echo ‘‘ . esc_html( $post_date ) . ‘ | ‘;
// Fetch meta values
$meta = array();
foreach ( $this->meta_keys as $key ) {
$meta_value = get_post_meta( $post_id, $key, true );
// Use ‘—’ for empty values for consistent display.
$meta[ $key ] = ( ” !== $meta_value ) ? $meta_value : ‘—’;
}
echo ‘‘ . esc_html( $meta[‘map’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘weight’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘pump_flow’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘power’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘pi’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘hr’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘spo2’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘inr’] ) . ‘ | ‘;
echo ‘‘ . esc_html( $meta[‘warfarin_dose’] ) . ‘ | ‘;
echo ‘‘ . wp_kses_post( wp_trim_words( get_the_content(), 15, ‘…’ ) ) . ‘ | ‘;
echo ‘
‘;
// Prepare chart arrays (keep same order as table: newest -> oldest)
$labels[] = get_the_date( ‘M j’, $post_id ); // Use a shorter date format for the chart
// For JS, use null for empty values so Chart.js can handle gaps.
$data_weight[] = is_numeric( $meta[‘weight’] ) ? floatval( $meta[‘weight’] ) : null;
$data_map[] = is_numeric( $meta[‘map’] ) ? floatval( $meta[‘map’] ) : null;
$data_pump_flow[] = is_numeric( $meta[‘pump_flow’] ) ? floatval( $meta[‘pump_flow’] ) : null;
}
echo ‘‘;
echo ‘
‘;
wp_reset_postdata();
// Chart container
echo ‘
‘;
echo ‘‘;
echo ‘
‘;
// Prepare JS data for chart in chronological order (oldest -> newest).
$labels_js = array_reverse( $labels );
$weight_js = array_reverse( $data_weight );
$map_js = array_reverse( $data_map );
$pump_js = array_reverse( $data_pump_flow );
// Enqueue Chart.js and add inline init script using a localized object.
wp_enqueue_script( ‘lvad-chartjs’ );
$chart_data = array(
‘labels’ => $labels_js,
‘datasets’ => array(
‘weight’ => array(
‘label’ => ‘Weight (kg)’,
‘data’ => $weight_js,
),
‘map’ => array(
‘label’ => ‘MAP’,
‘data’ => $map_js,
),
‘pump’ => array(
‘label’ => ‘Pump Flow (L/min)’,
‘data’ => $pump_js,
),
),
);
// Localize the data into a JS var LVAD_CHART_DATA attached to lvad-chartjs
wp_localize_script( ‘lvad-chartjs’, ‘LVAD_CHART_DATA’, $chart_data );
// Add inline script that initializes the Chart.
$inline_js = <<<'JS'
(function(){
// Wait until DOM and Chart are ready
document.addEventListener('DOMContentLoaded', function(){
if ( typeof Chart === 'undefined' || typeof LVAD_CHART_DATA === 'undefined' ) {
console.error('Chart.js or chart data not available.');
return;
}
const ctx = document.getElementById('lvadChart');
if (!ctx) return;
// Helper to check if a dataset has at least one numeric value.
function hasNumericData(dataArray) {
if (!Array.isArray(dataArray)) return false;
return dataArray.some(val => val !== null && !isNaN(val));
}
const datasets = [];
const chartConfig = LVAD_CHART_DATA.datasets;
// Dynamically build datasets array, only including those with data.
if (chartConfig.weight && hasNumericData(chartConfig.weight.data)) {
datasets.push({
label: chartConfig.weight.label,
data: chartConfig.weight.data,
borderColor: ‘rgb(75, 192, 192)’,
tension: 0.2,
yAxisID: ‘yWeight’,
});
}
if (chartConfig.map && hasNumericData(chartConfig.map.data)) {
datasets.push({
label: chartConfig.map.label,
data: chartConfig.map.data,
borderColor: ‘rgb(255, 99, 132)’,
tension: 0.2,
yAxisID: ‘yMap’,
});
}
if (chartConfig.pump && hasNumericData(chartConfig.pump.data)) {
datasets.push({
label: chartConfig.pump.label,
data: chartConfig.pump.data,
borderColor: ‘rgb(54, 162, 235)’,
tension: 0.2,
yAxisID: ‘yPump’,
});
}
if (datasets.length === 0) {
// Don’t render an empty chart
return;
}
new Chart(ctx, {
type: ‘line’,
data: {
labels: LVAD_CHART_DATA.labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
yWeight: {
type: ‘linear’,
display: true,
position: ‘left’,
title: {
display: true,
text: ‘Weight (kg)’
}
},
yMap: {
type: ‘linear’,
display: true,
position: ‘right’,
title: {
display: true,
text: ‘MAP’
},
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show
},
},
yPump: {
// This axis won’t be displayed, but it’s used to associate the dataset
display: false
}
}
}
});
});
})();
JS;
wp_add_inline_script( ‘lvad-chartjs’, $inline_js );
return ob_get_clean();
}
}
// Initialize the class.
LVAD_Health_Monitor::get_instance();
LVAD Clinical Decision Support — Scored Triage