From a652b1d38c311a73730ecf17bdfe29f89d337302 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Tue, 7 Oct 2025 23:03:20 +0200 Subject: [PATCH 01/19] Notes: basic support added to QSO page --- application/controllers/Notes.php | 46 +++++++++++++++------ application/views/notes/add.php | 19 +++++++-- application/views/qso/index.php | 2 +- assets/js/sections/qso.js | 66 ++++++++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 16 deletions(-) diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index 991b2e591..8e8e5f68b 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -2,7 +2,6 @@ // Notes controller: handles all note actions, with security and input validation class Notes extends CI_Controller { - // API endpoint: check for duplicate note title in category for user // Ensure only authorized users can access Notes controller function __construct() { parent::__construct(); @@ -41,19 +40,34 @@ class Notes extends CI_Controller { function add() { $this->load->model('note'); $this->load->library('form_validation'); - $this->load->library('callbook'); // Used for callsign parsing + $this->load->library('callbook'); // Used for callsign parsing + + // Support prefilled title/category from query string + $prefill_title = $this->input->get('title', TRUE); + $prefill_category = $this->input->get('category', TRUE); $suggested_title = null; // Validate form fields $this->form_validation->set_rules('title', 'Note Title', 'required|callback_contacts_title_unique'); // Custom callback for Contacts category $this->form_validation->set_rules('content', 'Content', 'required'); if ($this->form_validation->run() == FALSE) { + // Use POST if available, otherwise use prefill from query string $category = $this->input->post('category', TRUE); - if ($category === 'Contacts') { - - $suggested_title = strtoupper($this->callbook->get_plaincall($this->input->post('title', TRUE))); + if (empty($category) && !empty($prefill_category)) { + $category = $prefill_category; } + if ($category === 'Contacts') { + $title_input = $this->input->post('title', TRUE); + if (empty($title_input) && !empty($prefill_title)) { + $title_input = $prefill_title; + } + $suggested_title = strtoupper($this->callbook->get_plaincall($title_input)); + } + // Pass prefill values to view $data['suggested_title'] = $suggested_title; + $data['prefill_title'] = $prefill_title; + $data['prefill_category'] = $prefill_category; + $data['category'] = $category; $data['page_title'] = __("Add Notes"); $this->load->view('interface_assets/header', $data); $this->load->view('notes/add'); @@ -241,26 +255,36 @@ class Notes extends CI_Controller { $id = $this->input->get('id', TRUE); // Optional, for edit $check_title = $title; if ($category === 'Contacts') { - $check_title = strtoupper($title); + $this->load->library('callbook'); + $check_title = strtoupper($this->callbook->get_plaincall($title)); } $where = [ - 'category' => $category, + 'cat' => $category, 'user_id' => $user_id, 'title' => $check_title ]; $query = $this->db->get_where('notes', $where); - $duplicate = false; + $exists = false; + $note_id = null; if ($id) { foreach ($query->result() as $note) { if ($note->id != $id) { - $duplicate = true; + $exists = true; + $note_id = $note->id; break; } } } else { - $duplicate = $query->num_rows() > 0; + if ($query->num_rows() > 0) { + $exists = true; + $note_id = $query->row()->id; + } } - $this->output->set_content_type('application/json')->set_output(json_encode(['duplicate' => $duplicate])); + $response = ['exists' => $exists]; + if ($exists && $note_id) { + $response['id'] = $note_id; + } + $this->output->set_content_type('application/json')->set_output(json_encode($response)); } // Form validation callback for add: unique Contacts note title for user, only core callsign diff --git a/application/views/notes/add.php b/application/views/notes/add.php index 177ef3d50..1ad0172b1 100644 --- a/application/views/notes/add.php +++ b/application/views/notes/add.php @@ -29,7 +29,18 @@
- +
diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 93586d393..1aa182d38 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -154,7 +154,7 @@ switch ($date_format) {
-  " class="fas fa-search"> +  " class="fas fa-search"> " class="fas fa-sticky-note text-secondary">
diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index c363a4e92..71508290c 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -73,6 +73,15 @@ function getUTCDateStamp(el) { $(el).attr('value', formatted_date); } +// Note icon state logic +function setNoteIconState(enabled) { + var $icon = $('#note_create_edit'); + if (enabled) { + $icon.removeClass('text-secondary'); + } else { + $icon.addClass('text-secondary'); + } +} $('#stationProfile').on('change', function () { var stationProfile = $('#stationProfile').val(); @@ -399,7 +408,7 @@ function parseUserDate(user_provided_date) { // creates JS-Date out of user-prov month = parseInt(parts[1], 10) - 1; year = parseInt(parts[2], 10); } - if (isNaN(day) || day < 1 || day > 31 || isNaN(month) || month < 0 || month > 11 || isNaN(year)) return null; + if (isNaN(day) || day < 1 || day > 31 || isNaN(month) || month < 0 || month > 11 || isNaN(year)) return null; return new Date(year, month, day); } @@ -891,6 +900,7 @@ function reset_fields() { clearTimeout(); set_timers(); resetTimers(qso_manual); + setNoteIconState(false); // Always gray out note icon on reset } $("#callsign").on("focusout", function () { @@ -921,6 +931,25 @@ $("#callsign").on("focusout", function () { find_callsign = find_callsign.replaceAll('Ø', '0'); const url = `${base_url}index.php/logbook/json/${find_callsign}/${json_band}/${json_mode}/${stationProfile}/${startDate}/${last_qsos_count}`; + // Check note existence for this callsign + $.get( + window.base_url + 'index.php/notes/check_duplicate', + { + category: 'Contacts', + title: callsign + }, + function(data) { + if (typeof data === 'string') { + try { data = JSON.parse(data); } catch (e) { data = {}; } + } + if (data && data.exists === true) { + setNoteIconState(true); + } else { + setNoteIconState(false); + } + } + ); + // Replace / in a callsign with - to stop urls breaking lookupCall = $.getJSON(url, async function (result) { @@ -2173,6 +2202,8 @@ $(document).ready(function () { set_timers(); updateStateDropdown('#dxcc_id', '#stateInputLabel', '#location_us_county', '#stationCntyInputQso'); + setNoteIconState(false); /// Grey-out note icon + // Clear the localStorage for the qrg units, except the quicklogCallsign and a possible backlog clearQrgUnits(); set_qrg(); @@ -2400,6 +2431,39 @@ $(document).ready(function () { }); } + // Note create/edit icon click handler + $('#note_create_edit').on('click', function() { + var callsign = $('#callsign').val().trim(); + if (!callsign) { + alert('Please enter a callsign first.'); + return; + } + // AJAX to check if note exists for this callsign in Contacts category + $.get( + window.base_url + 'index.php/notes/check_duplicate', + { + category: 'Contacts', + title: callsign + }, + function(data) { + // Defensive: try to parse if string + if (typeof data === 'string') { + try { data = JSON.parse(data); } catch (e) { data = {}; } + } + if (data && data.exists === true && data.id) { + window.open(window.base_url + 'index.php/notes/edit/' + data.id, '_blank'); + } else if (data && data.exists === false) { + // Open add with prefilled title and Contacts category + var url = window.base_url + 'index.php/notes/add?title=' + encodeURIComponent(callsign) + '&category=Contacts'; + window.open(url, '_blank'); + } else { + // Unexpected response, show error + alert('Could not check note existence. Please try again.'); + } + } + ); + }); + // everything loaded and ready 2 go bc.postMessage('ready'); }); From 3de79d869bc6bcce04a8f577b07e62ce992a4eea Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Tue, 7 Oct 2025 23:52:13 +0200 Subject: [PATCH 02/19] Notes: basic support in the QSO view page --- application/controllers/Logbook.php | 6 ++++++ application/controllers/Notes.php | 30 ++++++++--------------------- application/models/Note.php | 18 +++++++++++++++++ application/views/view_log/qso.php | 8 +++++++- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/application/controllers/Logbook.php b/application/controllers/Logbook.php index a4f6766e4..e3e7dad7a 100644 --- a/application/controllers/Logbook.php +++ b/application/controllers/Logbook.php @@ -617,6 +617,12 @@ class Logbook extends CI_Controller { $data['query'] = $this->logbook_model->get_qso($id); $data['dxccFlag'] = $this->dxccflag->get($data['query']->result()[0]->COL_DXCC); + // Check for note for this callsign and current user + $callsign = $data['query']->result()[0]->COL_CALL; + $user_id = $this->session->userdata('user_id'); + $this->load->model('note'); + $data['contacts_note_id'] = $this->note->get_note_id_by_category($user_id, 'Contacts', $callsign); + if ($this->session->userdata('user_measurement_base') == NULL) { $data['measurement_base'] = $this->config->item('measurement_base'); } diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index 8e8e5f68b..997b14b52 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -253,31 +253,17 @@ class Notes extends CI_Controller { $category = $this->input->get('category', TRUE); $title = $this->input->get('title', TRUE); $id = $this->input->get('id', TRUE); // Optional, for edit - $check_title = $title; - if ($category === 'Contacts') { - $this->load->library('callbook'); - $check_title = strtoupper($this->callbook->get_plaincall($title)); - } - $where = [ - 'cat' => $category, - 'user_id' => $user_id, - 'title' => $check_title - ]; - $query = $this->db->get_where('notes', $where); $exists = false; $note_id = null; - if ($id) { - foreach ($query->result() as $note) { - if ($note->id != $id) { - $exists = true; - $note_id = $note->id; - break; - } - } - } else { - if ($query->num_rows() > 0) { + $this->load->model('note'); + $note_id_found = $this->note->get_note_id_by_category($user_id, $category, $title); + if ($note_id_found) { + // If editing, ignore current note + if ($id && $note_id_found == $id) { + $exists = false; + } else { $exists = true; - $note_id = $query->row()->id; + $note_id = $note_id_found; } } $response = ['exists' => $exists]; diff --git a/application/models/Note.php b/application/models/Note.php index ba1ef8438..c2b4d1d29 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -162,6 +162,24 @@ class Note extends CI_Model { return $result; } + // Return note ID for given category and title (callsign for Contacts), else false + public function get_note_id_by_category($user_id, $category, $title) { + $check_title = $title; + if ($category === 'Contacts') { + $this->load->library('callbook'); // Used for callsign parsing + $check_title = strtoupper($this->callbook->get_plaincall($title)); + } + $query = $this->db->get_where('notes', [ + 'cat' => $category, + 'user_id' => $user_id, + 'title' => $check_title + ]); + if ($query->num_rows() > 0) { + return $query->row()->id; + } + return false; + } + // Search notes with pagination and sorting for the logged-in user public function search_paginated($criteria = [], $page = 1, $per_page = 25, $sort_col = null, $sort_dir = null) { $user_id = $this->session->userdata('user_id'); diff --git a/application/views/view_log/qso.php b/application/views/view_log/qso.php index 3090954c0..9615b4f05 100644 --- a/application/views/view_log/qso.php +++ b/application/views/view_log/qso.php @@ -81,7 +81,13 @@ - COL_CALL)); ?> Lookup <?php echo strtoupper($row->COL_CALL); ?> on QRZ.com Lookup <?php echo strtoupper($row->COL_CALL); ?> on HamQTH Lookup <?php echo strtoupper($row->COL_CALL); ?> on eQSL.cc Clublog Log Search + COL_CALL)); ?> Lookup <?php echo strtoupper($row->COL_CALL); ?> on QRZ.com Lookup <?php echo strtoupper($row->COL_CALL); ?> on HamQTH Lookup <?php echo strtoupper($row->COL_CALL); ?> on eQSL.cc Clublog Log Search + + " style="margin-left:2px;vertical-align:middle;"> + + + + From 8db8cf6d3a9f011a91fd845f4f87447c8186c01c Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 8 Oct 2025 00:24:16 +0200 Subject: [PATCH 03/19] Changed to use plain SQL --- application/models/Note.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/application/models/Note.php b/application/models/Note.php index c2b4d1d29..d82cf387d 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -169,11 +169,8 @@ class Note extends CI_Model { $this->load->library('callbook'); // Used for callsign parsing $check_title = strtoupper($this->callbook->get_plaincall($title)); } - $query = $this->db->get_where('notes', [ - 'cat' => $category, - 'user_id' => $user_id, - 'title' => $check_title - ]); + $sql = "SELECT id FROM notes WHERE cat = ? AND user_id = ? AND title = ? LIMIT 1"; + $query = $this->db->query($sql, array($category, $user_id, $check_title)); if ($query->num_rows() > 0) { return $query->row()->id; } From 7fbcc56220a458207116481b98b2f0848900780a Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 8 Oct 2025 01:02:24 +0200 Subject: [PATCH 04/19] Notes: fix for translation and proper clearing of the field --- application/views/interface_assets/footer.php | 3 + application/views/qso/index.php | 2 +- assets/js/sections/qso.js | 71 ++++++++++++------- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index a98bc81d4..35c073d42 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -68,6 +68,9 @@ var lang_notes_duplicate_confirmation = ""; var lang_notes_duplication_disabled_short = ""; var lang_notes_not_found = ""; + var lang_qso_note_add = ""; + var lang_qso_note_edit = ""; + var lang_qso_note_no_callsign = ""; diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 1aa182d38..2915171e0 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -154,7 +154,7 @@ switch ($date_format) {
-  " class="fas fa-search"> " class="fas fa-sticky-note text-secondary"> +  " class="fas fa-search"> 
diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 71508290c..1f2df3662 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -74,12 +74,24 @@ function getUTCDateStamp(el) { } // Note icon state logic -function setNoteIconState(enabled) { +function setNoteIconState(state) { var $icon = $('#note_create_edit'); - if (enabled) { - $icon.removeClass('text-secondary'); + $icon.removeClass('text-secondary text-info'); + $icon.removeAttr('data-bs-original-title'); + $icon.removeAttr('title'); + if (state == 2) { + $icon.addClass('text-info'); + $icon.attr('data-bs-original-title', lang_qso_note_edit); + } else if (state == 1) { + $icon.attr('data-bs-original-title', lang_qso_note_add); + // do nothing - white icon } else { $icon.addClass('text-secondary'); + $icon.attr('data-bs-original-title', lang_qso_note_no_callsign); + } + // If Bootstrap tooltip is initialized, update it + if ($icon.data('bs.tooltip')) { + $icon.tooltip('dispose').tooltip(); } } @@ -900,9 +912,31 @@ function reset_fields() { clearTimeout(); set_timers(); resetTimers(qso_manual); - setNoteIconState(false); // Always gray out note icon on reset + setNoteIconState(0); // Always gray out note icon on reset } +// Set note icon state: 0 = gray, 1 = empty, 2 = filled based on callsign +function get_note_icon(callsign){ + $.get( + window.base_url + 'index.php/notes/check_duplicate', + { + category: 'Contacts', + title: callsign + }, + function(data) { + if (typeof data === 'string') { + try { data = JSON.parse(data); } catch (e) { data = {}; } + } + if (data && data.exists === true) { + setNoteIconState(2); + } else { + setNoteIconState(1); + } + } + ); +} + +// Lookup callsign on focusout - if the callsign is 3 chars or longer $("#callsign").on("focusout", function () { if ($(this).val().length >= 3 && preventLookup == false) { @@ -931,25 +965,6 @@ $("#callsign").on("focusout", function () { find_callsign = find_callsign.replaceAll('Ø', '0'); const url = `${base_url}index.php/logbook/json/${find_callsign}/${json_band}/${json_mode}/${stationProfile}/${startDate}/${last_qsos_count}`; - // Check note existence for this callsign - $.get( - window.base_url + 'index.php/notes/check_duplicate', - { - category: 'Contacts', - title: callsign - }, - function(data) { - if (typeof data === 'string') { - try { data = JSON.parse(data); } catch (e) { data = {}; } - } - if (data && data.exists === true) { - setNoteIconState(true); - } else { - setNoteIconState(false); - } - } - ); - // Replace / in a callsign with - to stop urls breaking lookupCall = $.getJSON(url, async function (result) { @@ -959,6 +974,9 @@ $("#callsign").on("focusout", function () { // Reset QSO fields resetDefaultQSOFields(); + // Set qso icon + get_note_icon(result.callsign); + if (result.dxcc.entity != undefined) { $('#country').val(convert_case(result.dxcc.entity)); $('#callsign_info').text(convert_case(result.dxcc.entity)); @@ -2140,6 +2158,8 @@ function resetDefaultQSOFields() { $('#callsign-image-content').text(""); $('.awardpane').remove(); $('#timesWorked').html(lang_qso_title_previous_contacts); + + setNoteIconState(0); // Always gray out note icon on reset } function closeModal() { @@ -2202,7 +2222,7 @@ $(document).ready(function () { set_timers(); updateStateDropdown('#dxcc_id', '#stateInputLabel', '#location_us_county', '#stationCntyInputQso'); - setNoteIconState(false); /// Grey-out note icon + setNoteIconState(0); /// Grey-out note icon // Clear the localStorage for the qrg units, except the quicklogCallsign and a possible backlog clearQrgUnits(); @@ -2434,8 +2454,7 @@ $(document).ready(function () { // Note create/edit icon click handler $('#note_create_edit').on('click', function() { var callsign = $('#callsign').val().trim(); - if (!callsign) { - alert('Please enter a callsign first.'); + if (!callsign || callsign.length < 3) { return; } // AJAX to check if note exists for this callsign in Contacts category From f0d3f87ee829a7defb38117584412532bebae731 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 8 Oct 2025 02:47:39 +0200 Subject: [PATCH 05/19] =?UTF-8?q?Notes:=20fix=20for=20"=C3=98"=20and=20sma?= =?UTF-8?q?ll=20refactor=20of=20the=20note=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/controllers/Notes.php | 31 +++++++++--------- application/models/Note.php | 52 +++++++++++++++---------------- assets/js/sections/notes.js | 12 +++++++ 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index 997b14b52..de681e717 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -62,6 +62,7 @@ class Notes extends CI_Controller { $title_input = $prefill_title; } $suggested_title = strtoupper($this->callbook->get_plaincall($title_input)); + $suggested_title = str_replace('0', 'Ø', $suggested_title); } // Pass prefill values to view $data['suggested_title'] = $suggested_title; @@ -121,6 +122,7 @@ class Notes extends CI_Controller { $category = $this->input->post('category', TRUE); if ($category === 'Contacts') { $suggested_title = strtoupper($this->callbook->get_plaincall($this->input->post('title', TRUE))); + $suggested_title = str_replace('0', 'Ø', $suggested_title); } $data['suggested_title'] = $suggested_title; $data['page_title'] = __("Edit Note"); @@ -282,15 +284,13 @@ class Notes extends CI_Controller { $core = strtoupper($this->callbook->get_plaincall($title)); // Only fail if prefix or suffix is present if (strtoupper($title) <> $core) { + $core = str_replace('0', 'Ø', $core); $this->form_validation->set_message('contacts_title_unique', sprintf(__("Contacts note title must be a callsign only, without prefix/suffix. Suggested: %s"), $core)); return FALSE; } - $existing = $this->db->get_where('notes', [ - 'cat' => 'Contacts', - 'user_id' => $user_id, - 'title' => $core - ])->num_rows(); - if ($existing > 0) { + // Check for existing note with the same title + $this->load->model('note'); + if ($this->note->get_note_id_by_category($user_id, 'Contacts', $core) > 0) { $this->form_validation->set_message('contacts_title_unique', __("A note with this callsign already exists in your Contacts. Please enter a unique callsign.")); return FALSE; } @@ -308,20 +308,17 @@ class Notes extends CI_Controller { $core = strtoupper($this->callbook->get_plaincall($title)); // Only fail if prefix or suffix is present if (strtoupper($title) <> $core) { + $core = str_replace('0', 'Ø', $core); $this->form_validation->set_message('contacts_title_unique_edit', sprintf(__("Contacts note title must be a callsign only, without prefix/suffix. Suggested: %s"),$core)); return FALSE; } - $query = $this->db->get_where('notes', [ - 'cat' => 'Contacts', - 'user_id' => $user_id, - 'title' => $core - ]); - foreach ($query->result() as $note) { - if ($note->id != $note_id) { - $this->form_validation->set_message('contacts_title_unique_edit', __("A note with this callsign already exists in your Contacts. Please enter a unique callsign.")); - return FALSE; - } - } + + // Check for existing note with the same title + $this->load->model('note'); + if ($this->note->get_note_id_by_category($user_id, 'Contacts', $core) > 0) { + $this->form_validation->set_message('contacts_title_unique_edit', __("A note with this callsign already exists in your Contacts. Please enter a unique callsign.")); + return FALSE; + } return TRUE; } return TRUE; diff --git a/application/models/Note.php b/application/models/Note.php index d82cf387d..b2b968060 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -16,6 +16,22 @@ class Note extends CI_Model { return array_keys(self::get_possible_categories()); } + // Return note ID for given category and title (callsign for Contacts), else false + public function get_note_id_by_category($user_id, $category, $title) { + $check_title = $title; + if ($category === 'Contacts') { + $this->load->library('callbook'); // Used for callsign parsing + $check_title = strtoupper($this->callbook->get_plaincall($title)); + $check_title_slashed = str_replace('0', 'Ø', $check_title); + } + $sql = "SELECT id FROM notes WHERE cat = ? AND user_id = ? AND (title = ? OR title = ?) LIMIT 1"; + $query = $this->db->query($sql, array($category, $user_id, $check_title, $check_title_slashed)); + if ($query->num_rows() > 0) { + return $query->row()->id; + } + return false; + } + // List all notes for a user or API key function list_all($api_key = null) { // Determine user ID @@ -37,11 +53,11 @@ class Note extends CI_Model { $user_id = $this->session->userdata('user_id'); $check_title = $title; if ($category === 'Contacts') { - $check_title = strtoupper($title); + $check_title = trim(strtoupper($title)); + $title = str_replace('0', 'Ø', $check_title); } - $sql = "SELECT COUNT(*) as count FROM notes WHERE cat = ? AND user_id = ? AND title = ?"; - $check_result = $this->db->query($sql, array($category, $user_id, $check_title)); - if ($check_result->row()->count > 0 && $category === 'Contacts') { + // Check for existing note with same title in Contacts category + if ($this->get_note_id_by_category($user_id, $category, $check_title) && $category === 'Contacts') { show_error(__("In Contacts category, the titles of the notes need to be unique.")); return; } @@ -60,16 +76,15 @@ class Note extends CI_Model { $user_id = $this->session->userdata('user_id'); $check_title = $title; if ($category === 'Contacts') { - $check_title = strtoupper($title); + $check_title = trim(strtoupper($title)); + $title = str_replace('0', 'Ø', $check_title); } - $check_sql = "SELECT id FROM notes WHERE cat = ? AND user_id = ? AND title = ?"; - $check_result = $this->db->query($check_sql, array($category, $user_id, $check_title)); - foreach ($check_result->result() as $note) { - if ($note->id != $note_id && $category === 'Contacts') { + // Check for existing note with same title in Contacts category + if ($this->get_note_id_by_category($user_id, $category, $check_title) && $category === 'Contacts') { show_error(__("In Contacts category, the titles of the notes need to be unique.")); - return; - } + return; } + $last_modified_utc = gmdate('Y-m-d H:i:s'); if ($local_time) { $dt = new DateTime($local_time, new DateTimeZone(date_default_timezone_get())); @@ -162,21 +177,6 @@ class Note extends CI_Model { return $result; } - // Return note ID for given category and title (callsign for Contacts), else false - public function get_note_id_by_category($user_id, $category, $title) { - $check_title = $title; - if ($category === 'Contacts') { - $this->load->library('callbook'); // Used for callsign parsing - $check_title = strtoupper($this->callbook->get_plaincall($title)); - } - $sql = "SELECT id FROM notes WHERE cat = ? AND user_id = ? AND title = ? LIMIT 1"; - $query = $this->db->query($sql, array($category, $user_id, $check_title)); - if ($query->num_rows() > 0) { - return $query->row()->id; - } - return false; - } - // Search notes with pagination and sorting for the logged-in user public function search_paginated($criteria = [], $page = 1, $per_page = 25, $sort_col = null, $sort_dir = null) { $user_id = $this->session->userdata('user_id'); diff --git a/assets/js/sections/notes.js b/assets/js/sections/notes.js index bfc5c5f22..c348ae189 100644 --- a/assets/js/sections/notes.js +++ b/assets/js/sections/notes.js @@ -92,6 +92,18 @@ if (typeof EasyMDE !== 'undefined') { // Main notes page functionality document.addEventListener('DOMContentLoaded', function() { + // Replace 0 with Ø in inputTitle as user types + var inputTitle = document.getElementById('inputTitle'); + if (inputTitle) { + inputTitle.addEventListener('input', function() { + var caret = inputTitle.selectionStart; + var newValue = inputTitle.value.replace(/0/g, 'Ø'); + if (inputTitle.value !== newValue) { + inputTitle.value = newValue; + inputTitle.setSelectionRange(caret, caret); + } + }); + } // Early exit if we're not on a notes page var notesTableBody = document.querySelector('#notesTable tbody'); var isNotesMainPage = notesTableBody !== null; From cdcea36108f9800630def238a865c0e75a54798e Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 8 Oct 2025 02:50:50 +0200 Subject: [PATCH 06/19] Notes: Fix - missing translation --- assets/js/sections/qso.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 1f2df3662..758585afb 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -2476,8 +2476,7 @@ $(document).ready(function () { var url = window.base_url + 'index.php/notes/add?title=' + encodeURIComponent(callsign) + '&category=Contacts'; window.open(url, '_blank'); } else { - // Unexpected response, show error - alert('Could not check note existence. Please try again.'); + // Unexpected response, do nothing } } ); From 55bf57a85162f9b4927afd31cfc1ea22dec114f2 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 8 Oct 2025 19:37:12 +0200 Subject: [PATCH 07/19] Notes: show/hide icons depending on user settings --- application/views/qso/index.php | 3 ++- application/views/view_log/qso.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 2915171e0..9dbaa8420 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -154,7 +154,8 @@ switch ($date_format) {
-  " class="fas fa-search">  +  " class="fas fa-search"> + session->userdata('user_show_notes')==1) { ?> 
diff --git a/application/views/view_log/qso.php b/application/views/view_log/qso.php index 9615b4f05..4780f53db 100644 --- a/application/views/view_log/qso.php +++ b/application/views/view_log/qso.php @@ -82,7 +82,7 @@ COL_CALL)); ?> Lookup <?php echo strtoupper($row->COL_CALL); ?> on QRZ.com Lookup <?php echo strtoupper($row->COL_CALL); ?> on HamQTH Lookup <?php echo strtoupper($row->COL_CALL); ?> on eQSL.cc Clublog Log Search - + session->userdata('user_show_notes')==1) { ?> " style="margin-left:2px;vertical-align:middle;"> From 314889e991e0b9651ed719e62f5d691cec4c28d8 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Wed, 8 Oct 2025 23:33:55 +0200 Subject: [PATCH 08/19] Notes: layout change --- application/views/interface_assets/footer.php | 5 ++ application/views/interface_assets/header.php | 2 +- application/views/qso/index.php | 31 ++++++++---- assets/js/sections/qso.js | 48 +++++++++++++++---- 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 35c073d42..73dcab6eb 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -302,6 +302,11 @@ function stopImpersonate_modal() { +uri->segment(1) == "qso" ) { ?> + + + + uri->segment(1) == "notes" && ($this->uri->segment(2) == "view") ) { ?> diff --git a/application/views/interface_assets/header.php b/application/views/interface_assets/header.php index e91b8ef9d..ae88c82ec 100644 --- a/application/views/interface_assets/header.php +++ b/application/views/interface_assets/header.php @@ -38,7 +38,7 @@ - uri->segment(1) == "notes" && ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit" || $this->uri->segment(2) == "view")) { ?> + uri->segment(1) == "notes" && ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit" || $this->uri->segment(2) == "view")) || $this->uri->segment(1) == "qso") { ?> diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 9dbaa8420..3af9573c1 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -68,7 +68,7 @@ switch ($date_format) {
- +
+ + + session->userdata('user_show_notes')==1) { ?> +
+
+

+ + "> + + +

+
+
+ +
+
+ +
- -
diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 758585afb..7ac238dfa 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -74,21 +74,29 @@ function getUTCDateStamp(el) { } // Note icon state logic -function setNoteIconState(state) { +function setNotesVisibility(state) { var $icon = $('#note_create_edit'); $icon.removeClass('text-secondary text-info'); $icon.removeAttr('data-bs-original-title'); $icon.removeAttr('title'); if (state == 2) { + // Callsign with existing note + $icon.show(); $icon.addClass('text-info'); $icon.attr('data-bs-original-title', lang_qso_note_edit); + $('#callsign-notes').show(); } else if (state == 1) { + // Callsign, no note yet + $icon.show(); $icon.attr('data-bs-original-title', lang_qso_note_add); - // do nothing - white icon + $('#callsign-notes').show(); } else { - $icon.addClass('text-secondary'); + // No callsign - hide icon + $icon.hide(); $icon.attr('data-bs-original-title', lang_qso_note_no_callsign); + $('#callsign-notes').hide(); } + // If Bootstrap tooltip is initialized, update it if ($icon.data('bs.tooltip')) { $icon.tooltip('dispose').tooltip(); @@ -912,7 +920,7 @@ function reset_fields() { clearTimeout(); set_timers(); resetTimers(qso_manual); - setNoteIconState(0); // Always gray out note icon on reset + setNotesVisibility(0); // Always gray out note icon on reset } // Set note icon state: 0 = gray, 1 = empty, 2 = filled based on callsign @@ -928,9 +936,9 @@ function get_note_icon(callsign){ try { data = JSON.parse(data); } catch (e) { data = {}; } } if (data && data.exists === true) { - setNoteIconState(2); + setNotesVisibility(2); } else { - setNoteIconState(1); + setNotesVisibility(1); } } ); @@ -2159,7 +2167,7 @@ function resetDefaultQSOFields() { $('.awardpane').remove(); $('#timesWorked').html(lang_qso_title_previous_contacts); - setNoteIconState(0); // Always gray out note icon on reset + setNotesVisibility(0); // Always gray out note icon on reset } function closeModal() { @@ -2222,7 +2230,7 @@ $(document).ready(function () { set_timers(); updateStateDropdown('#dxcc_id', '#stateInputLabel', '#location_us_county', '#stationCntyInputQso'); - setNoteIconState(0); /// Grey-out note icon + setNotesVisibility(0); /// Grey-out note icon // Clear the localStorage for the qrg units, except the quicklogCallsign and a possible backlog clearQrgUnits(); @@ -2482,6 +2490,26 @@ $(document).ready(function () { ); }); + if (document.getElementById('callsign_note_content')) { + if (typeof EasyMDE !== 'undefined') { + new EasyMDE({ + element: document.getElementById('callsign_note_content'), + spellChecker: false, + toolbar: [ + "bold", "italic", "heading", "|","preview", "|", + "quote", "unordered-list", "ordered-list", "|", + "link", "image", "|", + "guide" + ], + forceSync: true, + status: false, + maxHeight: '250px', + autoDownloadFontAwesome: false, + }); + } + } + // everything loaded and ready 2 go - bc.postMessage('ready'); -}); + bc.postMessage('ready'); + + }); From 877ee7b31d1b74ae6fae5be79a548651cc8e460b Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 01:40:06 +0200 Subject: [PATCH 09/19] Notes: Fixes, layout changes and card logic --- application/controllers/Notes.php | 29 ++++++- application/models/Note.php | 3 +- application/views/qso/index.php | 4 +- assets/js/sections/qso.js | 140 +++++++++++++++++++++--------- 4 files changed, 132 insertions(+), 44 deletions(-) diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index de681e717..6e5dcd8be 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -275,6 +275,30 @@ class Notes extends CI_Controller { $this->output->set_content_type('application/json')->set_output(json_encode($response)); } + // API endpoint to get note details by ID + public function get($id = null) { + $this->load->model('note'); + $clean_id = $this->security->xss_clean($id); + if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $this->session->userdata('user_id'))) { + $this->output->set_content_type('application/json')->set_output(json_encode(['error' => _("Not found or not allowed")])); + return; + } + $query = $this->note->view($clean_id); + if ($query && $query->num_rows() > 0) { + $row = $query->row(); + $response = [ + 'id' => $row->id, + 'category' => $row->cat, + 'user_id' => $row->user_id, + 'title' => $row->title, + 'content' => $row->note + ]; + $this->output->set_content_type('application/json')->set_output(json_encode($response)); + } else { + $this->output->set_content_type('application/json')->set_output(json_encode(['error' => _("Not found")])); + } + } + // Form validation callback for add: unique Contacts note title for user, only core callsign public function contacts_title_unique($title = null) { $category = $this->input->post('category', TRUE); @@ -315,7 +339,8 @@ class Notes extends CI_Controller { // Check for existing note with the same title $this->load->model('note'); - if ($this->note->get_note_id_by_category($user_id, 'Contacts', $core) > 0) { + $existing_id = $this->note->get_note_id_by_category($user_id, 'Contacts', $core); + if ($existing_id > 0 && $existing_id != $note_id) { $this->form_validation->set_message('contacts_title_unique_edit', __("A note with this callsign already exists in your Contacts. Please enter a unique callsign.")); return FALSE; } @@ -324,4 +349,6 @@ class Notes extends CI_Controller { return TRUE; } + + } diff --git a/application/models/Note.php b/application/models/Note.php index b2b968060..f5f247ea0 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -80,7 +80,8 @@ class Note extends CI_Model { $title = str_replace('0', 'Ø', $check_title); } // Check for existing note with same title in Contacts category - if ($this->get_note_id_by_category($user_id, $category, $check_title) && $category === 'Contacts') { + $existing_id = $this->get_note_id_by_category($user_id, $category, $check_title); + if ($existing_id > 0 && $existing_id != $note_id && $category === 'Contacts') { show_error(__("In Contacts category, the titles of the notes need to be unique.")); return; } diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 3af9573c1..9b9d3a9dc 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -720,7 +720,9 @@ switch ($date_format) {
- + + +
diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 7ac238dfa..33f6bdcb4 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -73,30 +73,92 @@ function getUTCDateStamp(el) { $(el).attr('value', formatted_date); } + + var saveBtn = document.getElementById('callsign-note-save-btn'); + function setNoteEditorState(state, noteText, saveBtn) { + if (!noteEditor) return; + if (state === 'no_callsign') { + + } else if (state === 'no_note') { + + } else if (state === 'has_note') { + + } + } + + // Note icon state logic -function setNotesVisibility(state) { +function setNotesVisibility(state, noteText = "") { var $icon = $('#note_create_edit'); $icon.removeClass('text-secondary text-info'); - $icon.removeAttr('data-bs-original-title'); - $icon.removeAttr('title'); - if (state == 2) { - // Callsign with existing note - $icon.show(); - $icon.addClass('text-info'); - $icon.attr('data-bs-original-title', lang_qso_note_edit); - $('#callsign-notes').show(); - } else if (state == 1) { - // Callsign, no note yet - $icon.show(); - $icon.attr('data-bs-original-title', lang_qso_note_add); - $('#callsign-notes').show(); - } else { - // No callsign - hide icon - $icon.hide(); - $icon.attr('data-bs-original-title', lang_qso_note_no_callsign); - $('#callsign-notes').hide(); + $icon.removeAttr('data-bs-original-title title'); + var $noteCard = $('#callsign-notes'); + var $saveBtn = $('#callsign-note-save-btn'); + var $editorElem = $('#callsign_note_content'); + var noteEditor = $editorElem.data('easymde'); + var $editBtn = $('#callsign-note-edit-btn'); + + // Initialize EasyMDE if not already done + if (!noteEditor && typeof EasyMDE !== 'undefined') { + noteEditor = new EasyMDE({ + element: $editorElem[0], + spellChecker: false, + toolbar: [ + "bold", "italic", "heading", "|","preview", "|", + "quote", "unordered-list", "ordered-list", "|", + "link", "image", "|", + "guide" + ], + forceSync: true, + status: false, + maxHeight: '250px', + autoDownloadFontAwesome: false, + autoRefresh: { delay: 250 }, + }); + $editorElem.data('easymde', noteEditor); } + if (state === 0) { + // No callsign + $icon.hide().attr('data-bs-original-title', lang_qso_note_no_callsign); + + // Hide note card + $noteCard.hide(); + + } else if (state === 1) { + // Callsign, no note yet + $icon.show().attr('data-bs-original-title', lang_qso_note_add); + + // Show note card + $noteCard.show(); + + // Hide editor toolbar, set value and show preview + document.querySelector('.EasyMDEContainer .editor-toolbar').style.display = 'none'; + noteEditor.value('No notes found for this callsign - click Edit Note to add notes.'); + noteEditor.togglePreview(); + noteEditor.codemirror.setOption('readOnly', true); + + } else if (state === 2) { + // Callsign with existing note + $icon.show().addClass('text-info').attr('data-bs-original-title', lang_qso_note_edit); + + // Show note card + $noteCard.show(); + + // Hide editor toolbar, set value and show preview + document.querySelector('.EasyMDEContainer .editor-toolbar').style.display = 'none'; + noteEditor.value(noteText); + noteEditor.togglePreview(); + noteEditor.codemirror.setOption('readOnly', true); + } + + // Show Edit button for states 1 and 2 + if (state === 1 || state === 2) { + $editBtn.removeClass('d-none').show(); + } else { + $editBtn.addClass('d-none').hide(); + } + // If Bootstrap tooltip is initialized, update it if ($icon.data('bs.tooltip')) { $icon.tooltip('dispose').tooltip(); @@ -923,7 +985,7 @@ function reset_fields() { setNotesVisibility(0); // Always gray out note icon on reset } -// Set note icon state: 0 = gray, 1 = empty, 2 = filled based on callsign +// Get status of notes for this callsign function get_note_icon(callsign){ $.get( window.base_url + 'index.php/notes/check_duplicate', @@ -935,8 +997,23 @@ function get_note_icon(callsign){ if (typeof data === 'string') { try { data = JSON.parse(data); } catch (e) { data = {}; } } - if (data && data.exists === true) { - setNotesVisibility(2); + if (data && data.exists === true && data.id) { + // Get the note content using the note ID + $.get( + window.base_url + 'index.php/notes/get/' + data.id, + function(noteData) { + if (typeof noteData === 'string') { + try { noteData = JSON.parse(noteData); } catch (e) { noteData = {}; } + } + if (noteData && noteData.content) { + setNotesVisibility(2, noteData.content); + } else { + setNotesVisibility(2,'Error'); + } + } + ).fail(function() { + setNotesVisibility(2,'Error'); + }); } else { setNotesVisibility(1); } @@ -2490,25 +2567,6 @@ $(document).ready(function () { ); }); - if (document.getElementById('callsign_note_content')) { - if (typeof EasyMDE !== 'undefined') { - new EasyMDE({ - element: document.getElementById('callsign_note_content'), - spellChecker: false, - toolbar: [ - "bold", "italic", "heading", "|","preview", "|", - "quote", "unordered-list", "ordered-list", "|", - "link", "image", "|", - "guide" - ], - forceSync: true, - status: false, - maxHeight: '250px', - autoDownloadFontAwesome: false, - }); - } - } - // everything loaded and ready 2 go bc.postMessage('ready'); From fb37d5394199319e96fbeee793cc1e7b0d401fb3 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 02:26:23 +0200 Subject: [PATCH 10/19] Notes: initial implementaion at the QSO page and Toasts proposals --- application/controllers/Notes.php | 64 +++++++++++++ application/models/Note.php | 6 ++ application/views/qso/index.php | 4 + assets/js/sections/qso.js | 150 +++++++++++++++++++++++++++++- 4 files changed, 223 insertions(+), 1 deletion(-) diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index 6e5dcd8be..36e9cd64e 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -299,6 +299,70 @@ class Notes extends CI_Controller { } } + // API endpoint to save note (create new or update existing or delete, based on presence of ID and content) + public function save($id = null) { + $this->load->model('note'); + $this->load->library('callbook'); + + $user_id = $this->session->userdata('user_id'); + $category = $this->input->post('category', TRUE); + $title = $this->input->post('title', TRUE); + $content = $this->input->post('content', TRUE); + + // Validate required fields + if (empty($category) || empty($title)) { + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'Category and title are required'])); + return; + } + + // Clean title for Contacts category + if ($category === 'Contacts') { + $title = strtoupper($this->callbook->get_plaincall($title)); + $title = str_replace('0', 'Ø', $title); + } + + if ($id !== null) { + // Edit existing note + $clean_id = $this->security->xss_clean($id); + if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $user_id)) { + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'Note not found or not allowed'])); + return; + } + + // If content is empty, delete the note + if (empty(trim($content))) { + $this->note->delete($clean_id); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => 'Note deleted', 'deleted' => true])); + } else { + // Update the note + $this->note->edit($clean_id, $category, $title, $content); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => 'Note updated', 'id' => $clean_id])); + } + } else { + // Create new note + if (empty(trim($content))) { + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'Cannot create empty note'])); + return; + } + + // Check for duplicate in Contacts category + if ($category === 'Contacts') { + $existing_id = $this->note->get_note_id_by_category($user_id, $category, $title); + if ($existing_id) { + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'A note with this callsign already exists'])); + return; + } + } + + // Create the note + $this->note->add($category, $title, $content); + + // Get the new note ID + $new_id = $this->note->get_note_id_by_category($user_id, $category, $title); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => 'Note created', 'id' => $new_id])); + } + } + // Form validation callback for add: unique Contacts note title for user, only core callsign public function contacts_title_unique($title = null) { $category = $this->input->post('category', TRUE); diff --git a/application/models/Note.php b/application/models/Note.php index f5f247ea0..044a93aa1 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -75,6 +75,12 @@ class Note extends CI_Model { function edit($note_id, $category, $title, $content, $local_time = null) { $user_id = $this->session->userdata('user_id'); $check_title = $title; + + if($this->belongs_to_user($note_id, $user_id) === false) { + show_404(); + return; + } + if ($category === 'Contacts') { $check_title = trim(strtoupper($title)); $title = str_replace('0', 'Ø', $check_title); diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 9b9d3a9dc..68ba3ce24 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -721,6 +721,7 @@ switch ($date_format) {
+
@@ -830,3 +831,6 @@ switch ($date_format) {
+ + +
diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 33f6bdcb4..33b65fe50 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -20,6 +20,45 @@ function resetTimers(qso_manual) { } } +// Show Bootstrap Toast - TBD move to general JS file +function showToast(title, text, type = 'bg-success text-white', delay = 3000) { + /* + Examples: + showToast('Saved', 'Your data was saved!', 'bg-success text-white', 3000); + showToast('Error', 'Failed to connect to server.', 'bg-danger text-white', 5000); + showToast('Warning', 'Please check your input.', 'bg-warning text-dark', 4000); + showToast('Info', 'System will restart soon.', 'bg-info text-dark', 4000); + */ + + const container = document.getElementById('toast-container'); + + // Create toast element + const toastEl = document.createElement('div'); + toastEl.className = `toast align-items-center ${type}`; + toastEl.setAttribute('role', 'alert'); + toastEl.setAttribute('aria-live', 'assertive'); + toastEl.setAttribute('aria-atomic', 'true'); + toastEl.setAttribute('data-bs-delay', delay); + + // Toast inner HTML + toastEl.innerHTML = ` +
+
+ ${title}
${text} +
+ +
+ `; + + // Append and show + container.appendChild(toastEl); + const bsToast = new bootstrap.Toast(toastEl); + bsToast.show(); + + // Remove from DOM when hidden + toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove()); +} + function getUTCTimeStamp(el) { var now = new Date(); $(el).attr('value', ("0" + now.getUTCHours()).slice(-2) + ':' + ("0" + now.getUTCMinutes()).slice(-2) + ':' + ("0" + now.getUTCSeconds()).slice(-2)); @@ -111,7 +150,7 @@ function setNotesVisibility(state, noteText = "") { ], forceSync: true, status: false, - maxHeight: '250px', + maxHeight: '150px', autoDownloadFontAwesome: false, autoRefresh: { delay: 250 }, }); @@ -152,6 +191,10 @@ function setNotesVisibility(state, noteText = "") { noteEditor.codemirror.setOption('readOnly', true); } + // Hide buttons per default here + $saveBtn.addClass('d-none').hide(); + $editBtn.addClass('d-none').hide(); + // Show Edit button for states 1 and 2 if (state === 1 || state === 2) { $editBtn.removeClass('d-none').show(); @@ -1006,6 +1049,7 @@ function get_note_icon(callsign){ try { noteData = JSON.parse(noteData); } catch (e) { noteData = {}; } } if (noteData && noteData.content) { + $('#callsign-note-id').val(data.id); setNotesVisibility(2, noteData.content); } else { setNotesVisibility(2,'Error'); @@ -2567,6 +2611,110 @@ $(document).ready(function () { ); }); + // Edit button click handler for inline editing + $(document).on('click', '#callsign-note-edit-btn', function() { + var $editorElem = $('#callsign_note_content'); + var noteEditor = $editorElem.data('easymde'); + var $saveBtn = $('#callsign-note-save-btn'); + var $editBtn = $('#callsign-note-edit-btn'); + var noteId = $('#callsign-note-id').val(); + + if (noteEditor) { + // Switch to edit mode + noteEditor.codemirror.setOption('readOnly', false); + if (noteEditor.isPreviewActive()) { + noteEditor.togglePreview(); // Exit preview mode + } + + // If no note exists (state 1), set dynamic timestamp content + if (!noteId || noteId === '') { + var timestamp = new Date().toLocaleString(); + noteEditor.value('#' + timestamp + '\n'); + } + + // Show toolbar and buttons + document.querySelector('.EasyMDEContainer .editor-toolbar').style.display = ''; + $saveBtn.removeClass('d-none').show(); + $editBtn.addClass('d-none').hide(); + } + }); + + // Save button click handler for saving notes + $(document).on('click', '#callsign-note-save-btn', function() { + var $editorElem = $('#callsign_note_content'); + var noteEditor = $editorElem.data('easymde'); + var noteId = $('#callsign-note-id').val(); + var callsign = $('#callsign').val().trim(); + var noteContent = noteEditor ? noteEditor.value() : ''; + + if (!callsign || callsign.length < 3) { + return; + } + + var isEdit = noteId && noteId !== ''; + var url = isEdit ? + window.base_url + 'index.php/notes/save/' + noteId : + window.base_url + 'index.php/notes/save'; + + var postData = { + category: 'Contacts', + title: callsign, + content: noteContent + }; + + if (isEdit) { + postData.id = noteId; + } + + $.post(url, postData) + .done(function(response) { + if (typeof response === 'string') { + try { response = JSON.parse(response); } catch (e) { response = {}; } + } + + if (response.success || response.status === 'ok') { + // Check if note was deleted (empty content) + if (response.deleted) { + // Clear the note ID since note was deleted + $('#callsign-note-id').val(''); + // Reset to state 1 (callsign, no note) + setNotesVisibility(1); + // Show success message + showToast("Note deleted", "Note deleted successfully"); + } else { + // Success - switch back to preview mode + if (noteEditor) { + noteEditor.codemirror.setOption('readOnly', true); + if (!noteEditor.isPreviewActive()) { + noteEditor.togglePreview(); // Switch to preview mode + } + document.querySelector('.EasyMDEContainer .editor-toolbar').style.display = 'none'; + } + $('#callsign-note-save-btn').addClass('d-none').hide(); + $('#callsign-note-edit-btn').removeClass('d-none').show(); + + // If it was a new note, store the returned ID + if (!isEdit && response.id) { + $('#callsign-note-id').val(response.id); + + // Show success message briefly + showToast("Note created", "Note created successfully"); + } else { + // Show success message briefly + showToast("Note saved", "Note saved successfully"); + } + + + } + } else { + alert('Error saving note: ' + (response.message || 'Unknown error')); + } + }) + .fail(function() { + alert('Failed to save note. Please try again.'); + }); + }); + // everything loaded and ready 2 go bc.postMessage('ready'); From 34f42cc419181d53fbc707b06ea6327cd4c25dce Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 19:00:29 +0200 Subject: [PATCH 11/19] Notes: fix for note counter in the category buttons --- assets/js/sections/notes.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/js/sections/notes.js b/assets/js/sections/notes.js index c348ae189..5bd91ca10 100644 --- a/assets/js/sections/notes.js +++ b/assets/js/sections/notes.js @@ -176,12 +176,18 @@ document.addEventListener('DOMContentLoaded', function() { function reloadCategoryCounters() { fetch(base_url + 'index.php/notes/get_category_counts', { method: 'POST' }) .then(response => response.json()) - .then(counts => { + .then(data => { domCache.categoryButtons.forEach(function(btn) { var cat = btn.getAttribute('data-category'); var countSpan = btn.querySelector('.badge'); - if (countSpan && counts[cat] !== undefined) { - countSpan.textContent = counts[cat]; + if (countSpan) { + if (cat === '__all__') { + // Handle "All Categories" button + countSpan.textContent = data.all_notes_count; + } else if (data.category_counts && data.category_counts[cat] !== undefined) { + // Handle specific category buttons + countSpan.textContent = data.category_counts[cat]; + } } }); }); From 015021fd699d3d00506b733db13ca9da9d851457 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 19:14:54 +0200 Subject: [PATCH 12/19] Notes: hidden Notes icon at QSO callsign text entry --- application/views/qso/index.php | 4 +-- assets/js/sections/qso.js | 62 ++++----------------------------- 2 files changed, 9 insertions(+), 57 deletions(-) diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 68ba3ce24..f6d43d8a2 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -154,7 +154,7 @@ switch ($date_format) {
-  " class="fas fa-search">  +  " class="fas fa-search">
@@ -714,7 +714,7 @@ switch ($date_format) {

- "> + ">

diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 33b65fe50..6dd3ef1e3 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -126,11 +126,8 @@ function getUTCDateStamp(el) { } -// Note icon state logic +// Note card state logic including EasyMDE initialization and handling function setNotesVisibility(state, noteText = "") { - var $icon = $('#note_create_edit'); - $icon.removeClass('text-secondary text-info'); - $icon.removeAttr('data-bs-original-title title'); var $noteCard = $('#callsign-notes'); var $saveBtn = $('#callsign-note-save-btn'); var $editorElem = $('#callsign_note_content'); @@ -158,17 +155,11 @@ function setNotesVisibility(state, noteText = "") { } if (state === 0) { - // No callsign - $icon.hide().attr('data-bs-original-title', lang_qso_note_no_callsign); - - // Hide note card + // No callsign - Hide note card $noteCard.hide(); } else if (state === 1) { - // Callsign, no note yet - $icon.show().attr('data-bs-original-title', lang_qso_note_add); - - // Show note card + // Callsign, no note yet - show note card with message $noteCard.show(); // Hide editor toolbar, set value and show preview @@ -178,10 +169,7 @@ function setNotesVisibility(state, noteText = "") { noteEditor.codemirror.setOption('readOnly', true); } else if (state === 2) { - // Callsign with existing note - $icon.show().addClass('text-info').attr('data-bs-original-title', lang_qso_note_edit); - - // Show note card + // Callsign with existing notes - show note card with notes $noteCard.show(); // Hide editor toolbar, set value and show preview @@ -201,11 +189,6 @@ function setNotesVisibility(state, noteText = "") { } else { $editBtn.addClass('d-none').hide(); } - - // If Bootstrap tooltip is initialized, update it - if ($icon.data('bs.tooltip')) { - $icon.tooltip('dispose').tooltip(); - } } $('#stationProfile').on('change', function () { @@ -1025,7 +1008,7 @@ function reset_fields() { clearTimeout(); set_timers(); resetTimers(qso_manual); - setNotesVisibility(0); // Always gray out note icon on reset + setNotesVisibility(0); // Set note card to hidden } // Get status of notes for this callsign @@ -2288,7 +2271,7 @@ function resetDefaultQSOFields() { $('.awardpane').remove(); $('#timesWorked').html(lang_qso_title_previous_contacts); - setNotesVisibility(0); // Always gray out note icon on reset + setNotesVisibility(0); // Set default note card visibility to 0 (hidden) } function closeModal() { @@ -2351,7 +2334,7 @@ $(document).ready(function () { set_timers(); updateStateDropdown('#dxcc_id', '#stateInputLabel', '#location_us_county', '#stationCntyInputQso'); - setNotesVisibility(0); /// Grey-out note icon + setNotesVisibility(0); // Set default note card visibility to 0 (hidden) // Clear the localStorage for the qrg units, except the quicklogCallsign and a possible backlog clearQrgUnits(); @@ -2580,37 +2563,6 @@ $(document).ready(function () { }); } - // Note create/edit icon click handler - $('#note_create_edit').on('click', function() { - var callsign = $('#callsign').val().trim(); - if (!callsign || callsign.length < 3) { - return; - } - // AJAX to check if note exists for this callsign in Contacts category - $.get( - window.base_url + 'index.php/notes/check_duplicate', - { - category: 'Contacts', - title: callsign - }, - function(data) { - // Defensive: try to parse if string - if (typeof data === 'string') { - try { data = JSON.parse(data); } catch (e) { data = {}; } - } - if (data && data.exists === true && data.id) { - window.open(window.base_url + 'index.php/notes/edit/' + data.id, '_blank'); - } else if (data && data.exists === false) { - // Open add with prefilled title and Contacts category - var url = window.base_url + 'index.php/notes/add?title=' + encodeURIComponent(callsign) + '&category=Contacts'; - window.open(url, '_blank'); - } else { - // Unexpected response, do nothing - } - } - ); - }); - // Edit button click handler for inline editing $(document).on('click', '#callsign-note-edit-btn', function() { var $editorElem = $('#callsign_note_content'); From d68ba1ac6f57dcc0d66575d4853eb2ad7d77741a Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 19:18:33 +0200 Subject: [PATCH 13/19] Notes: Fix - default text not being displayed on first edit --- assets/js/sections/qso.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 6dd3ef1e3..fbdef79cc 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -2582,6 +2582,7 @@ $(document).ready(function () { if (!noteId || noteId === '') { var timestamp = new Date().toLocaleString(); noteEditor.value('#' + timestamp + '\n'); + noteEditor.codemirror.refresh(); } // Show toolbar and buttons From 5a0ec5b74278c429017c3c17487a075f6950b301 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 19:21:33 +0200 Subject: [PATCH 14/19] Notes: Fix - missing icons --- application/views/qso/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/views/qso/index.php b/application/views/qso/index.php index f6d43d8a2..5944057d7 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -722,8 +722,8 @@ switch ($date_format) {
- - + +
From 49f0cef30d4c88604fe33324dd906187152fedd8 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 20:13:23 +0200 Subject: [PATCH 15/19] Notes: various fixes and translation --- application/controllers/Notes.php | 14 +++--- application/views/interface_assets/footer.php | 10 +++-- application/views/qso/index.php | 20 +++++---- assets/js/sections/qso.js | 44 ++++++++----------- 4 files changed, 42 insertions(+), 46 deletions(-) diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index 36e9cd64e..f7b95a85d 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -311,7 +311,7 @@ class Notes extends CI_Controller { // Validate required fields if (empty($category) || empty($title)) { - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'Category and title are required'])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => __("Category and title are required")])); return; } @@ -325,23 +325,23 @@ class Notes extends CI_Controller { // Edit existing note $clean_id = $this->security->xss_clean($id); if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $user_id)) { - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'Note not found or not allowed'])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => __("Note not found or not allowed")])); return; } // If content is empty, delete the note if (empty(trim($content))) { $this->note->delete($clean_id); - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => 'Note deleted', 'deleted' => true])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => __("Note deleted"), 'deleted' => true])); } else { // Update the note $this->note->edit($clean_id, $category, $title, $content); - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => 'Note updated', 'id' => $clean_id])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => __("Note updated"), 'id' => $clean_id])); } } else { // Create new note if (empty(trim($content))) { - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'Cannot create empty note'])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => __("Cannot create empty note")])); return; } @@ -349,7 +349,7 @@ class Notes extends CI_Controller { if ($category === 'Contacts') { $existing_id = $this->note->get_note_id_by_category($user_id, $category, $title); if ($existing_id) { - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => 'A note with this callsign already exists'])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => false, 'message' => __("A note with this callsign already exists")])); return; } } @@ -359,7 +359,7 @@ class Notes extends CI_Controller { // Get the new note ID $new_id = $this->note->get_note_id_by_category($user_id, $category, $title); - $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => 'Note created', 'id' => $new_id])); + $this->output->set_content_type('application/json')->set_output(json_encode(['success' => true, 'message' => __("Note created"), 'id' => $new_id])); } } diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 73dcab6eb..cb4a59326 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -68,10 +68,12 @@ var lang_notes_duplicate_confirmation = ""; var lang_notes_duplication_disabled_short = ""; var lang_notes_not_found = ""; - var lang_qso_note_add = ""; - var lang_qso_note_edit = ""; - var lang_qso_note_no_callsign = ""; - + var lang_qso_note_missing = ""; + var lang_qso_note_toast_title = ""; + var lang_qso_note_deleted = ""; + var lang_qso_note_created = ""; + var lang_qso_note_saved = ""; + var lang_qso_note_error_saving = ""; diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 5944057d7..ec37da3fd 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -637,7 +637,7 @@ switch ($date_format) { - session->userdata('user_show_notes')==1) { ?> + +

@@ -719,14 +722,13 @@ switch ($date_format) {

-
- - - - -
+
+ + + +
- +
diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index fbdef79cc..79d1eda9a 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -112,28 +112,20 @@ function getUTCDateStamp(el) { $(el).attr('value', formatted_date); } - - var saveBtn = document.getElementById('callsign-note-save-btn'); - function setNoteEditorState(state, noteText, saveBtn) { - if (!noteEditor) return; - if (state === 'no_callsign') { - - } else if (state === 'no_note') { - - } else if (state === 'has_note') { - - } - } - - // Note card state logic including EasyMDE initialization and handling -function setNotesVisibility(state, noteText = "") { +function setNotesVisibility(state, noteText = "",show_notes = user_show_notes) { var $noteCard = $('#callsign-notes'); var $saveBtn = $('#callsign-note-save-btn'); var $editorElem = $('#callsign_note_content'); var noteEditor = $editorElem.data('easymde'); var $editBtn = $('#callsign-note-edit-btn'); + // Do nothing if user preference is to hide notes + if (!show_notes) { + $noteCard.hide(); + return; + } + // Initialize EasyMDE if not already done if (!noteEditor && typeof EasyMDE !== 'undefined') { noteEditor = new EasyMDE({ @@ -164,7 +156,7 @@ function setNotesVisibility(state, noteText = "") { // Hide editor toolbar, set value and show preview document.querySelector('.EasyMDEContainer .editor-toolbar').style.display = 'none'; - noteEditor.value('No notes found for this callsign - click Edit Note to add notes.'); + noteEditor.value(lang_qso_note_missing); noteEditor.togglePreview(); noteEditor.codemirror.setOption('readOnly', true); @@ -1012,7 +1004,7 @@ function reset_fields() { } // Get status of notes for this callsign -function get_note_icon(callsign){ +function get_note_status(callsign){ $.get( window.base_url + 'index.php/notes/check_duplicate', { @@ -1035,11 +1027,11 @@ function get_note_icon(callsign){ $('#callsign-note-id').val(data.id); setNotesVisibility(2, noteData.content); } else { - setNotesVisibility(2,'Error'); + setNotesVisibility(2, lang_general_word_error); } } ).fail(function() { - setNotesVisibility(2,'Error'); + setNotesVisibility(2, lang_general_word_error); }); } else { setNotesVisibility(1); @@ -1087,7 +1079,7 @@ $("#callsign").on("focusout", function () { resetDefaultQSOFields(); // Set qso icon - get_note_icon(result.callsign); + get_note_status(result.callsign); if (result.dxcc.entity != undefined) { $('#country').val(convert_case(result.dxcc.entity)); @@ -2633,7 +2625,7 @@ $(document).ready(function () { // Reset to state 1 (callsign, no note) setNotesVisibility(1); // Show success message - showToast("Note deleted", "Note deleted successfully"); + showToast(lang_qso_note_toast_title, lang_qso_note_deleted); } else { // Success - switch back to preview mode if (noteEditor) { @@ -2651,24 +2643,24 @@ $(document).ready(function () { $('#callsign-note-id').val(response.id); // Show success message briefly - showToast("Note created", "Note created successfully"); + showToast(lang_qso_note_toast_title, lang_qso_note_created); } else { // Show success message briefly - showToast("Note saved", "Note saved successfully"); + showToast(lang_qso_note_toast_title, lang_qso_note_saved); } } } else { - alert('Error saving note: ' + (response.message || 'Unknown error')); + alert(lang_qso_note_error_saving + ': ' + (response.message || lang_general_word_error)); } }) .fail(function() { - alert('Failed to save note. Please try again.'); + alert(lang_qso_note_error_saving); }); }); // everything loaded and ready 2 go - bc.postMessage('ready'); + bc.postMessage('ready'); }); From e237f98b739a979008b9bee8b2a57792ab7f71ff Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 20:16:36 +0200 Subject: [PATCH 16/19] Fix: another translation fix --- application/controllers/Notes.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index f7b95a85d..546d7bbe4 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -215,7 +215,7 @@ class Notes extends CI_Controller { 'title' => $check_title ])->num_rows(); if ($existing > 0) { - $this->output->set_content_type('application/json')->set_output(json_encode(['status' => 'error', 'message' => 'Duplicate note title for this category and user - not allowed for Contacts category.'])); + $this->output->set_content_type('application/json')->set_output(json_encode(['status' => 'error', 'message' => __("Duplicate note title for this category and user - not allowed for Contacts category.")])); return; } // Duplicate note with new title @@ -280,7 +280,7 @@ class Notes extends CI_Controller { $this->load->model('note'); $clean_id = $this->security->xss_clean($id); if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $this->session->userdata('user_id'))) { - $this->output->set_content_type('application/json')->set_output(json_encode(['error' => _("Not found or not allowed")])); + $this->output->set_content_type('application/json')->set_output(json_encode(['error' => __("Not found or not allowed")])); return; } $query = $this->note->view($clean_id); @@ -295,7 +295,7 @@ class Notes extends CI_Controller { ]; $this->output->set_content_type('application/json')->set_output(json_encode($response)); } else { - $this->output->set_content_type('application/json')->set_output(json_encode(['error' => _("Not found")])); + $this->output->set_content_type('application/json')->set_output(json_encode(['error' => __("Not found")])); } } From db1964229403a8f8f3a2d009b27f0c801b998b92 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 20:26:23 +0200 Subject: [PATCH 17/19] Notes: added stroked zero button --- application/views/notes/main.php | 5 ++++- assets/js/sections/notes.js | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/application/views/notes/main.php b/application/views/notes/main.php index a4ba462c8..2b23abfba 100644 --- a/application/views/notes/main.php +++ b/application/views/notes/main.php @@ -24,7 +24,7 @@ - $value) { @@ -51,6 +51,9 @@
"> + diff --git a/assets/js/sections/notes.js b/assets/js/sections/notes.js index 5bd91ca10..d86d9b282 100644 --- a/assets/js/sections/notes.js +++ b/assets/js/sections/notes.js @@ -575,4 +575,29 @@ document.addEventListener('DOMContentLoaded', function() { if (domCache.notesTableBody) { performNotesSearch(); } + + // Add stroked zero (Ø) to search box + var addStrokedZeroBtn = document.getElementById('notesAddStrokedZero'); + if (addStrokedZeroBtn) { + addStrokedZeroBtn.addEventListener('click', function() { + var searchBox = domCache.searchBox; + if (searchBox) { + var currentValue = searchBox.value; + var cursorPos = searchBox.selectionStart; + + // Insert Ø at cursor position + var newValue = currentValue.slice(0, cursorPos) + 'Ø' + currentValue.slice(cursorPos); + searchBox.value = newValue; + + // Set cursor position after the inserted character + searchBox.focus(); + searchBox.setSelectionRange(cursorPos + 1, cursorPos + 1); + + // Trigger search if minimum length is met + if (newValue.length >= SEARCH_MIN_LENGTH) { + performNotesSearch(); + } + } + }); + } }); From a341cf0f29785a23d02aa4a1ec8e7b2e42dd472f Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Thu, 9 Oct 2025 20:32:49 +0200 Subject: [PATCH 18/19] Notes: Fix after changing Note state to lower value --- assets/js/sections/qso.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/js/sections/qso.js b/assets/js/sections/qso.js index 79d1eda9a..a3603f7a9 100644 --- a/assets/js/sections/qso.js +++ b/assets/js/sections/qso.js @@ -1027,13 +1027,16 @@ function get_note_status(callsign){ $('#callsign-note-id').val(data.id); setNotesVisibility(2, noteData.content); } else { + $('#callsign-note-id').val(''); setNotesVisibility(2, lang_general_word_error); } } ).fail(function() { + $('#callsign-note-id').val(''); setNotesVisibility(2, lang_general_word_error); }); } else { + $('#callsign-note-id').val(''); setNotesVisibility(1); } } From fba279ab2208ee29b57a70b88ce44410974c9c32 Mon Sep 17 00:00:00 2001 From: Szymon Porwolik Date: Sun, 26 Oct 2025 12:16:56 +0100 Subject: [PATCH 19/19] Rolldown/rollup button added --- application/views/qso/index.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/application/views/qso/index.php b/application/views/qso/index.php index a824da6e5..5cc5e116d 100644 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -715,15 +715,18 @@ switch ($date_format) {
-
-

+
+

">

+
-
+