Reports. Matches the real the reference product Reports screen's * structure: Overview / Payments / Submissions sub-tabs, a form + date-range * filter, stat tiles, and real charts computed from our own entries/payments * data — including Completion Rates (a genuine started-vs-submitted signal, * see Form_Start), Submissions By Country (from each form's own Country List * field answers, not IP geolocation — ChargeForms has no GeoIP data source), * and a Submission Timeline Patterns heatmap (derived straight from * entries.created_at, no new tracking needed). */ class Reports_Page { const SUBTABS = [ 'overview', 'payments', 'submissions' ]; public function __construct() { add_action( 'admin_menu', [ $this, 'register' ] ); add_action( 'admin_post_chargeforms_export_report', [ $this, 'handle_export' ] ); } public function register() { add_submenu_page( 'chargeforms', __( 'Reports', 'chargeforms' ), __( 'Reports', 'chargeforms' ), 'chargeforms_manage_forms', 'chargeforms-reports', [ $this, 'render' ] ); } private function current_subtab() { $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'overview'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return in_array( $tab, self::SUBTABS, true ) ? $tab : 'overview'; } /** * Same preset resolution used on Entries/Forms — default here is * 'last_month' to match the real Reports screen's default filter. * * roadmap issue #27 ("Date Handling Mixes GMT and Site Time"): `$today` * must be computed in GMT, not site-local time, because every actual * date-range comparison this resolves into (Entry::for_form()'s * date_from/date_to filters, Form_Start's counts) is built with * gmdate() against `created_at` columns whose value comes from * MySQL's own CURRENT_TIMESTAMP default — a site-local "today" from * current_time('Y-m-d') would silently shift which entries fall in * "today"/"this week"/etc. by the site's UTC offset, worse the further * a site's timezone is from UTC. */ private function resolve_date_preset( $preset, $custom_from, $custom_to ) { $today = current_time( 'Y-m-d', true ); switch ( $preset ) { case 'today': return [ $today, $today ]; case 'yesterday': $y = gmdate( 'Y-m-d', strtotime( $today . ' -1 day' ) ); return [ $y, $y ]; case 'last_week': return [ gmdate( 'Y-m-d', strtotime( $today . ' -7 days' ) ), $today ]; // the reference product's real Reports date-range quick-select (DateRangeControls.vue) // also offers 3/6-month and 1-year presets — ChargeForms previously // stopped at "Last month", a real parity gap for anyone reviewing a // longer trend than 30 days. case 'last_3_months': return [ gmdate( 'Y-m-d', strtotime( $today . ' -90 days' ) ), $today ]; case 'last_6_months': return [ gmdate( 'Y-m-d', strtotime( $today . ' -182 days' ) ), $today ]; case 'last_year': return [ gmdate( 'Y-m-d', strtotime( $today . ' -365 days' ) ), $today ]; case 'custom': return [ $custom_from, $custom_to ]; case 'all': return [ '', '' ]; default: // 'last_month' return [ gmdate( 'Y-m-d', strtotime( $today . ' -30 days' ) ), $today ]; } } /** * Returns the equal-length window immediately before the given range, * used for period-over-period change badges. When no real range is * active (e.g. "All time"), returns [ '', '' ] so callers can skip * the comparison. */ private function prior_period( $date_from, $date_to ) { if ( ! $date_from || ! $date_to ) { return [ '', '' ]; } $days = max( 1, ( strtotime( $date_to ) - strtotime( $date_from ) ) / DAY_IN_SECONDS + 1 ); $prior_to = gmdate( 'Y-m-d', strtotime( $date_from . ' -1 day' ) ); return [ gmdate( 'Y-m-d', strtotime( $prior_to . ' -' . ( $days - 1 ) . ' days' ) ), $prior_to ]; } public function render() { $subtab = $this->current_subtab(); $forms = Form::all(); $form_id = isset( $_GET['form_id'] ) ? (int) $_GET['form_id'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $date_preset = isset( $_GET['date_preset'] ) ? sanitize_key( wp_unslash( $_GET['date_preset'] ) ) : 'last_month'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended list( $date_from, $date_to ) = $this->resolve_date_preset( $date_preset, isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '' // phpcs:ignore WordPress.Security.NonceVerification.Recommended ); ?>

__( 'A brief look at your overall form performance', 'chargeforms' ), 'payments' => __( 'A brief look at all payments', 'chargeforms' ), 'submissions' => __( 'A brief look at submission performance', 'chargeforms' ), ]; ?>

Miscellaneous to see Reports again.', 'chargeforms' ); ?>

render_payments_tab( $date_from, $date_to ); break; case 'submissions': $this->render_submissions_tab( $forms, $date_from, $date_to ); break; default: $this->render_overview_tab( $form_id, $date_from, $date_to, $forms ); } ?>
$form_id ?: null, 'date_from' => $date_from ?: null, 'date_to' => $date_to ?: null, 'per_page' => 1, ] ); $total = Entry::query( $range_args )['total']; $series = Entry::counts_by_day_and_status( 14 ); $max_daily = max( 1, ...array_values( $series['total'] ) ); $total_change = Entry::percent_change( $form_id, $date_from, $date_to ); // Key Metrics content matches the mockup's own set exactly // (Submissions / Form Views / Conversion / Revenue) — a prior // version substituted a different set (Spam Blocked/Unread/Created // Forms), a real content mismatch, not just styling. $views = \ChargeForms\Includes\Form_Start::count_total( $form_id, $date_from, $date_to, 'view' ); $views_change = \ChargeForms\Includes\Form_Start::percent_change( $form_id, $date_from, $date_to, 'view' ); $conversion = $views > 0 ? round( ( $total / $views ) * 100, 1 ) : null; $revenue = \ChargeForms\Includes\Entry::payment_total( $date_from, $date_to ); $starts = \ChargeForms\Includes\Form_Start::count_total( $form_id, $date_from, $date_to, 'start' ); $completion_rate = $starts > 0 ? min( 100, round( ( $total / $starts ) * 100 ) ) : null; // Conversion and Revenue get the same current-vs-prior-equal-length // change badges the Submissions/Views tiles have - the prior period // is computed from real data (Entry/Form_Start queries), so all four // Key Metrics tiles carry a real badge like the mockup's. $conversion_change = null; $revenue_change = null; if ( $date_from && $date_to ) { list( $prior_from, $prior_to ) = $this->prior_period( $date_from, $date_to ); $prior_total = Entry::query( array_filter( [ 'form_id' => $form_id ?: null, 'date_from' => $prior_from, 'date_to' => $prior_to, 'per_page' => 1, ] ) )['total']; $prior_views = \ChargeForms\Includes\Form_Start::count_total( $form_id, $prior_from, $prior_to, 'view' ); $prior_conversion = $prior_views > 0 ? ( $prior_total / $prior_views ) * 100 : null; $conversion_change = ( null !== $conversion && null !== $prior_conversion && $prior_conversion > 0 ) ? round( ( ( $conversion - $prior_conversion ) / $prior_conversion ) * 100 ) : null; $prior_revenue = Entry::payment_total( $prior_from, $prior_to ); $revenue_change = $prior_revenue > 0 ? round( ( ( $revenue - $prior_revenue ) / $prior_revenue ) * 100 ) : null; } $export_url = wp_nonce_url( admin_url( 'admin-post.php?action=chargeforms_export_report&form_id=' . $form_id . '&date_from=' . $date_from . '&date_to=' . $date_to ), 'chargeforms_export_report' ); ?>
render_category_header( 'bar-chart', __( 'Key Metrics', 'chargeforms' ), __( 'The headline numbers for this period.', 'chargeforms' ), 'primary' ); ?>
render_change_badge( $total_change ); ?>
render_change_badge( $views_change ); ?>
render_change_badge( $conversion_change ); ?>
render_change_badge( $revenue_change ); ?>
render_category_header( 'trending-up', __( 'Submission Trends', 'chargeforms' ), __( 'Volume over time, and how far visitors get before submitting.', 'chargeforms' ), 'teal' ); ?>

