mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
Merge pull request #2395 from szporwolik/dev_sticky_note
[Notes] Basic notes support at the QSO logging and QSO view
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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"); ?>";
|
||||
</script>
|
||||
|
||||
<!-- General JS Files used across Wavelog -->
|
||||
@@ -299,6 +304,11 @@ function stopImpersonate_modal() {
|
||||
<script src="<?php echo base_url() ;?>assets/js/sections/notes.js"></script>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($this->uri->segment(1) == "qso" ) { ?>
|
||||
<!-- Javascript used for QSO Notes Area -->
|
||||
<script src="<?php echo base_url() ;?>assets/plugins/easymde/easymde.min.js"></script>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($this->uri->segment(1) == "notes" && ($this->uri->segment(2) == "view") ) { ?>
|
||||
<!-- Javascript used for Notes Area -->
|
||||
<script src="<?php echo base_url() ;?>assets/plugins/easymde/easymde.min.js"></script>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo base_url(); ?>assets/css/query-builder.default.min.css" />
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($this->uri->segment(1) == "notes" && ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit" || $this->uri->segment(2) == "view")) { ?>
|
||||
<?php if (($this->uri->segment(1) == "notes" && ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit" || $this->uri->segment(2) == "view")) || $this->uri->segment(1) == "qso") { ?>
|
||||
<link rel="stylesheet" type="text/css" href="<?php echo base_url(); ?>assets/plugins/easymde/easymde.css" />
|
||||
<?php } ?>
|
||||
|
||||
|
||||
@@ -29,7 +29,18 @@
|
||||
<form method="post" action="<?php echo site_url('notes/add'); ?>" name="notes_add" id="notes_add">
|
||||
<div class="mb-3">
|
||||
<label for="inputTitle"><?= __("Title"); ?></label>
|
||||
<input type="text" name="title" class="form-control" id="inputTitle" value="<?php echo isset($suggested_title) && $suggested_title ? $suggested_title : set_value('title'); ?>">
|
||||
<input type="text" name="title" class="form-control" id="inputTitle" value="<?php
|
||||
// Priority: suggested_title > POST value > prefill_title > ''
|
||||
if (isset($suggested_title) && $suggested_title) {
|
||||
echo $suggested_title;
|
||||
} elseif (set_value('title')) {
|
||||
echo set_value('title');
|
||||
} elseif (isset($prefill_title) && $prefill_title) {
|
||||
echo htmlspecialchars($prefill_title);
|
||||
} else {
|
||||
echo '';
|
||||
}
|
||||
?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="catSelect">
|
||||
@@ -39,8 +50,10 @@
|
||||
</span>
|
||||
</label>
|
||||
<select name="category" class="form-select" id="catSelect">
|
||||
<?php foreach (Note::get_possible_categories() as $category_key => $category_label): ?>
|
||||
<option value="<?= htmlspecialchars($category_key) ?>"<?= (set_value('category', 'General') == $category_key ? ' selected="selected"' : '') ?>><?= $category_label ?></option>
|
||||
<?php
|
||||
$selected_category = set_value('category', isset($category) ? $category : (isset($prefill_category) ? $prefill_category : 'General'));
|
||||
foreach (Note::get_possible_categories() as $category_key => $category_label): ?>
|
||||
<option value="<?= htmlspecialchars($category_key) ?>"<?= ($selected_category == $category_key ? ' selected="selected"' : '') ?>><?= $category_label ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary btn-light category-btn active" data-category="__all__">
|
||||
<?= __("All Categories"); ?> <span class="badge bg-secondary"><?= $all_notes_count ?></span>
|
||||
</button>
|
||||
<?php
|
||||
<?php
|
||||
// Decode HTML entities for proper display
|
||||
$decoded_categories = array();
|
||||
foreach ($categories as $key => $value) {
|
||||
@@ -51,6 +51,9 @@
|
||||
<!-- Search box and reset button -->
|
||||
<div class="input-group">
|
||||
<input type="text" id="notesSearchBox" class="form-control form-control-sm" maxlength="50" placeholder="<?= __("Search notes (min. 3 chars)") ?>">
|
||||
<button class="btn btn-outline-secondary btn-sm btn-light" id="notesAddStrokedZero" type="button" title="<?= __("Add stroked zero (Ø)") ?>" data-bs-toggle="tooltip">
|
||||
Ø
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm btn-light" id="notesSearchReset" type="button" title="<?= __("Reset search") ?>">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
|
||||
@@ -68,7 +68,7 @@ switch ($date_format) {
|
||||
<?php } ?>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#nav-notes" role="tab" aria-controls="notes" aria-selected="false"><?= __("Notes"); ?></a>
|
||||
<a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#nav-notes" role="tab" aria-controls="notes" aria-selected="false"><?= __("Comment"); ?></a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
@@ -154,7 +154,7 @@ switch ($date_format) {
|
||||
<!-- Callsign Input -->
|
||||
<div class="row">
|
||||
<div class="mb-3 col-md-12">
|
||||
<label for="callsign"><?= __("Callsign"); ?></label> <i id="check_cluster" data-bs-toggle="tooltip" title="<?= __("Search DXCluster for latest Spot"); ?>" class="fas fa-search"></i>
|
||||
<label for="callsign"><?= __("Callsign"); ?></label> <i id="check_cluster" data-bs-toggle="tooltip" title="<?= __("Search DXCluster for latest Spot"); ?>" class="fas fa-search"></i></label>
|
||||
<div class="input-group">
|
||||
<input tabindex="7" type="text" class="form-control uppercase" id="callsign" name="callsign" autocomplete="off" required>
|
||||
<span id="qrz_info" class="input-group-text btn-included-on-field d-none py-0"></span>
|
||||
@@ -602,8 +602,6 @@ switch ($date_format) {
|
||||
<input class="form-control" id="email" type="text" name="email" value="" />
|
||||
<small id="MailHelp" class="form-text text-muted"><?= __("E-mail address of QSO-partner"); ?></small>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Satellite Panel -->
|
||||
@@ -637,10 +635,10 @@ switch ($date_format) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Panel Contents -->
|
||||
<!-- Comment Panel Contents -->
|
||||
<div class="tab-pane fade" id="nav-notes" role="tabpanel" aria-labelledby="notes-tab">
|
||||
<div class="mb-3">
|
||||
<label for="notes"><?= __("Notes"); ?></label>
|
||||
<label for="notes"><?= __("QSO Comment"); ?></label>
|
||||
<textarea type="text" class="form-control" id="notes" name="notes" rows="10"></textarea>
|
||||
<div class="small form-text text-muted"><?= __("Note: Gets exported to third-party services.") ?></div>
|
||||
</div>
|
||||
@@ -710,9 +708,33 @@ switch ($date_format) {
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!--- Notes --->
|
||||
<script>
|
||||
var user_show_notes = <?php echo ($this->session->userdata('user_show_notes')) ? 'true' : 'false'; ?>;
|
||||
</script>
|
||||
|
||||
<div class="card callsign-notes" id="callsign-notes">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 style="font-size: 16px; font-weight: bold;" class="card-title mb-0">
|
||||
<?= __("Callsign Notes"); ?>
|
||||
<span class="ms-1" data-bs-toggle="tooltip" title="<?= __("Store private information about your QSO partner. These notes are never shared or exported to external services.") ?>">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</span>
|
||||
</h4>
|
||||
<button class="btn btn-sm btn-link p-0" type="button" data-bs-toggle="collapse" data-bs-target="#callsign-notes-body" aria-expanded="false" aria-controls="callsign-notes-body">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body collapse" id="callsign-notes-body">
|
||||
<textarea id="callsign_note_content" class="form-control" rows="6"></textarea>
|
||||
<input type="hidden" id="callsign-note-id" value="" />
|
||||
<button id="callsign-note-edit-btn" class="btn btn-primary mt-2" style="display:none;"><i class="fas fa-edit"></i> <?= __("Edit Note"); ?></button>
|
||||
<button id="callsign-note-save-btn" class="btn btn-primary mt-2" style="display:none;"><i class="fas fa-save"></i> <?= __("Save Note"); ?></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-sm-7">
|
||||
|
||||
<div id="noticer" role="alert"></div>
|
||||
@@ -833,6 +855,10 @@ switch ($date_format) {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification : TBD move to footer -->
|
||||
<div id="toast-container" class="position-fixed bottom-0 end-0 p-3" style="z-index: 1100;"></div>
|
||||
|
||||
<script>
|
||||
var station_callsign = "<?php echo $station_callsign; ?>";
|
||||
</script>
|
||||
|
||||
@@ -81,7 +81,13 @@
|
||||
|
||||
<tr>
|
||||
<td><?= __("Callsign"); ?></td>
|
||||
<td><b><?php echo str_replace("0","Ø",strtoupper($row->COL_CALL)); ?></b> <a target="_blank" href="https://www.qrz.com/db/<?php echo strtoupper($row->COL_CALL); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/qrz.png" alt="Lookup <?php echo strtoupper($row->COL_CALL); ?> on QRZ.com"></a> <a target="_blank" href="https://www.hamqth.com/<?php echo strtoupper($row->COL_CALL); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/hamqth.png" alt="Lookup <?php echo strtoupper($row->COL_CALL); ?> on HamQTH"></a> <a target="_blank" href="http://www.eqsl.cc/Member.cfm?<?php echo strtoupper($row->COL_CALL); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/eqsl.png" alt="Lookup <?php echo strtoupper($row->COL_CALL); ?> on eQSL.cc"></a> <a target="_blank" href="https://clublog.org/logsearch.php?log=<?php echo strtoupper($row->COL_CALL); ?>&call=<?php echo strtoupper($row->station_callsign); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/clublog.png" alt="Clublog Log Search"></a></td>
|
||||
<td><b><?php echo str_replace("0","Ø",strtoupper($row->COL_CALL)); ?></b> <a target="_blank" href="https://www.qrz.com/db/<?php echo strtoupper($row->COL_CALL); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/qrz.png" alt="Lookup <?php echo strtoupper($row->COL_CALL); ?> on QRZ.com"></a> <a target="_blank" href="https://www.hamqth.com/<?php echo strtoupper($row->COL_CALL); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/hamqth.png" alt="Lookup <?php echo strtoupper($row->COL_CALL); ?> on HamQTH"></a> <a target="_blank" href="http://www.eqsl.cc/Member.cfm?<?php echo strtoupper($row->COL_CALL); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/eqsl.png" alt="Lookup <?php echo strtoupper($row->COL_CALL); ?> on eQSL.cc"></a> <a target="_blank" href="https://clublog.org/logsearch.php?log=<?php echo strtoupper($row->COL_CALL); ?>&call=<?php echo strtoupper($row->station_callsign); ?>"><img width="16" height="16" src="<?php echo base_url(); ?>images/icons/clublog.png" alt="Clublog Log Search"></a>
|
||||
<?php if (!empty($contacts_note_id) && $this->session->userdata('user_show_notes')==1) { ?>
|
||||
<a href="<?php echo base_url(); ?>index.php/notes/view/<?php echo $contacts_note_id; ?>" target="_blank" title="<?= __("View note for this callsign"); ?>" style="margin-left:2px;vertical-align:middle;">
|
||||
<i class="fa fa-sticky-note text-info" style="font-size:16px;vertical-align:middle;"></i>
|
||||
</a>
|
||||
<?php } ?>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
@@ -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;
|
||||
@@ -164,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];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -557,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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}</strong><br>${text}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 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));
|
||||
@@ -73,6 +112,76 @@ function getUTCDateStamp(el) {
|
||||
$(el).attr('value', formatted_date);
|
||||
}
|
||||
|
||||
// Note card state logic including EasyMDE initialization and handling
|
||||
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({
|
||||
element: $editorElem[0],
|
||||
spellChecker: false,
|
||||
toolbar: [
|
||||
"bold", "italic", "heading", "|","preview", "|",
|
||||
"quote", "unordered-list", "ordered-list", "|",
|
||||
"link", "image", "|",
|
||||
"guide"
|
||||
],
|
||||
forceSync: true,
|
||||
status: false,
|
||||
maxHeight: '150px',
|
||||
autoDownloadFontAwesome: false,
|
||||
autoRefresh: { delay: 250 },
|
||||
});
|
||||
$editorElem.data('easymde', noteEditor);
|
||||
}
|
||||
|
||||
if (state === 0) {
|
||||
// No callsign - Hide note card
|
||||
$noteCard.hide();
|
||||
|
||||
} else if (state === 1) {
|
||||
// Callsign, no note yet - show note card with message
|
||||
$noteCard.show();
|
||||
|
||||
// Hide editor toolbar, set value and show preview
|
||||
document.querySelector('.EasyMDEContainer .editor-toolbar').style.display = 'none';
|
||||
noteEditor.value(lang_qso_note_missing);
|
||||
noteEditor.togglePreview();
|
||||
noteEditor.codemirror.setOption('readOnly', true);
|
||||
|
||||
} else if (state === 2) {
|
||||
// Callsign with existing notes - show note card with notes
|
||||
$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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
} else {
|
||||
$editBtn.addClass('d-none').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$('#stationProfile').on('change', function () {
|
||||
var stationProfile = $('#stationProfile').val();
|
||||
@@ -892,8 +1001,50 @@ function reset_fields() {
|
||||
clearTimeout();
|
||||
set_timers();
|
||||
resetTimers(qso_manual);
|
||||
setNotesVisibility(0); // Set note card to hidden
|
||||
}
|
||||
|
||||
// Get status of notes for this callsign
|
||||
function get_note_status(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 && 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) {
|
||||
$('#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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Lookup callsign on focusout - if the callsign is 3 chars or longer
|
||||
$("#callsign").on("focusout", function () {
|
||||
if ($(this).val().length >= 3 && preventLookup == false) {
|
||||
|
||||
@@ -936,6 +1087,9 @@ $("#callsign").on("focusout", function () {
|
||||
// Reset QSO fields
|
||||
resetDefaultQSOFields();
|
||||
|
||||
// Set qso icon
|
||||
get_note_status(result.callsign);
|
||||
|
||||
if (result.dxcc.entity != undefined) {
|
||||
$('#country').val(convert_case(result.dxcc.entity));
|
||||
$('#callsign_info').text(convert_case(result.dxcc.entity));
|
||||
@@ -2117,6 +2271,8 @@ function resetDefaultQSOFields() {
|
||||
$('#callsign-image-content').text("");
|
||||
$('.awardpane').remove();
|
||||
$('#timesWorked').html(lang_qso_title_previous_contacts);
|
||||
|
||||
setNotesVisibility(0); // Set default note card visibility to 0 (hidden)
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
@@ -2179,6 +2335,8 @@ $(document).ready(function () {
|
||||
set_timers();
|
||||
updateStateDropdown('#dxcc_id', '#stateInputLabel', '#location_us_county', '#stationCntyInputQso');
|
||||
|
||||
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();
|
||||
set_qrg();
|
||||
@@ -2406,6 +2564,112 @@ $(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');
|
||||
noteEditor.codemirror.refresh();
|
||||
}
|
||||
|
||||
// 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(lang_qso_note_toast_title, lang_qso_note_deleted);
|
||||
} 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(lang_qso_note_toast_title, lang_qso_note_created);
|
||||
} else {
|
||||
// Show success message briefly
|
||||
showToast(lang_qso_note_toast_title, lang_qso_note_saved);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} else {
|
||||
alert(lang_qso_note_error_saving + ': ' + (response.message || lang_general_word_error));
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
alert(lang_qso_note_error_saving);
|
||||
});
|
||||
});
|
||||
|
||||
// everything loaded and ready 2 go
|
||||
bc.postMessage('ready');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user