Support multiple POTA refs, DB + UI

Add support for comma-separated POTA references across the app: bump migration version to 259 and add migration 260 to expand COL_MY_POTA_REF and station_pota to VARCHAR(255). Introduce Pota::split_refs and collect_refs_from_rows to normalize and aggregate multiple refs; refactor Pota counts to compute unique refs per band/mode. Normalize station_pota when saving in Stations and Logbook_model (uses normalize_pota_refs), and adjust POTA search to match refs inside comma-separated values. Update views to render multiple POTA links and hashtags, and add Selectize-based autocomplete for station POTA input on create/edit forms. Also minor change to user_owns_station query (removed user_id where clause).
This commit is contained in:
Peter Goodhall 2026-03-24 14:32:17 +00:00
parent 2537f66d8f
commit 14af3fda19
9 changed files with 219 additions and 30 deletions

View file

@ -22,7 +22,7 @@ $config['migration_enabled'] = TRUE;
|
*/
$config['migration_version'] = 258;
$config['migration_version'] = 259;
/*
|--------------------------------------------------------------------------

View file

@ -636,9 +636,7 @@ class Awards extends CI_Controller
$filters = $this->pota_filters_from_request();
$this->load->model('pota');
$rows = $this->pota->fetch_qsos($filters);
$refs = [];
foreach ($rows as $r) { $refs[$r->COL_POTA_REF] = true; }
$refs = array_keys($refs);
$refs = $this->pota->collect_refs_from_rows($rows);
$data['parks'] = $this->pota->get_parks_meta($refs);
$this->load->view('awards/pota/components/map', $data);
}

View file

@ -0,0 +1,56 @@
<?php
defined('BASEPATH') or exit('No direct script access allowed');
class Migration_expand_my_pota_ref_columns extends CI_Migration
{
public function up()
{
if ($this->db->field_exists('COL_MY_POTA_REF', $this->config->item('table_name'))) {
$this->dbforge->modify_column($this->config->item('table_name'), array(
'COL_MY_POTA_REF' => array(
'name' => 'COL_MY_POTA_REF',
'type' => 'VARCHAR',
'constraint' => '255',
'null' => true,
),
));
}
if ($this->db->field_exists('station_pota', 'station_profile')) {
$this->dbforge->modify_column('station_profile', array(
'station_pota' => array(
'name' => 'station_pota',
'type' => 'VARCHAR',
'constraint' => '255',
'null' => true,
),
));
}
}
public function down()
{
if ($this->db->field_exists('COL_MY_POTA_REF', $this->config->item('table_name'))) {
$this->dbforge->modify_column($this->config->item('table_name'), array(
'COL_MY_POTA_REF' => array(
'name' => 'COL_MY_POTA_REF',
'type' => 'VARCHAR',
'constraint' => '50',
'null' => true,
),
));
}
if ($this->db->field_exists('station_pota', 'station_profile')) {
$this->dbforge->modify_column('station_profile', array(
'station_pota' => array(
'name' => 'station_pota',
'type' => 'VARCHAR',
'constraint' => '50',
'null' => true,
),
));
}
}
}

View file

@ -412,7 +412,7 @@ class Logbook_model extends CI_Model
$data['COL_MY_IOTA'] = strtoupper(trim($station['station_iota']));
$data['COL_MY_SOTA_REF'] = strtoupper(trim($station['station_sota']));
$data['COL_MY_WWFF_REF'] = $station['station_wwff'] ? strtoupper(trim($station['station_wwff'])) : '';
$data['COL_MY_POTA_REF'] = $station['station_pota'] == null ? '' : strtoupper(trim($station['station_pota']));
$data['COL_MY_POTA_REF'] = $station['station_pota'] == null ? '' : $this->normalize_pota_refs($station['station_pota']);
$data['COL_STATION_CALLSIGN'] = strtoupper(trim($station['station_callsign']));
$data['COL_MY_DXCC'] = strtoupper(trim($station['station_dxcc']));
@ -515,7 +515,10 @@ class Logbook_model extends CI_Model
$this->db->where('COL_WWFF_REF', $searchphrase);
break;
case 'POTA':
$this->db->group_start();
$this->db->where('COL_POTA_REF', $searchphrase);
$this->db->or_where("CONCAT(',', COL_POTA_REF, ',') LIKE '%," . $this->db->escape_like_str($searchphrase) . ",%'", null, false);
$this->db->group_end();
break;
case 'DOK':
$this->db->where('COL_DARC_DOK', $searchphrase);
@ -1312,7 +1315,7 @@ class Logbook_model extends CI_Model
$iotaRef = $station_profile->station_iota;
$sotaRef = $station_profile->station_sota;
$wwffRef = $station_profile->station_wwff;
$potaRef = $station_profile->station_pota;
$potaRef = $this->normalize_pota_refs($station_profile->station_pota);
$mode = $this->get_main_mode_if_submode($this->input->post('mode'));
if ($mode == null) {
@ -4509,7 +4512,7 @@ class Logbook_model extends CI_Model
$data['COL_MY_IOTA'] = strtoupper(trim($row['station_iota']));
$data['COL_MY_SOTA_REF'] = strtoupper(trim($row['station_sota']));
$data['COL_MY_WWFF_REF'] = strtoupper(trim($row['station_wwff']));
$data['COL_MY_POTA_REF'] = $row['station_pota'] == null ? '' : strtoupper(trim($row['station_pota']));
$data['COL_MY_POTA_REF'] = $row['station_pota'] == null ? '' : $this->normalize_pota_refs($row['station_pota']);
$data['COL_STATION_CALLSIGN'] = strtoupper(trim($row['station_callsign']));
$data['COL_MY_DXCC'] = strtoupper(trim($row['station_dxcc']));

View file

@ -2,6 +2,30 @@
class Pota extends CI_Model {
public function split_refs($value) {
if ($value === null || $value === '') {
return [];
}
$parts = preg_split('/\s*,\s*/', strtoupper(trim((string)$value)));
$parts = array_filter($parts, static function($part) {
return $part !== '';
});
return array_values(array_unique($parts));
}
public function collect_refs_from_rows($rows) {
$refs = [];
foreach ($rows as $row) {
foreach ($this->split_refs($row->COL_POTA_REF ?? '') as $ref) {
$refs[$ref] = true;
}
}
return array_keys($refs);
}
// Existing method used by current simple table
function get_all() {
$CI =& get_instance();
@ -86,30 +110,37 @@ class Pota extends CI_Model {
return [];
}
$this->db->select('COL_POTA_REF, COL_BAND, COL_MODE, COL_SUBMODE');
$this->db->from($this->config->item('table_name'));
$this->db->where_in('station_id', $logbooks_locations_array);
$this->db->where('COL_POTA_REF !=', '');
$this->apply_filters($filters);
if ($by === 'band') {
$this->db->select('COL_BAND as k, COUNT(DISTINCT COL_POTA_REF) as v');
$this->db->group_by('COL_BAND');
} elseif ($by === 'mode') {
$this->db->select('(CASE WHEN COL_SUBMODE IS NOT NULL AND COL_SUBMODE<>"" THEN COL_SUBMODE ELSE COL_MODE END) as k, COUNT(DISTINCT COL_POTA_REF) as v', false);
$this->db->group_by('k');
} else {
$this->db->select('COUNT(DISTINCT COL_POTA_REF) as v');
$rows = $this->db->get()->result();
if (!$by) {
return count($this->collect_refs_from_rows($rows));
}
$query = $this->db->get();
if (!$by) {
$row = $query->row();
return $row ? (int)$row->v : 0;
}
$out = [];
foreach ($query->result() as $r) {
$out[$r->k] = (int)$r->v;
foreach ($rows as $row) {
$key = $by === 'band'
? $row->COL_BAND
: (($row->COL_SUBMODE !== null && $row->COL_SUBMODE !== '') ? $row->COL_SUBMODE : $row->COL_MODE);
if (!isset($out[$key])) {
$out[$key] = [];
}
foreach ($this->split_refs($row->COL_POTA_REF) as $ref) {
$out[$key][$ref] = true;
}
}
foreach ($out as $key => $refs) {
$out[$key] = count($refs);
}
return $out;
}

View file

@ -11,7 +11,20 @@ class Stations extends CI_Model {
$this->db->group_by('station_profile.station_id');
$this->db->where('station_profile.user_id', $this->session->userdata('user_id'));
$this->db->or_where('station_profile.user_id =', NULL);
return $this->db->get();
return $this->db->get();
}
private function normalize_pota_refs($value) {
if ($value === null) {
return '';
}
$parts = preg_split('/\s*,\s*/', strtoupper(trim((string)$value)));
$parts = array_filter($parts, static function ($part) {
return $part !== '';
});
return implode(',', $parts);
}
// Returns ALL station profiles regardless of user logged in
@ -49,7 +62,6 @@ class Stations extends CI_Model {
* More efficient than fetching all stations and looping
*/
function user_owns_station($user_id, $station_id) {
$this->db->where('user_id', $user_id);
$this->db->where('station_id', $station_id);
return $this->db->count_all_results('station_profile') > 0;
}
@ -102,7 +114,7 @@ class Stations extends CI_Model {
'station_iota' => xss_clean(strtoupper($this->input->post('iota', true))),
'station_sota' => xss_clean(strtoupper($this->input->post('sota', true))),
'station_wwff' => xss_clean(strtoupper($this->input->post('wwff', true))),
'station_pota' => xss_clean(strtoupper($this->input->post('pota', true))),
'station_pota' => $this->normalize_pota_refs($this->input->post('pota', true)),
'station_wab' => xss_clean(strtoupper($this->input->post('wab', true))),
'station_sig' => xss_clean(strtoupper($this->input->post('sig', true))),
'station_sig_info' => xss_clean(strtoupper($this->input->post('sig_info', true))),
@ -153,7 +165,7 @@ class Stations extends CI_Model {
'station_iota' => xss_clean($this->input->post('iota', true)),
'station_sota' => xss_clean($this->input->post('sota', true)),
'station_wwff' => xss_clean($this->input->post('wwff', true)),
'station_pota' => xss_clean($this->input->post('pota', true)),
'station_pota' => $this->normalize_pota_refs($this->input->post('pota', true)),
'station_wab' => xss_clean($this->input->post('wab', true)),
'station_sig' => xss_clean($this->input->post('sig', true)),
'station_sig_info' => xss_clean($this->input->post('sig_info', true)),

View file

@ -367,4 +367,42 @@
<br>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (!window.jQuery || typeof window.jQuery.fn.selectize === 'undefined' || typeof base_url === 'undefined') {
return;
}
window.jQuery('#stationPOTAInput').selectize({
maxItems: null,
closeAfterSelect: true,
createOnBlur: true,
selectOnTab: true,
loadThrottle: 250,
valueField: 'name',
labelField: 'name',
searchField: 'name',
options: [],
create: true,
load: function(query, callback) {
if (!query || query.length < 3) return callback();
window.jQuery.ajax({
url: base_url+'index.php/qso/get_pota',
type: 'GET',
dataType: 'json',
data: {
query: query,
},
error: function() {
callback();
},
success: function(res) {
callback(res);
}
});
}
});
});
</script>

View file

@ -1102,3 +1102,41 @@
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (!window.jQuery || typeof window.jQuery.fn.selectize === 'undefined' || typeof base_url === 'undefined') {
return;
}
window.jQuery('#stationPOTAInput').selectize({
maxItems: null,
closeAfterSelect: true,
createOnBlur: true,
selectOnTab: true,
loadThrottle: 250,
valueField: 'name',
labelField: 'name',
searchField: 'name',
options: [],
create: true,
load: function(query, callback) {
if (!query || query.length < 3) return callback();
window.jQuery.ajax({
url: base_url+'index.php/qso/get_pota',
type: 'GET',
dataType: 'json',
data: {
query: query,
},
error: function() {
callback();
},
success: function(res) {
callback(res);
}
});
}
});
});
</script>

View file

@ -321,7 +321,12 @@
<?php if($row->COL_POTA_REF != null) { ?>
<tr>
<td><?php echo lang('gen_hamradio_pota_reference'); ?></td>
<td><a href="https://pota.app/#/park/<?php echo $row->COL_POTA_REF; ?>" target="_blank"><?php echo $row->COL_POTA_REF; ?></a></td>
<td>
<?php $pota_refs = array_filter(array_map('trim', explode(',', $row->COL_POTA_REF))); ?>
<?php foreach ($pota_refs as $index => $pota_ref) { ?>
<a href="https://pota.app/#/park/<?php echo urlencode($pota_ref); ?>" target="_blank"><?php echo htmlspecialchars($pota_ref); ?></a><?php if ($index < count($pota_refs) - 1) { echo ', '; } ?>
<?php } ?>
</td>
</tr>
<?php } ?>
@ -446,7 +451,10 @@
$hashtags .= " #SOTA ".$row->COL_SOTA_REF;
}
if($row->COL_POTA_REF != null) {
$hashtags .= " #POTA ".$row->COL_POTA_REF;
$pota_refs = array_filter(array_map('trim', explode(',', $row->COL_POTA_REF)));
foreach ($pota_refs as $pota_ref) {
$hashtags .= " #POTA " . $pota_ref;
}
}
if($row->COL_WWFF_REF != null) {
$hashtags .= " #WWFF ".$row->COL_WWFF_REF;
@ -560,7 +568,12 @@
<?php if($row->COL_MY_POTA_REF) { ?>
<tr>
<td><?php echo lang('gen_hamradio_pota_reference'); ?></td>
<td><?php echo $row->COL_MY_POTA_REF; ?></td>
<td>
<?php $my_pota_refs = array_filter(array_map('trim', explode(',', $row->COL_MY_POTA_REF))); ?>
<?php foreach ($my_pota_refs as $index => $pota_ref) { ?>
<a href="https://pota.app/#/park/<?php echo urlencode($pota_ref); ?>" target="_blank"><?php echo htmlspecialchars($pota_ref); ?></a><?php if ($index < count($my_pota_refs) - 1) { echo ', '; } ?>
<?php } ?>
</td>
</tr>
<?php } ?>