$count ) : ?>
render_completion_rate_card( $completion_rate, $starts, $total ); ?> render_country_card( $form_id, $date_from, $date_to ); ?>
render_category_header( 'users', __( 'Audience Insights', 'chargeforms' ), __( 'Who is submitting, and from what kind of device.', 'chargeforms' ), 'purple' ); ?>
render_device_card( $form_id, $date_from, $date_to ); ?> render_browser_card( $form_id, $date_from, $date_to ); ?>
render_category_header( 'clock', __( 'Activity Patterns', 'chargeforms' ), __( 'When submissions actually come in, by day and hour.', 'chargeforms' ), 'indigo' ); ?> render_timeline_card( $form_id, $date_from, $date_to ); ?> render_category_header( 'trophy', __( 'Performance', 'chargeforms' ), __( 'Which forms are actually converting.', 'chargeforms' ), 'amber' ); ?>

render_top_forms( $forms, $date_from, $date_to ); ?>
render_category_header( 'list', __( 'Field Analytics', 'chargeforms' ), __( 'Every chartable field on the form, shown at once.', 'chargeforms' ), 'blue' ); ?> render_field_analytics( $form_id, $forms ); ?> render_category_header( 'shield', __( 'Integration Deliveries', 'chargeforms' ), __( 'How outbound integrations — Slack, webhooks, and the rest — performed.', 'chargeforms' ), 'green' ); ?> render_integration_deliveries_card(); ?>

