diff --git a/Dockerfile b/Dockerfile index cfaf7523..c3c516f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,5 +24,16 @@ RUN apt-get update && apt-get install -y \ && docker-php-ext-install xml \ && a2enmod rewrite +# Copy script.sh and make it executable +COPY script.sh /usr/local/bin/startup.sh +RUN sed -i 's/\r$//' /usr/local/bin/startup.sh && chmod +x /usr/local/bin/startup.sh + +# Configure PHP for larger file uploads (30MB) +RUN echo "upload_max_filesize = 30M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "post_max_size = 35M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "memory_limit = 64M" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini \ + && echo "max_input_time = 300" >> /usr/local/etc/php/conf.d/uploads.ini + # Expose port 80 EXPOSE 80 \ No newline at end of file diff --git a/Dockerfile-db b/Dockerfile-db index ac550b76..e4ce0ce8 100644 --- a/Dockerfile-db +++ b/Dockerfile-db @@ -4,5 +4,9 @@ FROM mariadb:latest # Add the install.sql file to the docker image ADD install/assets/install.sql /docker-entrypoint-initdb.d +# Create a healthcheck script that uses mariadb-admin +RUN echo '#!/bin/bash\nmariadb-admin ping -h "localhost" --silent' > /usr/local/bin/healthcheck.sh \ + && chmod +x /usr/local/bin/healthcheck.sh + # Expose port 3306 EXPOSE 3306 \ No newline at end of file diff --git a/application/config/migration.php b/application/config/migration.php index 60928918..1b160613 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -22,7 +22,7 @@ $config['migration_enabled'] = TRUE; | */ -$config['migration_version'] = 204; +$config['migration_version'] = 205; /* |-------------------------------------------------------------------------- diff --git a/application/controllers/Adif.php b/application/controllers/Adif.php index 64750fc5..18cdb2ac 100644 --- a/application/controllers/Adif.php +++ b/application/controllers/Adif.php @@ -13,16 +13,6 @@ class adif extends CI_Controller { if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('notice', 'You\'re not allowed to do that!'); redirect('dashboard'); } } - public function test() { - if(validateADIFDate('20120228') == true){ - echo "valid date"; - } else { - echo "date incorrect"; - } - - - } - /* Shows Export Views */ public function export() { diff --git a/application/controllers/Dashboard.php b/application/controllers/Dashboard.php index 4cdc91d2..f133c081 100644 --- a/application/controllers/Dashboard.php +++ b/application/controllers/Dashboard.php @@ -3,6 +3,16 @@ class Dashboard extends CI_Controller { + public function __construct() + { + parent::__construct(); + + // Load common models that are used across multiple methods + $this->load->model('user_model'); + $this->load->model('logbook_model'); + $this->load->model('logbooks_model'); + } + public function index() { // If environment is set to development then show the debug toolbar @@ -13,10 +23,6 @@ class Dashboard extends CI_Controller // Load language files $this->lang->load('lotw'); - // Database connections - $this->load->model('logbook_model'); - $this->load->model('user_model'); - // LoTW infos $this->load->model('LotwCert'); @@ -31,7 +37,6 @@ class Dashboard extends CI_Controller redirect('user/login'); } - $this->load->model('logbooks_model'); $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); /* @@ -63,9 +68,11 @@ class Dashboard extends CI_Controller $this->load->model('stations'); $this->load->model('setup_model'); - $data['countryCount'] = $this->setup_model->getCountryCount(); - $data['logbookCount'] = $this->setup_model->getLogbookCount(); - $data['locationCount'] = $this->setup_model->getLocationCount(); + // Use consolidated setup counts instead of 3 separate queries + $setup_counts = $this->setup_model->getAllSetupCounts(); + $data['countryCount'] = $setup_counts['country_count']; + $data['logbookCount'] = $setup_counts['logbook_count']; + $data['locationCount'] = $setup_counts['location_count']; $data['current_active'] = $this->stations->find_active(); @@ -85,19 +92,21 @@ class Dashboard extends CI_Controller $data['radio_status'] = $this->cat->recent_status(); - // Store info - $data['todays_qsos'] = $this->logbook_model->todays_qsos($logbooks_locations_array); - $data['total_qsos'] = $this->logbook_model->total_qsos($logbooks_locations_array); - $data['month_qsos'] = $this->logbook_model->month_qsos($logbooks_locations_array); - $data['year_qsos'] = $this->logbook_model->year_qsos($logbooks_locations_array); + // Store info - Use consolidated query for QSO statistics + $qso_stats = $this->logbook_model->get_qso_statistics_consolidated($logbooks_locations_array); + $data['todays_qsos'] = $qso_stats['todays_qsos']; + $data['total_qsos'] = $qso_stats['total_qsos']; + $data['month_qsos'] = $qso_stats['month_qsos']; + $data['year_qsos'] = $qso_stats['year_qsos']; - // Load Countries Breakdown data into array - $CountriesBreakdown = $this->logbook_model->total_countries_confirmed($logbooks_locations_array); - - $data['total_countries'] = $CountriesBreakdown['Countries_Worked']; - $data['total_countries_confirmed_paper'] = $CountriesBreakdown['Countries_Worked_QSL']; - $data['total_countries_confirmed_eqsl'] = $CountriesBreakdown['Countries_Worked_EQSL']; - $data['total_countries_confirmed_lotw'] = $CountriesBreakdown['Countries_Worked_LOTW']; + // Use consolidated countries statistics instead of separate queries + $countries_stats = $this->logbook_model->get_countries_statistics_consolidated($logbooks_locations_array); + + $data['total_countries'] = $countries_stats['Countries_Worked']; + $data['total_countries_confirmed_paper'] = $countries_stats['Countries_Worked_QSL']; + $data['total_countries_confirmed_eqsl'] = $countries_stats['Countries_Worked_EQSL']; + $data['total_countries_confirmed_lotw'] = $countries_stats['Countries_Worked_LOTW']; + $current_countries = $countries_stats['Countries_Current']; $data['dashboard_upcoming_dx_card'] = false; $data['dashboard_qslcard_card'] = false; @@ -107,53 +116,23 @@ class Dashboard extends CI_Controller $dashboard_options = $this->user_options_model->get_options('dashboard')->result(); + // Optimize options processing - convert to associative array for O(1) lookup + $options_map = array(); foreach ($dashboard_options as $item) { - $option_name = $item->option_name; - $option_key = $item->option_key; - $option_value = $item->option_value; - - if ($option_name == 'dashboard_upcoming_dx_card' && $option_key == 'enabled') { - if($option_value == 'true') { - $data['dashboard_upcoming_dx_card'] = true; - } else { - $data['dashboard_upcoming_dx_card'] = false; - } - } + $options_map[$item->option_name][$item->option_key] = $item->option_value; + } - if ($option_name == 'dashboard_qslcards_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_qslcard_card'] = true; - } else { - $data['dashboard_qslcard_card'] = false; - } - } + // Quick lookups instead of nested loops + $data['dashboard_upcoming_dx_card'] = isset($options_map['dashboard_upcoming_dx_card']['enabled']) && $options_map['dashboard_upcoming_dx_card']['enabled'] == 'true'; + $data['dashboard_qslcard_card'] = isset($options_map['dashboard_qslcards_card']['enabled']) && $options_map['dashboard_qslcards_card']['enabled'] == 'true'; + $data['dashboard_eqslcard_card'] = isset($options_map['dashboard_eqslcards_card']['enabled']) && $options_map['dashboard_eqslcards_card']['enabled'] == 'true'; + $data['dashboard_lotw_card'] = isset($options_map['dashboard_lotw_card']['enabled']) && $options_map['dashboard_lotw_card']['enabled'] == 'true'; + $data['dashboard_vuccgrids_card'] = isset($options_map['dashboard_vuccgrids_card']['enabled']) && $options_map['dashboard_vuccgrids_card']['enabled'] == 'true'; - if ($option_name == 'dashboard_eqslcards_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_eqslcard_card'] = true; - } else { - $data['dashboard_eqslcard_card'] = false; - } - } - - if ($option_name == 'dashboard_lotw_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_lotw_card'] = true; - } else { - $data['dashboard_lotw_card'] = false; - } - } - - if ($option_name == 'dashboard_vuccgrids_card' && $option_key == 'enabled') { - if($item->option_value == 'true') { - $data['dashboard_vuccgrids_card'] = true; - - $data['vucc'] = $this->vucc->fetchVuccSummary(); - $data['vuccSAT'] = $this->vucc->fetchVuccSummary('SAT'); - } else { - $data['dashboard_vuccgrids_card'] = false; - } - } + // Only load VUCC data if the card is actually enabled + if ($data['dashboard_vuccgrids_card']) { + $data['vucc'] = $this->vucc->fetchVuccSummary(); + $data['vuccSAT'] = $this->vucc->fetchVuccSummary('SAT'); } @@ -185,12 +164,10 @@ class Dashboard extends CI_Controller $data['page_title'] = "Dashboard"; + // Optimize DXCC calculation - get count directly instead of loading all records $this->load->model('dxcc'); - $dxcc = $this->dxcc->list_current(); - - $current = $this->logbook_model->total_countries_current($logbooks_locations_array); - - $data['total_countries_needed'] = count($dxcc->result()) - $current; + $total_dxcc_count = $this->dxcc->get_total_dxcc_count(); + $data['total_countries_needed'] = $total_dxcc_count - $current_countries; $this->load->view('interface_assets/header', $data); $this->load->view('dashboard/index'); @@ -199,30 +176,23 @@ class Dashboard extends CI_Controller } public function todays_qso_component() { - $this->load->model('user_model'); - if ($this->user_model->validate_session() == 0) { // User is not logged in - } else { - $this->load->model('logbook_model'); - $this->load->model('logbooks_model'); + return; } $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); - $data['todays_qsos'] = $this->logbook_model->todays_qsos($logbooks_locations_array); + // Use consolidated query instead of individual todays_qsos call + $qso_stats = $this->logbook_model->get_qso_statistics_consolidated($logbooks_locations_array); + $data['todays_qsos'] = $qso_stats['todays_qsos']; $this->load->view('components/dashboard_todays_qsos', $data); - } public function logbook_display_component() { - $this->load->model('user_model'); - if ($this->user_model->validate_session() == 0) { // User is not logged in - } else { - $this->load->model('logbook_model'); - $this->load->model('logbooks_model'); + return; } // Get Logbook Locations diff --git a/application/controllers/Update.php b/application/controllers/Update.php index deb74779..bd8ba7cc 100644 --- a/application/controllers/Update.php +++ b/application/controllers/Update.php @@ -219,6 +219,16 @@ class Update extends CI_Controller { $this->update_status("DONE"); } + public function get_status() { + $status_file = $this->make_update_path("status.html"); + if (file_exists($status_file)) { + $content = file_get_contents($status_file); + echo $content; + } else { + echo "No status available"; + } + } + public function update_status($done=""){ if ($done != "Downloading file"){ @@ -234,7 +244,16 @@ class Update extends CI_Controller { $html = $done."....
"; } - file_put_contents($this->make_update_path("status.html"), $html); + $status_file = $this->make_update_path("status.html"); + if (file_put_contents($status_file, $html) === FALSE) { + log_message('error', 'Failed to write status file: ' . $status_file); + // Try to create the directory if it doesn't exist + $dir = dirname($status_file); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + file_put_contents($status_file, $html); + } + } } diff --git a/application/controllers/Workabledxcc.php b/application/controllers/Workabledxcc.php index e28ed67b..fda5adb8 100644 --- a/application/controllers/Workabledxcc.php +++ b/application/controllers/Workabledxcc.php @@ -29,70 +29,81 @@ class Workabledxcc extends CI_Controller public function dxcclist() { - $json = file_get_contents($this->optionslib->get_option('dxped_url')); - - // Decode the JSON data into a PHP array $dataResult = json_decode($json, true); - // Initialize an empty array to store the required data - $requiredData = array(); + if (empty($dataResult)) { + $data['dxcclist'] = array(); + $this->load->view('/workabledxcc/components/dxcclist', $data); + return; + } // Get Date format if ($this->session->userdata('user_date_format')) { - // If Logged in and session exists $custom_date_format = $this->session->userdata('user_date_format'); } else { - // Get Default date format from /config/cloudlog.php $custom_date_format = $this->config->item('qso_date_format'); } - // Iterate through the decoded JSON data - foreach ($dataResult as $item) { - // Create a new array with the required fields and add it to the main array - $oldStartDate = DateTime::createFromFormat('Y-m-d', $item['0']); + // Load models once + $this->load->model('logbook_model'); + $this->load->model('Workabledxcc_model'); + // Get all DXCC entities for all callsigns in one batch + $callsigns = array_column($dataResult, 'callsign'); + $dates = array_column($dataResult, '0'); + $dxccEntities = $this->Workabledxcc_model->batchDxccLookup($callsigns, $dates); + + // Get worked/confirmed status for all entities in batch + $uniqueEntities = array_unique(array_filter($dxccEntities)); + $dxccStatus = $this->Workabledxcc_model->batchDxccWorkedStatus($uniqueEntities); + + // If JSON contains iota fields, batch process IOTA status + $iotas = []; + foreach ($dataResult as $item) { + if (!empty($item['iota'])) { + $iotas[] = $item['iota']; + } + } + $uniqueIotas = array_unique($iotas); + $iotaStatus = []; + if (!empty($uniqueIotas)) { + $iotaStatus = $this->Workabledxcc_model->batchIotaWorkedStatus($uniqueIotas); + } + + // Process results + $requiredData = array(); + foreach ($dataResult as $index => $item) { + $oldStartDate = DateTime::createFromFormat('Y-m-d', $item['0']); $StartDate = $oldStartDate->format($custom_date_format); $oldEndDate = DateTime::createFromFormat('Y-m-d', $item['1']); - $EndDate = $oldEndDate->format($custom_date_format); - $oldStartDate1 = DateTime::createFromFormat('Y-m-d', $item['0']); - - $StartDate1 = $oldStartDate1->format('Y-m-d'); - - - $this->load->model('logbook_model'); - $dxccInfo = $this->logbook_model->dxcc_lookup($item['callsign'], $StartDate1); - - // Call DXCC Worked function to check if the DXCC has been worked before - if (isset($dxccInfo['entity'])) { - $dxccWorked = $this->dxccWorked($dxccInfo['entity']); - } else { - // Handle the case where 'entity' is not set in $dxccInfo - $dxccWorked = array( - 'workedBefore' => false, - 'confirmed' => false, - ); - } + // Get DXCC status for this callsign + $entity = $dxccEntities[$index] ?? null; + $worked = $entity && isset($dxccStatus[$entity]) ? $dxccStatus[$entity] : [ + 'workedBefore' => false, + 'confirmed' => false, + 'workedViaSatellite' => false + ]; $requiredData[] = array( 'clean_date' => $item['0'], 'start_date' => $StartDate, 'end_date' => $EndDate, 'country' => $item['2'], + 'iota' => isset($item['iota']) ? $item['iota'] : null, + 'iota_status' => (isset($item['iota']) && isset($iotaStatus[$item['iota']])) ? $iotaStatus[$item['iota']] : null, 'notes' => $item['6'], 'callsign' => $item['callsign'], - 'workedBefore' => $dxccWorked['workedBefore'], - 'confirmed' => $dxccWorked['confirmed'], + 'workedBefore' => $worked['workedBefore'], + 'confirmed' => $worked['confirmed'], + 'workedViaSatellite' => $worked['workedViaSatellite'], ); } $data['dxcclist'] = $requiredData; - - // Return the array with the required data - $this->load->view('/workabledxcc/components/dxcclist', $data); } @@ -102,6 +113,7 @@ class Workabledxcc extends CI_Controller $return = [ "workedBefore" => false, "confirmed" => false, + "workedViaSatellite" => false, ]; $user_default_confirmation = $this->session->userdata('user_default_confirmation'); @@ -110,16 +122,28 @@ class Workabledxcc extends CI_Controller $this->load->model('logbook_model'); if (!empty($logbooks_locations_array)) { + // Check terrestrial contacts $this->db->where('COL_PROP_MODE !=', 'SAT'); $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->where('COL_COUNTRY', urldecode($country)); + // Fix case sensitivity issue for DXCC country matching + $this->db->where('UPPER(COL_COUNTRY) = UPPER(?)', urldecode($country)); $query = $this->db->get($this->config->item('table_name'), 1, 0); foreach ($query->result() as $workedBeforeRow) { $return['workedBefore'] = true; } + // Check satellite contacts + $this->db->where('COL_PROP_MODE', 'SAT'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('UPPER(COL_COUNTRY) = UPPER(?)', urldecode($country)); + + $query = $this->db->get($this->config->item('table_name'), 1, 0); + foreach ($query->result() as $satelliteRow) { + $return['workedViaSatellite'] = true; + } + $extrawhere = ''; if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Q') !== false) { $extrawhere = "COL_QSL_RCVD='Y'"; @@ -155,7 +179,8 @@ class Workabledxcc extends CI_Controller $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->where('COL_COUNTRY', urldecode($country)); + // Fix case sensitivity issue for DXCC country matching + $this->db->where('UPPER(COL_COUNTRY) = UPPER(?)', urldecode($country)); $query = $this->db->get($this->config->item('table_name'), 1, 0); foreach ($query->result() as $workedBeforeRow) { diff --git a/application/migrations/139_modify_eQSL_url.php b/application/migrations/139_modify_eQSL_url.php index 09e4fc37..e3f04afc 100644 --- a/application/migrations/139_modify_eQSL_url.php +++ b/application/migrations/139_modify_eQSL_url.php @@ -1,4 +1,4 @@ -db->db_debug = false; + + // Check if index already exists + $index_exists = $this->db->query("SHOW INDEX FROM ".$this->config->item('table_name')." WHERE Key_name = 'idx_workable_dxcc'")->num_rows(); + + if ($index_exists == 0) { + $sql = "ALTER TABLE ".$this->config->item('table_name')." ADD INDEX `idx_workable_dxcc` (`COL_COUNTRY`, `station_id`, `COL_PROP_MODE`)"; + $this->db->query($sql); + } + + // Add index for confirmation status columns + $conf_index_exists = $this->db->query("SHOW INDEX FROM ".$this->config->item('table_name')." WHERE Key_name = 'idx_qsl_confirmations'")->num_rows(); + + if ($conf_index_exists == 0) { + $sql = "ALTER TABLE ".$this->config->item('table_name')." ADD INDEX `idx_qsl_confirmations` (`COL_QSL_RCVD`, `COL_LOTW_QSL_RCVD`, `COL_EQSL_QSL_RCVD`, `COL_QRZCOM_QSO_DOWNLOAD_STATUS`)"; + $this->db->query($sql); + } + + $this->db->db_debug = true; + } + + public function down() + { + $this->db->db_debug = false; + + // Drop the indexes if they exist + $index_exists = $this->db->query("SHOW INDEX FROM ".$this->config->item('table_name')." WHERE Key_name = 'idx_workable_dxcc'")->num_rows(); + if ($index_exists > 0) { + $this->db->query("ALTER TABLE ".$this->config->item('table_name')." DROP INDEX `idx_workable_dxcc`"); + } + + $conf_index_exists = $this->db->query("SHOW INDEX FROM ".$this->config->item('table_name')." WHERE Key_name = 'idx_qsl_confirmations'")->num_rows(); + if ($conf_index_exists > 0) { + $this->db->query("ALTER TABLE ".$this->config->item('table_name')." DROP INDEX `idx_qsl_confirmations`"); + } + + $this->db->db_debug = true; + } +} diff --git a/application/models/Dxcc.php b/application/models/Dxcc.php index b02e940d..ef02fea5 100644 --- a/application/models/Dxcc.php +++ b/application/models/Dxcc.php @@ -58,6 +58,12 @@ class DXCC extends CI_Model { return $this->db->get('dxcc_entities'); } + /* Optimized method to get count of current DXCC entities without loading all records */ + function get_total_dxcc_count() { + $this->db->where('end', null); + return $this->db->count_all_results('dxcc_entities'); + } + function get_dxcc_array($dxccArray, $bands, $postdata) { $CI =& get_instance(); $CI->load->model('logbooks_model'); diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 0b7e394d..c4d5bdb4 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -2079,7 +2079,7 @@ class Logbook_model extends CI_Model $this->db->where_in('station_id', $logbooks_locations_array); $this->db->group_start(); $this->db->like('SUBSTRING(COL_GRIDSQUARE, 1, 4)', substr($grid, 0, 4)); - $this->db->or_like('SUBSTRING(COL_VUCC_GRIDS, 1, 4)', substr($grid, 0, 4)); + $this->db->or_like('COL_VUCC_GRIDS', substr($grid, 0, 4)); $this->db->group_end(); if ($band != null && $band != 'SAT') { @@ -2621,95 +2621,99 @@ class Logbook_model extends CI_Model } if (!empty($logbooks_locations_array)) { - $this->db->select(' - COUNT(IF(COL_QSL_SENT="Y",COL_QSL_SENT,null)) as QSL_Sent, - COUNT(IF(COL_QSL_RCVD="Y",COL_QSL_RCVD,null)) as QSL_Received, - COUNT(IF(COL_QSL_SENT IN("Q", "R") ,COL_QSL_SENT,null)) as QSL_Requested, - COUNT(IF(COL_EQSL_QSL_SENT="Y",COL_EQSL_QSL_SENT,null)) as eQSL_Sent, - COUNT(IF(COL_EQSL_QSL_RCVD="Y",COL_EQSL_QSL_RCVD,null)) as eQSL_Received, - COUNT(IF(COL_LOTW_QSL_SENT="Y",COL_LOTW_QSL_SENT,null)) as LoTW_Sent, - COUNT(IF(COL_LOTW_QSL_RCVD="Y",COL_LOTW_QSL_RCVD,null)) as LoTW_Received, - COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS="Y",COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent, - COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS="Y",COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received, - COUNT(IF(COL_QSL_SENT="Y" and DATE(COL_QSLSDATE)=DATE(SYSDATE()),COL_QSL_SENT,null)) as QSL_Sent_today, - COUNT(IF(COL_QSL_RCVD="Y" and DATE(COL_QSLRDATE)=DATE(SYSDATE()),COL_QSL_RCVD,null)) as QSL_Received_today, - COUNT(IF(COL_QSL_SENT IN("Q", "R") and DATE(COL_QSLSDATE)=DATE(SYSDATE()) ,COL_QSL_SENT,null)) as QSL_Requested_today, - COUNT(IF(COL_EQSL_QSL_SENT="Y" and DATE(COL_EQSL_QSLSDATE)=DATE(SYSDATE()),COL_EQSL_QSL_SENT,null)) as eQSL_Sent_today, - COUNT(IF(COL_EQSL_QSL_RCVD="Y" and DATE(COL_EQSL_QSLRDATE)=DATE(SYSDATE()),COL_EQSL_QSL_RCVD,null)) as eQSL_Received_today, - COUNT(IF(COL_LOTW_QSL_SENT="Y" and DATE(COL_LOTW_QSLSDATE)=DATE(SYSDATE()),COL_LOTW_QSL_SENT,null)) as LoTW_Sent_today, - COUNT(IF(COL_LOTW_QSL_RCVD="Y" and DATE(COL_LOTW_QSLRDATE)=DATE(SYSDATE()),COL_LOTW_QSL_RCVD,null)) as LoTW_Received_today, - COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS="Y" and DATE(COL_QRZCOM_QSO_UPLOAD_DATE)=DATE(SYSDATE()),COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent_today, - COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS="Y" and DATE(COL_QRZCOM_QSO_DOWNLOAD_DATE)=DATE(SYSDATE()),COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received_today - '); + // Pre-calculate today's date for better performance + $today_date = date('Y-m-d'); + + $this->db->select(" + COUNT(IF(COL_QSL_SENT='Y',COL_QSL_SENT,null)) as QSL_Sent, + COUNT(IF(COL_QSL_RCVD='Y',COL_QSL_RCVD,null)) as QSL_Received, + COUNT(IF(COL_QSL_SENT IN('Q', 'R') ,COL_QSL_SENT,null)) as QSL_Requested, + COUNT(IF(COL_EQSL_QSL_SENT='Y',COL_EQSL_QSL_SENT,null)) as eQSL_Sent, + COUNT(IF(COL_EQSL_QSL_RCVD='Y',COL_EQSL_QSL_RCVD,null)) as eQSL_Received, + COUNT(IF(COL_LOTW_QSL_SENT='Y',COL_LOTW_QSL_SENT,null)) as LoTW_Sent, + COUNT(IF(COL_LOTW_QSL_RCVD='Y',COL_LOTW_QSL_RCVD,null)) as LoTW_Received, + COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS='Y',COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent, + COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y',COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received, + COUNT(IF(COL_QSL_SENT='Y' and DATE(COL_QSLSDATE)='$today_date',COL_QSL_SENT,null)) as QSL_Sent_today, + COUNT(IF(COL_QSL_RCVD='Y' and DATE(COL_QSLRDATE)='$today_date',COL_QSL_RCVD,null)) as QSL_Received_today, + COUNT(IF(COL_QSL_SENT IN('Q', 'R') and DATE(COL_QSLSDATE)='$today_date' ,COL_QSL_SENT,null)) as QSL_Requested_today, + COUNT(IF(COL_EQSL_QSL_SENT='Y' and DATE(COL_EQSL_QSLSDATE)='$today_date',COL_EQSL_QSL_SENT,null)) as eQSL_Sent_today, + COUNT(IF(COL_EQSL_QSL_RCVD='Y' and DATE(COL_EQSL_QSLRDATE)='$today_date',COL_EQSL_QSL_RCVD,null)) as eQSL_Received_today, + COUNT(IF(COL_LOTW_QSL_SENT='Y' and DATE(COL_LOTW_QSLSDATE)='$today_date',COL_LOTW_QSL_SENT,null)) as LoTW_Sent_today, + COUNT(IF(COL_LOTW_QSL_RCVD='Y' and DATE(COL_LOTW_QSLRDATE)='$today_date',COL_LOTW_QSL_RCVD,null)) as LoTW_Received_today, + COUNT(IF(COL_QRZCOM_QSO_UPLOAD_STATUS='Y' and DATE(COL_QRZCOM_QSO_UPLOAD_DATE)='$today_date',COL_QRZCOM_QSO_UPLOAD_STATUS,null)) as QRZ_Sent_today, + COUNT(IF(COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y' and DATE(COL_QRZCOM_QSO_DOWNLOAD_DATE)='$today_date',COL_QRZCOM_QSO_DOWNLOAD_STATUS,null)) as QRZ_Received_today + ", FALSE); $this->db->where_in('station_id', $logbooks_locations_array); if ($query = $this->db->get($this->config->item('table_name'))) { - $this->db->last_query(); - foreach ($query->result() as $row) { - $QSLBreakdown['QSL_Sent'] = $row->QSL_Sent; - $QSLBreakdown['QSL_Received'] = $row->QSL_Received; - $QSLBreakdown['QSL_Requested'] = $row->QSL_Requested; - $QSLBreakdown['eQSL_Sent'] = $row->eQSL_Sent; - $QSLBreakdown['eQSL_Received'] = $row->eQSL_Received; - $QSLBreakdown['LoTW_Sent'] = $row->LoTW_Sent; - $QSLBreakdown['LoTW_Received'] = $row->LoTW_Received; - $QSLBreakdown['QRZ_Sent'] = $row->QRZ_Sent; - $QSLBreakdown['QRZ_Received'] = $row->QRZ_Received; - $QSLBreakdown['QSL_Sent_today'] = $row->QSL_Sent_today; - $QSLBreakdown['QSL_Received_today'] = $row->QSL_Received_today; - $QSLBreakdown['QSL_Requested_today'] = $row->QSL_Requested_today; - $QSLBreakdown['eQSL_Sent_today'] = $row->eQSL_Sent_today; - $QSLBreakdown['eQSL_Received_today'] = $row->eQSL_Received_today; - $QSLBreakdown['LoTW_Sent_today'] = $row->LoTW_Sent_today; - $QSLBreakdown['LoTW_Received_today'] = $row->LoTW_Received_today; - $QSLBreakdown['QRZ_Sent_today'] = $row->QRZ_Sent_today; - $QSLBreakdown['QRZ_Received_today'] = $row->QRZ_Received_today; + if ($query->num_rows() > 0) { + $row = $query->row(); + return array( + 'QSL_Sent' => (int)$row->QSL_Sent, + 'QSL_Received' => (int)$row->QSL_Received, + 'QSL_Requested' => (int)$row->QSL_Requested, + 'eQSL_Sent' => (int)$row->eQSL_Sent, + 'eQSL_Received' => (int)$row->eQSL_Received, + 'LoTW_Sent' => (int)$row->LoTW_Sent, + 'LoTW_Received' => (int)$row->LoTW_Received, + 'QRZ_Sent' => (int)$row->QRZ_Sent, + 'QRZ_Received' => (int)$row->QRZ_Received, + 'QSL_Sent_today' => (int)$row->QSL_Sent_today, + 'QSL_Received_today' => (int)$row->QSL_Received_today, + 'QSL_Requested_today' => (int)$row->QSL_Requested_today, + 'eQSL_Sent_today' => (int)$row->eQSL_Sent_today, + 'eQSL_Received_today' => (int)$row->eQSL_Received_today, + 'LoTW_Sent_today' => (int)$row->LoTW_Sent_today, + 'LoTW_Received_today' => (int)$row->LoTW_Received_today, + 'QRZ_Sent_today' => (int)$row->QRZ_Sent_today, + 'QRZ_Received_today' => (int)$row->QRZ_Received_today + ); } - - return $QSLBreakdown; - } else { - $QSLBreakdown['QSL_Sent'] = 0; - $QSLBreakdown['QSL_Received'] = 0; - $QSLBreakdown['QSL_Requested'] = 0; - $QSLBreakdown['eQSL_Sent'] = 0; - $QSLBreakdown['eQSL_Received'] = 0; - $QSLBreakdown['LoTW_Sent'] = 0; - $QSLBreakdown['LoTW_Received'] = 0; - $QSLBreakdown['QRZ_Sent'] = 0; - $QSLBreakdown['QRZ_Received'] = 0; - $QSLBreakdown['QSL_Sent_today'] = 0; - $QSLBreakdown['QSL_Received_today'] = 0; - $QSLBreakdown['QSL_Requested_today'] = 0; - $QSLBreakdown['eQSL_Sent_today'] = 0; - $QSLBreakdown['eQSL_Received_today'] = 0; - $QSLBreakdown['LoTW_Sent_today'] = 0; - $QSLBreakdown['LoTW_Received_today'] = 0; - $QSLBreakdown['QRZ_Sent_today'] = 0; - $QSLBreakdown['QRZ_Received_today'] = 0; - - return $QSLBreakdown; } - } else { - $QSLBreakdown['QSL_Sent'] = 0; - $QSLBreakdown['QSL_Received'] = 0; - $QSLBreakdown['QSL_Requested'] = 0; - $QSLBreakdown['eQSL_Sent'] = 0; - $QSLBreakdown['eQSL_Received'] = 0; - $QSLBreakdown['LoTW_Sent'] = 0; - $QSLBreakdown['LoTW_Received'] = 0; - $QSLBreakdown['QRZ_Sent'] = 0; - $QSLBreakdown['QRZ_Received'] = 0; - $QSLBreakdown['QSL_Sent_today'] = 0; - $QSLBreakdown['QSL_Received_today'] = 0; - $QSLBreakdown['QSL_Requested_today'] = 0; - $QSLBreakdown['eQSL_Sent_today'] = 0; - $QSLBreakdown['eQSL_Received_today'] = 0; - $QSLBreakdown['LoTW_Sent_today'] = 0; - $QSLBreakdown['LoTW_Received_today'] = 0; - $QSLBreakdown['QRZ_Sent_today'] = 0; - $QSLBreakdown['QRZ_Received_today'] = 0; - return $QSLBreakdown; + // Return default values if no results + return array( + 'QSL_Sent' => 0, + 'QSL_Received' => 0, + 'QSL_Requested' => 0, + 'eQSL_Sent' => 0, + 'eQSL_Received' => 0, + 'LoTW_Sent' => 0, + 'LoTW_Received' => 0, + 'QRZ_Sent' => 0, + 'QRZ_Received' => 0, + 'QSL_Sent_today' => 0, + 'QSL_Received_today' => 0, + 'QSL_Requested_today' => 0, + 'eQSL_Sent_today' => 0, + 'eQSL_Received_today' => 0, + 'LoTW_Sent_today' => 0, + 'LoTW_Received_today' => 0, + 'QRZ_Sent_today' => 0, + 'QRZ_Received_today' => 0 + ); + } else { + return array( + 'QSL_Sent' => 0, + 'QSL_Received' => 0, + 'QSL_Requested' => 0, + 'eQSL_Sent' => 0, + 'eQSL_Received' => 0, + 'LoTW_Sent' => 0, + 'LoTW_Received' => 0, + 'QRZ_Sent' => 0, + 'QRZ_Received' => 0, + 'QSL_Sent_today' => 0, + 'QSL_Received_today' => 0, + 'QSL_Requested_today' => 0, + 'eQSL_Sent_today' => 0, + 'eQSL_Received_today' => 0, + 'LoTW_Sent_today' => 0, + 'LoTW_Received_today' => 0, + 'QRZ_Sent_today' => 0, + 'QRZ_Received_today' => 0 + ); } } @@ -2986,6 +2990,53 @@ class Logbook_model extends CI_Model } } + // Consolidated method to get all country statistics in one query + function get_countries_statistics_consolidated($StationLocationsArray = null) { + if ($StationLocationsArray == null) { + $CI = &get_instance(); + $CI->load->model('logbooks_model'); + $logbooks_locations_array = $CI->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + } else { + $logbooks_locations_array = $StationLocationsArray; + } + + if (!empty($logbooks_locations_array)) { + // Get both confirmed countries and current countries in one query + $this->db->select(' + COUNT(DISTINCT COL_COUNTRY) as Countries_Worked, + COUNT(DISTINCT IF(COL_QSL_RCVD = "Y", COL_COUNTRY, NULL)) as Countries_Worked_QSL, + COUNT(DISTINCT IF(COL_EQSL_QSL_RCVD = "Y", COL_COUNTRY, NULL)) as Countries_Worked_EQSL, + COUNT(DISTINCT IF(COL_LOTW_QSL_RCVD = "Y", COL_COUNTRY, NULL)) as Countries_Worked_LOTW, + COUNT(DISTINCT IF(dxcc_entities.end IS NULL, COL_COUNTRY, NULL)) as Countries_Current + '); + $this->db->join('dxcc_entities', 'dxcc_entities.adif = ' . $this->config->item('table_name') . '.col_dxcc', 'left'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('COL_COUNTRY !=', 'Invalid'); + $this->db->where('COL_DXCC >', '0'); + + if ($query = $this->db->get($this->config->item('table_name'))) { + if ($query->num_rows() > 0) { + $row = $query->row(); + return array( + 'Countries_Worked' => $row->Countries_Worked, + 'Countries_Worked_QSL' => $row->Countries_Worked_QSL, + 'Countries_Worked_EQSL' => $row->Countries_Worked_EQSL, + 'Countries_Worked_LOTW' => $row->Countries_Worked_LOTW, + 'Countries_Current' => $row->Countries_Current + ); + } + } + } + + return array( + 'Countries_Worked' => 0, + 'Countries_Worked_QSL' => 0, + 'Countries_Worked_EQSL' => 0, + 'Countries_Worked_LOTW' => 0, + 'Countries_Current' => 0 + ); + } + /* Return total number of countries confirmed with paper QSL */ function total_countries_confirmed_paper() { @@ -4806,6 +4857,7 @@ class Logbook_model extends CI_Model $plot = array('lat' => 0, 'lng' => 0, 'html' => '', 'label' => '', 'flag' => '', 'confirmed' => 'N'); $plot['label'] = $row->COL_CALL; + $plot['callsign'] = $row->COL_CALL; $flag = strtolower($CI->dxccflag->getISO($row->COL_DXCC)); $plot['flag'] = 'name))) . '"> '; $plot['html'] = ($row->COL_GRIDSQUARE != null ? "Grid: " . $row->COL_GRIDSQUARE . "
" : ""); @@ -4980,6 +5032,63 @@ class Logbook_model extends CI_Model // If all parsing fails, return the original input and let the database handle it return $date_input; } + + /* Consolidated QSO Statistics - Get all basic counts in a single query */ + function get_qso_statistics_consolidated($StationLocationsArray = null) + { + if ($StationLocationsArray == null) { + $this->load->model('logbooks_model'); + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + } else { + $logbooks_locations_array = $StationLocationsArray; + } + + if (!$logbooks_locations_array) { + return array( + 'total_qsos' => 0, + 'todays_qsos' => 0, + 'month_qsos' => 0, + 'year_qsos' => 0 + ); + } + + // Calculate date ranges + $today_morning = date('Y-m-d 00:00:00'); + $today_night = date('Y-m-d 23:59:59'); + $month_morning = date('Y-m-01 00:00:00'); + $date = new DateTime('now'); + $date->modify('last day of this month'); + $month_night = $date->format('Y-m-d') . " 23:59:59"; + $year_morning = date('Y-01-01 00:00:00'); + $year_night = date('Y-12-31 23:59:59'); + + // Build the consolidated query + $this->db->select(" + COUNT(*) as total_qsos, + COUNT(CASE WHEN COL_TIME_ON >= '$today_morning' AND COL_TIME_ON <= '$today_night' THEN 1 END) as todays_qsos, + COUNT(CASE WHEN COL_TIME_ON >= '$month_morning' AND COL_TIME_ON <= '$month_night' THEN 1 END) as month_qsos, + COUNT(CASE WHEN COL_TIME_ON >= '$year_morning' AND COL_TIME_ON <= '$year_night' THEN 1 END) as year_qsos + ", FALSE); + $this->db->where_in('station_id', $logbooks_locations_array); + $query = $this->db->get($this->config->item('table_name')); + + if ($query->num_rows() > 0) { + $row = $query->row(); + return array( + 'total_qsos' => (int)$row->total_qsos, + 'todays_qsos' => (int)$row->todays_qsos, + 'month_qsos' => (int)$row->month_qsos, + 'year_qsos' => (int)$row->year_qsos + ); + } + + return array( + 'total_qsos' => 0, + 'todays_qsos' => 0, + 'month_qsos' => 0, + 'year_qsos' => 0 + ); + } } // Function to validate ADIF date format diff --git a/application/models/Setup_model.php b/application/models/Setup_model.php index 66692014..eae2abd9 100644 --- a/application/models/Setup_model.php +++ b/application/models/Setup_model.php @@ -24,6 +24,25 @@ class Setup_model extends CI_Model { return $query->row()->count; } + + // Consolidated method to get all setup counts in one query + function getAllSetupCounts() { + $userid = xss_clean($this->session->userdata('user_id')); + + $sql = "SELECT + (SELECT COUNT(*) FROM dxcc_entities) as country_count, + (SELECT COUNT(*) FROM station_logbooks WHERE user_id = {$userid}) as logbook_count, + (SELECT COUNT(*) FROM station_profile WHERE user_id = {$userid}) as location_count"; + + $query = $this->db->query($sql); + $row = $query->row(); + + return array( + 'country_count' => $row->country_count, + 'logbook_count' => $row->logbook_count, + 'location_count' => $row->location_count + ); + } } ?> diff --git a/application/models/Stats.php b/application/models/Stats.php index 8fb9bf20..2cdbf1c7 100644 --- a/application/models/Stats.php +++ b/application/models/Stats.php @@ -88,10 +88,20 @@ } } + // Initialize bandcalls array with all bands set to 0 + foreach ($bands as $band) { + $bandcalls[$band] = 0; + } + foreach ($bandunique as $band) { $bandcalls[$band->band] = $band->calls; } + // Initialize modecalls array with all modes set to 0 + foreach ($modes as $mode) { + $modecalls[$mode] = 0; + } + foreach ($modeunique as $mode) { //if ($mode->col_submode == null) { if ($mode->col_submode == null || $mode->col_submode == "") { diff --git a/application/models/Workabledxcc_model.php b/application/models/Workabledxcc_model.php index d7a1860d..a09e54c3 100644 --- a/application/models/Workabledxcc_model.php +++ b/application/models/Workabledxcc_model.php @@ -2,87 +2,421 @@ class Workabledxcc_model extends CI_Model { + // Cache for DXCC lookups to avoid repeated queries + private $dxccCache = array(); + private $workedCache = array(); + + /** + * Batch DXCC lookup for multiple callsigns + * @param array $callsigns Array of callsigns + * @param array $dates Array of dates corresponding to callsigns + * @return array Array of DXCC entities indexed by callsign index + */ + public function batchDxccLookup($callsigns, $dates) + { + $this->load->model('logbook_model'); + $entities = array(); + + foreach ($callsigns as $index => $callsign) { + $cacheKey = $callsign . '_' . $dates[$index]; + + if (!isset($this->dxccCache[$cacheKey])) { + $dxccInfo = $this->logbook_model->dxcc_lookup($callsign, $dates[$index]); + $this->dxccCache[$cacheKey] = isset($dxccInfo['entity']) ? $dxccInfo['entity'] : null; + } + + $entities[$index] = $this->dxccCache[$cacheKey]; + } + + return $entities; + } + + /** + * Batch check if DXCC entities have been worked/confirmed + * @param array $entities Array of unique DXCC entities + * @return array Array of worked/confirmed status indexed by entity + */ + public function batchDxccWorkedStatus($entities) + { + if (empty($entities)) { + return array(); + } + + $user_default_confirmation = $this->session->userdata('user_default_confirmation'); + $this->load->model('logbooks_model'); + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + + if (empty($logbooks_locations_array)) { + return array_fill_keys($entities, [ + 'workedBefore' => false, + 'confirmed' => false, + 'workedViaSatellite' => false + ]); + } + + $results = array(); + + // Build confirmation criteria once + $confirmationCriteria = $this->buildConfirmationCriteria($user_default_confirmation); + + // Debug: Log entities being checked + log_message('debug', 'Workable DXCC: Checking entities: ' . implode(', ', $entities)); + + // Batch query for worked status (terrestrial) + $workedResults = $this->batchWorkedQuery($entities, $logbooks_locations_array); + + // Batch query for confirmed status (terrestrial) + $confirmedResults = $this->batchConfirmedQuery($entities, $logbooks_locations_array, $confirmationCriteria); + + // Batch query for satellite contacts + $satelliteResults = $this->batchSatelliteQuery($entities, $logbooks_locations_array); + + // Debug: Log results + log_message('debug', 'Workable DXCC: Worked results: ' . json_encode($workedResults)); + log_message('debug', 'Workable DXCC: Confirmed results: ' . json_encode($confirmedResults)); + log_message('debug', 'Workable DXCC: Satellite results: ' . json_encode($satelliteResults)); + + // Combine results + foreach ($entities as $entity) { + $results[$entity] = [ + 'workedBefore' => isset($workedResults[$entity]), + 'confirmed' => isset($confirmedResults[$entity]), + 'workedViaSatellite' => isset($satelliteResults[$entity]) + ]; + } + + return $results; + } + + /** + * Batch check IOTA worked/confirmed status + * @param array $iotas Array of unique IOTA tags + * @return array Array indexed by IOTA tag with ['worked'=>bool,'confirmed'=>bool] + */ + public function batchIotaWorkedStatus($iotas) + { + if (empty($iotas)) { + return array(); + } + + $user_default_confirmation = $this->session->userdata('user_default_confirmation'); + $this->load->model('logbooks_model'); + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + + if (empty($logbooks_locations_array)) { + $out = []; + foreach ($iotas as $i) { + $out[$i] = ['worked' => false, 'confirmed' => false]; + } + return $out; + } + + // Build confirmation criteria once + $confirmationCriteria = $this->buildConfirmationCriteria($user_default_confirmation); + + // Build case-insensitive WHERE conditions for COL_IOTA + $whereConditions = array(); + foreach ($iotas as $iota) { + $whereConditions[] = "UPPER(COL_IOTA) = UPPER('" . $this->db->escape_str($iota) . "')"; + } + + if (empty($whereConditions)) { + return array(); + } + + $whereClause = '(' . implode(' OR ', $whereConditions) . ')'; + + // Worked query (any mode) + $this->db->select('COL_IOTA') + ->distinct() + ->from($this->config->item('table_name')) + ->where_in('station_id', $logbooks_locations_array) + ->where($whereClause); + + $workedQuery = $this->db->get(); + log_message('debug', 'Workable DXCC IOTA worked query: ' . $this->db->last_query()); + + $workedResults = array(); + foreach ($workedQuery->result() as $row) { + foreach ($iotas as $iota) { + if (strtoupper($row->COL_IOTA) === strtoupper($iota)) { + $workedResults[$iota] = true; + break; + } + } + } + + // Confirmed query (apply confirmation criteria, exclude satellite confirmations if desired) + $confirmedResults = array(); + if ($confirmationCriteria !== '1=0') { + $this->db->select('COL_IOTA') + ->distinct() + ->from($this->config->item('table_name')) + ->where($confirmationCriteria) + ->where_in('station_id', $logbooks_locations_array) + ->where($whereClause); + + $confirmedQuery = $this->db->get(); + foreach ($confirmedQuery->result() as $row) { + foreach ($iotas as $iota) { + if (strtoupper($row->COL_IOTA) === strtoupper($iota)) { + $confirmedResults[$iota] = true; + break; + } + } + } + } + + $out = array(); + foreach ($iotas as $iota) { + $out[$iota] = [ + 'worked' => isset($workedResults[$iota]), + 'confirmed' => isset($confirmedResults[$iota]) + ]; + } + + // Debug + log_message('debug', 'Workable DXCC: IOTA worked results: ' . json_encode($workedResults)); + log_message('debug', 'Workable DXCC: IOTA confirmed results: ' . json_encode($confirmedResults)); + + return $out; + } + + /** + * Batch query to check which entities have been worked via satellite + */ + private function batchSatelliteQuery($entities, $logbooks_locations_array) + { + // Create case-insensitive matching for DXCC entities + $whereConditions = array(); + foreach ($entities as $entity) { + $whereConditions[] = "UPPER(COL_COUNTRY) = UPPER('" . $this->db->escape_str($entity) . "')"; + } + + if (empty($whereConditions)) { + return array(); + } + + $whereClause = '(' . implode(' OR ', $whereConditions) . ')'; + + $this->db->select('COL_COUNTRY') + ->distinct() + ->from($this->config->item('table_name')) + ->where('COL_PROP_MODE', 'SAT') // Only satellite contacts + ->where_in('station_id', $logbooks_locations_array) + ->where($whereClause); + + $query = $this->db->get(); + + // Debug: Log the SQL query + log_message('debug', 'Workable DXCC satellite query: ' . $this->db->last_query()); + + $results = array(); + + foreach ($query->result() as $row) { + // Store with the original entity case for lookup + foreach ($entities as $entity) { + if (strtoupper($row->COL_COUNTRY) === strtoupper($entity)) { + $results[$entity] = true; + log_message('debug', 'Workable DXCC: Found satellite match: ' . $entity . ' matches ' . $row->COL_COUNTRY); + break; + } + } + } + + return $results; + } + + /** + * Build confirmation criteria SQL based on user preferences + */ + private function buildConfirmationCriteria($user_default_confirmation) + { + $criteria = array(); + + if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Q') !== false) { + $criteria[] = "COL_QSL_RCVD='Y'"; + } + if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'L') !== false) { + $criteria[] = "COL_LOTW_QSL_RCVD='Y'"; + } + if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'E') !== false) { + $criteria[] = "COL_EQSL_QSL_RCVD='Y'"; + } + if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Z') !== false) { + $criteria[] = "COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y'"; + } + + return empty($criteria) ? '1=0' : '(' . implode(' OR ', $criteria) . ')'; + } + + /** + * Batch query to check which entities have been worked + */ + private function batchWorkedQuery($entities, $logbooks_locations_array) + { + // Create case-insensitive matching for DXCC entities + $whereConditions = array(); + foreach ($entities as $entity) { + $whereConditions[] = "UPPER(COL_COUNTRY) = UPPER('" . $this->db->escape_str($entity) . "')"; + } + + if (empty($whereConditions)) { + return array(); + } + + $whereClause = '(' . implode(' OR ', $whereConditions) . ')'; + + $this->db->select('COL_COUNTRY') + ->distinct() + ->from($this->config->item('table_name')) + ->where('COL_PROP_MODE !=', 'SAT') + ->where_in('station_id', $logbooks_locations_array) + ->where($whereClause); + + $query = $this->db->get(); + + // Debug: Log the SQL query + log_message('debug', 'Workable DXCC worked query: ' . $this->db->last_query()); + + $results = array(); + + foreach ($query->result() as $row) { + // Store with the original entity case for lookup + foreach ($entities as $entity) { + if (strtoupper($row->COL_COUNTRY) === strtoupper($entity)) { + $results[$entity] = true; + log_message('debug', 'Workable DXCC: Found worked match: ' . $entity . ' matches ' . $row->COL_COUNTRY); + break; + } + } + } + + return $results; + } + + /** + * Batch query to check which entities have been confirmed + */ + private function batchConfirmedQuery($entities, $logbooks_locations_array, $confirmationCriteria) + { + if ($confirmationCriteria === '1=0') { + return array(); + } + + // Create case-insensitive matching for DXCC entities + $whereConditions = array(); + foreach ($entities as $entity) { + $whereConditions[] = "UPPER(COL_COUNTRY) = UPPER('" . $this->db->escape_str($entity) . "')"; + } + + if (empty($whereConditions)) { + return array(); + } + + $whereClause = '(' . implode(' OR ', $whereConditions) . ')'; + + $this->db->select('COL_COUNTRY') + ->distinct() + ->from($this->config->item('table_name')) + ->where('COL_PROP_MODE !=', 'SAT') + ->where($confirmationCriteria) + ->where_in('station_id', $logbooks_locations_array) + ->where($whereClause); + + $query = $this->db->get(); + $results = array(); + + foreach ($query->result() as $row) { + // Store with the original entity case for lookup + foreach ($entities as $entity) { + if (strtoupper($row->COL_COUNTRY) === strtoupper($entity)) { + $results[$entity] = true; + break; + } + } + } + + return $results; + } public function GetThisWeek() { - $json = file_get_contents($this->optionslib->get_option('dxped_url')); - - // Step 2: Convert the JSON data to an array. + $json = file_get_contents($this->optionslib->get_option('dxped_url')); $data = json_decode($json, true); - // Step 3: Create a new array to hold the records for this week. - $thisWeekRecords = []; + if (empty($data)) { + return array(); + } - // Get the start and end of this week. + $thisWeekRecords = []; $startOfWeek = (new DateTime())->setISODate((new DateTime())->format('o'), (new DateTime())->format('W'), 1); $endOfWeek = (clone $startOfWeek)->modify('+6 days'); - // Step 4: Iterate over the array. - foreach ($data as $record) { + // Get Date format + if ($this->session->userdata('user_date_format')) { + $custom_date_format = $this->session->userdata('user_date_format'); + } else { + $custom_date_format = $this->config->item('qso_date_format'); + } - // Convert "0" and "1" to DateTime objects. + // First pass: filter records for this week + $weekRecords = array(); + foreach ($data as $record) { $startDate = new DateTime($record['0']); $endDate = new DateTime($record['1']); - // Step 5: Check if the start date or end date is within this week. - if (($startDate >= $startOfWeek && $startDate <= $endOfWeek) || ($endDate >= $startOfWeek && $endDate <= $endOfWeek)) { - $endDate = new DateTime($record['1']); - $now = new DateTime(); - $interval = $now->diff($endDate); - $daysLeft = $interval->days; - - // If daysLeft is 0, set it to "Last day" - if ($daysLeft == 0) { - $daysLeft = "Last day"; - } else { - $daysLeft = $daysLeft . " days left"; - } - - // Add daysLeft to record - $record['daysLeft'] = $daysLeft; - // Get Date format - if ($this->session->userdata('user_date_format')) { - // If Logged in and session exists - $custom_date_format = $this->session->userdata('user_date_format'); - } else { - // Get Default date format from /config/cloudlog.php - $custom_date_format = $this->config->item('qso_date_format'); - } - - // Create a new array with the required fields and add it to the main array - $oldStartDate = DateTime::createFromFormat('Y-m-d', $record['0']); - - $StartDate = $oldStartDate->format($custom_date_format); - $record['startDate'] = $StartDate; - - $oldEndDate = DateTime::createFromFormat('Y-m-d', $record['1']); - $EndDate = $oldEndDate->format($custom_date_format); - $record['endDate'] = $EndDate; - - $record['confirmed'] = true; // or false, depending on your logic - - $CI = &get_instance(); - $CI->load->model('logbook_model'); - $dxccInfo = $CI->logbook_model->dxcc_lookup($record['callsign'], $startDate->format('Y-m-d')); - - // Call DXCC Worked function to check if the DXCC has been worked before - if (isset($dxccInfo['entity'])) { - $dxccWorkedData = $this->dxccWorked($dxccInfo['entity']); - $record = array_merge($record, $dxccWorkedData); - } else { - // Handle the case where 'entity' is not set in $dxccInfo - $itemsToAdd = array( - 'workedBefore' => false, - 'confirmed' => false, - ); - $record = array_merge($record, $itemsToAdd); - } - - $thisWeekRecords[] = $record; + if (($startDate >= $startOfWeek && $startDate <= $endOfWeek) || + ($endDate >= $startOfWeek && $endDate <= $endOfWeek)) { + $weekRecords[] = $record; } } - return $thisWeekRecords; + if (empty($weekRecords)) { + return array(); + } + + // Batch process DXCC lookups + $callsigns = array_column($weekRecords, 'callsign'); + $dates = array_column($weekRecords, '0'); + $dxccEntities = $this->batchDxccLookup($callsigns, $dates); + + // Get worked/confirmed status for all entities in batch + $uniqueEntities = array_unique(array_filter($dxccEntities)); + $dxccStatus = $this->batchDxccWorkedStatus($uniqueEntities); + + // Process results + foreach ($weekRecords as $index => $record) { + $endDate = new DateTime($record['1']); + $now = new DateTime(); + $interval = $now->diff($endDate); + $daysLeft = $interval->days; + + $daysLeft = ($daysLeft == 0) ? "Last day" : $daysLeft . " days left"; + $record['daysLeft'] = $daysLeft; + + $oldStartDate = DateTime::createFromFormat('Y-m-d', $record['0']); + $record['startDate'] = $oldStartDate->format($custom_date_format); + + $oldEndDate = DateTime::createFromFormat('Y-m-d', $record['1']); + $record['endDate'] = $oldEndDate->format($custom_date_format); + + // Get DXCC status for this callsign + $entity = $dxccEntities[$index] ?? null; + $worked = $entity && isset($dxccStatus[$entity]) ? $dxccStatus[$entity] : [ + 'workedBefore' => false, + 'confirmed' => false, + 'workedViaSatellite' => false + ]; + + $record['workedBefore'] = $worked['workedBefore']; + $record['confirmed'] = $worked['confirmed']; + $record['workedViaSatellite'] = $worked['workedViaSatellite']; + + $thisWeekRecords[] = $record; + } + + return $thisWeekRecords; } function dxccWorked($country) @@ -91,6 +425,7 @@ class Workabledxcc_model extends CI_Model $return = [ "workedBefore" => false, "confirmed" => false, + "workedViaSatellite" => false, ]; $user_default_confirmation = $this->session->userdata('user_default_confirmation'); @@ -99,16 +434,28 @@ class Workabledxcc_model extends CI_Model $this->load->model('logbook_model'); if (!empty($logbooks_locations_array)) { + // Check for terrestrial contacts $this->db->where('COL_PROP_MODE !=', 'SAT'); $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->where('COL_COUNTRY', urldecode($country)); + // Fix case sensitivity issue for DXCC country matching + $this->db->where('UPPER(COL_COUNTRY) = UPPER(?)', urldecode($country)); $query = $this->db->get($this->config->item('table_name'), 1, 0); foreach ($query->result() as $workedBeforeRow) { $return['workedBefore'] = true; } + // Check for satellite contacts + $this->db->where('COL_PROP_MODE', 'SAT'); + $this->db->where_in('station_id', $logbooks_locations_array); + $this->db->where('UPPER(COL_COUNTRY) = UPPER(?)', urldecode($country)); + + $query = $this->db->get($this->config->item('table_name'), 1, 0); + foreach ($query->result() as $satelliteRow) { + $return['workedViaSatellite'] = true; + } + $extrawhere = ''; if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Q') !== false) { $extrawhere = "COL_QSL_RCVD='Y'"; @@ -144,7 +491,8 @@ class Workabledxcc_model extends CI_Model $this->db->where_in('station_id', $logbooks_locations_array); - $this->db->where('COL_COUNTRY', urldecode($country)); + // Fix case sensitivity issue for DXCC country matching + $this->db->where('UPPER(COL_COUNTRY) = UPPER(?)', urldecode($country)); $query = $this->db->get($this->config->item('table_name'), 1, 0); foreach ($query->result() as $workedBeforeRow) { diff --git a/application/views/adif/import.php b/application/views/adif/import.php index 2de27165..973de685 100644 --- a/application/views/adif/import.php +++ b/application/views/adif/import.php @@ -1,6 +1,12 @@
-

+
+
+

+

Import and export your amateur radio logs in ADIF format

+
+
+ -
+ + +
- +
+ } ?>" id="import" role="tabpanel" aria-labelledby="import-tab"> -

