CountyCollective Reference
CSV Tools Reference
Original CSV tools reference copy, including removed map stat-gathering logic.
<?php
/**
* County GOP Core — CSV export, import, and wipe tools.
*
* Extracted from theme/cgop-theme/inc/leadership-csv-tools.php in Pass 09A.
* Moved from Representatives (Leadership CPT) admin menu to GOP Setup → CSV Tools.
*
* Page slug, admin-post action names, nonce strings, helper function names, and
* stored identifiers are all frozen — do not rename without explicit owner approval.
*
* @package CountyGOPCore
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// ---------------------------------------------------------------------------
// Admin menu
// ---------------------------------------------------------------------------
function CGOP_csv_tools_add_menu() {
add_submenu_page(
CGOP_SETUP_MENU_SLUG,
__( 'CSV Tools', 'county-gop-core' ),
__( 'CSV Tools', 'county-gop-core' ),
CGOP_POSITION_KEYS_CAP,
'cgop-csv-tools',
'CGOP_csv_tools_screen'
);
}
add_action( 'admin_menu', 'CGOP_csv_tools_add_menu' );
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function CGOP_csv_tools_admin_url( array $args = array() ) {
return add_query_arg( $args, admin_url( 'admin.php?page=cgop-csv-tools' ) );
}
/**
* Redirect the legacy Representatives → CSV Tools URL to the new GOP Setup location.
*
* Old URL: edit.php?post_type=representative&page=cgop-csv-tools
* New URL: admin.php?page=cgop-csv-tools
*/
function CGOP_csv_tools_redirect_legacy() {
global $pagenow;
if ( 'edit.php' !== $pagenow ) {
return;
}
$post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '';
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
if ( 'representative' === $post_type && 'cgop-csv-tools' === $page ) {
wp_safe_redirect( admin_url( 'admin.php?page=cgop-csv-tools' ), 301 );
exit;
}
}
add_action( 'admin_init', 'CGOP_csv_tools_redirect_legacy' );
/**
* Return counts for the status panel.
*
* @return array { position_keys, rep_records, rep_only, multi_section }
*/
function CGOP_csv_tools_get_counts() {
global $wpdb;
$table = CGOP_position_keys_table();
// County-scoped count when column exists.
if ( function_exists( 'cgop_get_table_county_scope' ) ) {
$_pk_scope = cgop_get_table_county_scope( $table );
if ( null === $_pk_scope ) {
$position_keys = 0;
} elseif ( false !== $_pk_scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$position_keys = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$table}` WHERE county_id = %s", $_pk_scope ) );
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$position_keys = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
}
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$position_keys = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
}
$rep_query = new WP_Query( array(
'post_type' => 'representative',
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => true,
'update_post_term_cache' => false,
'meta_query' => array(
array(
'key' => 'leadership_sections',
'value' => '"representatives"',
'compare' => 'LIKE',
),
),
) );
$rep_records = 0;
$rep_only = 0;
$multi_section = 0;
foreach ( $rep_query->posts as $post_id ) {
$sections = CGOP_csv_tools_get_sections( $post_id );
$rep_records++;
if ( count( $sections ) <= 1 ) {
$rep_only++;
} else {
$multi_section++;
}
}
$overlay_count = 0;
if ( function_exists( 'CGOP_overlay_table' ) ) {
$ov_table = CGOP_overlay_table();
if ( function_exists( 'cgop_get_table_county_scope' ) ) {
$_ov_scope = cgop_get_table_county_scope( $ov_table );
if ( null === $_ov_scope ) {
$overlay_count = 0;
} elseif ( false !== $_ov_scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$overlay_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$ov_table}` WHERE county_id = %s", $_ov_scope ) );
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$overlay_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$ov_table}`" );
}
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$overlay_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$ov_table}`" );
}
}
$gis_beacon_layers = count( CGOP_gis_get_custom_layer_registry() );
$gis_generated_files = 0;
if ( defined( 'CGOP_GIS_IMPORT_STATUS_OPTION' ) ) {
$gis_status = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
if ( ! empty( $gis_status['layers'] ) && is_array( $gis_status['layers'] ) ) {
foreach ( $gis_status['layers'] as $glayer ) {
$gis_generated_files += count( $glayer['files'] ?? array() );
}
}
}
return compact( 'position_keys', 'rep_records', 'rep_only', 'multi_section', 'overlay_count', 'gis_beacon_layers', 'gis_generated_files' );
}
/**
* Render a county selector <tr> for CSV import/export form-tables.
*
* Returns an empty string when cgop is not active, the county_id column does
* not yet exist (pre-wizard), or the current user cannot manage any county.
*
* @param string $field_name POST field name, e.g. 'import_county_id'.
* @param string $selected_id Pre-selected county_id (defaults to current context).
* @param bool $for_export When true, label and description are export-flavoured.
* @return string HTML <tr>…</tr> or empty string.
*/
function CGOP_csv_county_selector_row( $field_name, $selected_id = '', $for_export = false ) {
if ( ! function_exists( 'cgop_get_table_county_scope' ) || ! function_exists( 'cgop_get_all_county_ids' ) ) {
return '';
}
$pk_table = function_exists( 'CGOP_position_keys_table' ) ? CGOP_position_keys_table() : '';
if ( ! $pk_table ) {
return '';
}
$scope = cgop_get_table_county_scope( $pk_table );
if ( false === $scope ) {
return ''; // Column not yet added — pre-wizard.
}
$all_ids = cgop_get_all_county_ids( false );
if ( empty( $all_ids ) ) {
return '';
}
$manageable = array_values( array_filter( $all_ids, function( $cid ) {
return function_exists( 'cgop_current_user_can_manage_county' ) && cgop_current_user_can_manage_county( $cid );
} ) );
if ( empty( $manageable ) ) {
return '';
}
if ( '' === $selected_id && function_exists( 'cgop_get_current_county_id' ) ) {
$selected_id = (string) cgop_get_current_county_id();
}
$label = $for_export
? __( 'County', 'county-gop-core' )
: __( 'Import County', 'county-gop-core' );
$desc = $for_export
? __( 'Export only rows belonging to this county.', 'county-gop-core' )
: __( 'Assign imported rows to this county. Only counties you manage are listed.', 'county-gop-core' );
ob_start();
?>
<tr>
<th scope="row"><label for="<?php echo esc_attr( $field_name ); ?>"><?php echo esc_html( $label ); ?> <span style="color:#a00;" aria-hidden="true">*</span></label></th>
<td>
<select id="<?php echo esc_attr( $field_name ); ?>" name="<?php echo esc_attr( $field_name ); ?>" required>
<option value=""><?php esc_html_e( '— Select county —', 'county-gop-core' ); ?></option>
<?php foreach ( $manageable as $cid ) :
$profile = function_exists( 'cgop_get_county_profile' ) ? cgop_get_county_profile( $cid ) : null;
$clabel = $profile
? esc_html( $profile->county_name . ' (' . $profile->county_slug . ')' )
: esc_html( $cid );
?>
<option value="<?php echo esc_attr( $cid ); ?>"<?php selected( $selected_id, $cid ); ?>><?php echo $clabel; // phpcs:ignore WordPress.Security.EscapeOutput ?></option>
<?php endforeach; ?>
</select>
<p class="description"><?php echo esc_html( $desc ); ?></p>
</td>
</tr>
<?php
return ob_get_clean();
}
/**
* Return the leadership_sections value for a post as a normalized flat array.
*/
function CGOP_csv_tools_get_sections( $post_id ) {
$sections = get_post_meta( $post_id, 'leadership_sections', true );
if ( empty( $sections ) ) {
return array();
}
if ( ! is_array( $sections ) ) {
$sections = maybe_unserialize( $sections );
}
if ( ! is_array( $sections ) ) {
$sections = array( $sections );
}
return array_values( array_filter( $sections ) );
}
/**
* Build a deterministic representative_import_key for a post.
*/
function CGOP_csv_tools_build_import_key( $post_id, $post_title ) {
$parts = array_filter( array(
sanitize_title( $post_title ),
sanitize_title( (string) get_post_meta( $post_id, 'leadership_role_title', true ) ),
sanitize_title( (string) get_post_meta( $post_id, 'leadership_district', true ) ),
sanitize_title( (string) get_post_meta( $post_id, 'leadership_service_start_year', true ) ),
sanitize_title( (string) get_post_meta( $post_id, 'leadership_service_end_year', true ) ),
) );
return implode( '-', $parts );
}
/**
* Parse an uploaded CSV file.
*
* @param string $file_key $_FILES key.
* @return array { headers: string[], rows: array[], error: string }
*/
function CGOP_csv_tools_parse_upload( $file_key ) {
$empty = array( 'headers' => array(), 'rows' => array(), 'error' => '' );
if ( empty( $_FILES[ $file_key ] ) || UPLOAD_ERR_OK !== (int) $_FILES[ $file_key ]['error'] ) {
return array_merge( $empty, array( 'error' => 'No file uploaded or upload error.' ) );
}
$tmp = $_FILES[ $file_key ]['tmp_name'];
if ( ! is_uploaded_file( $tmp ) ) {
return array_merge( $empty, array( 'error' => 'Invalid file upload.' ) );
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fh = fopen( $tmp, 'r' );
if ( ! $fh ) {
return array_merge( $empty, array( 'error' => 'Could not open uploaded file.' ) );
}
$raw_headers = fgetcsv( $fh );
if ( ! $raw_headers ) {
fclose( $fh );
return array_merge( $empty, array( 'error' => 'CSV file is empty or missing headers.' ) );
}
// Strip UTF-8 BOM from first header.
$raw_headers[0] = ltrim( $raw_headers[0], "\xEF\xBB\xBF" );
$headers = array_map( 'trim', $raw_headers );
$rows = array();
while ( ( $line = fgetcsv( $fh ) ) !== false ) {
if ( count( $line ) === count( $headers ) ) {
$rows[] = array_combine( $headers, $line );
}
}
fclose( $fh );
return array( 'headers' => $headers, 'rows' => $rows, 'error' => '' );
}
/**
* ACF field key map for leadership representative meta fields.
*/
function CGOP_csv_tools_acf_field_keys() {
return array(
'leadership_sections' => 'field_CGOP_leadership_sections',
'leadership_service_status' => 'field_CGOP_leadership_service_status',
'leadership_service_start_year' => 'field_CGOP_leadership_service_start_year',
'leadership_service_end_year' => 'field_CGOP_leadership_service_end_year',
'leadership_service_note' => 'field_CGOP_leadership_service_note',
'leadership_bio' => 'field_CGOP_leadership_bio',
'leadership_government_level' => 'field_CGOP_leadership_government_level',
'leadership_role_title' => 'field_CGOP_leadership_role_title',
'leadership_district' => 'field_CGOP_leadership_district',
'leadership_jurisdiction' => 'field_CGOP_leadership_jurisdiction',
'leadership_party_affiliation' => 'field_CGOP_leadership_party_affiliation',
'leadership_committee_assignments' => 'field_CGOP_leadership_committee_assignments',
'leadership_official_url' => 'field_CGOP_leadership_official_url',
// CSV column 'leadership_official_profile_embed_url' maps to this true/false ACF field.
'leadership_embed_official_profile' => 'field_CGOP_leadership_embed_official_profile',
'leadership_representative_history_key' => 'field_CGOP_leadership_representative_history_key',
'leadership_representative_position_status' => 'field_CGOP_leadership_representative_position_status',
'leadership_email' => 'field_CGOP_leadership_email',
'leadership_phone' => 'field_CGOP_leadership_phone',
'leadership_facebook_url' => 'field_CGOP_leadership_facebook_url',
'leadership_wikipedia_url' => 'field_CGOP_leadership_wikipedia_url',
);
}
// ---------------------------------------------------------------------------
// Export: Position Keys
// ---------------------------------------------------------------------------
function CGOP_csv_export_pkeys_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_export_pkeys' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
global $wpdb;
$table = CGOP_position_keys_table();
// County-scoped export: require county context when the column exists.
$selected_county = isset( $_POST['export_county_id'] ) ? sanitize_text_field( wp_unslash( $_POST['export_county_id'] ) ) : '';
$scope = function_exists( 'cgop_get_table_county_scope' ) ? cgop_get_table_county_scope( $table ) : false;
if ( false !== $scope ) {
if ( '' === $selected_county ) {
wp_die( esc_html__( 'Select a county before exporting.', 'county-gop-core' ) );
}
if ( ! function_exists( 'cgop_county_exists' ) || ! cgop_county_exists( $selected_county ) ) {
wp_die( esc_html__( 'Selected county was not found.', 'county-gop-core' ) );
}
if ( ! function_exists( 'cgop_current_user_can_manage_county' ) || ! cgop_current_user_can_manage_county( $selected_county ) ) {
wp_die( esc_html__( 'Not authorized for the selected county.', 'county-gop-core' ) );
}
$export_county = $selected_county;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM `{$table}` WHERE county_id = %s ORDER BY sort_order ASC, id ASC",
$export_county
), ARRAY_A );
} else {
// Pre-wizard: unscoped.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $wpdb->get_results( "SELECT * FROM `{$table}` ORDER BY sort_order ASC, id ASC", ARRAY_A );
}
$filename = 'cgop-position-keys-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fh = fopen( 'php://output', 'w' );
// Include county_id + county_slug when the county-gop-core plugin provides them.
$include_county = function_exists( 'cgop_get_county_slug' );
$header = array(
'position_key', 'leader_type', 'page_location', 'label', 'government_level',
'municipality_type', 'role_title', 'elected_how', 'district', 'jurisdiction',
'position_status', 'sort_order', 'description', 'live_in_map', 'voting_area_map',
);
if ( $include_county ) {
$header[] = 'county_id';
$header[] = 'county_slug';
}
fputcsv( $fh, $header );
foreach ( $rows as $row ) {
$values = array(
$row['position_key'],
$row['leader_type'],
$row['page_location'],
$row['label'],
$row['government_level'],
$row['municipality_type'],
$row['role_title'],
$row['elected_how'],
$row['district'],
$row['jurisdiction'],
$row['position_status'],
$row['sort_order'],
$row['description'],
$row['live_in_map'] ?? '',
$row['voting_area_map'] ?? '',
);
if ( $include_county ) {
$county_id = $row['county_id'] ?? '';
$values[] = $county_id;
$values[] = $county_id ? cgop_get_county_slug( $county_id ) : '';
}
fputcsv( $fh, $values );
}
fclose( $fh );
exit;
}
add_action( 'admin_post_CGOP_csv_export_pkeys', 'CGOP_csv_export_pkeys_handler' );
// ---------------------------------------------------------------------------
// Export: Representatives
// ---------------------------------------------------------------------------
function CGOP_csv_export_reps_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_export_reps' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$posts = get_posts( array(
'post_type' => 'representative',
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => -1,
'orderby' => 'menu_order',
'order' => 'ASC',
'update_post_meta_cache' => true,
'update_post_term_cache' => false,
'meta_query' => array(
array(
'key' => 'leadership_sections',
'value' => '"representatives"',
'compare' => 'LIKE',
),
),
) );
$filename = 'cgop-representatives-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fh = fopen( 'php://output', 'w' );
fputcsv( $fh, array(
'representative_import_key', 'legacy_post_id', 'post_title', 'post_name',
'post_status', 'menu_order', 'card_summary', 'profile_content',
'leadership_sections', 'portrait_attachment_id', 'portrait_url',
'leadership_service_status', 'leadership_service_start_year',
'leadership_service_end_year', 'leadership_service_note',
'leadership_government_level', 'leadership_role_title', 'leadership_district',
'leadership_jurisdiction', 'leadership_party_affiliation',
'leadership_representative_history_key', 'leadership_representative_position_status',
'leadership_bio', 'leadership_official_url', 'leadership_wikipedia_url',
'leadership_facebook_url', 'leadership_email', 'leadership_phone',
'leadership_committee_assignments', 'leadership_official_profile_embed_url',
'_CGOP_leadership_research_source', '_CGOP_leadership_review_note',
) );
foreach ( $posts as $post ) {
$pid = $post->ID;
$sections = CGOP_csv_tools_get_sections( $pid );
$portrait_id = (int) get_post_thumbnail_id( $pid );
$portrait_url = $portrait_id ? (string) wp_get_attachment_url( $portrait_id ) : '';
$stored_key = get_post_meta( $pid, '_CGOP_representative_import_key', true );
$import_key = $stored_key ? $stored_key : CGOP_csv_tools_build_import_key( $pid, $post->post_title );
fputcsv( $fh, array(
$import_key,
$pid,
$post->post_title,
$post->post_name,
$post->post_status,
$post->menu_order,
$post->post_excerpt,
$post->post_content,
implode( '|', $sections ),
$portrait_id ?: '',
$portrait_url,
get_post_meta( $pid, 'leadership_service_status', true ),
get_post_meta( $pid, 'leadership_service_start_year', true ),
get_post_meta( $pid, 'leadership_service_end_year', true ),
get_post_meta( $pid, 'leadership_service_note', true ),
get_post_meta( $pid, 'leadership_government_level', true ),
get_post_meta( $pid, 'leadership_role_title', true ),
get_post_meta( $pid, 'leadership_district', true ),
get_post_meta( $pid, 'leadership_jurisdiction', true ),
get_post_meta( $pid, 'leadership_party_affiliation', true ),
get_post_meta( $pid, 'leadership_representative_history_key', true ),
get_post_meta( $pid, 'leadership_representative_position_status', true ),
get_post_meta( $pid, 'leadership_bio', true ),
get_post_meta( $pid, 'leadership_official_url', true ),
get_post_meta( $pid, 'leadership_wikipedia_url', true ),
get_post_meta( $pid, 'leadership_facebook_url', true ),
get_post_meta( $pid, 'leadership_email', true ),
get_post_meta( $pid, 'leadership_phone', true ),
get_post_meta( $pid, 'leadership_committee_assignments', true ),
// 'leadership_official_profile_embed_url' column = leadership_embed_official_profile (true/false).
get_post_meta( $pid, 'leadership_embed_official_profile', true ),
get_post_meta( $pid, '_CGOP_leadership_research_source', true ),
get_post_meta( $pid, '_CGOP_leadership_review_note', true ),
) );
}
fclose( $fh );
exit;
}
add_action( 'admin_post_CGOP_csv_export_reps', 'CGOP_csv_export_reps_handler' );
// ---------------------------------------------------------------------------
// Import: Position Keys
// ---------------------------------------------------------------------------
function CGOP_csv_import_pkeys_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_import_pkeys' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$mode = isset( $_POST['pkeys_import_mode'] ) ? sanitize_key( $_POST['pkeys_import_mode'] ) : 'dry_run';
$dry_run = ( 'dry_run' === $mode );
$allow_valid_only = ! empty( $_POST['pkeys_import_valid_only'] );
$uid = get_current_user_id();
$result = array(
'mode' => $mode,
'dry_run' => $dry_run,
'parsed' => 0,
'valid' => 0,
'skipped' => 0,
'inserted' => 0,
'updated' => 0,
'generated_keys' => array(),
'errors' => array(),
);
$parsed = CGOP_csv_tools_parse_upload( 'pkeys_csv_file' );
if ( $parsed['error'] ) {
$result['errors'][] = array( 'row' => 0, 'msg' => $parsed['error'] );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
$headers = $parsed['headers'];
$rows = $parsed['rows'];
if ( ! in_array( 'position_key', $headers, true ) || ! in_array( 'label', $headers, true ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'CSV must include position_key and label columns.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
global $wpdb;
$table = CGOP_position_keys_table();
// County-scoped import: when county_id column exists, scope the existing_set.
$import_scope = function_exists( 'cgop_get_table_county_scope' ) ? cgop_get_table_county_scope( $table ) : false;
if ( false !== $import_scope && ( ! in_array( 'county_id', $headers, true ) || ! in_array( 'county_slug', $headers, true ) ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'CSV must include county_id and county_slug columns after county adoption.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
// Require an explicit authorized county when the county_id column exists.
$import_county = '';
if ( false !== $import_scope ) {
if ( ! isset( $_POST['import_county_id'] ) || '' === $_POST['import_county_id'] ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Import county is required. Select a county before importing.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
$import_county = sanitize_text_field( wp_unslash( $_POST['import_county_id'] ) );
if ( ! function_exists( 'cgop_county_exists' ) || ! cgop_county_exists( $import_county ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Selected import county was not found.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
if ( ! function_exists( 'cgop_current_user_can_manage_county' ) || ! cgop_current_user_can_manage_county( $import_county ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Not authorized for the selected county.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
if ( function_exists( 'cgop_set_current_county_id' ) ) {
cgop_set_current_county_id( $import_county );
}
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$existing_rows_query = false !== $import_scope && '' !== $import_county
? $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$table}` WHERE county_id = %s", $import_county ), ARRAY_A )
: $wpdb->get_results( "SELECT * FROM `{$table}`", ARRAY_A );
$existing_rows = (array) $existing_rows_query;
$existing_set = array();
foreach ( $existing_rows as $existing_row ) {
$existing_set[ $existing_row['position_key'] ] = $existing_row;
}
$valid_map_ids = array();
foreach ( CGOP_get_map_boundary_rows() as $mrow ) {
$valid_map_ids[ $mrow->map_id ] = true;
}
$generated_map_choices = CGOP_gis_get_generated_file_choices();
$seen_keys = array();
$valid_rows = array();
foreach ( $rows as $i => $row ) {
$line = $i + 2;
$result['parsed']++;
$raw_position_key = trim( $row['position_key'] ?? '' );
$position_key = sanitize_title( $raw_position_key );
$label = function_exists( 'CGOP_sanitize_position_label_template' )
? CGOP_sanitize_position_label_template( trim( $row['label'] ?? '' ) )
: sanitize_text_field( trim( $row['label'] ?? '' ) );
if ( '' === $position_key ) {
do {
$position_key = function_exists( 'CGOP_generate_leadership_position_key_id' )
? CGOP_generate_leadership_position_key_id()
: sanitize_title( uniqid( 'position-', true ) );
} while ( isset( $seen_keys[ $position_key ] ) );
$result['generated_keys'][] = array(
'row' => $line,
'key' => $position_key,
);
}
if ( '' === $label ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: label is blank for key '{$position_key}'." );
continue;
}
if ( isset( $seen_keys[ $position_key ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: duplicate position_key '{$position_key}' in CSV (first at row {$seen_keys[$position_key]})." );
continue;
}
$status = sanitize_key( $row['position_status'] ?? 'active' );
if ( ! in_array( $status, array( 'active', 'historical', 'inactive' ), true ) ) {
$status = 'active';
}
$live_in_map = sanitize_text_field( $row['live_in_map'] ?? '' );
$voting_area_map = sanitize_text_field( $row['voting_area_map'] ?? '' );
$live_in_generated_key = str_starts_with( $live_in_map, 'generated:' )
? ltrim( substr( $live_in_map, strlen( 'generated:' ) ), '/' )
: '';
$voting_generated_key = str_starts_with( $voting_area_map, 'generated:' )
? ltrim( substr( $voting_area_map, strlen( 'generated:' ) ), '/' )
: '';
if ( $live_in_generated_key && empty( $generated_map_choices[ $live_in_generated_key ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: live_in_map generated map '{$live_in_map}' was not found in generated GIS files." );
continue;
}
if ( $voting_generated_key && empty( $generated_map_choices[ $voting_generated_key ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: voting_area_map generated map '{$voting_area_map}' was not found in generated GIS files." );
continue;
}
if ( $live_in_map && ! $live_in_generated_key && ! isset( $valid_map_ids[ $live_in_map ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: live_in_map '{$live_in_map}' is not a valid map ID." );
continue;
}
if ( $voting_area_map && ! $voting_generated_key && ! isset( $valid_map_ids[ $voting_area_map ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: voting_area_map '{$voting_area_map}' is not a valid map ID." );
continue;
}
// Validate optional file ownership columns against the selected import county.
$row_county_id = $import_county;
if ( false !== $import_scope ) {
$file_county_id = sanitize_text_field( $row['county_id'] ?? '' );
$file_county_slug = sanitize_title( $row['county_slug'] ?? '' );
$import_slug = function_exists( 'cgop_get_county_slug' ) ? sanitize_title( cgop_get_county_slug( $import_county ) ) : '';
if ( ! $file_county_id || ! $file_county_slug ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: county_id and county_slug are required after county adoption." );
continue;
}
if ( $file_county_id && $file_county_id !== $import_county ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: county_id '{$file_county_id}' does not match selected import county '{$import_county}'." );
continue;
}
if ( $file_county_slug && $import_slug && $file_county_slug !== $import_slug ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: county_slug '{$file_county_slug}' does not match selected import county slug '{$import_slug}'." );
continue;
}
}
$seen_keys[ $position_key ] = $line;
$result['valid']++;
$valid_rows[] = array(
'line' => $line,
'position_key' => $position_key,
'leader_type' => sanitize_key( $row['leader_type'] ?? '' ),
'page_location' => sanitize_key( $row['page_location'] ?? '' ),
'label' => $label,
'government_level' => sanitize_text_field( $row['government_level'] ?? '' ),
'municipality_type' => sanitize_text_field( $row['municipality_type'] ?? '' ),
'role_title' => sanitize_text_field( $row['role_title'] ?? '' ),
'elected_how' => sanitize_text_field( $row['elected_how'] ?? '' ),
'district' => sanitize_text_field( $row['district'] ?? '' ),
'jurisdiction' => sanitize_text_field( $row['jurisdiction'] ?? '' ),
'position_status' => $status,
'sort_order' => isset( $row['sort_order'] ) ? (int) $row['sort_order'] : 0,
'description' => sanitize_textarea_field( $row['description'] ?? '' ),
'live_in_map' => $live_in_map,
'voting_area_map' => $voting_area_map,
'county_id' => $row_county_id,
'live_in_generated_key' => $live_in_generated_key,
'voting_generated_key' => $voting_generated_key,
'in_db' => isset( $existing_set[ $position_key ] ),
);
}
if ( ! $dry_run && ! empty( $result['errors'] ) && ( ! $allow_valid_only || 'replace_registry' === $mode ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Import aborted: validation errors found. Replace Registry requires every row to be valid.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
if ( $dry_run ) {
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
$now = current_time( 'mysql' );
if ( 'replace_registry' === $mode ) {
// County-scoped delete: remove only rows for the explicitly selected county.
if ( function_exists( 'cgop_get_table_county_scope' ) ) {
$scope = cgop_get_table_county_scope( $table );
if ( null === $scope ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Replace Registry requires an active county context. No rows deleted.' );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
if ( false !== $scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$delete_result = $wpdb->delete( $table, array( 'county_id' => $import_county ), array( '%s' ) );
if ( false === $delete_result ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Replace Registry delete failed: ' . ( $wpdb->last_error ?: 'unknown error' ) );
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
$existing_set = array();
} else {
// Pre-wizard (no column) — TRUNCATE only when plugin is not active.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query( "TRUNCATE TABLE {$table}" );
$existing_set = array();
}
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query( "TRUNCATE TABLE {$table}" );
$existing_set = array();
}
}
foreach ( $valid_rows as $vr ) {
$in_db = $vr['in_db'] && 'replace_registry' !== $mode;
if ( $in_db && 'insert_missing' === $mode ) {
$result['skipped']++;
continue;
}
$live_in_map = $vr['live_in_map'];
$voting_area_map = $vr['voting_area_map'];
if ( ! empty( $vr['live_in_generated_key'] ) ) {
$existing_live_in = $in_db && ! empty( $existing_set[ $vr['position_key'] ]['live_in_map'] )
? $existing_set[ $vr['position_key'] ]['live_in_map']
: '';
$generated_result = function_exists( 'CGOP_position_key_save_generated_map' )
? CGOP_position_key_save_generated_map( $vr['live_in_generated_key'], $existing_live_in, $vr['position_key'] )
: new WP_Error( 'generated_map_helper_missing', __( 'Generated map assignment helper is unavailable.', 'county-gop-core' ) );
if ( is_wp_error( $generated_result ) ) {
$result['errors'][] = array( 'row' => $vr['line'], 'msg' => 'Row ' . $vr['line'] . ': live_in_map could not be assigned: ' . $generated_result->get_error_message() );
continue;
}
$live_in_map = (string) $generated_result;
}
if ( ! empty( $vr['voting_generated_key'] ) ) {
$existing_voting = $in_db && ! empty( $existing_set[ $vr['position_key'] ]['voting_area_map'] )
? $existing_set[ $vr['position_key'] ]['voting_area_map']
: '';
$generated_result = function_exists( 'CGOP_position_key_save_generated_map' )
? CGOP_position_key_save_generated_map( $vr['voting_generated_key'], $existing_voting, $vr['position_key'] )
: new WP_Error( 'generated_map_helper_missing', __( 'Generated map assignment helper is unavailable.', 'county-gop-core' ) );
if ( is_wp_error( $generated_result ) ) {
$result['errors'][] = array( 'row' => $vr['line'], 'msg' => 'Row ' . $vr['line'] . ': voting_area_map could not be assigned: ' . $generated_result->get_error_message() );
continue;
}
$voting_area_map = (string) $generated_result;
}
$data = array(
'label' => $vr['label'],
'leader_type' => $vr['leader_type'],
'page_location' => $vr['page_location'],
'government_level' => $vr['government_level'],
'municipality_type' => $vr['municipality_type'],
'role_title' => $vr['role_title'],
'elected_how' => $vr['elected_how'],
'district' => $vr['district'],
'jurisdiction' => $vr['jurisdiction'],
'position_status' => $vr['position_status'],
'sort_order' => $vr['sort_order'],
'description' => $vr['description'],
'live_in_map' => $live_in_map,
'voting_area_map' => $voting_area_map,
'updated_at' => $now,
);
$fmt = array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s' );
// Persist county_id on every write when the column exists.
if ( false !== $import_scope && '' !== $import_county ) {
$data['county_id'] = $import_county;
$fmt[] = '%s';
}
if ( $in_db ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$where = array( 'position_key' => $vr['position_key'] );
$where_format = array( '%s' );
if ( false !== $import_scope ) {
$where['county_id'] = $import_county;
$where_format[] = '%s';
}
$write_result = $wpdb->update( $table, $data, $where, $fmt, $where_format );
if ( false === $write_result ) {
$result['errors'][] = array( 'row' => $vr['line'], 'msg' => "Row {$vr['line']}: DB update failed for '{$vr['position_key']}': " . ( $wpdb->last_error ?: 'unknown error' ) );
continue;
}
$result['updated']++;
} else {
$data['position_key'] = $vr['position_key'];
$data['created_at'] = $now;
$fmt[] = '%s';
$fmt[] = '%s';
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$write_result = $wpdb->insert( $table, $data, $fmt );
if ( false === $write_result ) {
$result['errors'][] = array( 'row' => $vr['line'], 'msg' => "Row {$vr['line']}: DB insert failed for '{$vr['position_key']}': " . ( $wpdb->last_error ?: 'unknown error' ) );
continue;
}
$result['inserted']++;
}
}
CGOP_position_keys_clear_cache();
set_transient( 'CGOP_csv_result_pkeys_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'pkeys' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_import_pkeys', 'CGOP_csv_import_pkeys_handler' );
// ---------------------------------------------------------------------------
// Import: Representatives
// ---------------------------------------------------------------------------
function CGOP_csv_import_reps_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_import_reps' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$mode = isset( $_POST['reps_import_mode'] ) ? sanitize_key( $_POST['reps_import_mode'] ) : 'dry_run';
$dry_run = ( 'dry_run' === $mode );
$allow_valid_only = ! empty( $_POST['reps_import_valid_only'] );
$uid = get_current_user_id();
$result = array(
'mode' => $mode,
'dry_run' => $dry_run,
'parsed' => 0,
'valid' => 0,
'skipped' => 0,
'inserted' => 0,
'updated' => 0,
'errors' => array(),
);
$parsed = CGOP_csv_tools_parse_upload( 'reps_csv_file' );
if ( $parsed['error'] ) {
$result['errors'][] = array( 'row' => 0, 'msg' => $parsed['error'] );
set_transient( 'CGOP_csv_result_reps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'reps' ) ) );
exit;
}
$headers = $parsed['headers'];
$rows = $parsed['rows'];
$required_cols = array(
'representative_import_key',
'post_title',
'leadership_representative_history_key',
'leadership_service_status',
'leadership_government_level',
'leadership_role_title',
);
foreach ( $required_cols as $col ) {
if ( ! in_array( $col, $headers, true ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => "CSV missing required column: {$col}" );
}
}
if ( ! empty( $result['errors'] ) ) {
set_transient( 'CGOP_csv_result_reps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'reps' ) ) );
exit;
}
// Load registry keys for validation.
$registry_set = array();
foreach ( CGOP_get_leadership_position_key_registry_rows() as $rrow ) {
$registry_set[ $rrow->position_key ] = true;
}
// Load existing representative import keys → post IDs.
$existing_import_keys = array();
$keyed_posts = get_posts( array(
'post_type' => 'representative',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
'meta_query' => array(
array( 'key' => '_CGOP_representative_import_key', 'compare' => 'EXISTS' ),
),
) );
foreach ( $keyed_posts as $pid ) {
$k = get_post_meta( $pid, '_CGOP_representative_import_key', true );
if ( $k ) {
$existing_import_keys[ $k ] = $pid;
}
}
$seen_keys = array();
$valid_rows = array();
foreach ( $rows as $i => $row ) {
$line = $i + 2;
$result['parsed']++;
$import_key = sanitize_text_field( trim( $row['representative_import_key'] ?? '' ) );
$post_title = sanitize_text_field( trim( $row['post_title'] ?? '' ) );
if ( '' === $import_key ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: representative_import_key is blank." );
continue;
}
if ( '' === $post_title ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: post_title is blank." );
continue;
}
if ( isset( $seen_keys[ $import_key ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: duplicate representative_import_key '{$import_key}' (first at row {$seen_keys[$import_key]})." );
continue;
}
$history_key = sanitize_title( trim( $row['leadership_representative_history_key'] ?? '' ) );
if ( $history_key && ! isset( $registry_set[ $history_key ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: position key '{$history_key}' not in registry." );
$seen_keys[ $import_key ] = $line;
continue;
}
$service_status = sanitize_key( $row['leadership_service_status'] ?? 'current' );
if ( ! in_array( $service_status, array( 'current', 'past' ), true ) ) {
$service_status = 'current';
}
$position_status = sanitize_key( $row['leadership_representative_position_status'] ?? 'active' );
if ( ! in_array( $position_status, array( 'active', 'historical' ), true ) ) {
$position_status = 'active';
}
$start_year = trim( $row['leadership_service_start_year'] ?? '' );
$end_year = trim( $row['leadership_service_end_year'] ?? '' );
if ( $start_year && ( ! ctype_digit( $start_year ) || 4 !== strlen( $start_year ) ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: leadership_service_start_year must be 4-digit year or blank." );
$seen_keys[ $import_key ] = $line;
continue;
}
if ( $end_year && ( ! ctype_digit( $end_year ) || 4 !== strlen( $end_year ) ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: leadership_service_end_year must be 4-digit year or blank." );
$seen_keys[ $import_key ] = $line;
continue;
}
$post_status = sanitize_key( $row['post_status'] ?? 'publish' );
if ( ! in_array( $post_status, array( 'publish', 'draft', 'private' ), true ) ) {
$post_status = 'publish';
}
$sections_raw = trim( $row['leadership_sections'] ?? 'representatives' );
$sections = array_values( array_filter( array_map( 'sanitize_key', explode( '|', $sections_raw ) ) ) );
if ( ! in_array( 'representatives', $sections, true ) ) {
$sections[] = 'representatives';
}
// CSV column 'leadership_official_profile_embed_url' = meta key 'leadership_embed_official_profile' (true/false).
$embed_raw = $row['leadership_official_profile_embed_url'] ?? '1';
$embed_val = ( '' !== $embed_raw && '0' !== $embed_raw ) ? 1 : 0;
$seen_keys[ $import_key ] = $line;
$result['valid']++;
$valid_rows[] = array(
'line' => $line,
'import_key' => $import_key,
'existing_post_id' => $existing_import_keys[ $import_key ] ?? 0,
'post_title' => $post_title,
'post_name' => sanitize_title( $row['post_name'] ?? '' ),
'post_status' => $post_status,
'menu_order' => isset( $row['menu_order'] ) ? (int) $row['menu_order'] : 0,
'post_excerpt' => sanitize_textarea_field( $row['card_summary'] ?? '' ),
'post_content' => wp_kses_post( $row['profile_content'] ?? '' ),
'sections' => $sections,
'portrait_attachment_id' => isset( $row['portrait_attachment_id'] ) ? (int) $row['portrait_attachment_id'] : 0,
'portrait_url' => esc_url_raw( $row['portrait_url'] ?? '' ),
'leadership_service_status' => $service_status,
'leadership_service_start_year' => $start_year,
'leadership_service_end_year' => $end_year,
'leadership_service_note' => sanitize_text_field( $row['leadership_service_note'] ?? '' ),
'leadership_government_level' => sanitize_key( $row['leadership_government_level'] ?? '' ),
'leadership_role_title' => sanitize_text_field( $row['leadership_role_title'] ?? '' ),
'leadership_district' => sanitize_text_field( $row['leadership_district'] ?? '' ),
'leadership_jurisdiction' => sanitize_text_field( $row['leadership_jurisdiction'] ?? '' ),
'leadership_party_affiliation' => sanitize_text_field( $row['leadership_party_affiliation'] ?? '' ),
'leadership_representative_history_key' => $history_key,
'leadership_representative_position_status' => $position_status,
'leadership_bio' => wp_kses_post( $row['leadership_bio'] ?? '' ),
'leadership_official_url' => esc_url_raw( $row['leadership_official_url'] ?? '' ),
'leadership_wikipedia_url' => esc_url_raw( $row['leadership_wikipedia_url'] ?? '' ),
'leadership_facebook_url' => esc_url_raw( $row['leadership_facebook_url'] ?? '' ),
'leadership_email' => sanitize_email( $row['leadership_email'] ?? '' ),
'leadership_phone' => sanitize_text_field( $row['leadership_phone'] ?? '' ),
'leadership_committee_assignments' => sanitize_textarea_field( $row['leadership_committee_assignments'] ?? '' ),
'leadership_embed_official_profile' => $embed_val,
'_CGOP_leadership_research_source' => esc_url_raw( $row['_CGOP_leadership_research_source'] ?? '' ),
'_CGOP_leadership_review_note' => sanitize_textarea_field( $row['_CGOP_leadership_review_note'] ?? '' ),
);
}
if ( ! $dry_run && ! empty( $result['errors'] ) && ! $allow_valid_only ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Import aborted: validation errors found. Check "import valid rows only" to skip invalid rows.' );
set_transient( 'CGOP_csv_result_reps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'reps' ) ) );
exit;
}
if ( $dry_run ) {
set_transient( 'CGOP_csv_result_reps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'reps' ) ) );
exit;
}
// Replace mode: wipe active representative records before importing.
// Validation passed above, so the wipe is safe. existing_post_id values
// captured before wipe still reference the (now-trashed) posts; wp_update_post
// on a trashed post restores it with the new data.
if ( 'replace_representatives' === $mode ) {
$acf_keys_wipe = CGOP_csv_tools_acf_field_keys();
$wipe_posts = get_posts( array(
'post_type' => 'representative',
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
'meta_query' => array(
array(
'key' => 'leadership_sections',
'value' => '"representatives"',
'compare' => 'LIKE',
),
),
) );
foreach ( $wipe_posts as $wipe_id ) {
$wipe_sections = CGOP_csv_tools_get_sections( $wipe_id );
if ( count( $wipe_sections ) <= 1 ) {
wp_trash_post( $wipe_id );
} else {
$new_sec = array_values( array_filter( $wipe_sections, function( $s ) {
return 'representatives' !== $s;
} ) );
update_post_meta( $wipe_id, 'leadership_sections', $new_sec );
update_post_meta( $wipe_id, '_leadership_sections', $acf_keys_wipe['leadership_sections'] );
}
}
}
$acf_keys = CGOP_csv_tools_acf_field_keys();
foreach ( $valid_rows as $vr ) {
$post_data = array(
'post_type' => 'representative',
'post_title' => $vr['post_title'],
'post_status' => $vr['post_status'],
'menu_order' => $vr['menu_order'],
'post_excerpt' => $vr['post_excerpt'],
'post_content' => $vr['post_content'],
);
if ( $vr['post_name'] ) {
$post_data['post_name'] = $vr['post_name'];
}
$existing_id = $vr['existing_post_id'];
if ( $existing_id ) {
$post_data['ID'] = $existing_id;
$post_id = wp_update_post( $post_data, true );
if ( is_wp_error( $post_id ) ) {
$result['errors'][] = array( 'row' => $vr['line'], 'msg' => "Row {$vr['line']}: update failed for '{$vr['import_key']}': " . $post_id->get_error_message() );
continue;
}
$result['updated']++;
} else {
$post_id = wp_insert_post( $post_data, true );
if ( is_wp_error( $post_id ) ) {
$result['errors'][] = array( 'row' => $vr['line'], 'msg' => "Row {$vr['line']}: insert failed for '{$vr['import_key']}': " . $post_id->get_error_message() );
continue;
}
$result['inserted']++;
}
update_post_meta( $post_id, '_CGOP_representative_import_key', $vr['import_key'] );
$acf_meta_fields = array(
'leadership_sections', 'leadership_service_status', 'leadership_service_start_year',
'leadership_service_end_year', 'leadership_service_note', 'leadership_government_level',
'leadership_role_title', 'leadership_district', 'leadership_jurisdiction',
'leadership_party_affiliation', 'leadership_committee_assignments', 'leadership_official_url',
'leadership_embed_official_profile', 'leadership_representative_history_key',
'leadership_representative_position_status', 'leadership_bio', 'leadership_email',
'leadership_phone', 'leadership_facebook_url', 'leadership_wikipedia_url',
);
foreach ( $acf_meta_fields as $field ) {
$val = ( 'leadership_sections' === $field ) ? $vr['sections'] : ( $vr[ $field ] ?? '' );
update_post_meta( $post_id, $field, $val );
if ( isset( $acf_keys[ $field ] ) ) {
update_post_meta( $post_id, '_' . $field, $acf_keys[ $field ] );
}
}
update_post_meta( $post_id, '_CGOP_leadership_research_source', $vr['_CGOP_leadership_research_source'] );
update_post_meta( $post_id, '_CGOP_leadership_review_note', $vr['_CGOP_leadership_review_note'] );
// Set featured image.
$portrait_id = $vr['portrait_attachment_id'];
if ( $portrait_id && 'attachment' === get_post_type( $portrait_id ) ) {
set_post_thumbnail( $post_id, $portrait_id );
} elseif ( $vr['portrait_url'] ) {
$resolved = attachment_url_to_postid( $vr['portrait_url'] );
if ( $resolved ) {
set_post_thumbnail( $post_id, $resolved );
}
}
}
set_transient( 'CGOP_csv_result_reps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'reps' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_import_reps', 'CGOP_csv_import_reps_handler' );
// ---------------------------------------------------------------------------
// Wipe: Position Keys
// ---------------------------------------------------------------------------
function CGOP_csv_wipe_pkeys_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_wipe_pkeys' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$typed = isset( $_POST['wipe_confirm_pkeys'] ) ? wp_unslash( $_POST['wipe_confirm_pkeys'] ) : '';
if ( 'WIPE POSITION KEYS' !== $typed ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'pkeys_confirm' ) ) );
exit;
}
global $wpdb;
$table = CGOP_position_keys_table();
// County-scoped delete: never TRUNCATE when county-gop-core is active.
if ( function_exists( 'cgop_get_table_county_scope' ) ) {
$scope = cgop_get_table_county_scope( $table );
if ( null === $scope ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'pkeys_no_county_context' ) ) );
exit;
}
if ( false !== $scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->delete( $table, array( 'county_id' => $scope ), array( '%s' ) );
CGOP_position_keys_clear_cache();
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_done' => 'pkeys' ) ) );
exit;
}
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query( "TRUNCATE TABLE {$table}" );
CGOP_position_keys_clear_cache();
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_done' => 'pkeys' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_wipe_pkeys', 'CGOP_csv_wipe_pkeys_handler' );
// ---------------------------------------------------------------------------
// Wipe: Representatives
// ---------------------------------------------------------------------------
function CGOP_csv_wipe_reps_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_wipe_reps' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$typed = isset( $_POST['wipe_confirm_reps'] ) ? wp_unslash( $_POST['wipe_confirm_reps'] ) : '';
if ( 'WIPE REPRESENTATIVES' !== $typed ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'reps_confirm' ) ) );
exit;
}
$permanent = ! empty( $_POST['wipe_reps_permanent'] );
$acf_keys = CGOP_csv_tools_acf_field_keys();
$posts = get_posts( array(
'post_type' => 'representative',
'post_status' => array( 'publish', 'draft', 'private' ),
'posts_per_page' => -1,
'fields' => 'ids',
'no_found_rows' => true,
'update_post_term_cache' => false,
'meta_query' => array(
array(
'key' => 'leadership_sections',
'value' => '"representatives"',
'compare' => 'LIKE',
),
),
) );
$trashed = 0;
$deleted = 0;
$stripped = 0;
foreach ( $posts as $post_id ) {
$sections = CGOP_csv_tools_get_sections( $post_id );
if ( count( $sections ) <= 1 ) {
if ( $permanent ) {
wp_delete_post( $post_id, true );
$deleted++;
} else {
wp_trash_post( $post_id );
$trashed++;
}
} else {
$new_sec = array_values( array_filter( $sections, function( $s ) {
return 'representatives' !== $s;
} ) );
update_post_meta( $post_id, 'leadership_sections', $new_sec );
update_post_meta( $post_id, '_leadership_sections', $acf_keys['leadership_sections'] );
$stripped++;
}
}
wp_safe_redirect( CGOP_csv_tools_admin_url( array(
'wipe_done' => 'reps',
'wipe_trashed' => $trashed,
'wipe_deleted' => $deleted,
'wipe_stripped' => $stripped,
) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_wipe_reps', 'CGOP_csv_wipe_reps_handler' );
// ---------------------------------------------------------------------------
// Wipe: Representative Overlays
// ---------------------------------------------------------------------------
function CGOP_csv_wipe_overlays_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_wipe_overlays' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$confirm = isset( $_POST['wipe_confirm_overlays'] ) ? trim( (string) wp_unslash( $_POST['wipe_confirm_overlays'] ) ) : '';
if ( 'WIPE REPRESENTATIVE OVERLAYS' !== $confirm ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'overlays_confirm' ) ) );
exit;
}
global $wpdb;
$table = function_exists( 'CGOP_overlay_table' ) ? CGOP_overlay_table() : $wpdb->prefix . 'cgop_representative_overlays';
// County-scoped delete: never TRUNCATE when county-gop-core is active.
if ( function_exists( 'cgop_get_table_county_scope' ) ) {
$scope = cgop_get_table_county_scope( $table );
if ( null === $scope ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'overlays_no_county_context' ) ) );
exit;
}
if ( false !== $scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->delete( $table, array( 'county_id' => $scope ), array( '%s' ) );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_done' => 'overlays' ) ) );
exit;
}
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query( "TRUNCATE TABLE `{$table}`" );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_done' => 'overlays' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_wipe_overlays', 'CGOP_csv_wipe_overlays_handler' );
// ---------------------------------------------------------------------------
// Export: Representative Overlays
// ---------------------------------------------------------------------------
function CGOP_csv_export_overlays_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_export_overlays' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
global $wpdb;
$table = function_exists( 'CGOP_overlay_table' ) ? CGOP_overlay_table() : $wpdb->prefix . 'cgop_representative_overlays';
$overlay_order = function_exists( 'CGOP_get_overlay_history_order_sql' )
? CGOP_get_overlay_history_order_sql()
: "CASE WHEN LOWER(TRIM(`leader_status`)) = 'current' THEN 0 ELSE 1 END ASC, COALESCE(`end_year`, `start_year`, 0) DESC, COALESCE(`start_year`, 0) DESC, `name` ASC, `leader_key` ASC";
// County-scoped overlay export.
$ov_selected_county = isset( $_POST['export_county_id'] ) ? sanitize_text_field( wp_unslash( $_POST['export_county_id'] ) ) : '';
$ov_scope = function_exists( 'cgop_get_table_county_scope' ) ? cgop_get_table_county_scope( $table ) : false;
if ( false !== $ov_scope ) {
if ( '' === $ov_selected_county ) {
wp_die( esc_html__( 'Select a county before exporting.', 'county-gop-core' ) );
}
if ( ! function_exists( 'cgop_county_exists' ) || ! cgop_county_exists( $ov_selected_county ) ) {
wp_die( esc_html__( 'Selected county was not found.', 'county-gop-core' ) );
}
if ( ! function_exists( 'cgop_current_user_can_manage_county' ) || ! cgop_current_user_can_manage_county( $ov_selected_county ) ) {
wp_die( esc_html__( 'Not authorized for the selected county.', 'county-gop-core' ) );
}
$ov_export_county = $ov_selected_county;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM `{$table}` WHERE county_id = %s ORDER BY `position_key` ASC, {$overlay_order}",
$ov_export_county
), ARRAY_A );
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$rows = $wpdb->get_results(
"SELECT * FROM `{$table}` ORDER BY `position_key` ASC, {$overlay_order}",
ARRAY_A
);
}
$filename = 'cgop-representative-overlays-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fh = fopen( 'php://output', 'w' );
$include_county_ov = function_exists( 'cgop_get_county_slug' );
$ov_header = array(
'leader_key', 'position_key', 'leader_status', 'leader_post', 'name',
'start_year', 'end_year', 'term_start', 'term_end', 'portrait_id',
'party_affiliation', 'official_url', 'inline_switch',
'wikipedia_url', 'facebook_url', 'email', 'phone', 'bio',
);
if ( $include_county_ov ) {
$ov_header[] = 'county_id';
$ov_header[] = 'county_slug';
}
fputcsv( $fh, $ov_header );
foreach ( (array) $rows as $row ) {
$ov_values = array(
$row['leader_key'] ?? '',
$row['position_key'] ?? '',
$row['leader_status'] ?? '',
$row['leader_post'] ?? '',
$row['name'] ?? '',
$row['start_year'] ?? '',
$row['end_year'] ?? '',
$row['term_start'] ?? '',
$row['term_end'] ?? '',
$row['portrait_id'] ?? '',
$row['party_affiliation'] ?? '',
$row['official_url'] ?? '',
$row['inline_switch'] ?? '',
$row['wikipedia_url'] ?? '',
$row['facebook_url'] ?? '',
$row['email'] ?? '',
$row['phone'] ?? '',
$row['bio'] ?? '',
);
if ( $include_county_ov ) {
$ov_county_id = $row['county_id'] ?? '';
$ov_values[] = $ov_county_id;
$ov_values[] = $ov_county_id ? cgop_get_county_slug( $ov_county_id ) : '';
}
fputcsv( $fh, $ov_values );
}
fclose( $fh );
exit;
}
add_action( 'admin_post_CGOP_csv_export_overlays', 'CGOP_csv_export_overlays_handler' );
// ---------------------------------------------------------------------------
// Import: Representative Overlays
// ---------------------------------------------------------------------------
function CGOP_csv_import_overlays_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_import_overlays' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$mode = isset( $_POST['overlays_import_mode'] ) ? sanitize_key( $_POST['overlays_import_mode'] ) : 'dry_run';
if ( 'upsert' === $mode ) {
$mode = 'update_existing';
}
if ( ! in_array( $mode, array( 'dry_run', 'insert_missing', 'update_existing', 'replace_registry' ), true ) ) {
$mode = 'dry_run';
}
$dry_run = ( 'dry_run' === $mode );
$allow_valid_only = ! empty( $_POST['overlays_import_valid_only'] );
$uid = get_current_user_id();
$result = array(
'mode' => $mode,
'dry_run' => $dry_run,
'parsed' => 0,
'valid' => 0,
'skipped' => 0,
'inserted' => 0,
'updated' => 0,
'errors' => array(),
'generated_keys' => array(),
);
// County scope — determine import county from explicit POST selector.
global $wpdb;
$ov_table = function_exists( 'CGOP_overlay_table' ) ? CGOP_overlay_table() : $wpdb->prefix . 'cgop_representative_overlays';
$import_scope = function_exists( 'cgop_get_table_county_scope' ) ? cgop_get_table_county_scope( $ov_table ) : false;
// Require an explicit county selector when the column exists — never infer from hostname.
$import_county = '';
if ( false !== $import_scope ) {
$raw_import_county = isset( $_POST['overlays_import_county_id'] ) ? sanitize_text_field( wp_unslash( $_POST['overlays_import_county_id'] ) ) : '';
if ( '' === $raw_import_county ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Import county is required. Select a county before importing.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( ! function_exists( 'cgop_county_exists' ) || ! cgop_county_exists( $raw_import_county ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Selected import county was not found.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( ! function_exists( 'cgop_current_user_can_manage_county' ) || ! cgop_current_user_can_manage_county( $raw_import_county ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Not authorized for the selected import county.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$import_county = $raw_import_county;
if ( function_exists( 'cgop_set_current_county_id' ) ) {
cgop_set_current_county_id( $import_county );
}
}
$expected_headers = array(
'leader_key', 'position_key', 'leader_status', 'leader_post', 'name',
'start_year', 'end_year', 'term_start', 'term_end', 'portrait_id',
'party_affiliation', 'official_url', 'inline_switch',
'wikipedia_url', 'facebook_url', 'email', 'phone', 'bio',
);
$parsed = CGOP_csv_tools_parse_upload( 'overlays_csv_file' );
if ( $parsed['error'] ) {
$result['errors'][] = array( 'row' => 0, 'msg' => $parsed['error'] );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$missing = array_diff( $expected_headers, $parsed['headers'] );
if ( $missing ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Missing required headers: ' . implode( ', ', $missing ) );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( false !== $import_scope && ( ! in_array( 'county_id', $parsed['headers'], true ) || ! in_array( 'county_slug', $parsed['headers'], true ) ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'CSV must include county_id and county_slug columns after county adoption.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
// Pre-load known position keys, scoped to the import county when active.
$known_pkeys = array();
if ( function_exists( 'CGOP_position_keys_table' ) ) {
$pk_table = CGOP_position_keys_table();
if ( false !== $import_scope && '' !== $import_county ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$known_pkeys = array_flip( (array) $wpdb->get_col( $wpdb->prepare( "SELECT `position_key` FROM `{$pk_table}` WHERE county_id = %s", $import_county ) ) );
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$known_pkeys = array_flip( (array) $wpdb->get_col( "SELECT `position_key` FROM `{$pk_table}`" ) );
}
}
// Pre-load existing overlay leader_keys, scoped to the import county when active.
$existing_keys = array();
if ( false !== $import_scope && '' !== $import_county ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$existing_keys = array_flip( (array) $wpdb->get_col( $wpdb->prepare( "SELECT `leader_key` FROM `{$ov_table}` WHERE county_id = %s", $import_county ) ) );
} elseif ( false === $import_scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$existing_keys = array_flip( (array) $wpdb->get_col( "SELECT `leader_key` FROM `{$ov_table}`" ) );
}
$valid_rows = array();
foreach ( $parsed['rows'] as $row_idx => $row ) {
$line = $row_idx + 2;
$result['parsed']++;
$raw = array();
foreach ( $expected_headers as $h ) {
$raw[ $h ] = isset( $row[ $h ] ) ? trim( $row[ $h ] ) : '';
}
if ( false !== $import_scope ) {
$file_county_id = sanitize_text_field( $row['county_id'] ?? '' );
$file_county_slug = sanitize_title( $row['county_slug'] ?? '' );
$import_slug = function_exists( 'cgop_get_county_slug' ) ? sanitize_title( cgop_get_county_slug( $import_county ) ) : '';
if ( ! $file_county_id || ! $file_county_slug ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: county_id and county_slug are required after county adoption." );
$result['skipped']++;
continue;
}
if ( $file_county_id && $file_county_id !== $import_county ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: county_id '{$file_county_id}' does not match selected import county '{$import_county}'." );
$result['skipped']++;
continue;
}
if ( $file_county_slug && $import_slug && $file_county_slug !== $import_slug ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: county_slug '{$file_county_slug}' does not match selected import county slug '{$import_slug}'." );
$result['skipped']++;
continue;
}
}
// Validate position_key exists.
$position_key = sanitize_key( $raw['position_key'] );
if ( ! $position_key ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: position_key is required." );
if ( ! $allow_valid_only ) {
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$result['skipped']++;
continue;
}
if ( ! isset( $known_pkeys[ $position_key ] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: position_key '{$position_key}' not found in registry." );
if ( ! $allow_valid_only ) {
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$result['skipped']++;
continue;
}
// Validate name.
if ( empty( $raw['name'] ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: name is required." );
if ( ! $allow_valid_only ) {
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$result['skipped']++;
continue;
}
// Validate portrait_id if provided.
$portrait_id_raw = $raw['portrait_id'];
if ( '' !== $portrait_id_raw ) {
if ( ! ctype_digit( $portrait_id_raw ) || (int) $portrait_id_raw <= 0 ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: portrait_id must be a positive integer attachment ID (not a URL)." );
if ( ! $allow_valid_only ) {
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$result['skipped']++;
continue;
}
$pid = (int) $portrait_id_raw;
if ( ! wp_attachment_is( 'image', $pid ) ) {
$result['errors'][] = array( 'row' => $line, 'msg' => "Row {$line}: portrait_id {$pid} does not resolve to an image attachment." );
if ( ! $allow_valid_only ) {
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
$result['skipped']++;
continue;
}
}
// Generate leader_key if blank.
$leader_key = sanitize_text_field( $raw['leader_key'] );
if ( empty( $leader_key ) ) {
$leader_key = function_exists( 'CGOP_generate_leader_key' ) ? CGOP_generate_leader_key() : wp_generate_uuid4();
$result['generated_keys'][] = array( 'row' => $line, 'key' => $leader_key, 'name' => $raw['name'] );
}
$result['valid']++;
$valid_rows[] = array(
'leader_key' => $leader_key,
'in_db' => isset( $existing_keys[ $leader_key ] ),
'position_key' => $position_key,
'leader_status' => in_array( sanitize_key( $raw['leader_status'] ), array( 'current', 'past' ), true ) ? sanitize_key( $raw['leader_status'] ) : 'current',
'leader_post' => function_exists( 'CGOP_normalize_overlay_leader_post' )
? CGOP_normalize_overlay_leader_post( $raw['leader_post'] )
: ( in_array( sanitize_key( $raw['leader_post'] ), array( 'published', 'unpublished' ), true ) ? sanitize_key( $raw['leader_post'] ) : 'unpublished' ),
'name' => sanitize_text_field( $raw['name'] ),
'start_year' => ( '' !== $raw['start_year'] && ctype_digit( $raw['start_year'] ) ) ? (int) $raw['start_year'] : null,
'end_year' => ( '' !== $raw['end_year'] && ctype_digit( $raw['end_year'] ) ) ? (int) $raw['end_year'] : null,
'term_start' => sanitize_text_field( $raw['term_start'] ),
'term_end' => sanitize_text_field( $raw['term_end'] ),
'portrait_id' => ( '' !== $raw['portrait_id'] && ctype_digit( $raw['portrait_id'] ) ) ? (int) $raw['portrait_id'] : null,
'party_affiliation' => sanitize_text_field( $raw['party_affiliation'] ),
'official_url' => esc_url_raw( sanitize_text_field( $raw['official_url'] ) ),
'inline_switch' => ! empty( $raw['inline_switch'] ) && '0' !== $raw['inline_switch'] ? 1 : 0,
'wikipedia_url' => esc_url_raw( sanitize_text_field( $raw['wikipedia_url'] ) ),
'facebook_url' => esc_url_raw( sanitize_text_field( $raw['facebook_url'] ) ),
'email' => sanitize_email( $raw['email'] ),
'phone' => sanitize_text_field( $raw['phone'] ),
'bio' => sanitize_textarea_field( $raw['bio'] ),
);
}
if ( ! $dry_run && ! empty( $result['errors'] ) && ( ! $allow_valid_only || 'replace_registry' === $mode ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Import aborted: validation errors found. Replace Registry requires every row to be valid.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( $dry_run ) {
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( ! function_exists( 'CGOP_overlay_insert' ) || ! function_exists( 'CGOP_overlay_update' ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Representative overlay functions not available.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( 'replace_registry' === $mode ) {
global $wpdb;
$table = CGOP_overlay_table();
// County-scoped delete: use the explicitly selected import county.
if ( function_exists( 'cgop_get_table_county_scope' ) ) {
$scope = cgop_get_table_county_scope( $table );
if ( null === $scope ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Replace Registry requires an active county context. No rows deleted.' );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
if ( false !== $scope ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$delete_result = $wpdb->delete( $table, array( 'county_id' => $import_county ), array( '%s' ) );
if ( false === $delete_result ) {
$result['errors'][] = array( 'row' => 0, 'msg' => 'Replace Registry delete failed: ' . ( $wpdb->last_error ?: 'unknown error' ) );
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
} else {
// Pre-wizard fallback — TRUNCATE only when plugin not yet active.
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query( "TRUNCATE TABLE `{$table}`" );
}
} else {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$wpdb->query( "TRUNCATE TABLE `{$table}`" );
}
$existing_keys = array();
foreach ( $valid_rows as $idx => $vr ) {
$valid_rows[ $idx ]['in_db'] = false;
}
}
foreach ( $valid_rows as $vr ) {
$data = $vr;
unset( $data['in_db'] );
$leader_key = $data['leader_key'];
unset( $data['leader_key'] );
if ( $vr['in_db'] && 'insert_missing' === $mode ) {
$result['skipped']++;
continue;
}
if ( $vr['in_db'] ) {
$update_result = CGOP_overlay_update( $leader_key, $data );
if ( is_wp_error( $update_result ) ) {
$result['errors'][] = array( 'row' => 0, 'msg' => "leader_key '{$leader_key}': update failed — " . $update_result->get_error_message() );
continue;
}
if ( false === $update_result ) {
$result['errors'][] = array( 'row' => 0, 'msg' => "leader_key '{$leader_key}': DB update failed." );
continue;
}
$result['updated']++;
} else {
// Direct INSERT preserving the supplied/generated leader_key.
$ins_table = CGOP_overlay_table();
$cols = array(
'leader_key', 'position_key', 'leader_status', 'leader_post', 'name',
'term_start', 'term_end', 'party_affiliation', 'official_url', 'inline_switch',
'wikipedia_url', 'facebook_url', 'email', 'phone', 'bio',
);
$vals = array(
$leader_key,
$data['position_key'],
$data['leader_status'],
$data['leader_post'],
$data['name'],
$data['term_start'],
$data['term_end'],
$data['party_affiliation'],
$data['official_url'],
(int) $data['inline_switch'],
$data['wikipedia_url'],
$data['facebook_url'],
$data['email'],
$data['phone'],
$data['bio'],
);
$fmts = array(
'%s', '%s', '%s', '%s', '%s',
'%s', '%s', '%s', '%s', '%d',
'%s', '%s', '%s', '%s', '%s',
);
foreach ( array( 'start_year' => '%d', 'end_year' => '%d', 'portrait_id' => '%d' ) as $col => $fmt ) {
if ( null !== $data[ $col ] ) {
$cols[] = $col;
$vals[] = (int) $data[ $col ];
$fmts[] = $fmt;
}
}
// Persist county_id when the column exists.
if ( false !== $import_scope && '' !== $import_county ) {
$cols[] = 'county_id';
$vals[] = $import_county;
$fmts[] = '%s';
}
$col_str = '`' . implode( '`, `', $cols ) . '`';
$ph_str = implode( ', ', $fmts );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
$ins_result = $wpdb->query( $wpdb->prepare( "INSERT INTO `{$ins_table}` ({$col_str}) VALUES ({$ph_str})", $vals ) );
if ( false === $ins_result ) {
$result['errors'][] = array( 'row' => 0, 'msg' => "leader_key '{$leader_key}': DB insert failed — " . ( $wpdb->last_error ?: 'unknown error' ) );
continue;
}
$result['inserted']++;
}
}
set_transient( 'CGOP_csv_result_overlays_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'overlays' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_import_overlays', 'CGOP_csv_import_overlays_handler' );
// ---------------------------------------------------------------------------
// Export: GIS Beacon Layers
// ---------------------------------------------------------------------------
function CGOP_csv_export_gis_layers_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_export_gis_layers' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$layers = CGOP_gis_get_custom_layer_registry();
usort( $layers, function( $a, $b ) {
$cmp = strnatcasecmp( $a['label'] ?? '', $b['label'] ?? '' );
if ( 0 !== $cmp ) {
return $cmp;
}
return strnatcasecmp( $a['id'] ?? '', $b['id'] ?? '' );
} );
$filename = 'cgop-gis-beacon-layers-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fh = fopen( 'php://output', 'w' );
fputcsv( $fh, array( 'id', 'label', 'beacon_layer_id', 'position_type', 'level', 'output_dir', 'preferred_fields', 'position_name_template', 'position_id_template' ) );
foreach ( $layers as $layer ) {
$pf = $layer['preferred_fields'] ?? array();
if ( is_array( $pf ) ) {
$pf = implode( ', ', $pf );
}
fputcsv( $fh, array(
$layer['id'] ?? '',
$layer['label'] ?? '',
$layer['beacon_layer_id'] ?? '',
$layer['position_type'] ?? '',
$layer['level'] ?? '',
$layer['output_dir'] ?? '',
$pf,
$layer['position_name_template'] ?? '',
$layer['position_id_template'] ?? '',
) );
}
fclose( $fh );
exit;
}
add_action( 'admin_post_CGOP_csv_export_gis_layers', 'CGOP_csv_export_gis_layers_handler' );
// ---------------------------------------------------------------------------
// Export: Generated GIS Maps
// ---------------------------------------------------------------------------
function CGOP_csv_export_gis_maps_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_export_gis_maps' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$status = array();
if ( defined( 'CGOP_GIS_IMPORT_STATUS_OPTION' ) ) {
$status = get_option( CGOP_GIS_IMPORT_STATUS_OPTION, array() );
}
$filename = 'cgop-gis-generated-maps-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Pragma: no-cache' );
header( 'Expires: 0' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fh = fopen( 'php://output', 'w' );
fputcsv( $fh, array( 'generated_map_id', 'layer_id', 'layer_label', 'beacon_layer_id', 'generated_at', 'position_id', 'label', 'source_key', 'file', 'url', 'existing_assignment_map_ids', 'existing_assignment_position_keys' ) );
$map_rows_by_url = array();
foreach ( CGOP_get_map_boundary_rows() as $map_row ) {
$resolved_url = CGOP_resolve_map_boundary_json_url( $map_row->json_file ?? '' );
if ( '' === $resolved_url ) {
continue;
}
if ( ! isset( $map_rows_by_url[ $resolved_url ] ) ) {
$map_rows_by_url[ $resolved_url ] = array();
}
$map_rows_by_url[ $resolved_url ][] = $map_row;
}
if ( ! empty( $status['layers'] ) && is_array( $status['layers'] ) ) {
foreach ( $status['layers'] as $layer ) {
$layer_id = $layer['layer_id'] ?? '';
$layer_label = $layer['label'] ?? '';
$beacon_layer_id = $layer['beacon_layer_id'] ?? '';
$generated_at = $layer['generated_at'] ?? '';
$files = $layer['files'] ?? array();
foreach ( (array) $files as $frow ) {
$rel_file = $frow['file'] ?? '';
$url = '' !== $rel_file ? CGOP_gis_get_generated_file_url( $rel_file ) : '';
$assigned_map_ids = array();
$assigned_position_keys = array();
if ( $url && ! empty( $map_rows_by_url[ $url ] ) ) {
foreach ( $map_rows_by_url[ $url ] as $map_row ) {
$assigned_map_ids[] = $map_row->map_id ?? '';
$assigned_position_keys[] = $map_row->position_key ?? '';
}
}
fputcsv( $fh, array(
'generated:' . ltrim( (string) $rel_file, '/' ),
$layer_id,
$layer_label,
$beacon_layer_id,
$generated_at,
$frow['position_id'] ?? '',
$frow['label'] ?? '',
$frow['source_key'] ?? '',
$rel_file,
$url,
implode( ', ', array_filter( $assigned_map_ids ) ),
implode( ', ', array_filter( $assigned_position_keys ) ),
) );
}
}
}
fclose( $fh );
exit;
}
add_action( 'admin_post_CGOP_csv_export_gis_maps', 'CGOP_csv_export_gis_maps_handler' );
// ---------------------------------------------------------------------------
// Import: GIS Beacon Layers
// ---------------------------------------------------------------------------
function CGOP_csv_import_gis_layers_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_import_gis_layers' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$mode = isset( $_POST['gis_layers_import_mode'] ) ? sanitize_key( wp_unslash( $_POST['gis_layers_import_mode'] ) ) : 'dry_run';
if ( ! in_array( $mode, array( 'dry_run', 'insert_missing', 'update_existing', 'replace_registry' ), true ) ) {
$mode = 'dry_run';
}
$result = array(
'parsed' => 0,
'valid' => 0,
'skipped' => 0,
'inserted' => 0,
'updated' => 0,
'errors' => array(),
);
$parsed_rows = CGOP_csv_tools_parse_upload( 'gis_layers_csv_file' );
if ( is_wp_error( $parsed_rows ) ) {
$result['errors'][] = $parsed_rows->get_error_message();
$uid = get_current_user_id();
set_transient( 'CGOP_csv_result_gis_layers_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'gis_layers' ) ) );
exit;
}
$required_cols = array( 'label', 'beacon_layer_id', 'position_id_template' );
$seen_ids = array();
$valid_rows = array();
foreach ( $parsed_rows as $row_num => $row ) {
$result['parsed']++;
$row_label = 'Row ' . ( $row_num + 2 );
$missing = array();
foreach ( $required_cols as $col ) {
if ( ! isset( $row[ $col ] ) || '' === trim( $row[ $col ] ) ) {
$missing[] = $col;
}
}
if ( $missing ) {
$result['errors'][] = $row_label . ': missing required column(s): ' . implode( ', ', $missing );
continue;
}
$beacon_id = (int) trim( $row['beacon_layer_id'] );
if ( $beacon_id <= 0 ) {
$result['errors'][] = $row_label . ': beacon_layer_id must be a positive integer.';
continue;
}
$raw_id = isset( $row['id'] ) ? trim( $row['id'] ) : '';
if ( '' !== $raw_id && isset( $seen_ids[ $raw_id ] ) ) {
$result['errors'][] = $row_label . ': duplicate id "' . $raw_id . '" in CSV (first seen at row ' . $seen_ids[ $raw_id ] . ').';
continue;
}
if ( '' !== $raw_id ) {
$seen_ids[ $raw_id ] = $row_num + 2;
}
$pf_raw = isset( $row['preferred_fields'] ) ? trim( $row['preferred_fields'] ) : '';
$pf_raw = str_replace( '|', ',', $pf_raw );
$raw_config = array(
'id' => $raw_id,
'label' => trim( $row['label'] ),
'beacon_layer_id' => $beacon_id,
'position_type' => isset( $row['position_type'] ) ? trim( $row['position_type'] ) : '',
'level' => isset( $row['level'] ) ? trim( $row['level'] ) : '',
'output_dir' => isset( $row['output_dir'] ) ? trim( $row['output_dir'] ) : '',
'preferred_fields' => $pf_raw,
'position_name_template' => isset( $row['position_name_template'] ) ? trim( $row['position_name_template'] ) : '',
'position_id_template' => trim( $row['position_id_template'] ),
);
$normalized = CGOP_gis_normalize_layer_config( $raw_config );
$result['valid']++;
$valid_rows[] = $normalized;
}
if ( 'dry_run' === $mode ) {
$uid = get_current_user_id();
set_transient( 'CGOP_csv_result_gis_layers_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'gis_layers' ) ) );
exit;
}
$option_key = defined( 'CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION' )
? CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION
: 'CGOP_gis_boundary_import_custom_layers';
$existing_registry = CGOP_gis_get_custom_layer_registry();
$existing_by_id = array();
foreach ( $existing_registry as $el ) {
$existing_by_id[ $el['id'] ] = $el;
}
if ( 'replace_registry' === $mode ) {
$new_registry = array();
foreach ( $valid_rows as $vrow ) {
$new_registry[ $vrow['id'] ] = $vrow;
$result['inserted']++;
}
update_option( $option_key, array_values( $new_registry ) );
} else {
$merged = $existing_by_id;
foreach ( $valid_rows as $vrow ) {
$vid = $vrow['id'];
if ( 'insert_missing' === $mode ) {
if ( isset( $merged[ $vid ] ) ) {
$result['skipped']++;
} else {
$merged[ $vid ] = $vrow;
$result['inserted']++;
}
} elseif ( 'update_existing' === $mode ) {
if ( isset( $merged[ $vid ] ) ) {
$merged[ $vid ] = $vrow;
$result['updated']++;
} else {
$merged[ $vid ] = $vrow;
$result['inserted']++;
}
}
}
update_option( $option_key, array_values( $merged ) );
}
$uid = get_current_user_id();
set_transient( 'CGOP_csv_result_gis_layers_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'gis_layers' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_import_gis_layers', 'CGOP_csv_import_gis_layers_handler' );
// ---------------------------------------------------------------------------
// Import: Generated GIS Maps
// ---------------------------------------------------------------------------
function CGOP_csv_import_gis_maps_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_import_gis_maps' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$mode = isset( $_POST['gis_maps_import_mode'] ) ? sanitize_key( wp_unslash( $_POST['gis_maps_import_mode'] ) ) : 'dry_run';
if ( ! in_array( $mode, array( 'dry_run', 'upsert_status', 'replace_status' ), true ) ) {
$mode = 'dry_run';
}
$result = array(
'parsed' => 0,
'valid' => 0,
'skipped' => 0,
'inserted' => 0,
'updated' => 0,
'errors' => array(),
);
$parsed_rows = CGOP_csv_tools_parse_upload( 'gis_maps_csv_file' );
if ( is_wp_error( $parsed_rows ) ) {
$result['errors'][] = $parsed_rows->get_error_message();
$uid = get_current_user_id();
set_transient( 'CGOP_csv_result_gis_maps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'gis_maps' ) ) );
exit;
}
$required_cols = array( 'layer_id', 'layer_label', 'beacon_layer_id', 'position_id', 'label', 'file' );
$generated_dir = CGOP_gis_get_generated_dir();
$is_dry_run = ( 'dry_run' === $mode );
$seen_layer_file = array();
$valid_rows = array();
foreach ( $parsed_rows as $row_num => $row ) {
$result['parsed']++;
$row_label = 'Row ' . ( $row_num + 2 );
$missing = array();
foreach ( $required_cols as $col ) {
if ( ! isset( $row[ $col ] ) || '' === trim( $row[ $col ] ) ) {
$missing[] = $col;
}
}
if ( $missing ) {
$result['errors'][] = $row_label . ': missing required column(s): ' . implode( ', ', $missing );
continue;
}
$beacon_id = (int) trim( $row['beacon_layer_id'] );
if ( $beacon_id <= 0 ) {
$result['errors'][] = $row_label . ': beacon_layer_id must be a positive integer.';
continue;
}
$rel_file = trim( $row['file'] );
if ( false !== strpos( $rel_file, '..' ) ) {
$result['errors'][] = $row_label . ': file path must not contain "..".';
continue;
}
$ext = strtolower( pathinfo( $rel_file, PATHINFO_EXTENSION ) );
if ( ! in_array( $ext, array( 'geojson', 'json' ), true ) ) {
$result['errors'][] = $row_label . ': file must end in .geojson or .json.';
continue;
}
if ( '' !== $generated_dir ) {
$abs_file = rtrim( $generated_dir, '/\\' ) . DIRECTORY_SEPARATOR . ltrim( $rel_file, '/\\' );
$real_gen = realpath( $generated_dir );
$real_abs = realpath( dirname( $abs_file ) );
if ( false !== $real_gen && false !== $real_abs && 0 !== strpos( $real_abs . DIRECTORY_SEPARATOR, $real_gen . DIRECTORY_SEPARATOR ) ) {
$result['errors'][] = $row_label . ': file path resolves outside the generated directory.';
continue;
}
if ( ! $is_dry_run && ! file_exists( $abs_file ) ) {
$result['errors'][] = $row_label . ': generated file does not exist on disk: ' . $rel_file;
continue;
}
}
$layer_id = sanitize_key( trim( $row['layer_id'] ) );
$combo_key = $layer_id . '|||' . $rel_file;
if ( isset( $seen_layer_file[ $combo_key ] ) ) {
$result['errors'][] = $row_label . ': duplicate layer_id + file combination (first seen at row ' . $seen_layer_file[ $combo_key ] . ').';
continue;
}
$seen_layer_file[ $combo_key ] = $row_num + 2;
$result['valid']++;
$valid_rows[] = array(
'layer_id' => $layer_id,
'layer_label' => sanitize_text_field( trim( $row['layer_label'] ) ),
'beacon_layer_id' => $beacon_id,
'generated_at' => isset( $row['generated_at'] ) ? sanitize_text_field( trim( $row['generated_at'] ) ) : '',
'position_id' => sanitize_text_field( trim( $row['position_id'] ) ),
'label' => sanitize_text_field( trim( $row['label'] ) ),
'source_key' => isset( $row['source_key'] ) ? sanitize_text_field( trim( $row['source_key'] ) ) : '',
'file' => $rel_file,
);
}
if ( $is_dry_run ) {
$uid = get_current_user_id();
set_transient( 'CGOP_csv_result_gis_maps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'gis_maps' ) ) );
exit;
}
$status_option = defined( 'CGOP_GIS_IMPORT_STATUS_OPTION' )
? CGOP_GIS_IMPORT_STATUS_OPTION
: 'CGOP_gis_boundary_import_status';
$existing_status = get_option( $status_option, array() );
$existing_layers = ( ! empty( $existing_status['layers'] ) && is_array( $existing_status['layers'] ) )
? $existing_status['layers']
: array();
if ( 'replace_status' === $mode ) {
$new_layers = array();
foreach ( $valid_rows as $vrow ) {
$lid = $vrow['layer_id'];
if ( ! isset( $new_layers[ $lid ] ) ) {
$new_layers[ $lid ] = array(
'layer_id' => $lid,
'label' => $vrow['layer_label'],
'beacon_layer_id' => $vrow['beacon_layer_id'],
'generated_at' => '' !== $vrow['generated_at'] ? $vrow['generated_at'] : gmdate( 'Y-m-d\TH:i:s\Z' ),
'feature_count' => 0,
'files' => array(),
);
}
if ( '' !== $vrow['generated_at'] ) {
$new_layers[ $lid ]['generated_at'] = $vrow['generated_at'];
}
$new_layers[ $lid ]['files'][] = array(
'position_id' => $vrow['position_id'],
'label' => $vrow['label'],
'source_key' => $vrow['source_key'],
'file' => $vrow['file'],
);
$result['inserted']++;
}
foreach ( $new_layers as &$nl ) {
$nl['feature_count'] = count( $nl['files'] );
}
unset( $nl );
$new_status = array( 'layers' => $new_layers );
} else {
// upsert_status
$merged_layers = $existing_layers;
foreach ( $valid_rows as $vrow ) {
$lid = $vrow['layer_id'];
if ( ! isset( $merged_layers[ $lid ] ) ) {
$merged_layers[ $lid ] = array(
'layer_id' => $lid,
'label' => $vrow['layer_label'],
'beacon_layer_id' => $vrow['beacon_layer_id'],
'generated_at' => '' !== $vrow['generated_at'] ? $vrow['generated_at'] : gmdate( 'Y-m-d\TH:i:s\Z' ),
'feature_count' => 0,
'files' => array(),
);
}
if ( '' !== $vrow['generated_at'] ) {
$merged_layers[ $lid ]['generated_at'] = $vrow['generated_at'];
}
$new_file_entry = array(
'position_id' => $vrow['position_id'],
'label' => $vrow['label'],
'source_key' => $vrow['source_key'],
'file' => $vrow['file'],
);
$found = false;
foreach ( $merged_layers[ $lid ]['files'] as $fi => $existing_file ) {
if ( ( $existing_file['file'] ?? '' ) === $vrow['file'] ) {
$merged_layers[ $lid ]['files'][ $fi ] = $new_file_entry;
$result['updated']++;
$found = true;
break;
}
}
if ( ! $found ) {
$merged_layers[ $lid ]['files'][] = $new_file_entry;
$result['inserted']++;
}
}
foreach ( $merged_layers as &$ml ) {
$ml['feature_count'] = count( $ml['files'] );
}
unset( $ml );
$new_status = array( 'layers' => $merged_layers );
}
update_option( $status_option, $new_status );
CGOP_gis_write_manifest( $new_status );
$uid = get_current_user_id();
set_transient( 'CGOP_csv_result_gis_maps_' . $uid, $result, 120 );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'csv_result' => 'gis_maps' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_import_gis_maps', 'CGOP_csv_import_gis_maps_handler' );
// ---------------------------------------------------------------------------
// Wipe: GIS Beacon Layers
// ---------------------------------------------------------------------------
function CGOP_csv_wipe_gis_layers_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_wipe_gis_layers' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$typed = isset( $_POST['wipe_confirm_gis_layers'] ) ? trim( (string) wp_unslash( $_POST['wipe_confirm_gis_layers'] ) ) : '';
if ( 'WIPE GIS BEACON LAYERS' !== $typed ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'gis_layers_confirm' ) ) );
exit;
}
$option_key = defined( 'CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION' )
? CGOP_GIS_IMPORT_CUSTOM_LAYERS_OPTION
: 'CGOP_gis_boundary_import_custom_layers';
update_option( $option_key, array() );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_done' => 'gis_layers' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_wipe_gis_layers', 'CGOP_csv_wipe_gis_layers_handler' );
// ---------------------------------------------------------------------------
// Wipe: Generated GIS Map Status
// ---------------------------------------------------------------------------
function CGOP_csv_wipe_gis_maps_handler() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission.', 'county-gop-core' ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : '';
if ( ! wp_verify_nonce( $nonce, 'CGOP_csv_wipe_gis_maps' ) ) {
wp_die( esc_html__( 'Security check failed.', 'county-gop-core' ) );
}
$typed = isset( $_POST['wipe_confirm_gis_maps'] ) ? trim( (string) wp_unslash( $_POST['wipe_confirm_gis_maps'] ) ) : '';
if ( 'WIPE GIS GENERATED MAP STATUS' !== $typed ) {
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_error' => 'gis_maps_confirm' ) ) );
exit;
}
$status_option = defined( 'CGOP_GIS_IMPORT_STATUS_OPTION' )
? CGOP_GIS_IMPORT_STATUS_OPTION
: 'CGOP_gis_boundary_import_status';
$empty_status = array( 'layers' => array() );
update_option( $status_option, $empty_status );
CGOP_gis_write_manifest( $empty_status );
wp_safe_redirect( CGOP_csv_tools_admin_url( array( 'wipe_done' => 'gis_maps' ) ) );
exit;
}
add_action( 'admin_post_CGOP_csv_wipe_gis_maps', 'CGOP_csv_wipe_gis_maps_handler' );
// ---------------------------------------------------------------------------
// Admin screen
// ---------------------------------------------------------------------------
function CGOP_csv_tools_screen() {
if ( ! current_user_can( CGOP_POSITION_KEYS_CAP ) ) {
wp_die( esc_html__( 'You do not have permission to view this page.', 'county-gop-core' ) );
}
$uid = get_current_user_id();
$pkeys_result = get_transient( 'CGOP_csv_result_pkeys_' . $uid );
$reps_result = get_transient( 'CGOP_csv_result_reps_' . $uid );
$overlays_result = get_transient( 'CGOP_csv_result_overlays_' . $uid );
$gis_layers_result = get_transient( 'CGOP_csv_result_gis_layers_' . $uid );
$gis_maps_result = get_transient( 'CGOP_csv_result_gis_maps_' . $uid );
if ( $pkeys_result ) {
delete_transient( 'CGOP_csv_result_pkeys_' . $uid );
}
if ( $reps_result ) {
delete_transient( 'CGOP_csv_result_reps_' . $uid );
}
if ( $overlays_result ) {
delete_transient( 'CGOP_csv_result_overlays_' . $uid );
}
if ( $gis_layers_result ) {
delete_transient( 'CGOP_csv_result_gis_layers_' . $uid );
}
if ( $gis_maps_result ) {
delete_transient( 'CGOP_csv_result_gis_maps_' . $uid );
}
$counts = CGOP_csv_tools_get_counts();
?>
<div class="wrap cgop-csv-tools">
<h1><?php esc_html_e( 'Representative Data CSV Tools', 'county-gop-core' ); ?></h1>
<style>
.cgop-csv-tools {
max-width: 1220px;
}
.cgop-csv-tools .notice.inline {
margin-left: 0;
}
.cgop-csv-counts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin: 18px 0;
max-width: 920px;
}
.cgop-csv-count {
background: #fff;
border: 1px solid #dcdcde;
border-left: 4px solid #7b2428;
border-radius: 3px;
padding: 14px 16px;
}
.cgop-csv-count strong {
display: block;
color: #1d2327;
font-size: 26px;
line-height: 1.1;
margin-bottom: 4px;
}
.cgop-csv-count span {
color: #50575e;
font-size: 13px;
}
.cgop-csv-section {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04);
margin: 20px 0;
max-width: 980px;
padding: 0;
}
.cgop-csv-section__header {
background: #f6f7f7;
border-bottom: 1px solid #dcdcde;
padding: 14px 18px;
}
.cgop-csv-section__header h2 {
margin: 0;
}
.cgop-csv-section__header p {
margin: 6px 0 0;
color: #50575e;
}
.cgop-csv-section__body {
padding: 18px;
}
.cgop-csv-action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 18px;
}
.cgop-csv-action {
border: 1px solid #dcdcde;
border-radius: 4px;
padding: 16px;
}
.cgop-csv-action h3 {
font-size: 15px;
margin: 0 0 8px;
}
.cgop-csv-action .form-table {
margin-top: 8px;
}
.cgop-csv-action .form-table th {
width: 145px;
}
.cgop-csv-radio-list label {
display: block;
margin: 0 0 7px;
}
.cgop-csv-workflow {
column-count: 2;
column-gap: 34px;
margin-bottom: 0;
max-width: 760px;
}
.cgop-csv-workflow li {
break-inside: avoid;
margin-bottom: 8px;
}
.cgop-csv-tools > h2 {
background: #fff;
border: 1px solid #dcdcde;
border-left: 4px solid #7b2428;
border-radius: 4px 4px 0 0;
margin: 22px 0 0;
max-width: 940px;
padding: 14px 18px;
}
.cgop-csv-tools > h3 {
background: #f6f7f7;
border: 1px solid #dcdcde;
border-bottom: 0;
margin: 16px 0 0;
max-width: 900px;
padding: 12px 18px;
}
.cgop-csv-tools > h2 + p,
.cgop-csv-tools > h3 + p,
.cgop-csv-tools > h2 + .description,
.cgop-csv-tools > h3 + .description,
.cgop-csv-tools > h2 + p + form,
.cgop-csv-tools > h3 + p + form,
.cgop-csv-tools > form {
background: #fff;
border-left: 1px solid #dcdcde;
border-right: 1px solid #dcdcde;
margin: 0;
max-width: 940px;
padding: 12px 18px 16px;
}
.cgop-csv-tools > form {
border-bottom: 1px solid #dcdcde;
border-radius: 0 0 4px 4px;
margin-bottom: 16px;
}
.cgop-csv-tools > .description + h3,
.cgop-csv-tools > form + h3 {
margin-top: 18px;
}
.cgop-csv-tools > hr {
border: 0;
border-top: 1px solid #dcdcde;
margin: 24px 0;
max-width: 980px;
}
.cgop-csv-tools .form-table {
margin-top: 0;
}
.cgop-csv-tools .form-table th {
padding-left: 0;
width: 170px;
}
.cgop-csv-tools .form-table td label {
display: block;
margin-bottom: 7px;
}
.cgop-csv-danger {
border-color: #d63638;
}
.cgop-csv-danger .cgop-csv-section__header {
background: #fcf0f1;
border-bottom-color: #f1c4c6;
}
.cgop-csv-danger-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 18px;
}
.cgop-csv-danger-card {
background: #fff8f8;
border: 1px solid #d63638;
border-radius: 4px;
padding: 16px;
}
.cgop-csv-danger-card h3 {
margin-top: 0;
}
</style>
<?php
$_wipe_done = isset( $_GET['wipe_done'] ) ? sanitize_key( $_GET['wipe_done'] ) : '';
$_wipe_error = isset( $_GET['wipe_error'] ) ? sanitize_key( $_GET['wipe_error'] ) : '';
if ( 'pkeys' === $_wipe_done ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Position key registry wiped.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'overlays' === $_wipe_done ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Representative overlays wiped.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'gis_layers' === $_wipe_done ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'GIS Beacon Layer registry wiped.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'gis_maps' === $_wipe_done ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'GIS Generated Map Status wiped and manifest updated.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'pkeys_confirm' === $_wipe_error ) : ?>
<div class="notice notice-error is-dismissible"><p><?php esc_html_e( 'Wipe cancelled: type exactly WIPE POSITION KEYS to confirm.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'overlays_confirm' === $_wipe_error ) : ?>
<div class="notice notice-error is-dismissible"><p><?php esc_html_e( 'Wipe cancelled: type exactly WIPE REPRESENTATIVE OVERLAYS to confirm.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'gis_layers_confirm' === $_wipe_error ) : ?>
<div class="notice notice-error is-dismissible"><p><?php esc_html_e( 'Wipe cancelled: type exactly WIPE GIS BEACON LAYERS to confirm.', 'county-gop-core' ); ?></p></div>
<?php elseif ( 'gis_maps_confirm' === $_wipe_error ) : ?>
<div class="notice notice-error is-dismissible"><p><?php esc_html_e( 'Wipe cancelled: type exactly WIPE GIS GENERATED MAP STATUS to confirm.', 'county-gop-core' ); ?></p></div>
<?php endif; ?>
<div class="cgop-csv-counts" aria-label="<?php esc_attr_e( 'CSV data counts', 'county-gop-core' ); ?>">
<div class="cgop-csv-count">
<strong><?php echo (int) $counts['position_keys']; ?></strong>
<span><?php esc_html_e( 'Position keys', 'county-gop-core' ); ?></span>
</div>
<div class="cgop-csv-count">
<strong><?php echo (int) ( $counts['overlay_count'] ?? 0 ); ?></strong>
<span><?php esc_html_e( 'Representative overlays', 'county-gop-core' ); ?></span>
</div>
<div class="cgop-csv-count">
<strong><?php echo (int) ( $counts['gis_beacon_layers'] ?? 0 ); ?></strong>
<span><?php esc_html_e( 'GIS Beacon layers', 'county-gop-core' ); ?></span>
</div>
<div class="cgop-csv-count">
<strong><?php echo (int) ( $counts['gis_generated_files'] ?? 0 ); ?></strong>
<span><?php esc_html_e( 'Generated map files', 'county-gop-core' ); ?></span>
</div>
</div>
<div class="notice notice-warning inline" style="max-width:700px;">
<p><strong><?php esc_html_e( 'Current workflow only.', 'county-gop-core' ); ?></strong>
<?php esc_html_e( 'Position Keys create the cards. Representative Overlays put people on those cards. Legacy CSV tools are intentionally hidden from this page.', 'county-gop-core' ); ?></p>
</div>
<div class="cgop-csv-section">
<div class="cgop-csv-section__header">
<h2><?php esc_html_e( 'Clean Reload Workflow', 'county-gop-core' ); ?></h2>
<p><?php esc_html_e( 'Use this order when exporting, editing, and loading cleaned representative data.', 'county-gop-core' ); ?></p>
</div>
<div class="cgop-csv-section__body">
<ol class="cgop-csv-workflow">
<li><?php esc_html_e( 'Export Position Keys CSV.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Export Representative Overlays CSV.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Edit CSV files locally.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Dry-run Position Keys import.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Dry-run Representative Overlays import.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Import cleaned Position Keys CSV.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Import cleaned Representative Overlays CSV.', 'county-gop-core' ); ?></li>
<li><?php esc_html_e( 'Visit the Representatives page and verify.', 'county-gop-core' ); ?></li>
</ol>
</div>
</div>
<h2><?php esc_html_e( 'Export Position Keys', 'county-gop-core' ); ?></h2>
<p><?php printf( esc_html__( '%d position key(s) will be exported.', 'county-gop-core' ), (int) $counts['position_keys'] ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_export_pkeys">
<?php wp_nonce_field( 'CGOP_csv_export_pkeys' ); ?>
<?php $pkeys_export_selector = CGOP_csv_county_selector_row( 'export_county_id', '', true );
if ( $pkeys_export_selector ) : ?>
<table class="form-table" style="max-width:700px;"><?php echo $pkeys_export_selector; // phpcs:ignore WordPress.Security.EscapeOutput ?></table>
<?php endif; ?>
<?php submit_button( __( 'Download Position Keys CSV', 'county-gop-core' ), 'secondary', 'submit', false ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'Import Position Keys', 'county-gop-core' ); ?></h2>
<?php if ( $pkeys_result ) {
CGOP_csv_tools_render_result( $pkeys_result, __( 'Position Keys', 'county-gop-core' ) );
if ( ! empty( $pkeys_result['generated_keys'] ) ) :
?>
<div class="notice notice-info inline" style="max-width:700px;margin-bottom:1em;">
<p><strong><?php esc_html_e( 'Generated position IDs for blank rows:', 'county-gop-core' ); ?></strong></p>
<ul style="margin:.25em 0 .5em 1.5em;font-family:monospace;font-size:12px;">
<?php foreach ( $pkeys_result['generated_keys'] as $gk ) : ?>
<li><?php printf( esc_html__( 'Row %1$d: %2$s', 'county-gop-core' ), (int) $gk['row'], esc_html( $gk['key'] ) ); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php
endif;
} ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="CGOP_csv_import_pkeys">
<?php wp_nonce_field( 'CGOP_csv_import_pkeys' ); ?>
<p class="description" style="max-width:700px;">
<?php esc_html_e( 'The position_key column may be blank for new rows. Blank values are assigned UUID-style position IDs during dry run and import. The live_in_map and voting_area_map columns accept existing Map Boundary IDs, or generated map IDs from the Generated GIS Maps export in the form generated:path/to/file.geojson.', 'county-gop-core' ); ?>
</p>
<table class="form-table" style="max-width:700px;">
<tr>
<th scope="row"><label for="pkeys_csv_file"><?php esc_html_e( 'Position Keys CSV', 'county-gop-core' ); ?></label></th>
<td><input type="file" id="pkeys_csv_file" name="pkeys_csv_file" accept=".csv,text/csv" required></td>
</tr>
<?php echo CGOP_csv_county_selector_row( 'import_county_id', '', false ); // phpcs:ignore WordPress.Security.EscapeOutput ?>
<tr>
<th scope="row"><?php esc_html_e( 'Import Mode', 'county-gop-core' ); ?></th>
<td>
<label><input type="radio" name="pkeys_import_mode" value="dry_run" checked> <?php esc_html_e( 'Dry Run - parse and report counts/errors, no writes', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="pkeys_import_mode" value="insert_missing"> <?php esc_html_e( 'Insert Missing - add rows not already in registry', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="pkeys_import_mode" value="update_existing"> <?php esc_html_e( 'Update Existing - upsert all rows', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="pkeys_import_mode" value="replace_registry"> <?php esc_html_e( 'Replace Registry - truncate position key table then import', 'county-gop-core' ); ?></label>
</td>
</tr>
<tr>
<th scope="row"></th>
<td><label><input type="checkbox" name="pkeys_import_valid_only" value="1"> <?php esc_html_e( 'Import valid rows only - skip invalid rows instead of aborting', 'county-gop-core' ); ?></label></td>
</tr>
</table>
<?php submit_button( __( 'Import Position Keys', 'county-gop-core' ) ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'Export Representative Overlays', 'county-gop-core' ); ?></h2>
<p><?php printf( esc_html__( '%d overlay row(s) will be exported.', 'county-gop-core' ), (int) ( $counts['overlay_count'] ?? 0 ) ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_export_overlays">
<?php wp_nonce_field( 'CGOP_csv_export_overlays' ); ?>
<?php $ov_export_selector = CGOP_csv_county_selector_row( 'export_county_id', '', true );
if ( $ov_export_selector ) : ?>
<table class="form-table" style="max-width:700px;"><?php echo $ov_export_selector; // phpcs:ignore WordPress.Security.EscapeOutput ?></table>
<?php endif; ?>
<?php submit_button( __( 'Download Representative Overlays CSV', 'county-gop-core' ), 'secondary', 'submit', false ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'Import Representative Overlays', 'county-gop-core' ); ?></h2>
<?php if ( $overlays_result ) {
CGOP_csv_tools_render_result( $overlays_result, __( 'Representative Overlays', 'county-gop-core' ) );
if ( ! empty( $overlays_result['generated_keys'] ) ) :
?>
<div class="notice notice-info inline" style="max-width:700px;margin-bottom:1em;">
<p><strong><?php esc_html_e( 'Generated leader keys for blank rows:', 'county-gop-core' ); ?></strong></p>
<ul style="margin:.25em 0 .5em 1.5em;font-family:monospace;font-size:12px;">
<?php foreach ( $overlays_result['generated_keys'] as $gk ) : ?>
<li><?php printf( esc_html__( 'Row %1$d - %2$s: %3$s', 'county-gop-core' ), (int) $gk['row'], esc_html( $gk['name'] ), esc_html( $gk['key'] ) ); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php
endif;
} ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="CGOP_csv_import_overlays">
<?php wp_nonce_field( 'CGOP_csv_import_overlays' ); ?>
<p class="description" style="max-width:700px;">
<?php esc_html_e( 'The party_affiliation column accepts the standard party keys or any custom party label. Custom values are imported as written after normal text sanitizing.', 'county-gop-core' ); ?>
</p>
<table class="form-table" style="max-width:700px;">
<tr>
<th scope="row"><label for="overlays_csv_file"><?php esc_html_e( 'Representative Overlays CSV', 'county-gop-core' ); ?></label></th>
<td><input type="file" id="overlays_csv_file" name="overlays_csv_file" accept=".csv,text/csv" required></td>
</tr>
<?php echo CGOP_csv_county_selector_row( 'overlays_import_county_id', '', false ); // phpcs:ignore WordPress.Security.EscapeOutput ?>
<tr>
<th scope="row"><?php esc_html_e( 'Import Mode', 'county-gop-core' ); ?></th>
<td>
<label><input type="radio" name="overlays_import_mode" value="dry_run" checked> <?php esc_html_e( 'Dry Run - parse and validate only, no writes', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="overlays_import_mode" value="insert_missing"> <?php esc_html_e( 'Insert Missing - add overlay rows not already in the table', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="overlays_import_mode" value="update_existing"> <?php esc_html_e( 'Update Existing - update matching rows and insert missing rows', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="overlays_import_mode" value="replace_registry"> <?php esc_html_e( 'Replace Registry - wipe representative overlay rows, then import this CSV', 'county-gop-core' ); ?></label>
</td>
</tr>
<tr>
<th scope="row"></th>
<td><label><input type="checkbox" name="overlays_import_valid_only" value="1"> <?php esc_html_e( 'Import valid rows only - skip invalid rows instead of aborting', 'county-gop-core' ); ?></label></td>
</tr>
</table>
<?php submit_button( __( 'Import Representative Overlays', 'county-gop-core' ) ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'GIS Beacon Layers', 'county-gop-core' ); ?></h2>
<p class="description" style="max-width:700px;">
<?php esc_html_e( 'Beacon Layer definitions control which Beacon API layers are imported to generate GeoJSON map files. Export to back up your configuration; import to restore or modify it. Importing does not regenerate map files or call Beacon.', 'county-gop-core' ); ?>
</p>
<h3><?php esc_html_e( 'Export GIS Beacon Layers', 'county-gop-core' ); ?></h3>
<p><?php printf( esc_html__( '%d beacon layer definition(s) will be exported.', 'county-gop-core' ), (int) ( $counts['gis_beacon_layers'] ?? 0 ) ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_export_gis_layers">
<?php wp_nonce_field( 'CGOP_csv_export_gis_layers' ); ?>
<?php submit_button( __( 'Download GIS Beacon Layers CSV', 'county-gop-core' ), 'secondary', 'submit', false ); ?>
</form>
<h3><?php esc_html_e( 'Import GIS Beacon Layers', 'county-gop-core' ); ?></h3>
<?php if ( $gis_layers_result ) {
CGOP_csv_tools_render_result( $gis_layers_result, __( 'GIS Beacon Layers', 'county-gop-core' ) );
} ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="CGOP_csv_import_gis_layers">
<?php wp_nonce_field( 'CGOP_csv_import_gis_layers' ); ?>
<p class="description" style="max-width:700px;">
<?php esc_html_e( 'Required columns: label, beacon_layer_id, position_id_template. The id and output_dir columns are optional; blank IDs are generated automatically. The preferred_fields column accepts comma-separated or newline-separated values.', 'county-gop-core' ); ?>
</p>
<table class="form-table" style="max-width:700px;">
<tr>
<th scope="row"><label for="gis_layers_csv_file"><?php esc_html_e( 'GIS Beacon Layers CSV', 'county-gop-core' ); ?></label></th>
<td><input type="file" id="gis_layers_csv_file" name="gis_layers_csv_file" accept=".csv,text/csv" required></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Import Mode', 'county-gop-core' ); ?></th>
<td>
<label><input type="radio" name="gis_layers_import_mode" value="dry_run" checked> <?php esc_html_e( 'Dry Run - validate only, no writes', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="gis_layers_import_mode" value="insert_missing"> <?php esc_html_e( 'Insert Missing - add layers not already in registry (skip existing ids)', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="gis_layers_import_mode" value="update_existing"> <?php esc_html_e( 'Update Existing - update matching ids and insert missing ones', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="gis_layers_import_mode" value="replace_registry"> <?php esc_html_e( 'Replace Registry - replace all layer definitions with valid rows from this CSV', 'county-gop-core' ); ?></label>
</td>
</tr>
</table>
<?php submit_button( __( 'Import GIS Beacon Layers', 'county-gop-core' ) ); ?>
</form>
<hr>
<h2><?php esc_html_e( 'GIS Generated Maps', 'county-gop-core' ); ?></h2>
<p class="description" style="max-width:700px;">
<?php esc_html_e( 'Generated map status tracks which GeoJSON files have been produced for each layer. Export for audit or backup; import to restore status records. The generated_map_id column is the value you can paste into live_in_map or voting_area_map in a Position Keys CSV. Importing Generated GIS Maps does not create or modify physical GeoJSON files and does not call Beacon. Physical files are managed from GOP Setup → GIS Boundary Import.', 'county-gop-core' ); ?>
</p>
<h3><?php esc_html_e( 'Export Generated GIS Maps', 'county-gop-core' ); ?></h3>
<p><?php printf( esc_html__( '%d generated map file record(s) will be exported.', 'county-gop-core' ), (int) ( $counts['gis_generated_files'] ?? 0 ) ); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_export_gis_maps">
<?php wp_nonce_field( 'CGOP_csv_export_gis_maps' ); ?>
<?php submit_button( __( 'Download Generated GIS Maps CSV', 'county-gop-core' ), 'secondary', 'submit', false ); ?>
</form>
<h3><?php esc_html_e( 'Import Generated GIS Maps', 'county-gop-core' ); ?></h3>
<?php if ( $gis_maps_result ) {
CGOP_csv_tools_render_result( $gis_maps_result, __( 'GIS Generated Maps', 'county-gop-core' ) );
} ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="CGOP_csv_import_gis_maps">
<?php wp_nonce_field( 'CGOP_csv_import_gis_maps' ); ?>
<p class="description" style="max-width:700px;">
<?php esc_html_e( 'Required columns: layer_id, layer_label, beacon_layer_id, position_id, label, file. The generated_map_id, existing_assignment_map_ids, and existing_assignment_position_keys export columns are informational and ignored on Generated GIS Maps import. The file column must be a relative path ending in .geojson or .json. On non-dry-run import, each file must exist on disk in the generated directory.', 'county-gop-core' ); ?>
</p>
<table class="form-table" style="max-width:700px;">
<tr>
<th scope="row"><label for="gis_maps_csv_file"><?php esc_html_e( 'Generated GIS Maps CSV', 'county-gop-core' ); ?></label></th>
<td><input type="file" id="gis_maps_csv_file" name="gis_maps_csv_file" accept=".csv,text/csv" required></td>
</tr>
<tr>
<th scope="row"><?php esc_html_e( 'Import Mode', 'county-gop-core' ); ?></th>
<td>
<label><input type="radio" name="gis_maps_import_mode" value="dry_run" checked> <?php esc_html_e( 'Dry Run - validate only, no writes', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="gis_maps_import_mode" value="upsert_status"> <?php esc_html_e( 'Upsert Status - merge rows into current status (update matching layer+file, insert new)', 'county-gop-core' ); ?></label><br>
<label><input type="radio" name="gis_maps_import_mode" value="replace_status"> <?php esc_html_e( 'Replace Status - replace all status records with valid rows from this CSV', 'county-gop-core' ); ?></label>
</td>
</tr>
</table>
<?php submit_button( __( 'Import Generated GIS Maps', 'county-gop-core' ) ); ?>
</form>
<hr>
<h2 style="color:#a00;"><?php esc_html_e( 'Danger Zone', 'county-gop-core' ); ?></h2>
<div style="border:2px solid #a00;padding:1.5em;max-width:700px;margin-bottom:2em;background:#fff8f8;">
<h3 style="margin-top:0;"><?php esc_html_e( 'Wipe Position Key Registry', 'county-gop-core' ); ?></h3>
<p><?php printf(
/* translators: %d: row count. */
esc_html__( 'Deletes all %d position key(s) from the registry. Overlay rows are not deleted, but will not display until matching position keys are re-imported.', 'county-gop-core' ),
(int) $counts['position_keys']
); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_wipe_pkeys">
<?php wp_nonce_field( 'CGOP_csv_wipe_pkeys' ); ?>
<p>
<label for="wipe_confirm_pkeys"><?php esc_html_e( 'Type exactly to confirm:', 'county-gop-core' ); ?> <strong>WIPE POSITION KEYS</strong></label><br>
<input type="text" id="wipe_confirm_pkeys" name="wipe_confirm_pkeys" class="regular-text" autocomplete="off" style="margin-top:.4em;">
</p>
<?php submit_button( __( 'Wipe Position Key Registry', 'county-gop-core' ), 'delete', 'submit', false ); ?>
</form>
</div>
<div style="border:2px solid #a00;padding:1.5em;max-width:700px;margin-bottom:2em;background:#fff8f8;">
<h3 style="margin-top:0;"><?php esc_html_e( 'Wipe Representative Overlays', 'county-gop-core' ); ?></h3>
<p><?php printf(
/* translators: %d: row count. */
esc_html__( 'Deletes all %d representative overlay row(s). Position keys are not deleted.', 'county-gop-core' ),
(int) ( $counts['overlay_count'] ?? 0 )
); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_wipe_overlays">
<?php wp_nonce_field( 'CGOP_csv_wipe_overlays' ); ?>
<p>
<label for="wipe_confirm_overlays"><?php esc_html_e( 'Type exactly to confirm:', 'county-gop-core' ); ?> <strong>WIPE REPRESENTATIVE OVERLAYS</strong></label><br>
<input type="text" id="wipe_confirm_overlays" name="wipe_confirm_overlays" class="regular-text" autocomplete="off" style="margin-top:.4em;">
</p>
<?php submit_button( __( 'Wipe Representative Overlays', 'county-gop-core' ), 'delete', 'submit', false ); ?>
</form>
</div>
<div style="border:2px solid #a00;padding:1.5em;max-width:700px;margin-bottom:2em;background:#fff8f8;">
<h3 style="margin-top:0;"><?php esc_html_e( 'Wipe GIS Beacon Layers', 'county-gop-core' ); ?></h3>
<p><?php printf(
/* translators: %d: layer count. */
esc_html__( 'Clears all %d GIS Beacon Layer definition(s) from the registry. Generated map files and status records are not affected.', 'county-gop-core' ),
(int) ( $counts['gis_beacon_layers'] ?? 0 )
); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_wipe_gis_layers">
<?php wp_nonce_field( 'CGOP_csv_wipe_gis_layers' ); ?>
<p>
<label for="wipe_confirm_gis_layers"><?php esc_html_e( 'Type exactly to confirm:', 'county-gop-core' ); ?> <strong>WIPE GIS BEACON LAYERS</strong></label><br>
<input type="text" id="wipe_confirm_gis_layers" name="wipe_confirm_gis_layers" class="regular-text" autocomplete="off" style="margin-top:.4em;">
</p>
<?php submit_button( __( 'Wipe GIS Beacon Layers', 'county-gop-core' ), 'delete', 'submit', false ); ?>
</form>
</div>
<div style="border:2px solid #a00;padding:1.5em;max-width:700px;background:#fff8f8;">
<h3 style="margin-top:0;"><?php esc_html_e( 'Wipe Generated GIS Map Status', 'county-gop-core' ); ?></h3>
<p><?php printf(
/* translators: %d: file count. */
esc_html__( 'Clears all %d generated map file status record(s) and rewrites the manifest. Physical GeoJSON files on disk are not deleted. Beacon Layer definitions are not affected.', 'county-gop-core' ),
(int) ( $counts['gis_generated_files'] ?? 0 )
); ?></p>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="CGOP_csv_wipe_gis_maps">
<?php wp_nonce_field( 'CGOP_csv_wipe_gis_maps' ); ?>
<p>
<label for="wipe_confirm_gis_maps"><?php esc_html_e( 'Type exactly to confirm:', 'county-gop-core' ); ?> <strong>WIPE GIS GENERATED MAP STATUS</strong></label><br>
<input type="text" id="wipe_confirm_gis_maps" name="wipe_confirm_gis_maps" class="regular-text" autocomplete="off" style="margin-top:.4em;">
</p>
<?php submit_button( __( 'Wipe Generated GIS Map Status', 'county-gop-core' ), 'delete', 'submit', false ); ?>
</form>
</div>
</div>
<?php
}
/**
* Render an import result summary block.
*
* @param array $result Result array from handler.
* @param string $label Human label (Position Keys / Representatives).
*/
function CGOP_csv_tools_render_result( $result, $label ) {
$type = empty( $result['errors'] ) ? 'success' : 'warning';
$dry = ! empty( $result['dry_run'] );
?>
<div class="notice notice-<?php echo esc_attr( $type ); ?> inline" style="max-width:700px;margin-bottom:1.25em;">
<p><strong><?php
if ( $dry ) {
/* translators: %s: data type label. */
printf( esc_html__( '%s — Dry Run Results', 'county-gop-core' ), esc_html( $label ) );
} else {
/* translators: %s: data type label. */
printf( esc_html__( '%s — Import Results', 'county-gop-core' ), esc_html( $label ) );
}
?></strong></p>
<ul style="margin:.25em 0 .5em 1.5em;">
<li><?php printf( esc_html__( 'Rows parsed: %d', 'county-gop-core' ), (int) $result['parsed'] ); ?></li>
<li><?php printf( esc_html__( 'Rows valid: %d', 'county-gop-core' ), (int) $result['valid'] ); ?></li>
<li><?php printf( esc_html__( 'Rows skipped: %d', 'county-gop-core' ), (int) $result['skipped'] ); ?></li>
<?php if ( ! $dry ) : ?>
<li><?php printf( esc_html__( 'Rows inserted: %d', 'county-gop-core' ), (int) $result['inserted'] ); ?></li>
<li><?php printf( esc_html__( 'Rows updated: %d', 'county-gop-core' ), (int) $result['updated'] ); ?></li>
<?php endif; ?>
</ul>
<?php if ( ! empty( $result['errors'] ) ) : ?>
<p><strong><?php esc_html_e( 'Errors:', 'county-gop-core' ); ?></strong></p>
<ul style="margin:.25em 0 .5em 1.5em;color:#a00;">
<?php foreach ( $result['errors'] as $e ) : ?>
<li><?php echo esc_html( $e['msg'] ); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php
}