$count ) : ?>
'', 'shield' => '', 'inbox' => '', 'document' => '', 'trending-up' => '', 'trending-down' => '', 'trophy' => '', 'users' => '', 'clock' => '', 'check-ring' => '', 'money' => '', 'list' => '', ]; return '' . ( $paths[ $name ] ?? $paths['bar-chart'] ) . ''; } /** * Renders a Key Metrics stat tile's trend sparkline as a real * polyline over $values (chronological order) — approved-redesign * detail, but only drawn where a real daily series backs it * (Submissions has one via counts_by_day_and_status(); Form Views/ * Conversion/Revenue don't have a daily series computed anywhere in * this codebase, so no sparkline is faked for them rather than * inventing numbers to fill the shape). */ private function render_sparkline( array $values, $color ) { $values = array_values( $values ); $count = count( $values ); if ( $count < 2 ) { return; } $width = 64; $height = 26; $max = max( 1, ...$values ); $min = min( $values ); $range = max( 1, $max - $min ); $points = []; foreach ( $values as $i => $v ) { $x = round( ( $i / ( $count - 1 ) ) * $width, 1 ); $y = round( $height - ( ( $v - $min ) / $range ) * ( $height - 4 ) - 2, 1 ); $points[] = "{$x} {$y}"; } printf( '', (int) $width, (int) $height, esc_attr( implode( ' L', $points ) ), esc_attr( $color ) ); } /** * A labeled section header — grouping the report widgets below it by * what they're actually about (trends, performance, audience, * timing) instead of one undifferentiated stack of cards. Each * category gets its own accent color so the sections read as visually * distinct at a glance, not just separated by a text label. */ /** * Restored the icon chip the mockup's own "Key Metrics"/"Submission * Trends" section headers actually use (30x30, 8px radius, ~10%-opacity * accent tint, icon in the solid accent color) — a prior pass dropped * this in favor of "the reference product's real" plain-text-with-underline heading, * which is the wrong authority per the .dc.html-over-everything-else * rule. Colors for the two mockup-confirmed accents (primary, teal) * are exact matches; amber/purple/indigo back this plugin's own extra * category headers (Performance/Audience/Activity) that group cards * beyond what the mockup itself splits out, so those three are a * reasonable extension of the same system rather than a sourced value. */ private function render_category_header( $icon, $title, $description = '', $accent = 'primary' ) { $colors = [ 'primary' => '#6c4ef5', 'teal' => '#00b27f', 'amber' => '#fcbe2d', 'purple' => '#8b5cf6', 'indigo' => '#6366f1', 'blue' => '#1a7efb', 'green' => '#00a86b', ]; $color = $colors[ $accent ] ?? $colors['primary']; ?>

· ' . esc_html__( 'No prior period', 'chargeforms' ) . ''; return; } $is_up = $change >= 0; printf( '%s %s%%', esc_attr( $is_up ? 'is-up' : 'is-down' ), wp_kses( self::icon_svg( $is_up ? 'trending-up' : 'trending-down' ), Icon::allowed_svg_html() ), esc_html( ( $is_up ? '+' : '' ) . $change ) ); } /** * Completion Rate - a real submitted-vs-started ratio, not a * placeholder. "Starts" come from Form_Start (see class-form-start.php * and the tracking snippet in class-shortcode.php), a real signal * recorded the first time a visitor actually interacts with a field - * distinct from a plain page view. */ private function render_completion_rate_card( $rate, $starts, $submissions ) { // Horizontal bar (matches the design handoff: a 40px violet rate, // a 10px track/fill, and started/submitted counts beneath), not a // semi-circle gauge. The rate stays a real submitted-vs-started // ratio from Form_Start tracking, never a view-count guess. ?>

%

$count ) : ?>
'monitor', 'Mobile' => 'smartphone', 'Tablet' => 'tablet' ]; ?>