-

B.

+ +
+
+ +
+
- - - - -
-
-
- - + + +
+
+
Station Selection
+
+ + +
+
+
+
File Upload
+
+ + +
Supported formats: .adi, .adif
-
-
-
- - + +
+
+
Import Options
+ + +
+
+
Basic Settings
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
-
-
-
-
-
-
- - -
-
-
-
- -
-
-
- - -
-
-
-
- -
-
-
- - -
-
-
-
- -
-
-
- - -
-
-
-
- -
-
-
- - + +
+
+
Mark as Uploaded to Online Logbooks
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
-
-
-
- - -
-
+ +
+
+
- -
-
+
+ + +
+
+
+

+ +
+
+
+
Export Settings
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
Export Options
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+
-
-
-

- -
- - -
- - - -
-
-
-
- - + +
+
+
+
+
+
+
+
+
+

Export all satellite contacts from your logbook

+ + + +
+
+

Export satellite contacts confirmed on LoTW

+ + + +
+
-
-
-
- - -
-
-
- - - - -

- -
-

- -

+
- -
-
- -

-
- - -
- - -
- -
+
+
+
+
LoTW Export Management
+ + + +
+
+
+
Mark QSOs as Exported to LoTW
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ } ?>" id="dcl" role="tabpanel" aria-labelledby="dcl-tab"> + -

