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:
Florian (DF2ET)
2025-10-26 13:25:40 +01:00
committed by GitHub
11 changed files with 562 additions and 71 deletions

View File

@@ -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');
}

View File

@@ -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;
}
}

View File

@@ -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()));

View File

@@ -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>

View File

@@ -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 } ?>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>&nbsp;<i id="check_cluster" data-bs-toggle="tooltip" title="<?= __("Search DXCluster for latest Spot"); ?>" class="fas fa-search"></i>
<label for="callsign"><?= __("Callsign"); ?></label>&nbsp;<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>

View File

@@ -81,7 +81,13 @@
<tr>
<td><?= __("Callsign"); ?></td>
<td><b><?php echo str_replace("0","&Oslash;",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","&Oslash;",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>

View File

@@ -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();
}
}
});
}
});

View File

@@ -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');
});
});