Implementing automated compliance reporting for custom hospital clinic appointments ledgers using native TCP printing streams
Architectural Overview: Bridging Clinic Data to Legacy Printers
This document details the implementation of an automated compliance reporting system for custom hospital clinic appointment ledgers. The core challenge lies in interfacing modern web-based data (managed via a WordPress plugin) with a ubiquitous, yet often overlooked, output mechanism: native TCP printing streams. This approach bypasses the need for complex printer drivers or intermediate spooling services, directly pushing formatted data to network-attached receipt printers, a common requirement in many clinical environments for immediate record-keeping and patient-facing summaries.
The system comprises three primary components:
- WordPress Plugin Backend: A custom PHP plugin within WordPress to manage appointment data, generate reports, and initiate the printing process.
- Data Formatting Layer: Logic within the plugin to transform structured appointment data into a printer-interpretable format, often a subset of ESC/POS commands or plain text with specific line breaks and character encoding.
- TCP Print Client: A PHP script or function that establishes a TCP socket connection to the printer’s IP address and port, sending the formatted data stream.
WordPress Plugin Development: Core Data Structures and Hooks
We’ll leverage WordPress’s robust plugin API. For this example, assume a custom post type `clinic_appointment` exists, with relevant meta fields for patient name, appointment time, doctor, and notes. The plugin will register a new admin page for report generation and a shortcode for on-demand printing from the front end.
The primary plugin file (e.g., `clinic-compliance-reporter.php`) will include:
Plugin Activation and Deactivation Hooks
Essential for setting up custom database tables (if needed, though we’ll primarily use post meta here) and flushing rewrite rules. For simplicity, we’ll rely on WordPress’s built-in post meta management.
<?php
/*
Plugin Name: Clinic Compliance Reporter
Description: Generates compliance reports and prints them to network printers.
Version: 1.0
Author: Antigravity
*/
// Ensure this file is called within the WordPress environment.
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Activation hook (optional for this example, but good practice)
register_activation_hook( __FILE__, 'ccr_activate' );
function ccr_activate() {
// Add any initial setup here, e.g., default options.
// For this example, we assume 'clinic_appointment' CPT is already registered.
}
// Deactivation hook (optional)
register_deactivation_hook( __FILE__, 'ccr_deactivate' );
function ccr_deactivate() {
// Clean up any transient options or custom tables if created.
}
?>
Admin Menu Registration
This creates a dedicated section in the WordPress admin dashboard for report generation.
add_action( 'admin_menu', 'ccr_admin_menu' );
function ccr_admin_menu() {
add_menu_page(
'Clinic Reports', // Page title
'Clinic Reports', // Menu title
'manage_options', // Capability required
'clinic-reports', // Menu slug
'ccr_render_report_page', // Callback function to display the page
'dashicons-chart-bar', // Icon URL or Dashicon class
80 // Position in the menu
);
}
function ccr_render_report_page() {
// Check user capabilities
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1>Generate Clinic Compliance Report</h1>
<form method="post" action="">
<!-- Nonce for security -->
<?php wp_nonce_field( 'ccr_generate_report_action', 'ccr_generate_report_nonce' ); ?>
<label for="report_date_start">Start Date:</label>
<input type="date" id="report_date_start" name="report_date_start" required><br><br>
<label for="report_date_end">End Date:</label>
<input type="date" id="report_date_end" name="report_date_end" required><br><br>
<label for="printer_ip">Printer IP Address:</label>
<input type="text" id="printer_ip" name="printer_ip" placeholder="e.g., 192.168.1.100" required><br><br>
<label for="printer_port">Printer Port:</label>
<input type="number" id="printer_port" name="printer_port" value="9100" required><br><br>
<input type="submit" name="ccr_generate_report" class="button button-primary" value="Generate and Print Report">
</form>
</div>
<?php
}
Report Generation and Printing Logic
This section handles the form submission, data retrieval, formatting, and the actual TCP printing. It’s crucial to validate all inputs and sanitize data before processing.
add_action( 'admin_init', 'ccr_handle_report_generation' );
function ccr_handle_report_generation() {
// Check if the form was submitted and nonce is valid
if ( isset( $_POST['ccr_generate_report'] ) && check_admin_referer( 'ccr_generate_report_action', 'ccr_generate_report_nonce' ) ) {
// Sanitize and validate inputs
$report_date_start = sanitize_text_field( $_POST['report_date_start'] );
$report_date_end = sanitize_text_field( $_POST['report_date_end'] );
$printer_ip = sanitize_text_field( $_POST['printer_ip'] );
$printer_port = intval( $_POST['printer_port'] );
// Basic IP validation (can be more robust)
if ( ! filter_var( $printer_ip, FILTER_VALIDATE_IP ) ) {
wp_die( 'Invalid Printer IP Address.' );
}
if ( $printer_port < 1 || $printer_port > 65535 ) {
wp_die( 'Invalid Printer Port Number.' );
}
// Fetch appointments within the date range
$appointments = ccr_get_appointments_for_report( $report_date_start, $report_date_end );
if ( empty( $appointments ) ) {
wp_die( 'No appointments found for the specified date range.' );
}
// Format the report
$report_content = ccr_format_report_for_printer( $appointments );
// Send to printer
if ( ! ccr_send_to_printer( $printer_ip, $printer_port, $report_content ) ) {
wp_die( 'Failed to send report to printer. Check IP, port, and printer status.' );
}
// Success message
add_action( 'admin_notices', function() {
echo '<div class="notice notice-success is-dismissible"><p>Report generated and sent to printer successfully!</p></div>';
});
}
}
function ccr_get_appointments_for_report( $start_date, $end_date ) {
$args = array(
'post_type' => 'clinic_appointment', // Your custom post type
'posts_per_page' => -1, // Get all posts
'meta_query' => array(
array(
'key' => 'appointment_datetime', // Assuming a datetime meta field
'value' => array( $start_date . ' 00:00:00', $end_date . ' 23:59:59' ),
'type' => 'DATETIME',
'compare' => 'BETWEEN',
),
),
'orderby' => 'meta_value',
'order' => 'ASC',
'meta_key' => 'appointment_datetime',
);
$query = new WP_Query( $args );
$appointments = array();
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$post_id = get_the_ID();
$appointments[] = array(
'id' => $post_id,
'patient' => get_post_meta( $post_id, 'patient_name', true ),
'datetime' => get_post_meta( $post_id, 'appointment_datetime', true ),
'doctor' => get_post_meta( $post_id, 'doctor_name', true ),
'notes' => get_post_meta( $post_id, 'appointment_notes', true ),
);
}
wp_reset_postdata();
}
return $appointments;
}
Data Formatting for Native TCP Printing
Printers that accept native TCP streams often use command sets like ESC/POS (Epson Standard Code for Printers of POS systems). While full ESC/POS implementation is complex, many basic receipt printers can handle plain text with specific control characters for line breaks, bolding, and character encoding. For compliance reports, a clear, structured text output is usually sufficient.
function ccr_format_report_for_printer( $appointments ) {
// Use UTF-8 for broad compatibility, but ensure printer supports it or use a fallback.
// For basic printers, ISO-8859-1 might be safer if UTF-8 fails.
$encoding = 'UTF-8';
$report = "";
// Add report header with ESC/POS commands for centering and bolding if supported.
// Example: ESC ( 0x1B 0x28 0x00 ) - Select parameter for Function-A
// Example: ESC ! ( 0x1B 0x21 ) - Select character style
// For simplicity, we'll use basic text and line breaks.
$report .= "========================================\n";
$report .= " Clinic Compliance Report\n";
$report .= "========================================\n\n";
// Add date range
$report .= "Date Range: " . $_POST['report_date_start'] . " to " . $_POST['report_date_end'] . "\n\n";
// Iterate through appointments
foreach ( $appointments as $appt ) {
$report .= "----------------------------------------\n";
$report .= "Patient: " . mb_substr( $appt['patient'], 0, 30, $encoding ) . "\n"; // Truncate for width
$report .= "Time: " . date( 'H:i', strtotime( $appt['datetime'] ) ) . "\n";
$report .= "Doctor: " . mb_substr( $appt['doctor'], 0, 30, $encoding ) . "\n";
if ( ! empty( $appt['notes'] ) ) {
$report .= "Notes: " . mb_substr( $appt['notes'], 0, 40, $encoding ) . "\n"; // Truncate notes
}
$report .= "----------------------------------------\n\n";
}
// Add report footer
$report .= "Generated on: " . date( 'Y-m-d H:i:s' ) . "\n";
$report .= "========================================\n";
// Add a form feed character to eject the page/cut if supported
// ESC FF (0x1B 0x0C) - Form Feed
// $report .= "\x1B\x0C";
// Convert to the target encoding if necessary.
// For basic printers, direct UTF-8 might work, or convert to ISO-8859-1.
// $report = mb_convert_encoding($report, 'ISO-8859-1', $encoding);
return $report;
}
TCP Print Client Implementation
This PHP function establishes a raw TCP socket connection to the printer and sends the formatted data. Error handling is critical here, as network issues or printer unavailability are common.
function ccr_send_to_printer( $ip_address, $port, $data ) {
$socket = null;
$timeout = 10; // Seconds to wait for connection and data transfer
// Attempt to open a socket connection
$socket = @fsockopen( $ip_address, $port, $errno, $errstr, $timeout );
if ( ! $socket ) {
error_log( "CCRSocketError: Could not connect to printer at {$ip_address}:{$port}. Error {$errno}: {$errstr}" );
return false;
}
// Set socket to non-blocking for potential timeouts during write
stream_set_blocking( $socket, true );
stream_set_timeout( $socket, $timeout );
// Send the data
$bytes_written = fwrite( $socket, $data );
if ( $bytes_written === false || $bytes_written < strlen( $data ) ) {
error_log( "CCRSocketError: Failed to write all data to printer at {$ip_address}:{$port}. Bytes written: {$bytes_written}" );
fclose( $socket );
return false;
}
// Optional: Read any response from the printer (e.g., status codes)
// This is highly printer-specific and often not implemented for basic receipt printers.
// $response = fread($socket, 1024);
// if ($response === false) {
// error_log("CCRSocketError: Error reading response from printer.");
// }
// Close the connection
fclose( $socket );
return true;
}
Shortcode for On-Demand Printing
Allowing users to print individual appointment details or summaries directly from the front end. This requires careful security considerations, especially if printing sensitive patient data.
add_shortcode( 'print_appointment_receipt', 'ccr_shortcode_print_receipt' );
function ccr_shortcode_print_receipt( $atts ) {
// Default attributes
$a = shortcode_atts( array(
'appointment_id' => 0,
'printer_ip' => get_option( 'ccr_default_printer_ip', '' ), // Store default in WP options
'printer_port' => get_option( 'ccr_default_printer_port', 9100 ),
), $atts, 'print_appointment_receipt' );
$appointment_id = intval( $a['appointment_id'] );
$printer_ip = sanitize_text_field( $a['printer_ip'] );
$printer_port = intval( $a['printer_port'] );
// Basic validation
if ( $appointment_id <= 0 || empty( $printer_ip ) || $printer_port < 1 || $printer_port > 65535 ) {
return '<p style="color: red;">Error: Invalid parameters for printing.</p>';
}
// Security: Ensure the current user has permission to view/print this appointment.
// This is crucial for HIPAA compliance. You might need to check post ownership,
// user roles, or specific meta data indicating permission.
// For this example, we'll assume a logged-in user with sufficient role can trigger it.
if ( ! is_user_logged_in() || ! current_user_can( 'read_private_posts' ) ) { // Adjust capability as needed
return '<p style="color: red;">You do not have permission to print this receipt.</p>';
}
// Fetch the appointment data
$post = get_post( $appointment_id );
if ( ! $post || $post->post_type !== 'clinic_appointment' ) {
return '<p style="color: red;">Error: Appointment not found.</p>';
}
// Format the receipt for a single appointment
$appointment_data = array(
'patient' => get_post_meta( $appointment_id, 'patient_name', true ),
'datetime' => get_post_meta( $appointment_id, 'appointment_datetime', true ),
'doctor' => get_post_meta( $appointment_id, 'doctor_name', true ),
'notes' => get_post_meta( $appointment_id, 'appointment_notes', true ),
);
// Use a simplified formatting function for a single receipt
$receipt_content = ccr_format_single_receipt( $appointment_data );
// Add a button to trigger the print action via AJAX or a form submission
// For simplicity, we'll use a form that submits to a handler.
// A more advanced solution would use AJAX.
$output = '<form method="post" action="">';
$output .= wp_nonce_field( 'ccr_print_single_action_' . $appointment_id, 'ccr_print_single_nonce', true, false );
$output .= '<input type="hidden" name="appointment_id_to_print" value="' . $appointment_id . '">';
$output .= '<input type="hidden" name="printer_ip_single" value="' . esc_attr( $printer_ip ) . '">';
$output .= '<input type="hidden" name="printer_port_single" value="' . esc_attr( $printer_port ) . '">';
$output .= '<input type="hidden" name="ccr_print_single_receipt" value="1">';
$output .= '<button type="submit" class="button button-secondary">Print Receipt</button>';
$output .= '</form>';
// Add a hidden div to hold the formatted content for potential AJAX printing
// $output .= '<div id="receipt-content-' . $appointment_id . '" style="display:none;">' . esc_html( $receipt_content ) . '</div>';
return $output;
}
// Handler for the shortcode form submission
add_action( 'template_redirect', 'ccr_handle_shortcode_print_submission' );
function ccr_handle_shortcode_print_submission() {
if ( isset( $_POST['ccr_print_single_receipt'] ) && isset( $_POST['appointment_id_to_print'] ) ) {
$appointment_id = intval( $_POST['appointment_id_to_print'] );
$printer_ip = sanitize_text_field( $_POST['printer_ip_single'] );
$printer_port = intval( $_POST['printer_port_single'] );
// Verify nonce
if ( ! isset( $_POST['ccr_print_single_nonce'] ) || ! wp_verify_nonce( $_POST['ccr_print_single_nonce'], 'ccr_print_single_action_' . $appointment_id ) ) {
wp_die( 'Security check failed.' );
}
// Re-validate permissions (important if user session changed)
if ( ! is_user_logged_in() || ! current_user_can( 'read_private_posts' ) ) {
wp_die( 'Permission denied.' );
}
// Fetch appointment data again
$post = get_post( $appointment_id );
if ( ! $post || $post->post_type !== 'clinic_appointment' ) {
wp_die( 'Appointment not found.' );
}
$appointment_data = array(
'patient' => get_post_meta( $appointment_id, 'patient_name', true ),
'datetime' => get_post_meta( $appointment_id, 'appointment_datetime', true ),
'doctor' => get_post_meta( $appointment_id, 'doctor_name', true ),
'notes' => get_post_meta( $appointment_id, 'appointment_notes', true ),
);
$receipt_content = ccr_format_single_receipt( $appointment_data );
if ( ! ccr_send_to_printer( $printer_ip, $printer_port, $receipt_content ) ) {
// Redirect back with an error message, or display inline.
// For simplicity, we'll use wp_die. In a real app, use redirects with query args.
wp_die( 'Failed to send receipt to printer. Check connection details and printer status.' );
}
// Redirect back to the page after successful printing
wp_redirect( remove_query_arg( 'print_error', $_SERVER['REQUEST_URI'] ) );
exit;
}
}
function ccr_format_single_receipt( $appt_data ) {
$encoding = 'UTF-8';
$receipt = "";
$receipt .= "========================================\n";
$receipt .= " Appointment Receipt\n";
$receipt .= "========================================\n\n";
$receipt .= "Patient: " . mb_substr( $appt_data['patient'], 0, 35, $encoding ) . "\n";
$receipt .= "Date: " . date( 'Y-m-d', strtotime( $appt_data['datetime'] ) ) . "\n";
$receipt .= "Time: " . date( 'H:i', strtotime( $appt_data['datetime'] ) ) . "\n";
$receipt .= "Doctor: " . mb_substr( $appt_data['doctor'], 0, 35, $encoding ) . "\n";
if ( ! empty( $appt_data['notes'] ) ) {
$receipt .= "Notes: " . mb_substr( $appt_data['notes'], 0, 40, $encoding ) . "\n";
}
$receipt .= "\n";
$receipt .= "----------------------------------------\n";
$receipt .= "Thank you!\n";
$receipt .= "========================================\n";
// $receipt .= "\x1B\x0C"; // Form Feed
return $receipt;
}
Configuration and Deployment Considerations
Printer Network Configuration: Ensure printers are accessible on the network via their IP addresses and that the specified port (commonly 9100 for raw TCP printing) is open in any firewalls. Static IP addresses for printers are highly recommended.
Character Encoding: The most common pitfall is character encoding. If the printer does not support UTF-8, you may need to convert the output string to a compatible encoding like ISO-8859-1 using mb_convert_encoding(). Test thoroughly with your specific printer model.
Security and HIPAA: Printing patient-specific appointment details requires strict adherence to HIPAA. Ensure that:
- Access to the reporting page and shortcodes is restricted to authorized personnel via WordPress roles and capabilities.
- Nonces are used for all form submissions to prevent CSRF attacks.
- The system logs printing attempts and any errors.
- Consider whether printing sensitive notes is appropriate or if they should be omitted from the printed report.
- Physical printer security: Ensure printers are located in secure areas to prevent unauthorized access to printed records.
Error Handling and Logging: Robust error logging (using error_log() or a more sophisticated logging framework) is essential for diagnosing connection issues, data formatting problems, or permission errors. WordPress’s debug log is invaluable here.
Scalability: For high volumes of reports, consider offloading the printing process to a background job queue (e.g., using WP-Cron with a robust queueing plugin or an external service) to prevent long-running requests that could time out the user’s browser or the web server.
Printer Command Sets: While this example uses basic text, advanced features like barcode printing, specific font styles, or image logos would require implementing the printer’s specific command set (e.g., ESC/POS commands). This involves sending byte sequences that control printer functions.