-
- -
-
-
- - +
+
+
DARC DCL Import
+ + -
-
-
-
- - + + +
+
+
DCL Import Settings
+
+
+
+
+
+ + +
+
+
+
Import Options
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
-
-
+
-
-
-
- - -
-
-
-
- - - +
diff --git a/application/views/bands/index.php b/application/views/bands/index.php index 3eac16fd..4733d38b 100644 --- a/application/views/bands/index.php +++ b/application/views/bands/index.php @@ -23,39 +23,203 @@ $wwff = 0;

+ +
+
+
Information
+
+
+ + +

+ +

+

+ +

+
+
+ + +
+
+
Band Management Controls
+
+
+ +
+
+
+
+
+
+ 0
+ Active for QSO Entry +
+
+
+ Total Bands Configured +
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+ +
+ +
+ +
+
+
+
+
+ + +
+
+
+
+
+ +
- -
+
+
Bands Configuration
+ Loading... +
+
-

- -

-

- -

+
+ - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + session->userdata('user_type') == '99') { ?> @@ -65,28 +229,28 @@ $wwff = 0; - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + session->userdata('user_type') == '99') { ?> - - @@ -95,7 +259,7 @@ $wwff = 0; - + @@ -115,7 +279,7 @@ $wwff = 0; -
Active CQDOKDXCCIOTAPOTASIGSOTACountiesVUCCWASWWFFGroupSSBDataCW
active == 1) {echo 'checked';}?>>band;?>cq == 1) {echo 'checked'; $cq++;}?>>dok == 1) {echo 'checked'; $dok++;}?>>dxcc == 1) {echo 'checked'; $dxcc++;}?>>iota == 1) {echo 'checked'; $iota++;}?>>iota == 1) {echo 'checked'; $pota++;}?>>sig == 1) {echo 'checked'; $sig++;}?>>sota == 1) {echo 'checked'; $sota++;}?>>uscounties == 1) {echo 'checked'; $uscounties++;}?>>vucc == 1) {echo 'checked'; $vucc++;}?>>was == 1) {echo 'checked'; $was++;}?>>wwff == 1) {echo 'checked'; $wwff++;}?>>bandgroup;?>ssb;?>data;?>cw;?>active == 1) {echo 'checked';}?>>band;?>cq == 1) {echo 'checked'; $cq++;}?>>dok == 1) {echo 'checked'; $dok++;}?>>dxcc == 1) {echo 'checked'; $dxcc++;}?>>iota == 1) {echo 'checked'; $iota++;}?>>pota == 1) {echo 'checked'; $pota++;}?>>sig == 1) {echo 'checked'; $sig++;}?>>sota == 1) {echo 'checked'; $sota++;}?>>uscounties == 1) {echo 'checked'; $uscounties++;}?>>vucc == 1) {echo 'checked'; $vucc++;}?>>was == 1) {echo 'checked'; $was++;}?>>wwff == 1) {echo 'checked'; $wwff++;}?>>bandgroup;?>ssb;?>data;?>cw;?> + +
Toggle All 0) echo 'checked';?>> 0) echo 'checked';?>>
+

diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 17d81303..c23b6011 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -784,6 +784,26 @@ if ($this->session->userdata('user_id') != null) { }); // Form "submit" // $('.custom-map-QSOs .btn_submit_map_custom').off('click').on('click', function() { + // Show loading spinner and disable button + const $button = $(this); + const $spinner = $('#load-spinner'); + + $button.prop('disabled', true); + $spinner.removeClass('d-none'); + + // Hide any previous alerts + $('.warningOnSubmit').hide(); + $('#map-success-alert').addClass('d-none'); + + // Failsafe timeout to prevent stuck spinner (60 seconds) + const failsafeTimeout = setTimeout(function() { + console.warn('Map loading timed out - forcing spinner hide'); + $button.prop('disabled', false); + $spinner.addClass('d-none'); + $('.warningOnSubmit .warningOnSubmit_txt').text('Map loading timed out. Please try again.'); + $('.warningOnSubmit').show(); + }, 60000); + var customdata = { 'dataPost': { 'date_from': $('.custom-map-QSOs input[name="from"]').val(), @@ -795,6 +815,58 @@ if ($this->session->userdata('user_id') != null) { }, 'map_id': '#custommap' }; + + // Add success and error callbacks to the customdata + customdata.onSuccess = function(plotjson) { + console.log('Map loading success callback called with data:', plotjson); + + // Clear the failsafe timeout + clearTimeout(failsafeTimeout); + + try { + // Update statistics + if (typeof updateMapStatistics === 'function') { + console.log('Calling updateMapStatistics function'); + updateMapStatistics(plotjson); + } else { + console.warn('updateMapStatistics function not found'); + } + + // Show success message + const qsoCount = plotjson['markers'] ? plotjson['markers'].length : 0; + console.log('QSO count:', qsoCount); + $('#qso-count-display').text(`Loaded ${qsoCount} QSOs successfully`); + $('#map-success-alert').removeClass('d-none'); + + setTimeout(() => { + $('#map-success-alert').addClass('d-none'); + }, 3000); + } catch (error) { + console.error('Error in success callback:', error); + $('.warningOnSubmit .warningOnSubmit_txt').text('Map loaded but encountered an error displaying statistics.'); + $('.warningOnSubmit').show(); + } finally { + // Always re-enable button and hide spinner + console.log('Re-enabling button and hiding spinner'); + $button.prop('disabled', false); + $spinner.addClass('d-none'); + } + }; + + customdata.onError = function() { + console.log('Map loading error callback called'); + + // Clear the failsafe timeout + clearTimeout(failsafeTimeout); + + $('.warningOnSubmit .warningOnSubmit_txt').text('Failed to load map data. Please try again.'); + $('.warningOnSubmit').show(); + + // Re-enable button and hide spinner + $button.prop('disabled', false); + $spinner.addClass('d-none'); + }; + initplot(qso_loc, customdata); }) }); @@ -806,6 +878,10 @@ if ($this->session->userdata('user_id') != null) { success: function(data) { document.getElementById('from').value = data; document.getElementById('to').value = new Date().toISOString().split('T')[0]; + // Update the date range display + if (typeof updateDateRangeDisplay === 'function') { + updateDateRangeDisplay(); + } }, error: function() {}, }); @@ -1927,21 +2003,59 @@ if ($this->session->userdata('user_id') != null) { diff --git a/application/views/map/custom_date.php b/application/views/map/custom_date.php index 58c982c7..69636328 100644 --- a/application/views/map/custom_date.php +++ b/application/views/map/custom_date.php @@ -1,107 +1,557 @@ + + + +