$count ) : ?>

$count ) : ?>
$hours ) { foreach ( $blocks as $b => $range ) { $sum = 0; for ( $h = $range[0]; $h <= $range[1]; $h++ ) { $sum += $hours[ $h ]; } $block_totals[ $dow ][ $b ] = $sum; $max = max( $max, $sum ); } } ?>

$sums ) : ?>
$count ) : ?> 0 ? max( 0.15, round( ( $count / $max ), 2 ) ) : 0; $tooltip = sprintf( /* translators: 1: day, 2: hour range (e.g. "12am–4am"), 3: submission count */ _n( '%1$s, %2$s — %3$s submission', '%1$s, %2$s — %3$s submissions', $count, 'chargeforms' ), $day_labels[ $dow ], date_i18n( 'ga', mktime( $range[0], 0, 0 ) ) . '–' . date_i18n( 'ga', mktime( $range[1] + 1 === 24 ? 0 : $range[1] + 1, 0, 0 ) ), number_format_i18n( $count ) ); ?> #6c4ef5 = rgb(108,78,245)), not blue — // this used to shade with rgba(26,115,232,...), a // real color-family mismatch, not just a shade off. ?> style="background: rgba(108, 78, 245, ); animation-delay: ms;" data-tooltip="" aria-label="" > 0 ? esc_html( number_format_i18n( $count ) ) : ''; ?>

' . wp_kses( Icon::html( 'bar-chart-3' ), Icon::allowed_svg_html() ) . '

' . esc_html__( 'No forms yet.', 'chargeforms' ) . '

'; return; } // roadmap issue #36 ("Reports May Become Expensive on Large Entry // Tables"): this used to run Entry::query() + Form_Start:: // count_for_form() ONCE PER FORM to find the top 5 — two GROUP BY // queries covering every form on the site, regardless of how many // there are, replaces what used to be 2 * count($forms) queries. $form_ids = array_column( $forms, 'id' ); $submission_counts = Entry::count_by_form( $form_ids, $date_from, $date_to ); $view_counts = \ChargeForms\Includes\Form_Start::count_by_form( $form_ids, 'view', $date_from, $date_to ); $ranked = array_map( function ( $form ) use ( $submission_counts, $view_counts ) { $form['submissions'] = $submission_counts[ $form['id'] ] ?? 0; $form['views'] = $view_counts[ $form['id'] ] ?? 0; return $form; }, $forms ); usort( $ranked, fn( $a, $b ) => $b['submissions'] <=> $a['submissions'] ); $ranked = array_slice( $ranked, 0, 5 ); if ( 0 === array_sum( array_column( $ranked, 'submissions' ) ) && 0 === array_sum( array_column( $ranked, 'views' ) ) ) { echo '
' . wp_kses( Icon::html( 'bar-chart-3' ), Icon::allowed_svg_html() ) . '

' . esc_html__( 'No form data available for the selected period', 'chargeforms' ) . '

