diff --git a/application/config/migration.php b/application/config/migration.php
index 77ec7206e..46009ce35 100644
--- a/application/config/migration.php
+++ b/application/config/migration.php
@@ -22,7 +22,7 @@
|
*/
-$config['migration_version'] = 264;
+$config['migration_version'] = 265;
/*
|--------------------------------------------------------------------------
diff --git a/application/controllers/Api.php b/application/controllers/Api.php
index 119f43221..8b8ae5432 100644
--- a/application/controllers/Api.php
+++ b/application/controllers/Api.php
@@ -177,6 +177,85 @@ function station_info($key)
}
}
+ /**
+ * Get station logbooks that have public slugs for the API key owner.
+ * GET /api/logbook_public_slugs/{key}
+ */
+ function logbook_public_slugs($key)
+ {
+ header('Content-type: application/json');
+
+ $this->load->model('api_model');
+ $this->load->model('logbooks_model');
+
+ if (!$key || $this->api_model->authorize($key) == 0) {
+ http_response_code(401);
+ echo json_encode(['status' => 'failed', 'reason' => 'missing or invalid api key']);
+ return;
+ }
+
+ $this->api_model->update_last_used($key);
+ $user_id = $this->api_model->key_userid($key);
+
+ $query = $this->logbooks_model->public_slugs_by_user($user_id);
+ $logbooks = array();
+
+ foreach ($query->result() as $row) {
+ $logbooks[] = array(
+ 'logbook_id' => (int)$row->logbook_id,
+ 'logbook_name' => $row->logbook_name,
+ 'public_slug' => $row->public_slug,
+ );
+ }
+
+ http_response_code(200);
+ echo json_encode(array(
+ 'status' => 'success',
+ 'count' => count($logbooks),
+ 'logbooks' => $logbooks,
+ ), JSON_PRETTY_PRINT);
+ }
+
+ /**
+ * Get all accessible station logbooks (owned + shared) that have public slugs for the API key owner.
+ * GET /api/logbook_public_slugs_accessible/{key}
+ */
+ function logbook_public_slugs_accessible($key)
+ {
+ header('Content-type: application/json');
+
+ $this->load->model('api_model');
+ $this->load->model('logbooks_model');
+
+ if (!$key || $this->api_model->authorize($key) == 0) {
+ http_response_code(401);
+ echo json_encode(['status' => 'failed', 'reason' => 'missing or invalid api key']);
+ return;
+ }
+
+ $this->api_model->update_last_used($key);
+ $user_id = $this->api_model->key_userid($key);
+
+ $query = $this->logbooks_model->public_slugs_accessible_by_user($user_id);
+ $logbooks = array();
+
+ foreach ($query->result() as $row) {
+ $logbooks[] = array(
+ 'logbook_id' => (int)$row->logbook_id,
+ 'logbook_name' => $row->logbook_name,
+ 'public_slug' => $row->public_slug,
+ 'access_level' => $row->access_level,
+ );
+ }
+
+ http_response_code(200);
+ echo json_encode(array(
+ 'status' => 'success',
+ 'count' => count($logbooks),
+ 'logbooks' => $logbooks,
+ ), JSON_PRETTY_PRINT);
+ }
+
/*
*
@@ -594,6 +673,187 @@ function logbook_check_country()
}
}
+ /**
+ * Check worked and confirmation status for callsign and country.
+ *
+ * @api POST /api/logbook_worked_status
+ * @header Content-Type application/json
+ *
+ * Required fields:
+ * - key
+ * - logbook_public_slug
+ * - callsign
+ *
+ * Optional fields:
+ * - band
+ * - mode
+ * - gridsquare
+ */
+ function logbook_worked_status()
+ {
+ header('Content-type: application/json');
+
+ $this->load->model('api_model');
+
+ $obj = json_decode(file_get_contents("php://input"), true);
+ if ($obj === NULL) {
+ http_response_code(400);
+ echo json_encode(['status' => 'failed', 'reason' => "wrong JSON"]);
+ return;
+ }
+
+ if (!isset($obj['key']) || $this->api_model->authorize($obj['key']) == 0) {
+ http_response_code(401);
+ echo json_encode(['status' => 'failed', 'reason' => "missing api key"]);
+ return;
+ }
+
+ if (!isset($obj['logbook_public_slug']) || !isset($obj['callsign'])) {
+ http_response_code(401);
+ echo json_encode(['status' => 'failed', 'reason' => "missing fields"]);
+ return;
+ }
+
+ $this->load->model('logbook_model');
+ $this->load->model('logbooks_model');
+
+ $logbook_slug = trim($obj['logbook_public_slug']);
+ $callsign = strtoupper(trim($obj['callsign']));
+ $band = isset($obj['band']) && trim($obj['band']) !== '' ? strtoupper(trim($obj['band'])) : null;
+ $mode = isset($obj['mode']) && trim($obj['mode']) !== '' ? strtoupper(trim($obj['mode'])) : null;
+ $gridsquare = isset($obj['gridsquare']) && trim($obj['gridsquare']) !== '' ? strtoupper(trim($obj['gridsquare'])) : null;
+
+ $date = date("Y-m-d");
+ $callsign_dxcc_lookup = $this->logbook_model->dxcc_lookup($callsign, $date);
+ $country = isset($callsign_dxcc_lookup['entity']) ? $callsign_dxcc_lookup['entity'] : '';
+
+ if (!$this->logbooks_model->public_slug_exists($logbook_slug)) {
+ http_response_code(404);
+ echo json_encode(['status' => 'failed', 'reason' => "logbook not found"]);
+ return;
+ }
+
+ $logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($logbook_slug);
+ if ($logbook_id == false) {
+ http_response_code(404);
+ echo json_encode(['status' => 'failed', 'reason' => $logbook_slug . " has no associated station locations"]);
+ return;
+ }
+
+ $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id);
+ if (!$logbooks_locations_array) {
+ http_response_code(404);
+ echo json_encode(['status' => 'failed', 'reason' => "Empty Logbook"]);
+ return;
+ }
+
+ $return = [
+ 'callsign' => $callsign,
+ 'country' => $country,
+ 'band' => $band,
+ 'mode' => $mode,
+ 'gridsquare' => $gridsquare,
+ 'worked' => [
+ 'callsign_overall' => false,
+ 'callsign_band' => $band !== null ? false : null,
+ 'country_overall' => false,
+ 'country_band' => $band !== null ? false : null,
+ 'grid_overall' => $gridsquare !== null ? false : null,
+ 'grid_band' => ($gridsquare !== null && $band !== null) ? false : null,
+ ],
+ 'confirmed' => [
+ 'callsign' => [
+ 'lotw' => false,
+ 'qsl' => false,
+ 'any' => false,
+ ],
+ 'country' => [
+ 'lotw' => false,
+ 'qsl' => false,
+ 'any' => false,
+ ],
+ ],
+ ];
+
+ $table_name = $this->config->item('table_name');
+ $main_mode = $mode !== null ? $this->logbook_model->get_main_mode_from_mode($mode) : null;
+
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_CALL', $callsign);
+ $callsign_overall_query = $this->db->get($table_name, 1, 0);
+ $return['worked']['callsign_overall'] = $callsign_overall_query->num_rows() > 0;
+
+ if ($band !== null) {
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_CALL', $callsign);
+ $this->db->where('COL_BAND', $band);
+ if ($main_mode !== null) {
+ $this->db->where('COL_MODE', $main_mode);
+ }
+ $callsign_band_query = $this->db->get($table_name, 1, 0);
+ $return['worked']['callsign_band'] = $callsign_band_query->num_rows() > 0;
+ }
+
+ if ($country !== '') {
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_COUNTRY', urldecode($country));
+ $country_overall_query = $this->db->get($table_name, 1, 0);
+ $return['worked']['country_overall'] = $country_overall_query->num_rows() > 0;
+
+ if ($band !== null) {
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_COUNTRY', urldecode($country));
+ $this->db->where('COL_BAND', $band);
+ if ($main_mode !== null) {
+ $this->db->where('COL_MODE', $main_mode);
+ }
+ $country_band_query = $this->db->get($table_name, 1, 0);
+ $return['worked']['country_band'] = $country_band_query->num_rows() > 0;
+ }
+ }
+
+ if ($gridsquare !== null) {
+ $grid_overall_result = $this->logbook_model->check_if_grid_worked_in_logbook($gridsquare, $logbooks_locations_array, null);
+ $return['worked']['grid_overall'] = $grid_overall_result > 0;
+
+ if ($band !== null) {
+ $grid_band_result = $this->logbook_model->check_if_grid_worked_in_logbook($gridsquare, $logbooks_locations_array, $band);
+ $return['worked']['grid_band'] = $grid_band_result > 0;
+ }
+ }
+
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_CALL', $callsign);
+ $this->db->where("COL_LOTW_QSL_RCVD='Y'");
+ $callsign_lotw_query = $this->db->get($table_name, 1, 0);
+ $return['confirmed']['callsign']['lotw'] = $callsign_lotw_query->num_rows() > 0;
+
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_CALL', $callsign);
+ $this->db->where("COL_QSL_RCVD='Y'");
+ $callsign_qsl_query = $this->db->get($table_name, 1, 0);
+ $return['confirmed']['callsign']['qsl'] = $callsign_qsl_query->num_rows() > 0;
+ $return['confirmed']['callsign']['any'] = $return['confirmed']['callsign']['lotw'] || $return['confirmed']['callsign']['qsl'];
+
+ if ($country !== '') {
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_COUNTRY', urldecode($country));
+ $this->db->where("COL_LOTW_QSL_RCVD='Y'");
+ $country_lotw_query = $this->db->get($table_name, 1, 0);
+ $return['confirmed']['country']['lotw'] = $country_lotw_query->num_rows() > 0;
+
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_COUNTRY', urldecode($country));
+ $this->db->where("COL_QSL_RCVD='Y'");
+ $country_qsl_query = $this->db->get($table_name, 1, 0);
+ $return['confirmed']['country']['qsl'] = $country_qsl_query->num_rows() > 0;
+ $return['confirmed']['country']['any'] = $return['confirmed']['country']['lotw'] || $return['confirmed']['country']['qsl'];
+ }
+
+ http_response_code(200);
+ echo json_encode($return, JSON_PRETTY_PRINT);
+ }
+
/* ENDPOINT for Rig Control */
function radio()
diff --git a/application/controllers/Logbook.php b/application/controllers/Logbook.php
index 939a6ffc2..756285e22 100644
--- a/application/controllers/Logbook.php
+++ b/application/controllers/Logbook.php
@@ -92,6 +92,30 @@ function jsonentity($adif)
echo json_encode($return, JSON_PRETTY_PRINT);
}
+ function jsondxcc($tempcallsign)
+ {
+ $callsign = $this->security->xss_clean($tempcallsign);
+
+ $this->load->model('user_model');
+ if (!$this->user_model->authorize($this->config->item('auth_mode'))) {
+ return;
+ }
+
+ // Convert - in Callsign to / Used for URL processing
+ $callsign = str_replace("-", "/", $callsign);
+ $callsign = str_replace("Ø", "0", $callsign);
+
+ $return = [
+ "callsign" => strtoupper($callsign),
+ "dxcc" => false,
+ ];
+
+ $return['dxcc'] = $this->cached_dxcc_lookup($callsign);
+
+ header('Content-Type: application/json');
+ echo json_encode($return, JSON_PRETTY_PRINT);
+ }
+
function json($tempcallsign, $temptype, $tempband, $tempmode, $tempstation_id = null)
{
// Cleaning for security purposes
@@ -134,18 +158,25 @@ function json($tempcallsign, $temptype, $tempband, $tempmode, $tempstation_id =
"qsl_manager" => "",
"bearing" => "",
"workedBefore" => false,
+ "callsignWorkedBefore" => false,
+ "callsignConfirmed" => false,
"timesWorked" => 0,
"lotw_member" => $lotw_member,
"lotw_days" => $lotw_days,
"image" => "",
];
- $return['dxcc'] = $this->dxcheck($callsign);
+ $return['dxcc'] = $this->cached_dxcc_lookup($callsign);
$lookupcall = $this->get_plaincall($callsign);
$return['partial'] = $this->partial($lookupcall);
+ $this->load->model('logbooks_model');
+ $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
+ $user_default_confirmation = $this->session->userdata('user_default_confirmation');
+ $recent_details = $this->logbook_model->get_recent_callsign_details($callsign, $this->session->userdata('user_id'));
+
$callbook = $this->logbook_model->loadCallBook($callsign, $this->config->item('use_fullname'));
if ($this->session->userdata('user_measurement_base') == NULL) {
@@ -154,16 +185,19 @@ function json($tempcallsign, $temptype, $tempband, $tempmode, $tempstation_id =
$measurement_base = $this->session->userdata('user_measurement_base');
}
- $return['callsign_name'] = $this->nval($callbook['name'] ?? '', $this->logbook_model->call_name($callsign));
- $return['callsign_qra'] = $this->nval($callbook['gridsquare'] ?? '', $this->logbook_model->call_qra($callsign));
+ $return['callsign_name'] = $this->nval($callbook['name'] ?? '', $recent_details['name']);
+ $return['callsign_qra'] = $this->nval($callbook['gridsquare'] ?? '', $recent_details['gridsquare']);
$return['callsign_distance'] = $this->distance($return['callsign_qra']);
- $return['callsign_qth'] = $this->nval($callbook['city'] ?? '', $this->logbook_model->call_qth($callsign));
- $return['callsign_iota'] = $this->nval($callbook['iota'] ?? '', $this->logbook_model->call_iota($callsign));
- $return['qsl_manager'] = $this->nval($callbook['qslmgr'] ?? '', $this->logbook_model->call_qslvia($callsign));
- $return['callsign_state'] = $this->nval($callbook['state'] ?? '', $this->logbook_model->call_state($callsign));
- $return['callsign_us_county'] = $this->nval($callbook['us_county'] ?? '', $this->logbook_model->call_us_county($callsign));
- $return['workedBefore'] = $this->worked_grid_before($return['callsign_qra'], $type, $band, $mode);
- $return['confirmed'] = $this->confirmed_grid_before($return['callsign_qra'], $type, $band, $mode);
+ $return['callsign_qth'] = $this->nval($callbook['city'] ?? '', $recent_details['qth']);
+ $return['callsign_iota'] = $this->nval($callbook['iota'] ?? '', $recent_details['iota']);
+ $return['qsl_manager'] = $this->nval($callbook['qslmgr'] ?? '', $recent_details['qsl_via']);
+ $return['callsign_state'] = $this->nval($callbook['state'] ?? '', $recent_details['state']);
+ $return['callsign_us_county'] = $this->nval($callbook['us_county'] ?? '', $recent_details['us_county']);
+ $return['workedBefore'] = $this->worked_grid_before($return['callsign_qra'], $type, $band, $mode, $logbooks_locations_array);
+ $return['confirmed'] = $this->confirmed_grid_before($return['callsign_qra'], $type, $band, $mode, $logbooks_locations_array, $user_default_confirmation);
+ $callsign_status = $this->callsign_status($callsign, $type, $band, $mode, $logbooks_locations_array, $user_default_confirmation);
+ $return['callsignWorkedBefore'] = $callsign_status['workedBefore'];
+ $return['callsignConfirmed'] = $callsign_status['confirmed'];
$return['timesWorked'] = $this->logbook_model->times_worked($lookupcall);
if ($this->session->userdata('user_show_profile_image')) {
@@ -213,39 +247,109 @@ function nval($val1, $val2)
return (($val2 ?? "") === "" ? ($val1 ?? "") : ($val2 ?? ""));
}
- function confirmed_grid_before($gridsquare, $type, $band, $mode)
+ private function build_confirmation_where($user_default_confirmation)
{
- if (strlen($gridsquare) < 4)
- return false;
-
- $this->load->model('logbooks_model');
- $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
- $user_default_confirmation = $this->session->userdata('user_default_confirmation');
-
- if (!empty($logbooks_locations_array)) {
- $extrawhere = '';
- if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Q') !== false) {
- $extrawhere = "COL_QSL_RCVD='Y'";
+ $extrawhere = '';
+ if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Q') !== false) {
+ $extrawhere = "COL_QSL_RCVD='Y'";
+ }
+ if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'L') !== false) {
+ if ($extrawhere != '') {
+ $extrawhere .= " OR";
}
- if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'L') !== false) {
- if ($extrawhere != '') {
- $extrawhere .= " OR";
- }
- $extrawhere .= " COL_LOTW_QSL_RCVD='Y'";
+ $extrawhere .= " COL_LOTW_QSL_RCVD='Y'";
+ }
+ if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'E') !== false) {
+ if ($extrawhere != '') {
+ $extrawhere .= " OR";
}
- if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'E') !== false) {
- if ($extrawhere != '') {
- $extrawhere .= " OR";
- }
- $extrawhere .= " COL_EQSL_QSL_RCVD='Y'";
+ $extrawhere .= " COL_EQSL_QSL_RCVD='Y'";
+ }
+
+ if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Z') !== false) {
+ if ($extrawhere != '') {
+ $extrawhere .= " OR";
}
+ $extrawhere .= " COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y'";
+ }
- if (isset($user_default_confirmation) && strpos($user_default_confirmation, 'Z') !== false) {
- if ($extrawhere != '') {
- $extrawhere .= " OR";
- }
- $extrawhere .= " COL_QRZCOM_QSO_DOWNLOAD_STATUS='Y'";
+ return $extrawhere;
+ }
+
+ private function callsign_status($callsign, $type, $band, $mode, $logbooks_locations_array, $user_default_confirmation)
+ {
+ $return = [
+ "workedBefore" => false,
+ "confirmed" => false,
+ ];
+
+ if (empty($logbooks_locations_array)) {
+ return $return;
+ }
+
+ $this->load->model('logbook_model');
+
+ if ($type == "SAT") {
+ $this->db->where('COL_PROP_MODE', 'SAT');
+ } else {
+ $this->db->where('COL_MODE', $this->logbook_model->get_main_mode_from_mode($mode));
+ $this->db->where('COL_BAND', $band);
+ $this->db->where('COL_PROP_MODE !=', 'SAT');
+ }
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_CALL', strtoupper($callsign));
+
+ $query = $this->db->get($this->config->item('table_name'), 1, 0);
+ foreach ($query->result() as $workedBeforeRow) {
+ $return['workedBefore'] = true;
+ }
+
+ $extrawhere = $this->build_confirmation_where($user_default_confirmation);
+
+ if ($type == "SAT") {
+ $this->db->where('COL_PROP_MODE', 'SAT');
+ if ($extrawhere != '') {
+ $this->db->where('(' . $extrawhere . ')');
+ } else {
+ $this->db->where("1=0");
}
+ } else {
+ $this->load->model('logbook_model');
+ $this->db->where('COL_MODE', $this->logbook_model->get_main_mode_from_mode($mode));
+ $this->db->where('COL_BAND', $band);
+ $this->db->where('COL_PROP_MODE !=', 'SAT');
+ if ($extrawhere != '') {
+ $this->db->where('(' . $extrawhere . ')');
+ } else {
+ $this->db->where("1=0");
+ }
+ }
+ $this->db->where_in('station_id', $logbooks_locations_array);
+ $this->db->where('COL_CALL', strtoupper($callsign));
+
+ $query = $this->db->get($this->config->item('table_name'), 1, 0);
+ foreach ($query->result() as $workedBeforeRow) {
+ $return['confirmed'] = true;
+ }
+
+ return $return;
+ }
+
+ function confirmed_grid_before($gridsquare, $type, $band, $mode, $logbooks_locations_array = null, $user_default_confirmation = null)
+ {
+ if (strlen($gridsquare) < 4)
+ return false;
+
+ if ($logbooks_locations_array === null) {
+ $this->load->model('logbooks_model');
+ $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
+ }
+ if ($user_default_confirmation === null) {
+ $user_default_confirmation = $this->session->userdata('user_default_confirmation');
+ }
+
+ if (!empty($logbooks_locations_array)) {
+ $extrawhere = $this->build_confirmation_where($user_default_confirmation);
if ($type == "SAT") {
@@ -283,13 +387,15 @@ function confirmed_grid_before($gridsquare, $type, $band, $mode)
return false;
}
- function worked_grid_before($gridsquare, $type, $band, $mode)
+ function worked_grid_before($gridsquare, $type, $band, $mode, $logbooks_locations_array = null)
{
if (strlen($gridsquare) < 4)
return false;
- $this->load->model('logbooks_model');
- $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($this->session->userdata('active_station_logbook'));
+ if ($logbooks_locations_array === null) {
+ $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)) {
if ($type == "SAT") {
@@ -1186,6 +1292,30 @@ function dxcheck($call = "", $date = "")
return $ans;
}
+ private function cached_dxcc_lookup($call)
+ {
+ $normalized_call = strtoupper(trim((string)$call));
+ if ($normalized_call === '') {
+ return false;
+ }
+
+ $cache_key = 'dxcc_lookup_' . md5($normalized_call . '|' . date('Y-m-d'));
+ $cache_ttl = 3600; // 1 hour
+
+ $this->load->driver('cache', array('adapter' => 'file', 'backup' => 'file'));
+ $cached = $this->cache->get($cache_key);
+ if ($cached !== false) {
+ return $cached;
+ }
+
+ $dxcc = $this->dxcheck($normalized_call);
+ if ($dxcc !== false && $dxcc !== null) {
+ $this->cache->save($cache_key, $dxcc, $cache_ttl);
+ }
+
+ return $dxcc;
+ }
+
function getentity($adif)
{
$this->load->model("logbook_model");
diff --git a/application/controllers/Qso.php b/application/controllers/Qso.php
index ce3df2d71..b34b21ea7 100755
--- a/application/controllers/Qso.php
+++ b/application/controllers/Qso.php
@@ -122,6 +122,12 @@ public function index()
'operator_callsign' => $this->input->post('operator_callsign'),
'transmit_power' => $this->input->post('transmit_power')
);
+
+ $propMode = strtoupper(trim((string)($qso_data['prop_mode'] ?? '')));
+ if ($propMode !== 'SAT') {
+ $qso_data['sat_name'] = '';
+ $qso_data['sat_mode'] = '';
+ }
// ];
setcookie("radio", $qso_data['radio'], time()+3600*24*99);
@@ -162,6 +168,83 @@ public function saveqso() {
$this->logbook_model->create_qso();
}
+ /*
+ * AJAX endpoint for QSO entry form to avoid full page reload on save.
+ */
+ public function ajax_saveqso() {
+ $this->load->library('form_validation');
+ $this->load->model('logbook_model');
+
+ $this->form_validation->set_rules('start_date', 'Date', 'required');
+ $this->form_validation->set_rules('start_time', 'Time', 'required');
+ $this->form_validation->set_rules('callsign', 'Callsign', 'required');
+ $this->form_validation->set_rules('band', 'Band', 'required');
+ $this->form_validation->set_rules('mode', 'Mode', 'required');
+ $this->form_validation->set_rules('locator', 'Locator', 'callback_check_locator');
+
+ if ($this->form_validation->run() == FALSE) {
+ $validation_errors = array();
+ $fields = array('start_date', 'start_time', 'callsign', 'band', 'mode', 'locator');
+ foreach ($fields as $field) {
+ $field_error = form_error($field, '', '');
+ if (!empty($field_error)) {
+ $validation_errors[$field] = strip_tags($field_error);
+ }
+ }
+
+ return $this->output
+ ->set_content_type('application/json')
+ ->set_output(json_encode(array(
+ 'status' => 'error',
+ 'message' => 'Please correct the form errors and try again.',
+ 'validation_errors' => $validation_errors,
+ )));
+ }
+
+ $qso_data = array(
+ 'start_date' => $this->input->post('start_date'),
+ 'start_time' => $this->input->post('start_time'),
+ 'end_time' => $this->input->post('end_time'),
+ 'time_stamp' => time(),
+ 'band' => $this->input->post('band'),
+ 'band_rx' => $this->input->post('band_rx'),
+ 'freq' => $this->input->post('freq_display'),
+ 'freq_rx' => $this->input->post('freq_display_rx'),
+ 'mode' => $this->input->post('mode'),
+ 'sat_name' => $this->input->post('sat_name'),
+ 'sat_mode' => $this->input->post('sat_mode'),
+ 'prop_mode' => $this->input->post('prop_mode'),
+ 'radio' => $this->input->post('radio'),
+ 'station_profile_id' => $this->input->post('station_profile'),
+ 'operator_callsign' => $this->input->post('operator_callsign'),
+ 'transmit_power' => $this->input->post('transmit_power')
+ );
+
+ $propMode = strtoupper(trim((string)($qso_data['prop_mode'] ?? '')));
+ if ($propMode !== 'SAT') {
+ $qso_data['sat_name'] = '';
+ $qso_data['sat_mode'] = '';
+ }
+
+ setcookie("radio", $qso_data['radio'], time() + 3600 * 24 * 99);
+ setcookie("station_profile_id", $qso_data['station_profile_id'], time() + 3600 * 24 * 99);
+
+ $this->session->set_userdata($qso_data);
+
+ if ($this->input->post('sat_name')) {
+ $this->session->set_userdata('prop_mode', 'SAT');
+ }
+
+ $this->logbook_model->create_qso();
+
+ return $this->output
+ ->set_content_type('application/json')
+ ->set_output(json_encode(array(
+ 'status' => 'ok',
+ 'message' => 'QSO Added',
+ )));
+ }
+
function edit() {
$this->load->model('logbook_model');
@@ -740,6 +823,12 @@ public function component_past_contacts() {
$data['current_page'] = $page;
$data['limit'] = $limit;
+ // This endpoint is polled by HTMX and must not be cached.
+ $this->output->set_header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
+ $this->output->set_header('Cache-Control: post-check=0, pre-check=0', false);
+ $this->output->set_header('Pragma: no-cache');
+ $this->output->set_header('Expires: Sat, 01 Jan 2000 00:00:00 GMT');
+
// Load view
$this->load->view('qso/components/previous_contacts', $data);
}
diff --git a/application/controllers/Stationdiary.php b/application/controllers/Stationdiary.php
index bad844bbb..480e686b5 100644
--- a/application/controllers/Stationdiary.php
+++ b/application/controllers/Stationdiary.php
@@ -70,7 +70,7 @@ public function index($callsign = NULL, $offset = 0)
$cleanCallsign = strtoupper($resolution['callsign']);
$pageOffset = is_numeric($offset) ? (int)$offset : 0;
$cacheVersion = $this->note->get_public_diary_cache_version($user_id);
- $renderVersion = 'public_diary_render_v2';
+ $renderVersion = 'public_diary_render_v5';
$cacheKey = 'public_station_diary_' . md5($cleanCallsign . '_' . $pageOffset . '_' . $cacheVersion . '_' . $renderVersion);
$cachedHtml = $this->cache->get($cacheKey);
@@ -110,12 +110,13 @@ public function index($callsign = NULL, $offset = 0)
$this->pagination->initialize($config);
$data['callsign'] = $cleanCallsign;
- $data['entries'] = $this->note->get_public_station_diary_entries($user_id, $perPage, $pageOffset);
+ $data['entries'] = $this->note->get_public_station_diary_entries($user_id, $perPage, $pageOffset, FALSE);
$data['pagination_links'] = $this->pagination->create_links();
$data['page_title'] = 'Station Diary - ' . $cleanCallsign;
$data['rss_url'] = site_url('station-diary/' . rawurlencode($cleanCallsign) . '/rss');
$data['qso_datetime_format'] = $this->get_public_qso_datetime_format($resolution['user_date_format'] ?? NULL);
$data['is_single_entry'] = false;
+ $data['defer_qso_list'] = true;
$data['current_entry_permalink'] = '';
$html = $this->load->view('station_diary/public_index', $data, TRUE);
@@ -192,6 +193,7 @@ public function entry($callsign = NULL, $entry_id = 0)
$data['rss_url'] = site_url('station-diary/' . rawurlencode($cleanCallsign) . '/rss');
$data['qso_datetime_format'] = $this->get_public_qso_datetime_format($resolution['user_date_format'] ?? NULL);
$data['is_single_entry'] = true;
+ $data['defer_qso_list'] = false;
$data['current_entry_permalink'] = site_url('station-diary/' . rawurlencode($cleanCallsign) . '/entry/' . (int)$entry->id);
$data['entry_reaction_totals'] = $entryReactionTotals;
$data['visitor_reaction'] = null;
@@ -247,7 +249,11 @@ public function react($callsign = NULL, $entry_id = 0)
return;
}
- $this->note->invalidate_public_diary_cache_for_note($entryId, $user_id);
+ $cleanCallsign = strtoupper($resolution['callsign']);
+ $cacheVersion = $this->note->get_public_diary_cache_version($user_id);
+ $renderVersion = 'public_diary_render_v4';
+ $entryCacheKey = 'public_station_diary_entry_' . md5($cleanCallsign . '_' . $entryId . '_' . $cacheVersion . '_' . $renderVersion);
+ $this->cache->delete($entryCacheKey);
$totals = $this->note->get_station_diary_reaction_totals($entryId);
$visitorReaction = $this->note->get_station_diary_visitor_reaction($entryId, $visitorHash);
@@ -259,6 +265,67 @@ public function react($callsign = NULL, $entry_id = 0)
));
}
+ public function get_filtered_qsos()
+ {
+ $this->output->set_content_type('application/json');
+
+ if (strtolower($this->input->method()) !== 'post') {
+ echo json_encode(array('success' => false, 'message' => 'Invalid request method'));
+ return;
+ }
+
+ $callsign = $this->input->post('callsign', TRUE);
+ $entryId = (int)$this->input->post('entry_id', TRUE);
+ $startDate = trim((string)$this->input->post('start_date', TRUE));
+ $endDate = trim((string)$this->input->post('end_date', TRUE));
+ $logbookId = (int)$this->input->post('logbook_id', TRUE);
+ $satOnly = $this->input->post('sat_only', TRUE) === '1';
+
+ if ($this->security->xss_clean($callsign, TRUE) === FALSE || $entryId <= 0) {
+ echo json_encode(array('success' => false, 'message' => 'Invalid request'));
+ return;
+ }
+
+ $resolution = $this->note->resolve_public_user_by_callsign($callsign);
+ if (!isset($resolution['status']) || $resolution['status'] !== 'ok') {
+ echo json_encode(array('success' => false, 'message' => 'Not found'));
+ return;
+ }
+
+ $userId = (int)$resolution['user_id'];
+ $entry = $this->note->get_public_station_diary_entry($userId, $entryId);
+ if (!$entry) {
+ echo json_encode(array('success' => false, 'message' => 'Entry not found'));
+ return;
+ }
+
+ $entryDate = date('Y-m-d', strtotime($entry->created_at));
+ if ($startDate === '') {
+ $startDate = !empty($entry->qso_date_start) ? $entry->qso_date_start : $entryDate;
+ }
+ if ($endDate === '') {
+ $endDate = !empty($entry->qso_date_end) ? $entry->qso_date_end : $entryDate;
+ }
+
+ if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
+ echo json_encode(array('success' => false, 'message' => 'Invalid date format'));
+ return;
+ }
+
+ if ($logbookId <= 0) {
+ $logbookId = !empty($entry->logbook_id) ? (int)$entry->logbook_id : 0;
+ }
+
+ $qsoSummary = $this->note->get_qso_summary_for_date_range($userId, $startDate, $endDate, $logbookId, $satOnly);
+ $qsoList = $this->note->get_qso_list_for_date_range($userId, $startDate, $endDate, $logbookId, $satOnly);
+
+ echo json_encode(array(
+ 'success' => true,
+ 'qso_list' => $qsoList,
+ 'highlight_dx' => $qsoSummary['highlight_dx'] ?? null,
+ ));
+ }
+
public function get_qso_map_data()
{
$this->output->set_content_type('application/json');
@@ -329,4 +396,5 @@ public function get_qso_map_data()
// Merge and return
echo json_encode(array_merge($plotArray, $stationArray));
- }}
\ No newline at end of file
+ }
+}
\ No newline at end of file
diff --git a/application/controllers/User.php b/application/controllers/User.php
index dc4d6f676..409698e10 100644
--- a/application/controllers/User.php
+++ b/application/controllers/User.php
@@ -685,6 +685,8 @@ function edit()
$data['dashboard_lotw_card'] = false;
$data['dashboard_vuccgrids_card'] = false;
$data['dashboard_map_greyline'] = true;
+ $data['menu_show_qsl_cards'] = true;
+ $data['menu_show_sstv_images'] = false;
$dashboard_options = $this->user_options_model->get_options('dashboard')->result();
@@ -750,6 +752,17 @@ function edit()
}
}
+ $menu_options = $this->user_options_model->get_options('menu')->result();
+ foreach ($menu_options as $item) {
+ if ($item->option_name == 'show_qsl_cards' && $item->option_key == 'enabled') {
+ $data['menu_show_qsl_cards'] = ($item->option_value == 'true');
+ }
+
+ if ($item->option_name == 'show_sstv_images' && $item->option_key == 'enabled') {
+ $data['menu_show_sstv_images'] = ($item->option_value == 'true');
+ }
+ }
+
// [QSO Form] Load field visibility preferences
$data['qso_fields'] = [
'rst' => true, 'name' => true, 'qth' => true, 'locator' => true, 'comment' => true,
@@ -934,6 +947,22 @@ function edit()
$this->user_options_model->set_option('dashboard', 'dashboard_map_greyline', array('enabled' => 'false'));
}
+ if (isset($_POST['user_menu_show_sstv_images'])) {
+ $this->user_options_model->set_option('menu', 'show_sstv_images', array('enabled' => 'true'));
+ $this->session->set_userdata('user_show_sstv_images', true);
+ } else {
+ $this->user_options_model->set_option('menu', 'show_sstv_images', array('enabled' => 'false'));
+ $this->session->set_userdata('user_show_sstv_images', false);
+ }
+
+ if (isset($_POST['user_menu_show_qsl_cards'])) {
+ $this->user_options_model->set_option('menu', 'show_qsl_cards', array('enabled' => 'true'));
+ $this->session->set_userdata('user_show_qsl_cards', true);
+ } else {
+ $this->user_options_model->set_option('menu', 'show_qsl_cards', array('enabled' => 'false'));
+ $this->session->set_userdata('user_show_qsl_cards', false);
+ }
+
// [QSO Form] Save field visibility preferences
$qso_field_keys = ['rst', 'name', 'qth', 'locator', 'comment',
'station_tab', 'freq_tx', 'freq_rx', 'band_rx', 'transmit_power', 'operator_callsign',
diff --git a/application/migrations/265_add_station_time_on_index.php b/application/migrations/265_add_station_time_on_index.php
new file mode 100644
index 000000000..eb26bec29
--- /dev/null
+++ b/application/migrations/265_add_station_time_on_index.php
@@ -0,0 +1,36 @@
+config->item('table_name');
+ if (empty($table)) {
+ return;
+ }
+
+ $this->db->db_debug = false;
+ $indexExists = $this->db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = 'idx_station_time_on'")->num_rows();
+ if ($indexExists == 0) {
+ $this->db->query("ALTER TABLE `{$table}` ADD INDEX `idx_station_time_on` (`station_id`, `COL_TIME_ON`)");
+ }
+ $this->db->db_debug = true;
+ }
+
+ public function down()
+ {
+ $table = $this->config->item('table_name');
+ if (empty($table)) {
+ return;
+ }
+
+ $this->db->db_debug = false;
+ $indexExists = $this->db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = 'idx_station_time_on'")->num_rows();
+ if ($indexExists > 0) {
+ $this->db->query("ALTER TABLE `{$table}` DROP INDEX `idx_station_time_on`");
+ }
+ $this->db->db_debug = true;
+ }
+}
diff --git a/application/migrations/266_tag_2_8_12.php b/application/migrations/266_tag_2_8_12.php
new file mode 100644
index 000000000..640485d27
--- /dev/null
+++ b/application/migrations/266_tag_2_8_12.php
@@ -0,0 +1,30 @@
+db->where('option_name', 'version');
+ $this->db->update('options', array('option_value' => '2.8.12'));
+
+ // Trigger Version Info Dialog
+ $this->db->where('option_type', 'version_dialog');
+ $this->db->where('option_name', 'confirmed');
+ $this->db->update('user_options', array('option_value' => 'false'));
+
+ }
+
+ public function down()
+ {
+ $this->db->where('option_name', 'version');
+ $this->db->update('options', array('option_value' => '2.8.11'));
+ }
+}
\ No newline at end of file
diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php
index bb2dfbf10..5363310e0 100755
--- a/application/models/Logbook_model.php
+++ b/application/models/Logbook_model.php
@@ -1639,6 +1639,74 @@ function call_lookup_result($callsign)
return $data;
}
+ /**
+ * Get recent callsign details for a user in a single query and derive the
+ * latest non-empty value per field in PHP.
+ */
+ function get_recent_callsign_details($callsign, $user_id, $limit = 100)
+ {
+ $details = array(
+ 'name' => '',
+ 'gridsquare' => '',
+ 'qth' => '',
+ 'iota' => '',
+ 'qsl_via' => '',
+ 'state' => '',
+ 'us_county' => null,
+ );
+
+ $this->db->select('COL_NAME, COL_GRIDSQUARE, COL_QTH, COL_IOTA, COL_QSL_VIA, COL_STATE, COL_CNTY, COL_TIME_ON');
+ $this->db->join('station_profile', 'station_profile.station_id = ' . $this->config->item('table_name') . '.station_id');
+ $this->db->where('COL_CALL', $callsign);
+ $this->db->where('station_profile.user_id', $user_id);
+ $this->db->order_by('COL_TIME_ON', 'desc');
+ $this->db->limit($limit);
+ $query = $this->db->get($this->config->item('table_name'));
+
+ if ($query->num_rows() === 0) {
+ return $details;
+ }
+
+ foreach ($query->result() as $row) {
+ if ($details['name'] === '' && !empty($row->COL_NAME)) {
+ $details['name'] = $row->COL_NAME;
+ }
+ if ($details['gridsquare'] === '' && !empty($row->COL_GRIDSQUARE)) {
+ $details['gridsquare'] = strtoupper($row->COL_GRIDSQUARE);
+ }
+ if ($details['qth'] === '' && !empty($row->COL_QTH)) {
+ $details['qth'] = $row->COL_QTH;
+ }
+ if ($details['iota'] === '' && !empty($row->COL_IOTA)) {
+ $details['iota'] = $row->COL_IOTA;
+ }
+ if ($details['qsl_via'] === '' && !empty($row->COL_QSL_VIA)) {
+ $details['qsl_via'] = $row->COL_QSL_VIA;
+ }
+ if ($details['state'] === '' && !empty($row->COL_STATE)) {
+ $details['state'] = $row->COL_STATE;
+ }
+ if ($details['us_county'] === null && !empty($row->COL_CNTY)) {
+ $county_parts = explode(',', $row->COL_CNTY, 2);
+ $details['us_county'] = isset($county_parts[1]) ? trim($county_parts[1]) : trim($county_parts[0]);
+ }
+
+ if (
+ $details['name'] !== '' &&
+ $details['gridsquare'] !== '' &&
+ $details['qth'] !== '' &&
+ $details['iota'] !== '' &&
+ $details['qsl_via'] !== '' &&
+ $details['state'] !== '' &&
+ $details['us_county'] !== null
+ ) {
+ break;
+ }
+ }
+
+ return $details;
+ }
+
/* Callsign QRA */
function call_qra($callsign)
{
diff --git a/application/models/Logbooks_model.php b/application/models/Logbooks_model.php
index 00d832907..78cbd78eb 100644
--- a/application/models/Logbooks_model.php
+++ b/application/models/Logbooks_model.php
@@ -243,6 +243,42 @@ function public_slug_exists_logbook_id($slug) {
}
}
+ function public_slugs_by_user($user_id) {
+ $clean_user_id = $this->security->xss_clean($user_id);
+
+ $this->db->select('logbook_id, logbook_name, public_slug');
+ $this->db->from('station_logbooks');
+ $this->db->where('user_id', $clean_user_id);
+ $this->db->where('public_slug IS NOT NULL', null, false);
+ $this->db->where("public_slug != ''", null, false);
+ $this->db->order_by('logbook_name', 'ASC');
+
+ return $this->db->get();
+ }
+
+ function public_slugs_accessible_by_user($user_id) {
+ $clean_user_id = $this->security->xss_clean($user_id);
+
+ $this->db->select('station_logbooks.logbook_id, station_logbooks.logbook_name, station_logbooks.public_slug,
+ CASE
+ WHEN station_logbooks.user_id = '.$this->db->escape($clean_user_id).' THEN "owner"
+ ELSE slp.permission_level
+ END as access_level', FALSE);
+ $this->db->from('station_logbooks');
+ $this->db->join('station_logbooks_permissions slp',
+ 'slp.logbook_id = station_logbooks.logbook_id AND slp.user_id = '.$this->db->escape($clean_user_id),
+ 'left');
+ $this->db->group_start();
+ $this->db->where('station_logbooks.user_id', $clean_user_id);
+ $this->db->or_where('slp.user_id', $clean_user_id);
+ $this->db->group_end();
+ $this->db->where('station_logbooks.public_slug IS NOT NULL', null, false);
+ $this->db->where("station_logbooks.public_slug != ''", null, false);
+ $this->db->order_by('station_logbooks.logbook_name', 'ASC');
+
+ return $this->db->get();
+ }
+
function is_public_slug_available($slug) {
// Clean public_slug
$clean_slug = $this->security->xss_clean($slug);
diff --git a/application/models/Note.php b/application/models/Note.php
index 260a589ab..a7ec9742f 100644
--- a/application/models/Note.php
+++ b/application/models/Note.php
@@ -401,7 +401,7 @@ public function count_public_station_diary_entries($user_id) {
return (int)$this->db->count_all_results();
}
- public function get_public_station_diary_entries($user_id, $limit = 10, $offset = 0) {
+ public function get_public_station_diary_entries($user_id, $limit = 10, $offset = 0, $include_qso_list = TRUE) {
$this->db->from('notes');
$this->db->where('user_id', (int)$user_id);
$this->db->where('cat', 'Station Diary');
@@ -419,6 +419,8 @@ public function get_public_station_diary_entries($user_id, $limit = 10, $offset
}
$imagesMap = $this->get_diary_images($ids);
+ $qsoSummaryMemo = array();
+ $qsoListMemo = array();
foreach ($entries as $entry) {
$entry->images = isset($imagesMap[$entry->id]) ? $imagesMap[$entry->id] : array();
$entry->qso_summary = null;
@@ -430,14 +432,37 @@ public function get_public_station_diary_entries($user_id, $limit = 10, $offset
$dateStart = !empty($entry->qso_date_start) ? $entry->qso_date_start : $entryDate;
$dateEnd = !empty($entry->qso_date_end) ? $entry->qso_date_end : $entryDate;
$satOnly = (int)$entry->qso_satellite_only === 1;
+ $memoKey = implode('|', array(
+ (int)$user_id,
+ (string)$dateStart,
+ (string)$dateEnd,
+ (string)($entry->logbook_id ?? ''),
+ $satOnly ? '1' : '0',
+ ));
// Use date range filtering if dates are set, otherwise fall back to single-day filtering
if (!empty($entry->qso_date_start) || !empty($entry->qso_date_end)) {
- $entry->qso_summary = $this->get_qso_summary_for_date_range($user_id, $dateStart, $dateEnd, $entry->logbook_id, $satOnly);
- $entry->qso_list = $this->get_qso_list_for_date_range($user_id, $dateStart, $dateEnd, $entry->logbook_id, $satOnly);
+ if (!array_key_exists($memoKey, $qsoSummaryMemo)) {
+ $qsoSummaryMemo[$memoKey] = $this->get_qso_summary_for_date_range($user_id, $dateStart, $dateEnd, $entry->logbook_id, $satOnly);
+ }
+ $entry->qso_summary = $qsoSummaryMemo[$memoKey];
+ if ($include_qso_list) {
+ if (!array_key_exists($memoKey, $qsoListMemo)) {
+ $qsoListMemo[$memoKey] = $this->get_qso_list_for_date_range($user_id, $dateStart, $dateEnd, $entry->logbook_id, $satOnly);
+ }
+ $entry->qso_list = $qsoListMemo[$memoKey];
+ }
} else {
- $entry->qso_summary = $this->get_qso_summary_for_date($user_id, $entryDate, $entry->logbook_id);
- $entry->qso_list = $this->get_qso_list_for_date($user_id, $entryDate, $entry->logbook_id);
+ if (!array_key_exists($memoKey, $qsoSummaryMemo)) {
+ $qsoSummaryMemo[$memoKey] = $this->get_qso_summary_for_date($user_id, $entryDate, $entry->logbook_id);
+ }
+ $entry->qso_summary = $qsoSummaryMemo[$memoKey];
+ if ($include_qso_list) {
+ if (!array_key_exists($memoKey, $qsoListMemo)) {
+ $qsoListMemo[$memoKey] = $this->get_qso_list_for_date($user_id, $entryDate, $entry->logbook_id);
+ }
+ $entry->qso_list = $qsoListMemo[$memoKey];
+ }
}
}
}
@@ -577,9 +602,16 @@ private function get_user_station_ids($user_id) {
}
private function get_station_ids_for_summary($user_id, $logbook_id = NULL) {
+ static $memoized_station_ids = array();
+ $cacheKey = (int)$user_id . '|' . (string)($logbook_id === NULL ? 'null' : $logbook_id);
+ if (array_key_exists($cacheKey, $memoized_station_ids)) {
+ return $memoized_station_ids[$cacheKey];
+ }
+
if ($logbook_id === NULL || $logbook_id === '' || $logbook_id === 0 || $logbook_id === '0') {
// No logbook specified, use all user stations
- return $this->get_user_station_ids($user_id);
+ $memoized_station_ids[$cacheKey] = $this->get_user_station_ids($user_id);
+ return $memoized_station_ids[$cacheKey];
}
// Get stations associated with the specified logbook
@@ -605,10 +637,121 @@ private function get_station_ids_for_summary($user_id, $logbook_id = NULL) {
foreach ($verify_query->result() as $row) {
$verified_ids[] = (int)$row->station_id;
}
- return $verified_ids;
+ $memoized_station_ids[$cacheKey] = $verified_ids;
+ return $memoized_station_ids[$cacheKey];
}
- return $station_ids;
+ $memoized_station_ids[$cacheKey] = $station_ids;
+ return $memoized_station_ids[$cacheKey];
+ }
+
+ private function get_day_bounds($date) {
+ $start = date('Y-m-d 00:00:00', strtotime($date));
+ $end = date('Y-m-d 00:00:00', strtotime($date . ' +1 day'));
+ return array($start, $end);
+ }
+
+ private function get_date_range_bounds($start_date, $end_date) {
+ $start = date('Y-m-d 00:00:00', strtotime($start_date));
+ $end = date('Y-m-d 00:00:00', strtotime($end_date . ' +1 day'));
+ return array($start, $end);
+ }
+
+ private function get_primary_qso_grid($candidate) {
+ $grid = strtoupper(trim((string)($candidate->COL_GRIDSQUARE ?? '')));
+ if ($grid !== '') {
+ return $grid;
+ }
+
+ $vuccGrids = strtoupper(trim((string)($candidate->COL_VUCC_GRIDS ?? '')));
+ if ($vuccGrids === '') {
+ return '';
+ }
+
+ $parts = explode(',', $vuccGrids);
+ foreach ($parts as $part) {
+ $part = trim($part);
+ if ($part !== '') {
+ return $part;
+ }
+ }
+
+ return '';
+ }
+
+ private function get_dxcc_coordinates_for_candidate($candidate, &$dxccLookupCache) {
+ $dxccLat = $candidate->dxcc_lat ?? null;
+ $dxccLong = $candidate->dxcc_long ?? null;
+
+ if (is_numeric($dxccLat) && is_numeric($dxccLong)) {
+ return array((float)$dxccLat, (float)$dxccLong);
+ }
+
+ $call = strtoupper(trim((string)($candidate->COL_CALL ?? '')));
+ if ($call === '') {
+ return array(null, null);
+ }
+
+ $date = date('Ymd');
+ if (!empty($candidate->COL_TIME_ON)) {
+ $timestamp = strtotime($candidate->COL_TIME_ON);
+ if ($timestamp !== FALSE) {
+ $date = date('Ymd', $timestamp);
+ }
+ }
+
+ $cacheKey = $call . '|' . $date;
+ if (!array_key_exists($cacheKey, $dxccLookupCache)) {
+ $this->load->model('logbook_model');
+ $lookup = $this->logbook_model->dxcc_lookup($call, $date);
+ if (is_array($lookup) && isset($lookup['lat'], $lookup['long']) && is_numeric($lookup['lat']) && is_numeric($lookup['long'])) {
+ $dxccLookupCache[$cacheKey] = array((float)$lookup['lat'], (float)$lookup['long']);
+ } else {
+ $dxccLookupCache[$cacheKey] = array(null, null);
+ }
+ }
+
+ return $dxccLookupCache[$cacheKey];
+ }
+
+ private function resolve_highlight_candidate_distance_km($candidate, &$dxccLookupCache) {
+ $stationGrid = trim((string)($candidate->station_gridsquare ?? ''));
+
+ if ($stationGrid !== '') {
+ $workedGrid = $this->get_primary_qso_grid($candidate);
+ if ($workedGrid !== '') {
+ return (float)$this->qra->distance($stationGrid, $workedGrid, 'K');
+ }
+
+ list($dxccLat, $dxccLong) = $this->get_dxcc_coordinates_for_candidate($candidate, $dxccLookupCache);
+ if ($dxccLat !== null && $dxccLong !== null) {
+ $stationCoords = $this->qra->qra2latlong($stationGrid);
+ if (is_array($stationCoords) && isset($stationCoords[0], $stationCoords[1]) && is_numeric($stationCoords[0]) && is_numeric($stationCoords[1])) {
+ return (float)distance((float)$stationCoords[0], (float)$stationCoords[1], $dxccLat, $dxccLong, 'K');
+ }
+ }
+ }
+
+ return (float)($candidate->COL_DISTANCE ?? 0);
+ }
+
+ private function build_highlight_dx_from_candidates($highlight_candidates) {
+ $highlight = null;
+ if (!empty($highlight_candidates)) {
+ $this->load->library('Qra');
+ $best_distance = 0;
+ $dxccLookupCache = array();
+ foreach ($highlight_candidates as $candidate) {
+ $dist = $this->resolve_highlight_candidate_distance_km($candidate, $dxccLookupCache);
+ if ($dist > $best_distance) {
+ $best_distance = $dist;
+ $highlight = $candidate;
+ $highlight->COL_DISTANCE = (int)round($dist);
+ }
+ }
+ }
+
+ return $highlight;
}
public function get_qso_list_for_date($user_id, $date, $logbook_id = NULL) {
@@ -618,11 +761,13 @@ public function get_qso_list_for_date($user_id, $date, $logbook_id = NULL) {
}
$table = $this->config->item('table_name');
+ list($dayStart, $dayEnd) = $this->get_day_bounds($date);
$this->db->select('COL_CALL, COL_TIME_ON, COL_BAND, COL_MODE, COL_SUBMODE, COL_COUNTRY, COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_RST_SENT, COL_RST_RCVD, COL_FREQ, COL_DXCC, COL_DISTANCE, COL_PROP_MODE, COL_SAT_NAME, COL_SAT_MODE');
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON)', $date);
+ $this->db->where('COL_TIME_ON >=', $dayStart);
+ $this->db->where('COL_TIME_ON <', $dayEnd);
$this->db->order_by('COL_TIME_ON', 'ASC');
$this->db->limit(100); // Reasonable limit for public display
@@ -637,18 +782,21 @@ public function get_qso_summary_for_date($user_id, $date, $logbook_id = NULL) {
}
$table = $this->config->item('table_name');
+ list($dayStart, $dayEnd) = $this->get_day_bounds($date);
$this->db->select('COUNT(*) AS total_qsos, COUNT(DISTINCT COL_DXCC) AS dxcc_worked');
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON)', $date);
+ $this->db->where('COL_TIME_ON >=', $dayStart);
+ $this->db->where('COL_TIME_ON <', $dayEnd);
$overview = $this->db->get()->row();
$this->db->distinct();
$this->db->select('LOWER(COL_BAND) AS band, COL_BAND+0 AS band_num', FALSE);
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON)', $date);
+ $this->db->where('COL_TIME_ON >=', $dayStart);
+ $this->db->where('COL_TIME_ON <', $dayEnd);
$this->db->where('COL_BAND IS NOT NULL', null, FALSE);
$this->db->where('COL_BAND !=', '');
$this->db->order_by('band_num', 'ASC');
@@ -658,19 +806,22 @@ public function get_qso_summary_for_date($user_id, $date, $logbook_id = NULL) {
$this->db->select('(CASE WHEN COL_SUBMODE IS NOT NULL AND COL_SUBMODE != "" THEN UPPER(COL_SUBMODE) ELSE UPPER(COL_MODE) END) AS mode_label', FALSE);
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON)', $date);
+ $this->db->where('COL_TIME_ON >=', $dayStart);
+ $this->db->where('COL_TIME_ON <', $dayEnd);
$this->db->where('COL_MODE IS NOT NULL', null, FALSE);
$this->db->where('COL_MODE !=', '');
$this->db->order_by('mode_label', 'ASC');
$modesResult = $this->db->get()->result();
- // Get highlight DX - prefer stored distance, but also consider QSOs with a
- // gridsquare (or VUCC grids) that have no stored distance so they are not excluded.
- $this->db->select('t.COL_CALL, t.COL_COUNTRY, t.COL_DISTANCE, t.COL_GRIDSQUARE, t.COL_VUCC_GRIDS, sp.station_gridsquare', FALSE);
+ // Get highlight DX candidates. Distance is re-calculated in PHP using grid first,
+ // and a DXCC location fallback when worked grid is missing.
+ $this->db->select('t.COL_CALL, t.COL_COUNTRY, t.COL_TIME_ON, t.COL_DXCC, t.COL_DISTANCE, t.COL_GRIDSQUARE, t.COL_VUCC_GRIDS, sp.station_gridsquare, de.lat AS dxcc_lat, de.`long` AS dxcc_long', FALSE);
$this->db->from($table . ' t');
$this->db->join('station_profile sp', 'sp.station_id = t.station_id', 'left');
+ $this->db->join('dxcc_entities de', 'de.adif = t.COL_DXCC', 'left');
$this->db->where_in('t.station_id', $station_ids);
- $this->db->where('DATE(t.COL_TIME_ON)', $date);
+ $this->db->where('t.COL_TIME_ON >=', $dayStart);
+ $this->db->where('t.COL_TIME_ON <', $dayEnd);
$this->db->group_start();
$this->db->where('t.COL_DISTANCE >', 0);
$this->db->or_group_start();
@@ -690,25 +841,7 @@ public function get_qso_summary_for_date($user_id, $date, $logbook_id = NULL) {
$this->db->limit(50);
$highlight_candidates = $this->db->get()->result();
- $highlight = null;
- if (!empty($highlight_candidates)) {
- $this->load->library('Qra');
- $best_distance = 0;
- foreach ($highlight_candidates as $candidate) {
- $dist = (float)($candidate->COL_DISTANCE ?? 0);
- if ($dist <= 0 && !empty($candidate->station_gridsquare)) {
- $grid = !empty($candidate->COL_GRIDSQUARE) ? $candidate->COL_GRIDSQUARE : ($candidate->COL_VUCC_GRIDS ?? '');
- if (!empty($grid)) {
- $dist = (float)$this->qra->distance($candidate->station_gridsquare, $grid, 'K');
- }
- }
- if ($dist > $best_distance) {
- $best_distance = $dist;
- $highlight = $candidate;
- $highlight->COL_DISTANCE = (int)round($dist);
- }
- }
- }
+ $highlight = $this->build_highlight_dx_from_candidates($highlight_candidates);
$bands = array();
foreach ($bandsResult as $band) {
@@ -798,12 +931,13 @@ public function get_qso_list_for_date_range($user_id, $start_date, $end_date, $l
}
$table = $this->config->item('table_name');
+ list($rangeStart, $rangeEnd) = $this->get_date_range_bounds($start_date, $end_date);
$this->db->select('COL_CALL, COL_TIME_ON, COL_BAND, COL_MODE, COL_SUBMODE, COL_COUNTRY, COL_GRIDSQUARE, COL_VUCC_GRIDS, COL_RST_SENT, COL_RST_RCVD, COL_FREQ, COL_DXCC, COL_DISTANCE, COL_PROP_MODE, COL_SAT_NAME, COL_SAT_MODE');
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON) >=', $start_date);
- $this->db->where('DATE(COL_TIME_ON) <=', $end_date);
+ $this->db->where('COL_TIME_ON >=', $rangeStart);
+ $this->db->where('COL_TIME_ON <', $rangeEnd);
if ($sat_only) {
$this->db->where('COL_PROP_MODE', 'SAT');
@@ -823,12 +957,13 @@ public function get_qso_summary_for_date_range($user_id, $start_date, $end_date,
}
$table = $this->config->item('table_name');
+ list($rangeStart, $rangeEnd) = $this->get_date_range_bounds($start_date, $end_date);
$this->db->select('COUNT(*) AS total_qsos, COUNT(DISTINCT COL_DXCC) AS dxcc_worked');
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON) >=', $start_date);
- $this->db->where('DATE(COL_TIME_ON) <=', $end_date);
+ $this->db->where('COL_TIME_ON >=', $rangeStart);
+ $this->db->where('COL_TIME_ON <', $rangeEnd);
if ($sat_only) {
$this->db->where('COL_PROP_MODE', 'SAT');
@@ -841,8 +976,8 @@ public function get_qso_summary_for_date_range($user_id, $start_date, $end_date,
$this->db->select('LOWER(COL_BAND) AS band, COL_BAND+0 AS band_num', FALSE);
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON) >=', $start_date);
- $this->db->where('DATE(COL_TIME_ON) <=', $end_date);
+ $this->db->where('COL_TIME_ON >=', $rangeStart);
+ $this->db->where('COL_TIME_ON <', $rangeEnd);
$this->db->where('COL_BAND IS NOT NULL', null, FALSE);
$this->db->where('COL_BAND !=', '');
@@ -858,8 +993,8 @@ public function get_qso_summary_for_date_range($user_id, $start_date, $end_date,
$this->db->select('(CASE WHEN COL_SUBMODE IS NOT NULL AND COL_SUBMODE != "" THEN UPPER(COL_SUBMODE) ELSE UPPER(COL_MODE) END) AS mode_label', FALSE);
$this->db->from($table);
$this->db->where_in('station_id', $station_ids);
- $this->db->where('DATE(COL_TIME_ON) >=', $start_date);
- $this->db->where('DATE(COL_TIME_ON) <=', $end_date);
+ $this->db->where('COL_TIME_ON >=', $rangeStart);
+ $this->db->where('COL_TIME_ON <', $rangeEnd);
$this->db->where('COL_MODE IS NOT NULL', null, FALSE);
$this->db->where('COL_MODE !=', '');
@@ -870,14 +1005,15 @@ public function get_qso_summary_for_date_range($user_id, $start_date, $end_date,
$this->db->order_by('mode_label', 'ASC');
$modesResult = $this->db->get()->result();
- // Get highlight DX - prefer stored distance, but also consider QSOs with a
- // gridsquare (or VUCC grids) that have no stored distance so they are not excluded.
- $this->db->select('t.COL_CALL, t.COL_COUNTRY, t.COL_DISTANCE, t.COL_GRIDSQUARE, t.COL_VUCC_GRIDS, sp.station_gridsquare', FALSE);
+ // Get highlight DX candidates. Distance is re-calculated in PHP using grid first,
+ // and a DXCC location fallback when worked grid is missing.
+ $this->db->select('t.COL_CALL, t.COL_COUNTRY, t.COL_TIME_ON, t.COL_DXCC, t.COL_DISTANCE, t.COL_GRIDSQUARE, t.COL_VUCC_GRIDS, sp.station_gridsquare, de.lat AS dxcc_lat, de.`long` AS dxcc_long', FALSE);
$this->db->from($table . ' t');
$this->db->join('station_profile sp', 'sp.station_id = t.station_id', 'left');
+ $this->db->join('dxcc_entities de', 'de.adif = t.COL_DXCC', 'left');
$this->db->where_in('t.station_id', $station_ids);
- $this->db->where('DATE(t.COL_TIME_ON) >=', $start_date);
- $this->db->where('DATE(t.COL_TIME_ON) <=', $end_date);
+ $this->db->where('t.COL_TIME_ON >=', $rangeStart);
+ $this->db->where('t.COL_TIME_ON <', $rangeEnd);
if ($sat_only) {
$this->db->where('t.COL_PROP_MODE', 'SAT');
@@ -902,25 +1038,7 @@ public function get_qso_summary_for_date_range($user_id, $start_date, $end_date,
$this->db->limit(50);
$highlight_candidates = $this->db->get()->result();
- $highlight = null;
- if (!empty($highlight_candidates)) {
- $this->load->library('Qra');
- $best_distance = 0;
- foreach ($highlight_candidates as $candidate) {
- $dist = (float)($candidate->COL_DISTANCE ?? 0);
- if ($dist <= 0 && !empty($candidate->station_gridsquare)) {
- $grid = !empty($candidate->COL_GRIDSQUARE) ? $candidate->COL_GRIDSQUARE : ($candidate->COL_VUCC_GRIDS ?? '');
- if (!empty($grid)) {
- $dist = (float)$this->qra->distance($candidate->station_gridsquare, $grid, 'K');
- }
- }
- if ($dist > $best_distance) {
- $best_distance = $dist;
- $highlight = $candidate;
- $highlight->COL_DISTANCE = (int)round($dist);
- }
- }
- }
+ $highlight = $this->build_highlight_dx_from_candidates($highlight_candidates);
$bands = array();
foreach ($bandsResult as $band) {
diff --git a/application/models/User_model.php b/application/models/User_model.php
index 7b79b877e..731cc3a4e 100644
--- a/application/models/User_model.php
+++ b/application/models/User_model.php
@@ -430,6 +430,16 @@ function update_session($id) {
$CI =& get_instance();
$CI->load->model('user_options_model');
$callbook_type_object = $CI->user_options_model->get_options('callbook')->result();
+ $show_qsl_cards_option = $CI->user_options_model->get_options(
+ 'menu',
+ array('option_name' => 'show_qsl_cards', 'option_key' => 'enabled'),
+ $id
+ )->row();
+ $show_sstv_images_option = $CI->user_options_model->get_options(
+ 'menu',
+ array('option_name' => 'show_sstv_images', 'option_key' => 'enabled'),
+ $id
+ )->row();
// Get the callbook type
if (isset($callbook_type_object[1]->option_value)) {
@@ -453,6 +463,15 @@ function update_session($id) {
}
$u = $this->get_by_id($id);
+ $has_eqsl_credentials = ($u->row()->user_eqsl_name != '' && $u->row()->user_eqsl_password != '');
+ $show_qsl_cards = true;
+ if (isset($show_qsl_cards_option->option_value)) {
+ $show_qsl_cards = ($show_qsl_cards_option->option_value == 'true');
+ }
+ $show_sstv_images = false;
+ if (isset($show_sstv_images_option->option_value)) {
+ $show_sstv_images = ($show_sstv_images_option->option_value == 'true');
+ }
$userdata = array(
'user_id' => $u->row()->user_id,
@@ -464,6 +483,9 @@ function update_session($id) {
'user_lotw_name' => $u->row()->user_lotw_name,
'user_eqsl_name' => $u->row()->user_eqsl_name,
'user_eqsl_qth_nickname' => $u->row()->user_eqsl_qth_nickname,
+ 'has_eqsl_credentials' => $has_eqsl_credentials,
+ 'user_show_qsl_cards' => $show_qsl_cards,
+ 'user_show_sstv_images' => $show_sstv_images,
'user_hash' => $this->_hash($u->row()->user_id."-".$u->row()->user_type),
'radio' => isset($_COOKIE["radio"])?$_COOKIE["radio"]:"",
'station_profile_id' => isset($_COOKIE["station_profile_id"])?$_COOKIE["station_profile_id"]:"",
diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php
index ff16237eb..9a88042ca 100644
--- a/application/views/interface_assets/footer.php
+++ b/application/views/interface_assets/footer.php
@@ -2093,6 +2093,10 @@ function getUTCDateStamp(el) {
\ No newline at end of file
diff --git a/application/views/version_dialog/index.php b/application/views/version_dialog/index.php
index 03981fa3a..1103694ee 100644
--- a/application/views/version_dialog/index.php
+++ b/application/views/version_dialog/index.php
@@ -90,8 +90,116 @@
optionslib) ? $this->optionslib->get_option('version_dialog_text') : null;
if ($versionDialogText !== null) {
- $versionDialogTextWithLinks = preg_replace('/(https?:\/\/[^\s<]+)/', '$1', $versionDialogText);
- echo nl2br($versionDialogTextWithLinks);
+ // Apply markdown conversion to custom text
+ $htmlContent = $versionDialogText;
+
+ // Convert inline code (`) - protect inline code from other conversions
+ $htmlContent = preg_replace('/`([^`]+)`/', '$1', $htmlContent);
+
+ // Convert markdown horizontal rules (---, ***, ___) - split by line for robustness
+ $lines = preg_split('/\r?\n/', $htmlContent);
+ $processedLines = array();
+ foreach ($lines as $line) {
+ $trimmed = trim($line);
+ // Check if line is ONLY HR markers (3+ of same character, optionally with spaces)
+ if (preg_match('/^[ \t]{0,3}([\*\-_])\1{2,}[ \t]*$/', $trimmed)) {
+ $processedLines[] = '
\n" . $blockquoteContent . "\n"; + }, + $htmlContent + ); + + // Convert strikethrough (~~text~~) + $htmlContent = preg_replace('/~~(.+?)~~/s', '
$1', $htmlContent);
+ // Convert markdown horizontal rules (---, ***, ___) - split by line for robustness
+ $lines = preg_split('/\r?\n/', $htmlContent);
+ $processedLines = array();
+ foreach ($lines as $line) {
+ $trimmed = trim($line);
+ // Check if line is ONLY HR markers (3+ of same character, optionally with spaces)
+ if (preg_match('/^[ \t]{0,3}([\*\-_])\1{2,}[ \t]*$/', $trimmed)) {
+ $processedLines[] = 'tags + return "\n\n" . $blockquoteContent . "\n"; + }, + $htmlContent + ); + + // Convert strikethrough (~~text~~) + $htmlContent = preg_replace('/~~(.+?)~~/s', '$1', $htmlContent); + // Convert bullet points to list items (must be done before bold/italic to avoid interference) $htmlContent = preg_replace_callback( '/((?:^[ \t]*[\*\-\+] .+$(?:\r?\n)?)+)/m', function($matches) { $listContent = $matches[1]; - // Convert each bullet point to- $listContent = preg_replace('/^[ \t]*[\*\-\+] (.+)$/m', ' $1 ', $listContent); + // Handle task lists first: [ ] or [x] or [X] + $listContent = preg_replace_callback( + '/^[ \t]*[\*\-\+] \[[ xX]\]/m', + function($m) { + $isChecked = strpos($m[0], '[x') !== false || strpos($m[0], '[X') !== false; + return ' '; + }, + $listContent + ); + // Convert each bullet point to, stripping trailing whitespace + $listContent = preg_replace('/^[ \t]*[\*\-\+] (.+?)[ \t]*$/m', ' $1 ', $listContent); + // Remove newlines between list items to prevent nl2br from adding extra breaks + $listContent = preg_replace('/(<\/li>)\r?\n(?=)/', '$1', $listContent); // Wrap in tags return "\n
\n" . $listContent . "
\n"; }, @@ -159,8 +308,10 @@ function($matches) { '/((?:^[ \t]*\d+\. .+$(?:\r?\n)?)+)/m', function($matches) { $listContent = $matches[1]; - // Convert each numbered item to- - $listContent = preg_replace('/^[ \t]*\d+\. (.+)$/m', '
- $1
', $listContent); + // Convert each numbered item to- , stripping trailing whitespace + $listContent = preg_replace('/^[ \t]*\d+\. (.+?)[ \t]*$/m', '
- $1
', $listContent); + // Remove newlines between list items to prevent nl2br from adding extra breaks + $listContent = preg_replace('/(<\/li>)\r?\n(?=- )/', '$1', $listContent); // Wrap in
tags return "\n
\n" . $listContent . "
\n"; }, @@ -194,8 +345,9 @@ function($matches) { $htmlContent = preg_replace('/(?|>\s)(https?:\/\/[^\s<]+)/', '$1', $htmlContent); // Replace newlines after block-level elements with a marker to prevent nl2br from converting them - $htmlContent = preg_replace('/(<\/(?:h[1-6]|ul|ol|li|pre)>)\r?\n/', '$1', $htmlContent); - $htmlContent = preg_replace('/(<(?:ul|ol|pre)>)\r?\n/', '$1', $htmlContent); + $htmlContent = preg_replace('/(<\/(?:h[1-6]|ul|ol|li|pre|blockquote)>)\r?\n/', '$1', $htmlContent); + $htmlContent = preg_replace('/(<(?:ul|ol|pre|blockquote)>)\r?\n/', '$1', $htmlContent); + $htmlContent = preg_replace('/(
\r?\n)/', '$1', $htmlContent); // Convert line breaks to
tags $htmlContent = nl2br($htmlContent); @@ -206,12 +358,14 @@ function($matches) { $htmlContent = preg_replace('//', '', $htmlContent); // Additional cleanup: remove br tags immediately before and after block elements - $htmlContent = preg_replace('/
\s*(<(?:h[1-6]|ul|ol|li|pre|code)[^>]*>)/i', '$1', $htmlContent); - $htmlContent = preg_replace('/(<\/(?:h[1-6]|ul|ol|li|pre)>)\s*
/i', '$1', $htmlContent); - - // Remove multiple consecutive br tags (more than 2) + // Remove ALL consecutive br tags before block elements + $htmlContent = preg_replace('/(?:
\s*)+(<(?:h[1-6]|ul|ol|li|pre|code|hr|blockquote)[^>]*>)/i', '$1', $htmlContent); + $htmlContent = preg_replace('/(<\/(?:h[1-6]|ul|ol|li|pre|blockquote)>|
\s*)\s*(?:
\s*)+/i', '$1', $htmlContent); $htmlContent = preg_replace('/(
){3,}/', '
', $htmlContent); + // Remove br tags inside list items + $htmlContent = preg_replace('/(- [^<]*)
(\s*<\/li>)/i', '$1$2', $htmlContent); + echo "" . $htmlContent . ""; } else { echo 'v' . $current_version . '
'; diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index f850b48b6..a99d6e3b6 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -1,5 +1,12 @@ var lastCallsignUpdated="" var callsignLookupRequestId = 0; +var callsignDxccQuickRequestId = 0; +var callsignDxccQuickTimer = null; +var isSubmitting = false; +var lastResetCatSyncNoticeAt = 0; +var suppressNextResetHandler = false; +var previousContactsLookupMode = false; +var htmxAutoRefreshTimer = null; function hasFieldValue(value) { return value !== null && value !== undefined && String(value).trim() !== ""; @@ -9,6 +16,42 @@ function normalizeFieldValue(value) { return String(value ?? "").trim(); } +function escapeNoticeValue(value) { + return String(value || '').replace(/[&<>"']/g, function(char) { + var escapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return escapes[char] || char; + }); +} + +function showQsoNotice(message, alertType) { + var safeType = alertType || 'info'; + var $container = $('#notice-alerts-container'); + if ($container.length === 0) { + $container = $(''); + var $rightColumn = $('.col-sm-7').first(); + var $mapCard = $rightColumn.find('.qso-map').first(); + if ($mapCard.length > 0) { + $container.insertBefore($mapCard); + } else if ($rightColumn.length > 0) { + $rightColumn.prepend($container); + } + } + + $container.html('' + message + ''); + + setTimeout(function() { + $('#notice-alerts').fadeOut(300, function() { + $(this).remove(); + }); + }, 5000); +} + function shouldReplaceLookupField($field, incomingValue, fieldKey, approval) { if (!hasFieldValue(incomingValue)) { return false; @@ -541,11 +584,11 @@ var favs={}; }); // Test Consistency value on submit form // - var isSubmitting = false; $("#qso_input").off('submit').on('submit', function(e){ + e.preventDefault(); + // Prevent double submission if (isSubmitting) { - e.preventDefault(); return false; } @@ -557,6 +600,10 @@ var favs={}; if (_submit) { // Mark as submitting and disable the submit button isSubmitting = true; + $('#qso_input .warningOnSubmit').hide(); + $('#qso_input .warningOnSubmit_txt').empty(); + + var $form = $(this); var submitBtn = $(this).find('button[type="submit"]'); var originalText = submitBtn.data('original-text'); if (!originalText) { @@ -566,9 +613,88 @@ var favs={}; } submitBtn.prop('disabled', true); submitBtn.html(' Saving...'); + + var ajaxSaveUrl = $form.data('ajax-save-url') || (base_url + 'index.php/qso/ajax_saveqso'); + + $.ajax({ + url: ajaxSaveUrl, + type: 'POST', + data: $form.serialize(), + dataType: 'json', + success: function(response) { + if (response && response.status === 'ok') { + var savedCallsign = normalizeFieldValue($('#callsign').val()).toUpperCase(); + var savedBand = normalizeFieldValue($('#band').val()); + var savedMode = normalizeFieldValue($('#mode').val()); + var savedSatName = normalizeFieldValue($('#sat_name').val()); + var savedSatMode = normalizeFieldValue($('#sat_mode').val()); + var postSaveDefaults = { + band: savedBand, + mode: savedMode, + sat_name: savedSatName, + sat_mode: savedSatMode + }; + var saveMessage = (response && response.message) ? response.message : 'QSO Added'; + if (savedCallsign && savedBand) { + saveMessage += ': ' + escapeNoticeValue(savedCallsign) + ' on ' + escapeNoticeValue(savedBand) + ''; + } else if (savedCallsign) { + saveMessage += ': ' + escapeNoticeValue(savedCallsign) + ''; + } else if (savedBand) { + saveMessage += ': on ' + escapeNoticeValue(savedBand) + ''; + } + + var qsoFormElement = document.getElementById('qso_input'); + if (qsoFormElement) { + suppressNextResetHandler = true; + qsoFormElement.reset(); + } + + reset_fields(); + if (document.getElementById('qsp-tab')) { + new bootstrap.Tab(document.getElementById('qsp-tab')).show(); + } + reapplyPostSaveDefaults(postSaveDefaults); + showQsoNotice(saveMessage, 'info'); + + if (typeof htmx !== 'undefined' && document.getElementById('qso-last-table')) { + htmx.ajax('GET', base_url + 'index.php/qso/component_past_contacts', { + target: '#qso-last-table', + swap: 'innerHTML' + }); + } + + $('#callsign').focus(); + $('#qso_input').data('initialForm', $('#qso_input').serialize()); + } else { + var warningMessage = (response && response.message) ? response.message : 'Unable to save QSO. Please try again.'; + + if (response && response.validation_errors) { + var validationMessages = []; + $.each(response.validation_errors, function(_, msg) { + if (msg) { + validationMessages.push(msg); + } + }); + if (validationMessages.length > 0) { + warningMessage = validationMessages.join('
'); + } + } + + $('#qso_input .warningOnSubmit_txt').html(warningMessage); + $('#qso_input .warningOnSubmit').show(); + } + }, + error: function() { + $('#qso_input .warningOnSubmit_txt').html('Unable to save QSO due to a network or server error.'); + $('#qso_input .warningOnSubmit').show(); + }, + complete: function() { + resetSubmissionState(); + } + }); } - return _submit; + return false; }) // Prevent Enter key from causing double submissions @@ -730,10 +856,47 @@ function resetSubmissionState() { } } +function resetCallsignLookupState() { + lastCallsignUpdated = ''; + // Invalidate in-flight responses from older callsign lookups. + callsignLookupRequestId++; + callsignDxccQuickRequestId++; + if (callsignDxccQuickTimer) { + clearTimeout(callsignDxccQuickTimer); + callsignDxccQuickTimer = null; + } +} + +function isLookupStillCurrent(requestId, find_callsign) { + if (requestId !== callsignLookupRequestId) { + return false; + } + + var currentCallsign = $('#callsign').val().toUpperCase().replace(/\//g, "-").replace('Ø', '0'); + return currentCallsign === find_callsign; +} + +function clearSatelliteFields() { + $('#sat_name').val(''); + $('#sat_mode').val(''); + $('.satellite_modes_list').find('option').remove().end(); + selected_sat = ''; + selected_sat_mode = ''; + + if ($('#selectPropagation').val() === 'SAT') { + $('#selectPropagation').val(''); + } +} + +function clearCatTrackedFieldState() { + $('#frequency, #frequency_rx, #sat_name, #sat_mode, #transmit_power, #selectPropagation, #mode').removeData('catValue'); +} + /* Function: reset_fields is used to reset the fields on the QSO page */ function reset_fields() { // Reset submission state resetSubmissionState(); + resetCallsignLookupState(); $('#locator_info').text(""); $('#country').val(""); @@ -745,13 +908,17 @@ function reset_fields() { $('#qrz_info').text(""); $('#hamqth_info').text(""); $('#sota_info').text(""); + $('#wwff_info').html('').attr('title', ''); + $('#pota_info').html('').attr('title', ''); $('#dxcc_id').val("").trigger('change'); $('#cqz').val(""); $('#name').val(""); $('#qth').val(""); $('#locator').val(""); $('#iota_ref').val(""); - $('#sota_ref').val(""); + $select = $('#sota_ref').selectize(); + selectize = $select[0] ? $select[0].selectize : null; + if (selectize) selectize.clear(); $("#locator").removeClass("confirmedGrid"); $("#locator").removeClass("workedGrid"); $("#locator").removeClass("newGrid"); @@ -767,7 +934,10 @@ function reset_fields() { $('#callsign_info').text(""); $('#input_usa_state').val(""); $('#qso-last-table').show(); + $('#partial_view').html(''); $('#partial_view').hide(); + previousContactsLookupMode = false; + resumeAutoRefresh(); var $select = $('#wwff_ref').selectize(); var selectize = $select[0] ? $select[0].selectize : null; if (selectize) selectize.clear(); @@ -781,12 +951,46 @@ function reset_fields() { selectize = $select[0] ? $select[0].selectize : null; if (selectize) selectize.clear(); + clearSatelliteFields(); + clearCatTrackedFieldState(); + mymap.setView(pos, 12); mymap.removeLayer(markers); $('.callsign-suggest').hide(); $('.dxccsummary').remove(); $('#timesWorked').html(lang_qso_title_previous_contacts); renderQsoCallhistoryPanel([], 'Type a callsign to see membership details from your uploaded call history files.'); + + // Reapply default RST values for the current mode (e.g., CW => 599). + if (typeof setRst === 'function') { + setRst($('.mode').val()); + } +} + +function reapplyPostSaveDefaults(defaults) { + if (!defaults) { + return; + } + + if (typeof defaults.band !== 'undefined') { + $('#band').val(defaults.band); + } + + if (typeof defaults.mode !== 'undefined') { + $('#mode').val(defaults.mode); + } + + if (typeof defaults.sat_name !== 'undefined') { + $('#sat_name').val(defaults.sat_name); + } + + if (typeof defaults.sat_mode !== 'undefined') { + $('#sat_mode').val(defaults.sat_mode); + } + + if (typeof setRst === 'function') { + setRst($('#mode').val()); + } } function resetTimers(manual) { @@ -830,7 +1034,7 @@ $("#callsign").focusout(function() { // Replace / in a callsign with - to stop urls breaking $.getJSON(base_url + 'index.php/logbook/json/' + find_callsign + '/' + sat_type + '/' + json_band + '/' + json_mode + '/' + $('#stationProfile').val(), function(result) { - if (requestId !== callsignLookupRequestId) { + if (!isLookupStillCurrent(requestId, find_callsign)) { return; } @@ -838,63 +1042,37 @@ $("#callsign").focusout(function() { var currentCallsign = $('#callsign').val().toUpperCase().replace(/\//g, "-").replace('Ø', '0'); if(currentCallsign === find_callsign) { - // Reset QSO fields - resetDefaultQSOFields(); - - if(result.dxcc.entity != undefined) { - $('#country').val(convert_case(result.dxcc.entity)); - $('#callsign_info').text(convert_case(result.dxcc.entity)); - - if($("#sat_name" ).val() != "") { - //logbook/jsonlookupgrid/io77/SAT/0/0 - $.getJSON(base_url + 'index.php/logbook/jsonlookupcallsign/' + find_callsign + '/SAT/0/0', function(result) - { - // Reset CSS values before updating - $('#callsign').removeClass("workedGrid"); - $('#callsign').removeClass("confirmedGrid"); - $('#callsign').removeClass("newGrid"); - $('#callsign').attr('title', ''); - - if (result.confirmed) { - $('#callsign').addClass("confirmedGrid"); - $('#callsign').attr('title', 'Callsign was already worked and confirmed in the past on this band and mode!'); - } else if (result.workedBefore) { - $('#callsign').addClass("workedGrid"); - $('#callsign').attr('title', 'Callsign was already worked in the past on this band and mode!'); - } - else - { - $('#callsign').addClass("newGrid"); - $('#callsign').attr('title', 'New Callsign!'); - } - }) - } else { - $.getJSON(base_url + 'index.php/logbook/jsonlookupcallsign/' + find_callsign + '/0/' + $("#band").val() +'/' + $("#mode").val(), function(result) - { - // Reset CSS values before updating - $('#callsign').removeClass("confirmedGrid"); - $('#callsign').removeClass("workedGrid"); - $('#callsign').removeClass("newGrid"); - $('#callsign').attr('title', ''); - - if (result.confirmed) { - $('#callsign').addClass("confirmedGrid"); - $('#callsign').attr('title', 'Callsign was already worked and confirmed in the past on this band and mode!'); - } else if (result.workedBefore) { - $('#callsign').addClass("workedGrid"); - $('#callsign').attr('title', 'Callsign was already worked in the past on this band and mode!'); - } else { - $('#callsign').addClass("newGrid"); - $('#callsign').attr('title', 'New Callsign!'); - } - - }) - } - - changebadge(result.dxcc.entity); - + // Enter lookup mode - pause auto-refresh of logbook pagination + previousContactsLookupMode = true; + pauseAutoRefresh(); + + // Reset QSO fields but keep current DXCC badge/country to avoid flicker. + resetDefaultQSOFields(true); + + if(result.dxcc.entity != undefined) { + $('#country').val(convert_case(result.dxcc.entity)); + $('#callsign_info').text(convert_case(result.dxcc.entity)); + + // Reset CSS values before updating + $('#callsign').removeClass("confirmedGrid"); + $('#callsign').removeClass("workedGrid"); + $('#callsign').removeClass("newGrid"); + $('#callsign').attr('title', ''); + + if (result.callsignConfirmed || result.confirmed) { + $('#callsign').addClass("confirmedGrid"); + $('#callsign').attr('title', 'Callsign was already worked and confirmed in the past on this band and mode!'); + } else if (result.callsignWorkedBefore) { + $('#callsign').addClass("workedGrid"); + $('#callsign').attr('title', 'Callsign was already worked in the past on this band and mode!'); + } else { + $('#callsign').addClass("newGrid"); + $('#callsign').attr('title', 'New Callsign!'); } + changebadge(result.dxcc.entity); + } + if(result.lotw_member == "active") { $('#lotw_info').text("LoTW"); if (result.lotw_days > 365) { @@ -920,6 +1098,9 @@ $("#callsign").focusout(function() { var dok_selectize = $dok_select[0] ? $dok_select[0].selectize : null; if (result.dxcc.adif == '230') { $.get(base_url + 'index.php/lookup/dok/' + $('#callsign').val().toUpperCase(), function(result) { + if (!isLookupStillCurrent(requestId, find_callsign)) { + return; + } if (result && dok_selectize) { dok_selectize.addOption({name: result}); dok_selectize.setValue(result, false); @@ -929,7 +1110,7 @@ $("#callsign").focusout(function() { if (dok_selectize) dok_selectize.clear(); } - $('#dxcc_id').val(result.dxcc.adif).trigger('change'); + $('#dxcc_id').val(result.dxcc.adif); $('#cqz').val(result.dxcc.cqz); $('#ituz').val(result.dxcc.ituz); @@ -956,7 +1137,7 @@ $("#callsign").focusout(function() { var overwriteConflicts = getLookupOverwriteConflicts(result); showLookupOverwriteModal(overwriteConflicts).then(function(approval) { - if (requestId !== callsignLookupRequestId) { + if (!isLookupStillCurrent(requestId, find_callsign)) { return; } @@ -1013,12 +1194,22 @@ $("#callsign").focusout(function() { if($('#iota_ref').val() == "") { $('#iota_ref').val(result.callsign_iota); } - // Hide the last QSO table - $('#qso-last-table').hide(); - $('#partial_view').show(); /* display past QSOs */ - $('#partial_view').html(result.partial); - + var partialHtml = (typeof result.partial === 'string') ? result.partial : ''; + + if (partialHtml.trim() !== '') { + // State 2: Callsign found in log with past QSOs + $('#partial_view').html(partialHtml); + setPreviousContactsPanelState(true); + } else { + // State 3: Callsign found but NO past QSOs in database + var noQsoMsg = '' + + '' + + 'No past QSOs with ' + escapeNoticeValue(find_callsign) + '—see callbook details above.' + + ''; + $('#partial_view').html(noQsoMsg); + setPreviousContactsPanelState(true); + } // Get DXX Summary getDxccResult(result.dxcc.adif, convert_case(result.dxcc.entity)); } @@ -1031,6 +1222,43 @@ $("#callsign").focusout(function() { } }) +function pauseAutoRefresh() { + if (typeof htmx !== 'undefined' && document.getElementById('qso-last-table-content')) { + var elem = document.getElementById('qso-last-table-content'); + if (elem) { + elem.removeAttribute('hx-trigger'); + } + } +} + +function resumeAutoRefresh() { + if (typeof htmx !== 'undefined' && document.getElementById('qso-last-table-content')) { + var elem = document.getElementById('qso-last-table-content'); + if (elem) { + elem.setAttribute('hx-trigger', 'every 5s'); + htmx.process(elem); + } + } +} + +function setPreviousContactsPanelState(showLookupDetails) { + if (showLookupDetails) { + // Show callsign-specific results (either QSOs found or "not found" message) + $('#qso-last-table').hide(); + $('#qso-last-table').next('small').hide(); + $('#partial_view').show(); + previousContactsLookupMode = true; + return; + } + + // Show paginated logbook (no callsign lookup active) + $('#qso-last-table').show(); + $('#qso-last-table').next('small').show(); + $('#partial_view').html(''); + $('#partial_view').hide(); + previousContactsLookupMode = false; +} + // Function to reset back to Previous Contacts tab function resetToPreviousContactsTab() { // Clear DXCC Summary tab content @@ -1040,15 +1268,66 @@ function resetToPreviousContactsTab() { var previousContactsTab = new bootstrap.Tab(document.getElementById('previous-contacts-tab')); previousContactsTab.show(); } - // Show the previous contacts table - $('#qso-last-table').show(); - $('#partial_view').hide(); + // Show the default previous contacts table and hide lookup details. + setPreviousContactsPanelState(false); +} + +// Re-apply visibility state after HTMX updates the previous contacts markup. +if (typeof htmx !== 'undefined' && document.body) { + document.body.addEventListener('htmx:afterSwap', function(evt) { + var detail = evt && evt.detail ? evt.detail : null; + var target = detail && detail.target ? detail.target : null; + if (!target) { + return; + } + + if (target.id !== 'qso-last-table' && target.id !== 'qso-last-table-content') { + return; + } + + // If we're in lookup mode, keep showing the partial_view (callsign-specific results) + // If we're not in lookup mode, show the main pagination table + if (previousContactsLookupMode) { + setPreviousContactsPanelState(true); + } else if ($('#partial_view').html().trim() !== '') { + setPreviousContactsPanelState(true); + } + }); +} + +// If a radio is selected, prefer current CAT values over stale form defaults. +function syncFromSelectedRadioAfterReset() { + var selectedRadioID = String($('select.radios option:selected').val() || '0'); + if (selectedRadioID === '0') { + return false; + } + + if (typeof updateFromCAT === 'function') { + var now = Date.now(); + if (now - lastResetCatSyncNoticeAt > 1000) { + showQsoNotice('Form reset. Syncing live data from selected radio.', 'info'); + lastResetCatSyncNoticeAt = now; + } + updateFromCAT(selectedRadioID); + return true; + } + + return false; } // Reset to Previous Contacts tab when form is reset $('#qso_input').on('reset', function() { + resetCallsignLookupState(); + + if (suppressNextResetHandler) { + suppressNextResetHandler = false; + return; + } + setTimeout(function() { + reset_fields(); resetToPreviousContactsTab(); + syncFromSelectedRadioAfterReset(); }, 100); }); @@ -1058,11 +1337,36 @@ function resetQsoEntryOnEscape() { return; } + // Capture the operating context the user currently has selected BEFORE native + // form reset clobbers it with server-session defaults (last logged QSO values). + var preBand = $('#band').val(); + var preMode = $('#mode').val(); + var preSatName = $('#sat_name').val(); + var preSatMode = $('#sat_mode').val(); + var prePropMode = $('#selectPropagation').val(); + qsoForm.reset(); - lastCallsignUpdated = ''; + resetCallsignLookupState(); resetDefaultQSOFields(); resetToPreviousContactsTab(); $('#callsign').trigger('focus'); + + // The on('reset') handler runs reset_fields() + clearSatelliteFields() in 100ms. + // Re-apply the captured context afterwards so the user stays on the band/mode/ + // satellite they had selected, not the one from the previous QSO session. + setTimeout(function() { + $('#band').val(preBand); + $('#mode').val(preMode); + if (preSatName) { + $('#sat_name').val(preSatName); + $('#sat_mode').val(preSatMode); + $('#selectPropagation').val(prePropMode || 'SAT'); + } + if (typeof setRst === 'function') { + setRst($('#mode').val()); + } + syncFromSelectedRadioAfterReset(); + }, 150); } // Global ESC handling on the QSO page: reset form, return to Previous Contacts, and focus callsign. @@ -1087,7 +1391,7 @@ $(document).off('keydown.qsoEscapeReset').on('keydown.qsoEscapeReset', function( // Also handle when callsign is cleared (empty value entered) $('#callsign').on('input keyup', function() { if ($(this).val() === '' && lastCallsignUpdated !== '') { - lastCallsignUpdated = ''; + resetCallsignLookupState(); resetDefaultQSOFields(); resetToPreviousContactsTab(); } @@ -1392,8 +1696,48 @@ $("#callsign").on("keypress", function(e) { } }); +function quickLookupDxcc(callsign) { + if (!callsign || callsign.length < 3) { + return; + } + + var requestId = ++callsignDxccQuickRequestId; + var find_callsign = callsign.toUpperCase().replace(/\//g, "-").replace('Ø', '0'); + + $.getJSON(base_url + 'index.php/logbook/jsondxcc/' + find_callsign, function(result) { + if (requestId !== callsignDxccQuickRequestId) { + return; + } + + var currentCallsign = $('#callsign').val().toUpperCase().replace(/\//g, "-").replace('Ø', '0'); + if (currentCallsign !== find_callsign) { + return; + } + + if (result.dxcc && result.dxcc.entity !== undefined) { + $('#country').val(convert_case(result.dxcc.entity)); + $('#callsign_info').text(convert_case(result.dxcc.entity)); + changebadge(result.dxcc.entity); + $('#dxcc_id').val(result.dxcc.adif); + $('#cqz').val(result.dxcc.cqz); + $('#ituz').val(result.dxcc.ituz); + } + }); +} + // On Key up check and suggest callsigns $("#callsign").keyup(function() { + if (callsignDxccQuickTimer) { + clearTimeout(callsignDxccQuickTimer); + } + + var currentCall = $(this).val(); + if (currentCall.length >= 3) { + callsignDxccQuickTimer = setTimeout(function() { + quickLookupDxcc(currentCall); + }, 200); + } + if ($(this).val().length >= 3) { $('.callsign-suggest').show(); $callsign = $(this).val().replace('Ø', '0'); @@ -1414,7 +1758,26 @@ $("#callsign").keyup(function() { }); //Reset QSO form Fields function -function resetDefaultQSOFields() { +function resetDefaultQSOFields(preserveDxccState) { + var keepDxcc = preserveDxccState === true; + var preservedCountry = ''; + var preservedCallsignInfoText = ''; + var preservedCallsignInfoTitle = ''; + var preservedCallsignInfoClass = ''; + var preservedDxccId = ''; + var preservedCqz = ''; + var preservedItuz = ''; + + if (keepDxcc) { + preservedCountry = $('#country').val(); + preservedCallsignInfoText = $('#callsign_info').text(); + preservedCallsignInfoTitle = $('#callsign_info').attr('title') || ''; + preservedCallsignInfoClass = $('#callsign_info').attr('class') || ''; + preservedDxccId = $('#dxcc_id').val(); + preservedCqz = $('#cqz').val(); + preservedItuz = $('#ituz').val(); + } + $('#callsign_info').text(""); $('#locator_info').text(""); $('#country').val(""); @@ -1427,6 +1790,7 @@ function resetDefaultQSOFields() { $('#hamqth_info').text(""); $('#dxcc_id').val("").trigger('change'); $('#cqz').val(""); + $('#ituz').val(""); $("#locator").removeClass("workedGrid"); $("#locator").removeClass("confirmedGrid"); $("#locator").removeClass("newGrid"); @@ -1440,6 +1804,16 @@ function resetDefaultQSOFields() { $('#callsign-image-content').text(""); $('.dxccsummary').remove(); $('#timesWorked').html(lang_qso_title_previous_contacts); + + if (keepDxcc) { + $('#country').val(preservedCountry); + $('#callsign_info').text(preservedCallsignInfoText); + $('#callsign_info').attr('title', preservedCallsignInfoTitle); + $('#callsign_info').attr('class', preservedCallsignInfoClass); + $('#dxcc_id').val(preservedDxccId); + $('#cqz').val(preservedCqz); + $('#ituz').val(preservedItuz); + } } function closeModal() {