-

QSOs (Custom Dates)

+ + +
+
+
+
+
+
+

+ + QSOs (Custom Dates) +

+
+
+
+
+
+
session->flashdata('notice')) { ?> -
-
-
- - -
- -
- - -
- -
- -
+ +
+
+
+ Filter Controls +
+
+ + + +
+
+
+
+
+ Quick Filters +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
-
-
- - -
+ +
+
+
+
+
+ Date Range +
+
+
+ + +
+
+ + +
+
+
+
+
+
-
- - -
- lang('gen_hamradio_propagation_AS'), - 'AUR' => lang('gen_hamradio_propagation_AUR'), - 'AUE' => lang('gen_hamradio_propagation_AUE'), - 'BS' => lang('gen_hamradio_propagation_BS'), - 'ECH' => lang('gen_hamradio_propagation_ECH'), - 'EME' => lang('gen_hamradio_propagation_EME'), - 'ES' => lang('gen_hamradio_propagation_ES'), - 'FAI' => lang('gen_hamradio_propagation_FAI'), - 'F2' => lang('gen_hamradio_propagation_F2'), - 'INTERNET' => lang('gen_hamradio_propagation_INTERNET'), - 'ION' => lang('gen_hamradio_propagation_ION'), - 'IRL' => lang('gen_hamradio_propagation_IRL'), - 'MS' => lang('gen_hamradio_propagation_MS'), - 'RPT' => lang('gen_hamradio_propagation_RPT'), - 'RS' => lang('gen_hamradio_propagation_RS'), - 'SAT' => lang('gen_hamradio_propagation_SAT'), - 'TEP' => lang('gen_hamradio_propagation_TEP'), - 'TR' => lang('gen_hamradio_propagation_TR')]; - asort($prop_modes); - ?> -
- - -
+ +
+
+
+
+
+ Advanced Filters +
+
+
+ + +
+ +
+ + +
+ + lang('gen_hamradio_propagation_AS'), + 'AUR' => lang('gen_hamradio_propagation_AUR'), + 'AUE' => lang('gen_hamradio_propagation_AUE'), + 'BS' => lang('gen_hamradio_propagation_BS'), + 'ECH' => lang('gen_hamradio_propagation_ECH'), + 'EME' => lang('gen_hamradio_propagation_EME'), + 'ES' => lang('gen_hamradio_propagation_ES'), + 'FAI' => lang('gen_hamradio_propagation_FAI'), + 'F2' => lang('gen_hamradio_propagation_F2'), + 'INTERNET' => lang('gen_hamradio_propagation_INTERNET'), + 'ION' => lang('gen_hamradio_propagation_ION'), + 'IRL' => lang('gen_hamradio_propagation_IRL'), + 'MS' => lang('gen_hamradio_propagation_MS'), + 'RPT' => lang('gen_hamradio_propagation_RPT'), + 'RS' => lang('gen_hamradio_propagation_RS'), + 'SAT' => lang('gen_hamradio_propagation_SAT'), + 'TEP' => lang('gen_hamradio_propagation_TEP'), + 'TR' => lang('gen_hamradio_propagation_TR')]; + asort($prop_modes); + ?> +
+ + +
+
+
+
+
+
+ + +
+
+ + +
+
+ +
+ + Map loaded successfully +
+
+
+
- -
-
- -
-
- -
-
- +
- -
+ +
+
+
+
+
+ World Map View +
+ Showing: +
+
+
+
+ QSOs Displayed: + 0 +
+
+ Map loaded successfully +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
- \ No newline at end of file + +
+
+
+
+
+
+ Legend: + + + QSO + + + + Home Station + +
+
+ + + Click on markers for QSO details • Use + to toggle callsign labels + +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/application/views/station_profile/index.php b/application/views/station_profile/index.php index 83899cb7..b6800a0f 100644 --- a/application/views/station_profile/index.php +++ b/application/views/station_profile/index.php @@ -1,99 +1,228 @@ -
+