'; return; } $max_submissions = max( 1, ...array_column( $ranked, 'submissions' ) ); $max_views = max( 1, ...array_column( $ranked, 'views' ) ); ?>
$form ) : ?>
$date_from, 'date_to' => $date_to, 'per_page' => 1 ] ); $daily = Entry::payment_totals_by_day( 14 ); $max_daily = max( 0.01, ...array_values( $daily ) ); $transactions = (int) $result['total']; $combined = $stripe_total + $paypal_total; $avg_order = $transactions > 0 ? $combined / $transactions : 0; // Change badges for the flat payment tiles, same current-vs-prior // equal-length-period math the Overview Key Metrics use (real data). $revenue_change = null; $transactions_change = null; $avg_order_change = null; if ( $date_from && $date_to ) { list( $prior_from, $prior_to ) = $this->prior_period( $date_from, $date_to ); $prior_stripe = Entry::payment_total( $prior_from, $prior_to, 'stripe' ); $prior_paypal = Entry::payment_total( $prior_from, $prior_to, 'paypal' ); $prior_total = $prior_stripe + $prior_paypal; $prior_count = (int) Entry::payment_transactions( [ 'date_from' => $prior_from, 'date_to' => $prior_to, 'per_page' => 1 ] )['total']; $prior_avg = $prior_count > 0 ? $prior_total / $prior_count : 0; $revenue_change = $prior_total > 0 ? round( ( ( $combined - $prior_total ) / $prior_total ) * 100 ) : null; $transactions_change = $prior_count > 0 ? round( ( ( $transactions - $prior_count ) / $prior_count ) * 100 ) : null; $avg_order_change = ( $prior_avg > 0 && $avg_order > 0 ) ? round( ( ( $avg_order - $prior_avg ) / $prior_avg ) * 100 ) : null; } // By-gateway breakdown: real per-gateway transaction counts and // amounts (legacy rows COALESCE to Stripe inside payment_*()). $gateway_rows = []; foreach ( [ 'stripe', 'paypal' ] as $gw ) { $gateway_count = (int) Entry::payment_transactions( [ 'date_from' => $date_from, 'date_to' => $date_to, 'gateway' => $gw, 'per_page' => 1 ] )['total']; $gateway_amount = Entry::payment_total( $date_from, $date_to, $gw ); if ( $gateway_count > 0 ) { $gateway_rows[] = [ 'key' => $gw, 'label' => 'stripe' === $gw ? 'STRIPE' : 'PAYPAL', 'tag' => 'stripe' === $gw ? '#635bff' : '#003087', 'amount' => $gateway_amount, 'currency' => 'stripe' === $gw ? $stripe_currency : $paypal_currency, 'count' => $gateway_count, ]; } } $gateway_amounts = array_column( $gateway_rows, 'amount' ); $gateway_max = $gateway_amounts ? max( ...$gateway_amounts ) : 1; ?>
0 ) : ?>
render_change_badge( $revenue_change ); ?>
render_change_badge( $revenue_change ); ?>
render_change_badge( $revenue_change ); ?>
render_change_badge( $transactions_change ); ?>
render_change_badge( $avg_order_change ); ?>

0 ) : ?>

$amount ) : ?>

Payments screen.', 'chargeforms' ) ), esc_url( admin_url( 'admin.php?page=chargeforms-payments' ) ) ); ?>

__( 'Dropdown', 'chargeforms' ), 'radio' => __( 'Radio', 'chargeforms' ), 'checkbox' => __( 'Checkbox', 'chargeforms' ), 'multi_select' => __( 'Multi-Select', 'chargeforms' ), 'multiple_choice' => __( 'Multiple Choice', 'chargeforms' ), 'country' => __( 'Country', 'chargeforms' ), ]; ?>

$count ) : ?>

' . esc_html__( 'Submissions per form', 'chargeforms' ) . '

' . esc_html__( 'No forms yet.', 'chargeforms' ) . '

'; return; } $form_ids = array_column( $forms, 'id' ); $started_by_form = \ChargeForms\Includes\Form_Start::count_by_form( $form_ids, 'start', $date_from, $date_to ); $submitted_by_form = Entry::count_by_form( $form_ids, $date_from, $date_to ); ?>

