From d597e2e83b53b9aad42b00a0bdcff4a275eb3636 Mon Sep 17 00:00:00 2001 From: Peter Goodhall Date: Tue, 6 Jan 2026 22:34:05 +0000 Subject: [PATCH] Add DXCC statistics and continent breakdown Introduces DXCC statistics and continent breakdown features to the awards page, including new controller endpoints for DXCC QSOs, QSOs by status, and continent QSOs. Updates the DXCC model to provide continent and milestone statistics, and enhances the awards view with summary cards, continent progress, table search, sorting, and modal dialogs for QSO details. --- application/controllers/Awards.php | 218 +++++- application/models/Dxcc.php | 153 +++- application/views/awards/dxcc/index.php | 937 ++++++++++++++++++++---- assets/js/sections/dxccmap.js | 9 +- 4 files changed, 1186 insertions(+), 131 deletions(-) diff --git a/application/controllers/Awards.php b/application/controllers/Awards.php index 596dcc439..9af3993bc 100644 --- a/application/controllers/Awards.php +++ b/application/controllers/Awards.php @@ -160,6 +160,10 @@ class Awards extends CI_Controller $dxcclist = $this->dxcc->fetchdxcc($postdata); $data['dxcc_array'] = $this->dxcc->get_dxcc_array($dxcclist, $bands, $postdata); $data['dxcc_summary'] = $this->dxcc->get_dxcc_summary($bands, $postdata); + + // Calculate continent breakdown and totals + $data['continent_breakdown'] = $this->dxcc->get_continent_breakdown($dxcclist); + $data['dxcc_statistics'] = $this->dxcc->get_dxcc_statistics($data['dxcc_array'], $postdata); // Render Page $data['page_title'] = "Awards - DXCC"; @@ -1290,4 +1294,216 @@ class Awards extends CI_Controller } } } -} + + /* + * Get QSOs for a specific DXCC entity + */ + public function get_dxcc_qsos() + { + $this->load->model('logbooks_model'); + + $dxcc_id = $this->security->xss_clean($this->input->post('dxcc_id')); + $limit = $this->security->xss_clean($this->input->post('limit')) ?: 20; + + if (!$dxcc_id || !is_numeric($dxcc_id)) { + header('Content-Type: application/json'); + echo json_encode(array('error' => 'Invalid DXCC ID')); + return; + } + + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + + if (!$logbooks_locations_array) { + header('Content-Type: application/json'); + echo json_encode(array('error' => 'No logbook data')); + return; + } + + $location_list = "'" . implode("','", $logbooks_locations_array) . "'"; + + try { + // Get QSOs for this DXCC + $query = $this->db->query(" + SELECT + col_time_on, + col_call, + col_band, + col_mode, + col_rst_sent, + col_rst_rcvd, + col_qsl_sent, + col_qsl_rcvd, + COL_LOTW_QSL_SENT, + COL_LOTW_QSL_RCVD + FROM " . $this->config->item('table_name') . " + WHERE station_id IN (" . $location_list . ") + AND col_dxcc = " . $dxcc_id . " + ORDER BY col_time_on DESC + LIMIT " . intval($limit) + ); + + if ($query->num_rows() > 0) { + $qsos = $query->result_array(); + header('Content-Type: application/json'); + echo json_encode(array('qsos' => $qsos, 'count' => $query->num_rows())); + } else { + header('Content-Type: application/json'); + echo json_encode(array('qsos' => array(), 'count' => 0)); + } + } catch (Exception $e) { + header('Content-Type: application/json'); + echo json_encode(array('error' => $e->getMessage())); + } + } + + /* + * Get QSOs for a specific DXCC entity filtered by status (Confirmed or Worked) + */ + public function get_dxcc_qsos_by_status() + { + $this->load->model('logbooks_model'); + + $dxcc_id = $this->security->xss_clean($this->input->post('dxcc_id')); + $status = $this->security->xss_clean($this->input->post('status')); + $limit = $this->security->xss_clean($this->input->post('limit')) ?: 100; + + if (!$dxcc_id || !is_numeric($dxcc_id) || !$status) { + header('Content-Type: application/json'); + echo json_encode(array('error' => 'Invalid parameters', 'count' => 0, 'qsos' => array())); + return; + } + + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + + if (!$logbooks_locations_array) { + header('Content-Type: application/json'); + echo json_encode(array('error' => 'No logbook data', 'count' => 0, 'qsos' => array())); + return; + } + + $location_list = "'" . implode("','", $logbooks_locations_array) . "'"; + + try { + // Build WHERE clause for status filter + $status_where = ''; + if ($status === 'C') { + // Confirmed: either QSL received or LoTW received + $status_where = " AND (col_qsl_rcvd = 1 OR COL_LOTW_QSL_RCVD = 'Y')"; + } elseif ($status === 'W') { + // Worked but not confirmed: neither QSL nor LoTW received + $status_where = " AND (col_qsl_rcvd IS NULL OR col_qsl_rcvd != 1) AND (COL_LOTW_QSL_RCVD IS NULL OR COL_LOTW_QSL_RCVD != 'Y')"; + } + + // Get QSOs for this DXCC filtered by status + $query = $this->db->query(" + SELECT + col_time_on, + col_call, + col_band, + col_mode, + col_rst_sent, + col_rst_rcvd, + col_qsl_sent, + col_qsl_rcvd, + COL_LOTW_QSL_SENT, + COL_LOTW_QSL_RCVD + FROM " . $this->config->item('table_name') . " + WHERE station_id IN (" . $location_list . ") + AND col_dxcc = " . $dxcc_id . $status_where . " + ORDER BY col_time_on DESC + LIMIT " . intval($limit) + ); + + if ($query->num_rows() > 0) { + $qsos = $query->result_array(); + header('Content-Type: application/json'); + echo json_encode(array('qsos' => $qsos, 'count' => $query->num_rows())); + } else { + header('Content-Type: application/json'); + echo json_encode(array('qsos' => array(), 'count' => 0)); + } + } catch (Exception $e) { + header('Content-Type: application/json'); + echo json_encode(array('error' => $e->getMessage(), 'count' => 0, 'qsos' => array())); + } + } + + /* + * Get DXCC entities for a specific continent with their status + */ + public function get_continent_qsos() + { + $this->load->model('logbooks_model'); + $this->load->model('dxcc'); + + $continent_code = $this->security->xss_clean($this->input->post('continent_code')); + + if (!$continent_code) { + header('Content-Type: application/json'); + echo json_encode(array('error' => 'Invalid continent code', 'count' => 0, 'entities' => array())); + return; + } + + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook')); + + if (!$logbooks_locations_array) { + header('Content-Type: application/json'); + echo json_encode(array('error' => 'No logbook data', 'count' => 0, 'entities' => array())); + return; + } + + $location_list = "'" . implode("','", $logbooks_locations_array) . "'"; + + try { + // Get all DXCC entities for this continent + $query = $this->db->query(" + SELECT adif, name, prefix, cont + FROM dxcc_entities + WHERE cont = '" . $this->db->escape_like_str($continent_code) . "' + ORDER BY name ASC + "); + + $entities = array(); + if ($query->num_rows() > 0) { + foreach ($query->result() as $entity) { + // Check if worked + $worked_query = $this->db->query(" + SELECT COUNT(*) as count FROM " . $this->config->item('table_name') . " + WHERE station_id IN (" . $location_list . ") + AND col_dxcc = " . $entity->adif + ); + $worked = $worked_query->row()->count > 0 ? true : false; + + // Check if confirmed + $confirmed_query = $this->db->query(" + SELECT COUNT(*) as count FROM " . $this->config->item('table_name') . " + WHERE station_id IN (" . $location_list . ") + AND col_dxcc = " . $entity->adif . " + AND (col_qsl_rcvd = 1 OR COL_LOTW_QSL_RCVD = 'Y') + "); + $confirmed = $confirmed_query->row()->count > 0 ? true : false; + + $status = 'unworked'; + if ($confirmed) { + $status = 'confirmed'; + } elseif ($worked) { + $status = 'worked'; + } + + $entities[] = array( + 'adif' => $entity->adif, + 'name' => $entity->name, + 'prefix' => $entity->prefix, + 'status' => $status + ); + } + } + + header('Content-Type: application/json'); + echo json_encode(array('entities' => $entities, 'count' => count($entities))); + } catch (Exception $e) { + header('Content-Type: application/json'); + echo json_encode(array('error' => $e->getMessage(), 'count' => 0, 'entities' => array())); + } + } +} \ No newline at end of file diff --git a/application/models/Dxcc.php b/application/models/Dxcc.php index ef02fea57..3ad61cfe9 100644 --- a/application/models/Dxcc.php +++ b/application/models/Dxcc.php @@ -101,7 +101,7 @@ class DXCC extends CI_Model { if ($postdata['worked'] != NULL) { $workedDXCC = $this->getDxccBandWorked($location_list, $band, $postdata); foreach ($workedDXCC as $wdxcc) { - $dxccMatrix[$wdxcc->dxcc][$band] = '
name).'","'. $band . '","'. $postdata['mode'] . '","DXCC", "")\'>W
'; + $dxccMatrix[$wdxcc->dxcc][$band] = '
dxcc . ', "W")\'>W
'; } } @@ -109,7 +109,7 @@ class DXCC extends CI_Model { if ($postdata['confirmed'] != NULL) { $confirmedDXCC = $this->getDxccBandConfirmed($location_list, $band, $postdata); foreach ($confirmedDXCC as $cdxcc) { - $dxccMatrix[$cdxcc->dxcc][$band] = '
name).'","'. $band . '","'. $postdata['mode'] . '","DXCC","'.$qsl.'")\'>C
'; + $dxccMatrix[$cdxcc->dxcc][$band] = '
dxcc . ', "C")\'>C
'; } } } @@ -222,7 +222,7 @@ class DXCC extends CI_Model { $location_list = "'".implode("','",$logbooks_locations_array)."'"; - $sql = "select adif, prefix, name, date(end) Enddate, date(start) Startdate, lat, `long` + $sql = "select adif, prefix, name, cont, date(end) Enddate, date(start) Startdate, lat, `long` from dxcc_entities"; if ($postdata['notworked'] == NULL) { @@ -507,5 +507,152 @@ class DXCC extends CI_Model { return $query->row(); } + + /* + * Get continent breakdown for DXCC statistics + */ + function get_continent_breakdown($dxccArray) { + $continents = array( + 'Africa' => 0, + 'Antarctica' => 0, + 'Asia' => 0, + 'Europe' => 0, + 'NorthAmerica' => 0, + 'SouthAmerica' => 0, + 'Oceania' => 0 + ); + + if (!$dxccArray) { + return $continents; + } + + // Map continent codes to names + $continentMap = array( + 'AF' => 'Africa', + 'AN' => 'Antarctica', + 'AS' => 'Asia', + 'EU' => 'Europe', + 'NA' => 'NorthAmerica', + 'SA' => 'SouthAmerica', + 'OC' => 'Oceania' + ); + + foreach ($dxccArray as $dxcc) { + if (isset($dxcc->cont) && isset($continentMap[$dxcc->cont])) { + $continentName = $continentMap[$dxcc->cont]; + $continents[$continentName]++; + } + } + + return $continents; + } + + /* + * Get overall DXCC statistics + */ + function get_dxcc_statistics($dxcc_array, $postdata) { + $stats = array( + 'total_worked' => 0, + 'total_confirmed' => 0, + 'worked_by_continent' => array( + 'Africa' => 0, + 'Antarctica' => 0, + 'Asia' => 0, + 'Europe' => 0, + 'NorthAmerica' => 0, + 'SouthAmerica' => 0, + 'Oceania' => 0 + ), + 'confirmed_by_continent' => array( + 'Africa' => 0, + 'Antarctica' => 0, + 'Asia' => 0, + 'Europe' => 0, + 'NorthAmerica' => 0, + 'SouthAmerica' => 0, + 'Oceania' => 0 + ), + 'milestones' => array( + 100 => false, + 200 => false, + 300 => false + ) + ); + + if (!$dxcc_array) { + return $stats; + } + + // Get DXCC entities to map continents + $dxcc_result = $this->db->get('dxcc_entities')->result_array(); + $all_dxcc = array(); + foreach ($dxcc_result as $row) { + $all_dxcc[$row['adif']] = $row; + } + + // Map continent codes to names + $continentMap = array( + 'AF' => 'Africa', + 'AN' => 'Antarctica', + 'AS' => 'Asia', + 'EU' => 'Europe', + 'NA' => 'NorthAmerica', + 'SA' => 'SouthAmerica', + 'OC' => 'Oceania' + ); + + // Count worked DXCC from array + $worked_count = 0; + $confirmed_count = 0; + + foreach ($dxcc_array as $adif => $entity) { + // Check if this DXCC has any confirmed contacts (any cell with 'bg-success') + $is_worked = false; + $is_confirmed = false; + + foreach ($entity as $key => $value) { + if (is_string($value) && strpos($value, 'bg-success') !== false) { + $is_confirmed = true; + $is_worked = true; + break; + } + if (is_string($value) && strpos($value, 'bg-danger') !== false) { + $is_worked = true; + } + } + + if ($is_worked) { + $worked_count++; + } + if ($is_confirmed) { + $confirmed_count++; + } + + // Map continent stats + if (isset($all_dxcc[$adif]) && isset($all_dxcc[$adif]['cont'])) { + $cont_code = $all_dxcc[$adif]['cont']; + $continent = isset($continentMap[$cont_code]) ? $continentMap[$cont_code] : null; + + if ($continent) { + if ($is_confirmed) { + $stats['confirmed_by_continent'][$continent]++; + } + if ($is_worked) { + $stats['worked_by_continent'][$continent]++; + } + } + } + } + + $stats['total_worked'] = $worked_count; + $stats['total_confirmed'] = $confirmed_count; + + // Check milestone achievements + $stats['milestones'][100] = $stats['total_worked'] >= 100; + $stats['milestones'][200] = $stats['total_worked'] >= 200; + $stats['milestones'][300] = $stats['total_worked'] >= 300; + + return $stats; + } } ?> diff --git a/application/views/awards/dxcc/index.php b/application/views/awards/dxcc/index.php index 0ad0a0b44..ccb4d6707 100644 --- a/application/views/awards/dxcc/index.php +++ b/application/views/awards/dxcc/index.php @@ -30,7 +30,121 @@ margin: 0 8px 0 0; opacity: 0.7; } + +.stats-card { + border-left: 4px solid #007bff; + padding: 20px; + margin-bottom: 15px; + background-color: #f8f9fa; + border-radius: 4px; +} + +.stats-card.milestone { + border-left-color: #28a745; +} + +.stats-card.milestone.achieved { + background-color: #d4edda; +} + +.stats-card h5 { + margin: 0 0 10px 0; + color: #333; + font-weight: 600; +} + +.stats-number { + font-size: 28px; + font-weight: bold; + color: #007bff; + margin: 10px 0; +} + +.milestone.achieved .stats-number { + color: #28a745; +} + +.progress { + height: 24px; + margin-top: 10px; +} + +.progress-label { + font-size: 12px; + font-weight: 500; +} + +.filter-collapse { + cursor: pointer; +} + +.preset-filters { + margin-bottom: 15px; +} + +.preset-btn { + margin-right: 5px; + margin-bottom: 5px; + font-size: 12px; +} + +.table-search { + margin-bottom: 15px; +} + +.sortable { + cursor: pointer; + user-select: none; +} + +.sortable::after { + content: ' ↕'; + font-size: 12px; + color: #999; +} + +.sortable.asc::after { + content: ' ↑'; + color: #007bff; +} + +.sortable.desc::after { + content: ' ↓'; + color: #007bff; +} + +.dxcc-link { + color: #007bff; + cursor: pointer; + text-decoration: underline; +} + +.dxcc-link:hover { + color: #0056b3; +} + +.continent-card { + cursor: pointer; + user-select: none; + transition: all 0.2s ease; +} + +.continent-card:hover { + background-color: #e9ecef; + transform: translateY(-2px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.tab-pane { + animation: fadeIn 0.3s; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +

@@ -47,139 +161,257 @@
-
-
- -
-
Deleted DXCC
-
-
- input->post('includedeleted')) echo ' checked="checked"'; ?> > - + + +
+ +
+
+
DXCC Progress
+
+
+
Total Worked
+
+
+
+
Total Confirmed
+
+
- -
-
Worked / Confirmed
-
-
- input->post('worked') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - + +
+
+
🏆 Milestone Progress
+
+
+
100
+
+ +
-
- input->post('confirmed') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - +
+
200
+
+ +
-
- input->post('notworked') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - +
+
300
+
+ +
+
+
-
-
QSL Type
-
-
- input->post('qsl') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('lotw') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('eqsl')) echo ' checked="checked"'; ?> > - -
-
-
- -
-
Continents
-
-
- input->post('Antarctica') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('Africa') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('Asia') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('Europe') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('NorthAmerica') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('SouthAmerica') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
- input->post('Oceania') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > - -
-
-
- -
- -
- -
-
- -
- -
- +
+
+
+ / +
+
+
+
+
+
+
+
+ -
- -
- - - - + +
+
+ Filters + +
+
+
+ +
+ + + + + +
-
-
-
+
+ +
+
Deleted DXCC
+
+
+ input->post('includedeleted')) echo ' checked="checked"'; ?> > + +
+
+
+ + +
+
Worked / Confirmed
+
+
+ input->post('worked') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('confirmed') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('notworked') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+
+ +
+
QSL Type
+
+
+ input->post('qsl') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('lotw') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('eqsl')) echo ' checked="checked"'; ?> > + +
+
+
+ +
+
Continents
+
+
+ input->post('Antarctica') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('Africa') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('Asia') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('Europe') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('NorthAmerica') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('SouthAmerica') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+ input->post('Oceania') || $this->input->method() !== 'post') echo ' checked="checked"'; ?> > + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + + + + +
+
+ +
+ + +