session->flashdata('message')) { ?> -
-

session->flashdata('message'); ?>

+ -
-

- + +
+
+
+
+

+

Manage your station locations.

+
+ + + +
+
-
-
-

-

-

- - num_rows() > 0) { ?> - - - - - - = 1) && ($is_admin)) { ?> - - - -
- - - - - - - - - - - - - - - - result() as $row) { ?> - - - - - - - - - - - - - - -
- station_profile_name;?>
-
station_callsign;?>station_country == '' ? '- NONE -' : $row->station_country; if ($row->dxcc_end != NULL) { echo ' '.lang('gen_hamradio_deleted_dxcc').''; } ?>station_gridsquare;?> - station_active != 1) { ?> - station_id; ?>" class="btn btn-outline-secondary btn-sm" onclick="return confirm(' station_profile_name; ?>');"> - - - - -
- ID: station_id;?> - qso_total;?> -
- user_id == "") { ?> - station_id; ?>" class="btn btn-outline-primary btn-sm"> - - station_id; ?>" title="" class="btn btn-outline-primary btn-sm"> - - station_id; ?>" title="" class="btn btn-outline-primary btn-sm"> - station_id; ?>" class="btn btn-danger btn-sm" title="" onclick="return confirm('');"> - - station_active != 1) { ?> - station_id; ?>" class="btn btn-danger btn-sm" title="" onclick="return confirm(' station_profile_name; ?> ');"> - -
-
- -
+ +
+
+
+
+
About Station Locations
+
+
+
+
+
+ +
+
Location Management
+

+
+
+
+
+
+ +
+
Multiple Locations
+

+
+
+
+
+
+ +
+
Active Location
+

+
+
+
+
+
+
+
+ += 1) && ($is_admin))) { ?> +
+
+ + + + + = 1) && ($is_admin)) { ?> + + +
+
+ + + +num_rows() > 0) { ?> +
+
+
+
+
+
Station Locations (num_rows(); ?>)
+ Click on a station name to set it as active +
+
+
+
+ + + + + + + + + + + + + result() as $row) { ?> + station_active == 1) { echo 'class="table-success"'; } ?>> + + + + + + + + + +
StatusActions
+
+
+ station_profile_name;?> + station_active == 1) { ?> + + + + +
+ + ID: station_id;?> • + qso_total;?> + +
+
+
+ station_callsign;?> + + station_country == '' ? '- NONE -' : $row->station_country; ?> + dxcc_end != NULL) { echo '
'.lang('gen_hamradio_deleted_dxcc').''; } ?> +
+ station_gridsquare == '' ? '-' : $row->station_gridsquare;?> + + station_active != 1) { ?> + station_id; ?>" + class="btn btn-outline-success btn-sm" + onclick="return confirm(' station_profile_name; ?>');", + title=""> + + + + + + + + + +
+
+
+
+
+
+ + +
+
+
+
+ +
No Station Locations
+

You haven't created any station locations yet. Create your first station location to get started.

+ + + +
+
+
+
+
diff --git a/application/views/statistics/qsotable.php b/application/views/statistics/qsotable.php index 7f963fb3..0b80943f 100644 --- a/application/views/statistics/qsotable.php +++ b/application/views/statistics/qsotable.php @@ -17,7 +17,7 @@ if ($qsoarray) { foreach ($value as $key => $val) { echo '' . $val . ''; } - echo '' . $modetotal[$mode] . ''; + echo '' . (isset($modetotal[$mode]) ? $modetotal[$mode] : 0) . ''; echo ''; } echo ''.lang('statistics_total').''; diff --git a/application/views/statistics/uniquetable.php b/application/views/statistics/uniquetable.php index 0a25b170..7685bf2c 100644 --- a/application/views/statistics/uniquetable.php +++ b/application/views/statistics/uniquetable.php @@ -17,13 +17,13 @@ if ($qsoarray) { foreach ($value as $key => $val) { echo '' . $val . ''; } - echo '' . $modeunique[$mode] . ''; + echo '' . (isset($modeunique[$mode]) ? $modeunique[$mode] : 0) . ''; echo ''; } echo ''.lang('statistics_total').''; foreach($bands as $band) { - echo '' . $bandunique[$band] . ''; + echo '' . (isset($bandunique[$band]) ? $bandunique[$band] : 0) . ''; } echo '' . $total->calls . ''; echo ''; diff --git a/application/views/update/index.php b/application/views/update/index.php index cf05c2ab..4f29910b 100644 --- a/application/views/update/index.php +++ b/application/views/update/index.php @@ -25,9 +25,14 @@
Check for DXCC Data Updates
- + -
Status:
+
+ Status: Ready to update +


@@ -45,11 +50,6 @@ This function can be used to update QSO continent information for all QSOs in Cloudlog missing that information.

Check QSOs missing continent data

-
diff --git a/application/views/version_dialog/index.php b/application/views/version_dialog/index.php index 6d70492f..b84a8fae 100644 --- a/application/views/version_dialog/index.php +++ b/application/views/version_dialog/index.php @@ -9,39 +9,7 @@ - - - optionslib) ? $this->optionslib->get_option('version_dialog') : 'release_notes'; if ($versionDialogMode == 'custom_text' || $versionDialogMode == 'both') { ?> @@ -75,22 +43,72 @@ $data = json_decode($response, true); $current_version = $this->optionslib->get_option('version'); + if ($data !== null && !empty($data)) { + $firstRelease = null; foreach ($data as $singledata) { if ($singledata['tag_name'] == $current_version) { $firstRelease = $singledata; - continue; + break; } } - $releaseBody = isset($firstRelease['body']) ? $firstRelease['body'] : 'No release information available'; - $htmlReleaseBody = htmlspecialchars($releaseBody); - $htmlReleaseBodyWithLinks = preg_replace('/(https?:\/\/[^\s<]+)/', '$1', $htmlReleaseBody); + if ($firstRelease !== null) { + $releaseBody = isset($firstRelease['body']) ? $firstRelease['body'] : 'No release information available'; - $releaseName = isset($firstRelease['name']) ? $firstRelease['name'] : 'No version name information available'; - echo "

v" . $releaseName . "

"; - echo ""; - echo "
"; + $releaseName = isset($firstRelease['name']) ? $firstRelease['name'] : 'No version name information available'; + echo "

v" . $releaseName . "

"; + + // Convert markdown to HTML using PHP + $htmlContent = $releaseBody; + + // Escape HTML first to prevent issues + $htmlContent = htmlspecialchars($htmlContent); + + // Convert headers + $htmlContent = preg_replace('/^## (.+)$/m', '

$1

', $htmlContent); + $htmlContent = preg_replace('/^# (.+)$/m', '

$1

', $htmlContent); + + // Convert bullet points to list items + // First, find all bullet point sections and convert them to proper lists + $htmlContent = preg_replace_callback( + '/(?:^[ ]*\* .+(?:\r?\n|$))+/m', + function($matches) { + $listContent = $matches[0]; + // Convert each bullet point to
  • , removing any trailing newlines + $listContent = preg_replace('/^[ ]*\* (.+?)(?:\r?\n|$)/m', '
  • $1
  • ', $listContent); + // Wrap in
      tags + return '
        ' . trim($listContent) . '
      '; + }, + $htmlContent + ); + + // Convert links (markdown style) + $htmlContent = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $htmlContent); + + // Convert plain URLs to links + $htmlContent = preg_replace('/(https?:\/\/[^\s<]+)/', '$1', $htmlContent); + + // Convert GitHub usernames (@username) to profile links + $htmlContent = preg_replace('/@([a-zA-Z0-9_-]+)/', '@$1', $htmlContent); + + // Convert bold text + $htmlContent = preg_replace('/\*\*([^*]+)\*\*/', '$1', $htmlContent); + + // Convert line breaks to
      tags + $htmlContent = nl2br($htmlContent); + + // Clean up: remove
      tags that appear right before or after list tags + $htmlContent = preg_replace('/\s*(<\/?ul>)/', '$1', $htmlContent); + $htmlContent = preg_replace('/(<\/?ul>)\s*/', '$1', $htmlContent); + $htmlContent = preg_replace('/\s*(<\/?li>)/', '$1', $htmlContent); + $htmlContent = preg_replace('/(<\/?li>)\s*/', '$1', $htmlContent); + + echo "
      " . $htmlContent . "
      "; + } else { + echo '

      v' . $current_version . '

      '; + echo '

      No release information found for this version on GitHub.

      '; + } } else { echo 'Error decoding JSON data or received empty response.'; } diff --git a/application/views/workabledxcc/components/dxcclist.php b/application/views/workabledxcc/components/dxcclist.php index ce891bac..5eda33d2 100644 --- a/application/views/workabledxcc/components/dxcclist.php +++ b/application/views/workabledxcc/components/dxcclist.php @@ -49,6 +49,36 @@ foreach ($grouped as $month => $dxccs) { echo 'Confirmed'; } + // Add satellite badge if worked via satellite + if (isset($dxcc['workedViaSatellite']) && $dxcc['workedViaSatellite']) { + echo ' Worked via Satellite'; + } + + // IOTA handling: show badge if JSON contained an iota field + if (isset($dxcc['iota']) && !empty($dxcc['iota'])) { + $iotaTag = $dxcc['iota']; + $mapUrl = 'https://www.iota-world.org/iotamaps/?grpref=' . $iotaTag; + // Anchor inside badge should inherit readable text colour + $iotaAnchor = '' . $iotaTag . ''; + + if (isset($dxcc['iota_status'])) { + $s = $dxcc['iota_status']; + + if (!empty($s) && isset($s['worked']) && $s['worked']) { + echo ' IOTA ' . $iotaAnchor . ' Worked'; + } else { + echo ' IOTA ' . $iotaAnchor . ' Needed'; + } + + if (!empty($s) && isset($s['confirmed']) && $s['confirmed']) { + echo ' Confirmed'; + } + } else { + // No status; render a neutral badge containing the link + echo ' ' . $iotaAnchor . ''; + } + } + echo ' ' . $dxcc['notes'] . ' '; diff --git a/application/views/workabledxcc/index.php b/application/views/workabledxcc/index.php index 77aa8dcc..ed3d1397 100644 --- a/application/views/workabledxcc/index.php +++ b/application/views/workabledxcc/index.php @@ -1,5 +1,16 @@

      -
      +
      + +
      +
      +
      + Loading... +
      +
      Generating Table
      +

      Processing DXPedition data and checking your logbook...

      +
      +
      +
      diff --git a/assets/css/general.css b/assets/css/general.css index e3f2a813..b6137aac 100644 --- a/assets/css/general.css +++ b/assets/css/general.css @@ -74,6 +74,200 @@ thead>tr>td { margin-bottom: 0rem; } +.adif .card { + border: none; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.adif .card-body { + padding: 1.5rem; +} + +.adif .form-check { + padding-left: 0; +} + +.adif .form-check-input { + margin-right: 0.5rem; +} + +.adif .form-check-label { + font-size: 0.9rem; + line-height: 1.4; +} + +.adif .badge { + font-size: 0.7rem; + padding: 0.25rem 0.5rem; +} + +.adif .btn { + border-radius: 0.375rem; + font-weight: 500; + padding: 0.5rem 1rem; +} + +.adif .btn-lg { + padding: 0.75rem 1.5rem; + font-size: 1.1rem; +} + +.adif .form-control, +.adif .form-select { + border-radius: 0.375rem; + border-color: #ced4da; +} + +.adif .form-control:focus, +.adif .form-select:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); +} + +.adif .card .card-header h6 { + margin-bottom: 0; + font-weight: 600; + color: #495057; +} + +.adif .alert { + border-radius: 0.5rem; + border: none; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +/* Station Management Page Styling */ +.station-management { + padding-top: 15px; +} + +.station-management .table-success { + background-color: rgba(25, 135, 84, 0.1); +} + +.station-management .card { + border: none; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.station-management .card-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 1rem 1.25rem; +} + +.station-management .badge { + font-size: 0.7rem; + padding: 0.25rem 0.5rem; +} + +.station-management .btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + border-radius: 0.375rem; +} + +/* DataTable styling within cards */ +.station-management .dataTables_wrapper { + padding: 0; +} + +.station-management .dataTables_length, +.station-management .dataTables_filter { + margin-bottom: 1rem; +} + +.station-management .dataTables_info, +.station-management .dataTables_paginate { + margin-top: 1rem; +} + +.station-management .dataTables_length label, +.station-management .dataTables_filter label { + font-weight: 500; + margin-bottom: 0; +} + +.station-management .dataTables_filter input { + margin-left: 0.5rem; + border-radius: 0.375rem; + border: 1px solid #ced4da; + padding: 0.375rem 0.75rem; +} + +.station-management .dataTables_length select { + margin: 0 0.5rem; + border-radius: 0.375rem; + border: 1px solid #ced4da; + padding: 0.375rem 0.75rem; +} + +/* Logbooks Management Page Styling */ +.logbooks-management { + padding-top: 15px; +} + +.logbooks-management .table-success { + background-color: rgba(25, 135, 84, 0.1); +} + +.logbooks-management .card { + border: none; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.logbooks-management .card-header { + background-color: #f8f9fa; + border-bottom: 1px solid #dee2e6; + padding: 1rem 1.25rem; +} + +.logbooks-management .badge { + font-size: 0.7rem; + padding: 0.25rem 0.5rem; +} + +.logbooks-management .btn-sm { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + border-radius: 0.375rem; +} + +/* DataTable styling within cards */ +.logbooks-management .dataTables_wrapper { + padding: 0; +} + +.logbooks-management .dataTables_length, +.logbooks-management .dataTables_filter { + margin-bottom: 1rem; +} + +.logbooks-management .dataTables_info, +.logbooks-management .dataTables_paginate { + margin-top: 1rem; +} + +.logbooks-management .dataTables_length label, +.logbooks-management .dataTables_filter label { + font-weight: 500; + margin-bottom: 0; +} + +.logbooks-management .dataTables_filter input { + margin-left: 0.5rem; + border-radius: 0.375rem; + border: 1px solid #ced4da; + padding: 0.375rem 0.75rem; +} + +.logbooks-management .dataTables_length select { + margin: 0 0.5rem; + border-radius: 0.375rem; + border: 1px solid #ced4da; + padding: 0.375rem 0.75rem; +} + .api .alert p { margin-bottom: 0rem; } diff --git a/assets/css/map-enhancements.css b/assets/css/map-enhancements.css new file mode 100644 index 00000000..f8a8c67d --- /dev/null +++ b/assets/css/map-enhancements.css @@ -0,0 +1,74 @@ +/* Custom Map Interface Enhancements */ + +/* Ensure grid square labels stay together (fix for separated letters) */ +.leaflet-marker-icon.my-div-icon, +.my-div-icon .grid-text, +.my-div-icon .grid-text font, +span.grid-text, +span.grid-text > font { + letter-spacing: 0 !important; + word-spacing: 0 !important; + white-space: nowrap !important; + font-family: monospace !important; + display: inline-block !important; +} + +/* Map container enhancements */ +#custommap { + border-radius: 0 0 0.375rem 0.375rem; + overflow: hidden; + transition: height 0.3s ease; + width: 100%; + height: 1000px !important; /* Maintain original screenshot-friendly height */ +} + +/* Fullscreen map styling */ +#custommap:fullscreen { + border-radius: 0; +} + +/* Statistics badges */ +.custom-map-QSOs .badge { + font-size: 0.85em; + padding: 0.5em 0.75em; +} + + +/* Legend styling */ +.custom-map-QSOs .fas { + width: 1em; + text-align: center; +} + +/* Responsive improvements */ +@media (max-width: 768px) { + .custom-map-QSOs .card-body { + padding: 1rem 0.5rem; + } + + .custom-map-QSOs .btn-group .btn { + padding: 0.375rem 0.5rem; + font-size: 0.8rem; + } + + /* Keep map height at 1000px even on mobile for screenshots */ + #custommap { + height: 1000px !important; + min-height: 1000px !important; + } +} + +/* Animation for statistics updates */ +.custom-map-QSOs .badge { + transition: all 0.3s ease; +} + +.custom-map-QSOs .badge.updated { + animation: pulse 0.5s ease-in-out; +} + +@keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } +} \ No newline at end of file diff --git a/assets/js/leaflet/leafembed.js b/assets/js/leaflet/leafembed.js index b2a59bf9..5c57e689 100644 --- a/assets/js/leaflet/leafembed.js +++ b/assets/js/leaflet/leafembed.js @@ -65,12 +65,25 @@ function initplot(_url_qso, options={}) { } function askForPlots(_url_qso, options={}) { + console.log('askForPlots called with URL:', _url_qso, 'options:', options); removeMarkers(); if (typeof options.dataPost !== "undefined") { _dataPost = options.dataPost; } else { _dataPost = {}; } $.ajax({ - url: _url_qso, type: 'POST', dataType: 'json', data: _dataPost, - error: function() { console.log('[ERROR] ajax askForPlots() function return error.'); }, + url: _url_qso, + type: 'POST', + dataType: 'json', + data: _dataPost, + timeout: 50000, // 50 second timeout + error: function(xhr, status, error) { + console.log('[ERROR] ajax askForPlots() function return error:', status, error, xhr); + // Call custom error callback if provided + if (typeof options.onError === 'function') { + console.log('Calling custom error callback'); + options.onError(); + } + }, success: function(plotjson) { + console.log('askForPlots AJAX success, plotjson:', plotjson); if ((typeof plotjson['markers'] !== "undefined")&&(plotjson['markers'].length>0)) { for (i=0;i=14.14" } diff --git a/script.sh b/script.sh index e713e866..b4cf4631 100755 --- a/script.sh +++ b/script.sh @@ -60,32 +60,51 @@ if [ ! -d "$DEST_DIR" ]; then mkdir -p $DEST_DIR fi +# Check if configuration has already been processed +if [ -f "$DEST_DIR/database.php" ] && [ -f "$DEST_DIR/config.php" ]; then + echo "Configuration files already exist, skipping processing..." +else + echo "Processing configuration files..." + + # Use sed with a different delimiter (`|`) to avoid conflicts with special characters + # Strip any trailing whitespace/newlines from variables before using them + CLEAN_DATABASE=$(echo "${MYSQL_DATABASE}" | tr -d '\r\n') + CLEAN_USER=$(echo "${MYSQL_USER}" | tr -d '\r\n') + CLEAN_PASSWORD=$(echo "${MYSQL_PASSWORD}" | tr -d '\r\n') + CLEAN_HOST=$(echo "${MYSQL_HOST}" | tr -d '\r\n') + CLEAN_LOCATOR=$(echo "${BASE_LOCATOR}" | tr -d '\r\n') + CLEAN_URL=$(echo "${WEBSITE_URL}" | tr -d '\r\n') + CLEAN_DIRECTORY=$(echo "${DIRECTORY}" | tr -d '\r\n') + + sed -i "s|%DATABASE%|${CLEAN_DATABASE}|g" $DATABASE_FILE + sed -i "s|%USERNAME%|${CLEAN_USER}|g" $DATABASE_FILE + sed -i "s|%PASSWORD%|${CLEAN_PASSWORD}|g" $DATABASE_FILE + sed -i "s|%HOSTNAME%|${CLEAN_HOST}|g" $DATABASE_FILE + sed -i "s|%baselocator%|${CLEAN_LOCATOR}|g" $CONFIG_FILE + sed -i "s|%websiteurl%|${CLEAN_URL}|g" $CONFIG_FILE + sed -i "s|%directory%|${CLEAN_DIRECTORY}|g" $CONFIG_FILE -# Use sed with a different delimiter (`|`) to avoid conflicts with special characters -sed -i "s|%DATABASE%|${MYSQL_DATABASE}|g" $DATABASE_FILE -sed -i "s|%USERNAME%|${MYSQL_USER}|g" $DATABASE_FILE -sed -i "s|%PASSWORD%|${MYSQL_PASSWORD}|g" $DATABASE_FILE -sed -i "s|%HOSTNAME%|${MYSQL_HOST}|g" $DATABASE_FILE -sed -i "s|%baselocator%|${BASE_LOCATOR}|g" $CONFIG_FILE -sed -i "s|%websiteurl%|${WEBSITE_URL}|g" $CONFIG_FILE -sed -i "s|%directory%|${DIRECTORY}|g" $CONFIG_FILE + # Move the files to the destination directory + mv $CONFIG_FILE $DEST_DIR + mv $DATABASE_FILE $DEST_DIR -# Move the files to the destination directory -mv $CONFIG_FILE $DEST_DIR -mv $DATABASE_FILE $DEST_DIR - -# Delete the /install directory -rm -rf /install + # Delete the /install directory + rm -rf /install +fi echo "Replacement complete." # Wait for database to be ready echo "Waiting for database to be ready..." -until mysql -h"$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -e "SELECT 1" > /dev/null 2>&1; do - echo "Database is not ready yet. Waiting 5 seconds..." - sleep 5 -done -echo "Database is ready!" +echo "Connecting to: Host=$MYSQL_HOST, User=$MYSQL_USER, Database=$MYSQL_DATABASE" + +# Give the database a moment, then test connection once +sleep 2 +if mariadb -h"$MYSQL_HOST" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -D"$MYSQL_DATABASE" -e "SELECT 1;" >/dev/null 2>&1; then + echo "Database is ready!" +else + echo "Database connection failed, but continuing anyway since healthcheck passed..." +fi # Set Permissions chown -R root:www-data /var/www/html/application/config/