$form ) : ?> 0 ? round( ( $submitted / $started ) * 100, 1 ) : 0; $latest = Entry::query( [ 'form_id' => $form['id'], 'per_page' => 1 ] )['items'][0]['created_at'] ?? ''; $last_label = ''; if ( $latest ) { $last_time = strtotime( $latest ); $recent_ago = human_time_diff( $last_time, current_time( 'timestamp' ) ); $last_label = ( time() - $last_time ) < 2 * DAY_IN_SECONDS ? sprintf( /* translators: %s: human time diff */ __( '%s ago', 'chargeforms' ), $recent_ago ) : date_i18n( 'M j, H:i', $last_time ); } else { $last_label = __( 'No entries yet', 'chargeforms' ); } ?> >
$b['count'] <=> $a['count'] ); $countries = Entry::country_breakdown( $form_id, $date_from, $date_to ); $devices = Entry::device_breakdown( $form_id, $date_from, $date_to ); $browsers = Entry::browser_breakdown( $form_id, $date_from, $date_to ); $timeline = Entry::submissions_by_hour_and_dow( $form_id, $date_from, $date_to ); $starts = \ChargeForms\Includes\Form_Start::count_total( $form_id, $date_from, $date_to ); nocache_headers(); if ( 'json' === $format ) { header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="chargeforms-report-' . gmdate( 'Y-m-d' ) . '.json"' ); echo wp_json_encode( [ 'daily_submissions' => $daily, 'top_forms' => array_map( fn( $f ) => [ 'title' => $f['title'], 'submissions' => $f['count'] ], $ranked ), 'submissions_by_country' => $countries, 'device_breakdown' => $devices, 'browser_breakdown' => $browsers, 'timeline_hour_by_dow' => $timeline, 'form_starts' => $starts, ] ); exit; } header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="chargeforms-report-' . gmdate( 'Y-m-d' ) . '.csv"' ); $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen fputcsv( $out, [ __( 'Daily Submissions (last 14 days)', 'chargeforms' ) ] ); fputcsv( $out, [ __( 'Date', 'chargeforms' ), __( 'Submissions', 'chargeforms' ) ] ); foreach ( $daily as $day => $count ) { fputcsv( $out, [ $day, $count ] ); } fputcsv( $out, [] ); fputcsv( $out, [ __( 'Top Performing Forms', 'chargeforms' ) ] ); fputcsv( $out, [ __( 'Form', 'chargeforms' ), __( 'Submissions', 'chargeforms' ) ] ); foreach ( $ranked as $form ) { fputcsv( $out, [ $form['title'], $form['count'] ] ); } fputcsv( $out, [] ); fputcsv( $out, [ __( 'Submissions By Country', 'chargeforms' ) ] ); fputcsv( $out, [ __( 'Country', 'chargeforms' ), __( 'Submissions', 'chargeforms' ) ] ); foreach ( $countries as $country => $count ) { fputcsv( $out, [ $country, $count ] ); } fputcsv( $out, [] ); fputcsv( $out, [ __( 'Device Breakdown', 'chargeforms' ) ] ); fputcsv( $out, [ __( 'Device', 'chargeforms' ), __( 'Submissions', 'chargeforms' ) ] ); foreach ( $devices as $device => $count ) { fputcsv( $out, [ $device, $count ] ); } fputcsv( $out, [] ); fputcsv( $out, [ __( 'Browser Breakdown', 'chargeforms' ) ] ); fputcsv( $out, [ __( 'Browser', 'chargeforms' ), __( 'Submissions', 'chargeforms' ) ] ); foreach ( $browsers as $browser => $count ) { fputcsv( $out, [ $browser, $count ] ); } fputcsv( $out, [] ); fputcsv( $out, [ __( 'Completion Rate', 'chargeforms' ) ] ); fputcsv( $out, [ __( 'Form Starts', 'chargeforms' ), __( 'Submissions', 'chargeforms' ) ] ); fputcsv( $out, [ $starts, Entry::query( array_filter( [ 'form_id' => $form_id ?: null, 'date_from' => $date_from ?: null, 'date_to' => $date_to ?: null, 'per_page' => 1 ] ) )['total'] ] ); fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } }