diff --git a/application/controllers/Logbook.php b/application/controllers/Logbook.php index 56d75669c..13e3970d9 100644 --- a/application/controllers/Logbook.php +++ b/application/controllers/Logbook.php @@ -621,6 +621,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 991b2e591..546d7bbe4 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,35 @@ 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)); + $suggested_title = str_replace('0', 'Ø', $suggested_title); + } + // 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'); @@ -107,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"); @@ -199,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 @@ -239,28 +255,112 @@ 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') { - $check_title = strtoupper($title); + $exists = false; + $note_id = null; + $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 = $note_id_found; + } } - $where = [ - 'category' => $category, - 'user_id' => $user_id, - 'title' => $check_title - ]; - $query = $this->db->get_where('notes', $where); - $duplicate = false; - if ($id) { - foreach ($query->result() as $note) { - if ($note->id != $id) { - $duplicate = true; - break; - } + $response = ['exists' => $exists]; + if ($exists && $note_id) { + $response['id'] = $note_id; + } + $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")])); + } + } + + // 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 { - $duplicate = $query->num_rows() > 0; + // 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])); } - $this->output->set_content_type('application/json')->set_output(json_encode(['duplicate' => $duplicate])); } // Form validation callback for add: unique Contacts note title for user, only core callsign @@ -272,15 +372,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; } @@ -298,23 +396,23 @@ 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'); + $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; + } return TRUE; } return TRUE; } + + } diff --git a/application/models/Note.php b/application/models/Note.php index ba1ef8438..044a93aa1 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; } @@ -59,17 +75,23 @@ 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 = 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 + $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; - } + 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())); diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 9b700f4f2..c010aac2f 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -68,7 +68,12 @@ var lang_notes_duplicate_confirmation = "= __("Duplicate this note?"); ?>"; var lang_notes_duplication_disabled_short = "= __("Duplication Disabled"); ?>"; var lang_notes_not_found = "= __("No notes were found"); ?>"; - + var lang_qso_note_missing = "= __("No notes for this callsign"); ?>"; + var lang_qso_note_toast_title = "= __("Callsign Note"); ?>"; + var lang_qso_note_deleted = "= __("Note deleted successfully"); ?>"; + var lang_qso_note_created = "= __("Note created successfully"); ?>"; + var lang_qso_note_saved = "= __("Note saved successfully"); ?>"; + var lang_qso_note_error_saving = "= __("Error saving note"); ?>"; @@ -299,6 +304,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 512ab1422..46f2d7ee2 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/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 @@
+ + + + +