Notes overhaul: updated view of the notes, added "Contacts" category with required logic, first step for callsign sticky notes (#2362)

* feat(notes): initial overhaul of notes with improved view, contact category, and better forms/validation

* Notes: changes after draft PR review

- improved security, avoided writing to $_POST
- removing redundant $data variable passing
- cleanup of xss_clean usage
- changed to use existing callbook library instead new helper
- migrated to plain SQL
- changed name of the cat variable to category to avoid confusion
- fixed translation (multiple places)

* Translation of notes.js

* Deleted not needed line.

* Notes: Multiple fixes for after PR code review

* Update application/views/notes/add.php

Co-authored-by: Florian (DF2ET) <github@florian-wolters.de>

* Layout improvmen of the error box suggested by phl0

* Add checking of note ownership during duplication.

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: improved translation of one of the strings

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: fix translation shall be using double quote

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Note: translation shall use double quote

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: fix, translation shall be using sprintf

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Note: update -> translations shall be using double quotes

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: fix, translation shall be using double quotes

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: fix, translation shall be using double quotes

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: missing translation of the error message.

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: fix translation shall be using double quote

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: translation of the error message

Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>

* Notes: moved loading of the library out of the constructor

---------

Co-authored-by: Florian (DF2ET) <github@florian-wolters.de>
Co-authored-by: Fabian Berg <fabian.berg@hb9hil.org>
This commit is contained in:
Szymon Porwolik
2025-10-02 20:20:35 +02:00
committed by GitHub
parent b4076d4d7f
commit b349755031
8 changed files with 1333 additions and 369 deletions

View File

@@ -1,119 +1,320 @@
<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
// 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();
$this->load->model('user_model');
if (!$this->user_model->authorize(2)) {
$this->session->set_flashdata('error', __("You're not allowed to do that!"));
redirect('dashboard');
exit;
}
}
function __construct()
{
parent::__construct();
// Main notes page: lists notes and categories
public function index() {
$this->load->model('note');
$data = [];
// Get all notes for logged-in user
$data['notes'] = $this->note->list_all();
// Get possible categories
$data['categories'] = Note::get_possible_categories();
// Get note counts per category
$category_counts = [];
foreach (Note::get_possible_category_keys() as $category) {
$category_counts[$category] = $this->note->count_by_category($category);
}
$data['category_counts'] = $category_counts;
// Get total notes count
$data['all_notes_count'] = $this->note->count_by_category();
$data['page_title'] = __("Notes");
// Render views
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/main');
$this->load->view('interface_assets/footer');
}
$this->load->model('user_model');
if(!$this->user_model->authorize(2)) { $this->session->set_flashdata('error', __("You're not allowed to do that!")); redirect('dashboard'); }
}
// Add a new note
function add() {
$this->load->model('note');
$this->load->library('form_validation');
$this->load->library('callbook'); // Used for callsign parsing
$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) {
$category = $this->input->post('category', TRUE);
if ($category === 'Contacts') {
/* Displays all notes in a list */
public function index() {
$suggested_title = strtoupper($this->callbook->get_plaincall($this->input->post('title', TRUE)));
}
$data['suggested_title'] = $suggested_title;
$data['page_title'] = __("Add Notes");
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/add');
$this->load->view('interface_assets/footer');
} else {
$category = $this->input->post('category', TRUE);
$title = $this->input->post('title', TRUE);
$content = $this->input->post('content', TRUE);
$local_time = $this->input->post('local_time', TRUE);
if ($category === 'Contacts') {
$title = strtoupper($this->callbook->get_plaincall($title));
}
$this->note->add($category, $title, $content, $local_time);
redirect('notes');
}
}
// View a single note
function view($id = null) {
$this->load->model('note');
$clean_id = $this->security->xss_clean($id);
// Validate note ID and ownership
if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $this->session->userdata('user_id'))) {
show_404();
}
$data['note'] = $this->note->view($clean_id);
$data['page_title'] = __("Note");
// Render note view
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/view');
$this->load->view('interface_assets/footer');
}
// Edit a note
function edit($id = null) {
$this->load->model('note');
$clean_id = $this->security->xss_clean($id);
// Validate note ID and ownership
if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $this->session->userdata('user_id'))) {
show_404();
}
$this->load->library('callbook'); // Used for callsign parsing
$data['id'] = $clean_id;
$data['note'] = $this->note->view($clean_id);
$this->load->library('form_validation');
$suggested_title = null;
// Validate form fields
$this->form_validation->set_rules('title', 'Note Title', 'required|callback_contacts_title_unique_edit'); // Custom callback for Contacts category
$this->form_validation->set_rules('content', 'Content', 'required');
if ($this->form_validation->run() == FALSE) {
$category = $this->input->post('category', TRUE);
if ($category === 'Contacts') {
$suggested_title = strtoupper($this->callbook->get_plaincall($this->input->post('title', TRUE)));
}
$data['suggested_title'] = $suggested_title;
$data['page_title'] = __("Edit Note");
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/edit');
$this->load->view('interface_assets/footer');
} else {
$category = $this->input->post('category', TRUE);
$title = $this->input->post('title', TRUE);
$content = $this->input->post('content', TRUE);
$local_time = $this->input->post('local_time', TRUE);
$note_id = $this->input->post('id', TRUE);
if ($category === 'Contacts') {
$title = strtoupper($this->callbook->get_plaincall($title));
}
$this->note->edit($note_id, $category, $title, $content, $local_time);
redirect('notes');
}
}
// API search for notes
function search() {
$this->load->model('note');
// Map and sanitize search parameters
$searchCriteria = $this->mapParameters();
// Get pagination and sorting parameters
$page = (int)$this->input->post('page', TRUE);
$per_page = (int)$this->input->post('per_page', TRUE);
$sort_col = $this->input->post('sort_col', TRUE);
$sort_dir = $this->input->post('sort_dir', TRUE);
if ($per_page < 1) $per_page = 15;
if ($page < 1) $page = 1;
// Get paginated, sorted notes
$result = $this->note->search_paginated($searchCriteria, $page, $per_page, $sort_col, $sort_dir);
$response = [
'notes' => $result['notes'],
'total' => $result['total']
];
$this->output
->set_content_type('application/json')
->set_output(json_encode($response));
}
// Map and sanitize search parameters
function mapParameters() {
return array(
'cat' => $this->input->post('cat', TRUE),
'search' => $this->input->post('search', TRUE)
);
}
// Delete a note
function delete($id = null) {
$this->load->model('note');
$data['notes'] = $this->note->list_all();
$data['page_title'] = __("Notes");
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/main');
$this->load->view('interface_assets/footer');
}
/* Provides function for adding notes to the system. */
function add() {
$this->load->model('note');
$this->load->library('form_validation');
$this->form_validation->set_rules('title', 'Note Title', 'required');
$this->form_validation->set_rules('content', 'Content', 'required');
$clean_id = $this->security->xss_clean($id);
// Validate note ID and ownership
if (!is_numeric($clean_id) || !$this->note->belongs_to_user($clean_id, $this->session->userdata('user_id'))) {
show_404();
}
$this->note->delete($clean_id);
redirect('notes');
}
// Duplicate a note by ID for the logged-in user, appending #[timestamp] to title
public function duplicate($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'))) {
show_404();
}
$timestamp = $this->input->post('timestamp', TRUE);
$user_id = $this->session->userdata('user_id');
// Get original note
$query = $this->db->get_where('notes', array('id' => $clean_id, 'user_id' => $user_id));
if ($query->num_rows() !== 1) {
show_404();
}
$note = $query->row();
// Simple duplicate check
$category = $note->cat;
$new_title = $note->title . ' #' . $timestamp;
$check_title = $new_title;
if ($category === 'Contacts') {
$check_title = strtoupper($new_title);
}
$existing = $this->db->get_where('notes', [
'cat' => $category,
'user_id' => $user_id,
'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.']));
return;
}
// Duplicate note with new title
$data = array(
'cat' => $note->cat,
'title' => $new_title,
'note' => $note->note,
'user_id' => $user_id,
'creation_date' => date('Y-m-d H:i:s'),
'last_modified' => date('Y-m-d H:i:s')
);
$this->db->insert('notes', $data);
$this->output->set_content_type('application/json')->set_output(json_encode(['status' => 'ok']));
}
if ($this->form_validation->run() == FALSE)
{
$data['page_title'] = __("Add Notes");
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/add');
$this->load->view('interface_assets/footer');
}
else
{
$this->note->add();
redirect('notes');
}
}
/* View Notes */
function view($id) {
// API endpoint to get note counts per category and total
public function get_category_counts() {
$this->load->model('note');
$categories = Note::get_possible_category_keys();
$category_counts = [];
foreach ($categories as $category) {
$category_counts[$category] = $this->note->count_by_category($category);
}
$all_notes_count = $this->note->count_by_category();
$response = [
'category_counts' => $category_counts,
'all_notes_count' => $all_notes_count
];
$this->output
->set_content_type('application/json')
->set_output(json_encode($response));
}
$clean_id = $this->security->xss_clean($id);
// API endpoint to check for duplicate note title in category for user
public function check_duplicate() {
$user_id = $this->session->userdata('user_id');
$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);
}
$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;
}
}
} else {
$duplicate = $query->num_rows() > 0;
}
$this->output->set_content_type('application/json')->set_output(json_encode(['duplicate' => $duplicate]));
}
if (! is_numeric($clean_id)) {
show_404();
}
// Form validation callback for add: unique Contacts note title for user, only core callsign
public function contacts_title_unique($title = null) {
$category = $this->input->post('category', TRUE);
if ($category === 'Contacts') {
$this->load->library('callbook'); // Used for callsign parsing
$user_id = $this->session->userdata('user_id');
$core = strtoupper($this->callbook->get_plaincall($title));
// Only fail if prefix or suffix is present
if (strtoupper($title) <> $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) {
$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;
}
return TRUE;
}
return TRUE;
}
// Form validation callback for edit: unique Contacts note title for user (ignore current note), only core callsign
public function contacts_title_unique_edit($title = null) {
$category = $this->input->post('category', TRUE);
if ($category === 'Contacts') {
$this->load->library('callbook'); // Used for callsign parsing
$user_id = $this->session->userdata('user_id');
$note_id = $this->input->post('id', TRUE);
$core = strtoupper($this->callbook->get_plaincall($title));
// Only fail if prefix or suffix is present
if (strtoupper($title) <> $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;
}
}
return TRUE;
}
return TRUE;
}
$this->load->model('note');
$data['note'] = $this->note->view($clean_id);
// Display
$data['page_title'] = __("Note");
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/view');
$this->load->view('interface_assets/footer');
}
/* Edit Notes */
function edit($id) {
$clean_id = $this->security->xss_clean($id);
if (! is_numeric($clean_id)) {
show_404();
}
$this->load->model('note');
$data['id'] = $clean_id;
$data['note'] = $this->note->view($clean_id);
$this->load->library('form_validation');
$this->form_validation->set_rules('title', 'Note Title', 'required');
$this->form_validation->set_rules('content', 'Content', 'required');
if ($this->form_validation->run() == FALSE)
{
$data['page_title'] = __("Edit Note");
$this->load->view('interface_assets/header', $data);
$this->load->view('notes/edit');
$this->load->view('interface_assets/footer');
}
else
{
$this->note->edit();
redirect('notes');
}
}
/* Delete Note */
function delete($id) {
$clean_id = $this->security->xss_clean($id);
if (! is_numeric($clean_id)) {
show_404();
}
$this->load->model('note');
$this->note->delete($clean_id);
redirect('notes');
}
}
}

View File

@@ -1,9 +1,25 @@
<?php
class Note extends CI_Model {
// Get list of possible note categories with translations
public static function get_possible_categories() {
return [
'Contacts' => __("Contacts"), // QSO partner notes
'General' => __("General"), // General notes
'Antennas' => __("Antennas"), // Antenna-related notes
'Satellites' => __("Satellites") // Satellite-related notes
];
}
// Get list of possible note category keys (for backwards compatibility)
public static function get_possible_category_keys() {
return array_keys(self::get_possible_categories());
}
// List all notes for a user or API key
function list_all($api_key = null) {
if ($api_key == null) {
// Determine user ID
if ($api_key == null) {
$user_id = $this->session->userdata('user_id');
} else {
$this->load->model('api_model');
@@ -12,64 +28,205 @@ class Note extends CI_Model {
$user_id = $this->api_model->key_userid($api_key);
}
}
$this->db->where('user_id', $user_id);
return $this->db->get('notes');
$sql = "SELECT * FROM notes WHERE user_id = ?";
return $this->db->query($sql, array($user_id));
}
function add() {
$data = array(
'cat' => $this->input->post('category', TRUE),
'title' => $this->input->post('title', TRUE),
'note' => $this->input->post('content', TRUE),
'user_id' => $this->session->userdata('user_id')
);
$this->db->insert('notes', $data);
// Add a new note for the logged-in user
function add($category, $title, $content, $local_time = null) {
$user_id = $this->session->userdata('user_id');
$check_title = $title;
if ($category === 'Contacts') {
$check_title = strtoupper($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') {
show_error(__("In Contacts category, the titles of the notes need to be unique."));
return;
}
$creation_date_utc = gmdate('Y-m-d H:i:s');
if ($local_time) {
$dt = new DateTime($local_time, new DateTimeZone(date_default_timezone_get()));
$dt->setTimezone(new DateTimeZone('UTC'));
$creation_date_utc = $dt->format('Y-m-d H:i:s');
}
$sql = "INSERT INTO notes (cat, title, note, user_id, creation_date, last_modified) VALUES (?, ?, ?, ?, ?, ?)";
$this->db->query($sql, array($category, $title, $content, $user_id, $creation_date_utc, $creation_date_utc));
}
function edit() {
$data = array(
'cat' => $this->input->post('category', TRUE),
'title' => $this->input->post('title', TRUE),
'note' => $this->input->post('content', TRUE)
);
$this->db->where('id', $this->input->post('id', TRUE));
$this->db->where('user_id', $this->session->userdata('user_id'));
$this->db->update('notes', $data);
// Edit an existing note for the logged-in user
function edit($note_id, $category, $title, $content, $local_time = null) {
$user_id = $this->session->userdata('user_id');
$check_title = $title;
if ($category === 'Contacts') {
$check_title = strtoupper($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') {
show_error(__("In Contacts category, the titles of the notes need to be unique."));
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()));
$dt->setTimezone(new DateTimeZone('UTC'));
$last_modified_utc = $dt->format('Y-m-d H:i:s');
}
$sql = "UPDATE notes SET cat = ?, title = ?, note = ?, last_modified = ? WHERE id = ? AND user_id = ?";
$this->db->query($sql, array($category, $title, $content, $last_modified_utc, $note_id, $user_id));
}
// Delete a note by ID for the logged-in user
function delete($id) {
$clean_id = $this->security->xss_clean($id);
if (! is_numeric($clean_id)) {
if (!is_numeric($clean_id)) {
show_404();
}
$this->db->delete('notes', array('id' => $clean_id, 'user_id' => $this->session->userdata('user_id')));
$sql = "DELETE FROM notes WHERE id = ? AND user_id = ?";
$this->db->query($sql, array($clean_id, $this->session->userdata('user_id')));
}
// View a note by ID for the logged-in user
function view($id) {
$clean_id = $this->security->xss_clean($id);
if (! is_numeric($clean_id)) {
if (!is_numeric($clean_id)) {
show_404();
}
// Get Note
$this->db->where('id', $clean_id);
$this->db->where('user_id', $this->session->userdata('user_id'));
return $this->db->get('notes');
$sql = "SELECT * FROM notes WHERE id = ? AND user_id = ?";
return $this->db->query($sql, array($clean_id, $this->session->userdata('user_id')));
}
// Check if note belongs to a user
public function belongs_to_user($note_id, $user_id) {
$sql = "SELECT COUNT(*) as count FROM notes WHERE id = ? AND user_id = ?";
$query = $this->db->query($sql, array($note_id, $user_id));
return $query->row()->count > 0;
}
// Search notes by category and/or text for the logged-in user
public function search($criteria = []) {
$user_id = $this->session->userdata('user_id');
$params = array($user_id);
$sql = "SELECT * FROM notes WHERE user_id = ?";
// Filter by category
if (!empty($criteria['cat'])) {
$cats = array_map('trim', explode(',', $criteria['cat']));
if (count($cats) > 0) {
$placeholders = str_repeat('?,', count($cats) - 1) . '?';
$sql .= " AND cat IN ($placeholders)";
$params = array_merge($params, $cats);
}
}
// Filter by search term (title or note)
if (!empty($criteria['search'])) {
$search = '%' . $criteria['search'] . '%';
$sql .= " AND (title LIKE ? OR note LIKE ?)";
$params[] = $search;
$params[] = $search;
}
$query = $this->db->query($sql, $params);
return $query->result();
}
// Count notes by category for the logged-in user
public function count_by_category($category = null) {
$user_id = $this->session->userdata('user_id');
$params = array($user_id);
$sql = "SELECT COUNT(*) as count FROM notes WHERE user_id = ?";
if ($category !== null) {
$sql .= " AND cat = ?";
$params[] = $category;
}
$query = $this->db->query($sql, $params);
return $query->row()->count;
}
// Get categories with their respective note counts for the logged-in user
public function get_categories_with_counts() {
$user_id = $this->session->userdata('user_id');
$sql = "SELECT cat, COUNT(*) as count FROM notes WHERE user_id = ? GROUP BY cat";
$query = $this->db->query($sql, array($user_id));
$result = [];
foreach ($query->result() as $row) {
$result[$row->cat] = (int)$row->count;
}
return $result;
}
// Count all notes with user_id NULL (system notes)
function CountAllNotes() {
// count all notes
$this->db->where('user_id =', NULL);
$query = $this->db->get('notes');
return $query->num_rows();
$sql = "SELECT COUNT(*) as count FROM notes WHERE user_id IS NULL";
$query = $this->db->query($sql);
return $query->row()->count;
}
// Search notes with pagination and sorting for the logged-in user
public function search_paginated($criteria = [], $page = 1, $per_page = 25, $sort_col = null, $sort_dir = null) {
$user_id = $this->session->userdata('user_id');
$params = array($user_id);
$where_clause = "WHERE user_id = ?";
// Filter by category
if (!empty($criteria['cat'])) {
$cats = array_map('trim', explode(',', $criteria['cat']));
if (count($cats) > 0) {
$placeholders = str_repeat('?,', count($cats) - 1) . '?';
$where_clause .= " AND cat IN ($placeholders)";
$params = array_merge($params, $cats);
}
}
// Filter by search term (title or note)
if (!empty($criteria['search'])) {
$search = '%' . $criteria['search'] . '%';
$where_clause .= " AND (title LIKE ? OR note LIKE ?)";
$params[] = $search;
$params[] = $search;
}
// Get total count
$count_sql = "SELECT COUNT(*) as count FROM notes $where_clause";
$count_query = $this->db->query($count_sql, $params);
$total = $count_query->row()->count;
// Build main query with sorting
$sql = "SELECT id, cat, title, note, creation_date, last_modified FROM notes $where_clause";
// Sorting
$columns = ['cat', 'title', 'creation_date', 'last_modified'];
if ($sort_col !== null && in_array($sort_col, $columns) && ($sort_dir === 'asc' || $sort_dir === 'desc')) {
$sql .= " ORDER BY $sort_col $sort_dir";
}
// Pagination
$offset = ($page - 1) * $per_page;
$sql .= " LIMIT $per_page OFFSET $offset";
$query = $this->db->query($sql, $params);
$notes = [];
foreach ($query->result() as $row) {
$notes[] = [
'id' => $row->id,
'cat' => $row->cat,
'title' => $row->title,
'note' => $row->note,
'creation_date' => $row->creation_date,
'last_modified' => $row->last_modified
];
}
return [
'notes' => $notes,
'total' => $total
];
}
}

View File

@@ -56,6 +56,19 @@
var lang_general_word_please_wait = "<?= __("Please Wait ..."); ?>";
var lang_general_states_deprecated = "<?= _pgettext("Word for country states that are deprecated but kept for legacy reasons.", "deprecated"); ?>";
var lang_gen_hamradio_sat_info = "<?= __("Satellite Information"); ?>";
var lang_notes_error_loading = "<?= __("Error loading notes"); ?>";
var lang_notes_sort = "<?= __("Sorting"); ?>";
var lang_notes_duplication_disabled = "<?= __("Duplication is disabled for Contacts notes"); ?>";
var lang_notes_duplicate = "<?= __("Duplicate"); ?>";
var lang_general_word_delete = "<?= __("Delete"); ?>";
var lang_general_word_duplicate = "<?= __("Duplicate"); ?>";
var lang_notes_delete = "<?= __("Delete Note"); ?>";
var lang_notes_duplicate = "<?= __("Duplicate Note"); ?>";
var lang_notes_delete_confirmation = "<?= __("Delete this note?"); ?>";
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"); ?>";
</script>
<!-- General JS Files used across Wavelog -->
@@ -278,9 +291,11 @@ function stopImpersonate_modal() {
<script src="<?php echo base_url() ;?>assets/js/jszip.min.js"></script>
<?php } ?>
<?php if ($this->uri->segment(1) == "notes" && ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit") ) { ?>
<?php if ($this->uri->segment(1) == "notes" ) { ?>
<!-- Javascript used for Notes Area -->
<?php if ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit") { ?>
<script src="<?php echo base_url() ;?>assets/plugins/easymde/easymde.min.js"></script>
<?php } ?>
<script src="<?php echo base_url() ;?>assets/js/sections/notes.js"></script>
<?php } ?>

View File

@@ -1,52 +1,61 @@
<!-- Notes add view: form for creating a new note -->
<div class="container notes">
<div class="card">
<div class="card-header">
<h2 class="card-title"><?= __("Create Note"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('notes'); ?>"><?= __("Notes"); ?></a>
</li>
<li class="nav-item">
<a class="nav-link active" href="<?php echo site_url('notes/add'); ?>"><?= __("Create Note"); ?></a>
</li>
</ul>
</div>
<div class="card-body">
<?php if (!empty(validation_errors())): ?>
<div class="alert alert-danger">
<a class="btn-close" data-bs-dismiss="alert" title="close">x</a>
<ul><?php echo (validation_errors('<li>', '</li>')); ?></ul>
<div class="card">
<div class="card-header">
<h2 class="card-title"><?= __("Notes"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" href="<?= site_url('notes'); ?>">
<i class="fa fa-sticky-note"></i> <?= __("Your Notes"); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="<?= site_url('notes/add'); ?>">
<i class="fa fa-plus-square"></i> <?= __("Create Note"); ?>
</a>
</li>
</ul>
</div>
<div class="card-body">
<!-- Show validation errors if any -->
<?php if (!empty(validation_errors())): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert" style="margin-top: 1rem;">
<span class="badge text-bg-info"><?= __("Warning"); ?></span>
<?php echo (validation_errors('<span>', '</span>')); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<!-- Note creation form -->
<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'); ?>">
</div>
<div class="mb-3">
<label for="catSelect">
<?= __("Category"); ?>
<span class="ms-1" data-bs-toggle="tooltip" title="<?= __("Contacts is a special note category used in various places of Wavelog to store information about QSO partners. This notes are private and are not shared with other users nor exported to external services.") ?>">
<i class="fa fa-question-circle text-info"></i>
</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 endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="inputTitle"><?= __("Note Contents"); ?></label>
<textarea name="content" style="display:none" id="notes"><?php echo set_value('content'); ?></textarea>
</div>
<div class="row">
<div class="col text-end">
<button type="submit" value="Submit" class="btn btn-primary">
<i class="fa fa-pencil-square-o btn-sm"></i> <?= __("Save Note"); ?>
</button>
</div>
</div>
</form>
</div>
<?php endif; ?>
<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">
</div>
<div class="mb-3">
<label for="catSelect"><?= __("Category"); ?></label>
<select name="category" class="form-select" id="catSelect">
<option value="General" selected="selected"><?= __("General"); ?></option>
<option value="Antennas"><?= __("Antennas"); ?></option>
<option value="Satellites"><?= __("Satellites"); ?></option>
</select>
</div>
<div class="mb-3">
<label for="inputTitle"><?= __("Note Contents"); ?></label>
<textarea name="content" style="display:none" id="notes"></textarea>
</div>
<button type="submit" value="Submit" class="btn btn-primary"><?= __("Save Note"); ?></button>
</form>
</div>
</div>
</div>

View File

@@ -1,57 +1,72 @@
<!-- Notes edit view: form for editing an existing note -->
<div class="container notes">
<?php foreach ($note->result() as $row) { ?>
<div class="card">
<div class="card-header">
<h2 class="card-title"><?= __("Edit Note"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('notes'); ?>"><?= __("Notes"); ?></a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('notes/add'); ?>"><?= __("Create Note"); ?></a>
</li>
</ul>
</div>
<div class="card-body">
<?php if (!empty(validation_errors())): ?>
<div class="alert alert-danger">
<a class="btn-close" data-bs-dismiss="alert" title="close">x</a>
<ul><?php echo (validation_errors('<li>', '</li>')); ?></ul>
<div class="card">
<div class="card-header">
<h2 class="card-title"><?= __("Notes"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" href="<?= site_url('notes'); ?>">
<i class="fa fa-sticky-note"></i> <?= __("Your Notes"); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= site_url('notes/add'); ?>">
<i class="fa fa-plus-square"></i> <?= __("Create Note"); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="<?= site_url('notes/add'); ?>">
<i class="fa fa-pencil-square-o"></i> <?= __("Edit Note"); ?>
</a>
</li>
</ul>
</div>
<div class="card-body">
<!-- Show validation errors if any -->
<?php if (!empty(validation_errors())): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert" style="margin-top: 1rem;">
<span class="badge text-bg-info"><?= __("Warning"); ?></span>
<?php echo (validation_errors('<span>', '</span>')); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<!-- Note edit form -->
<form method="post" action="<?php echo site_url('notes/edit'); ?>/<?php echo $id; ?>" name="notes_add" id="notes_add">
<div class="mb-3">
<label for="inputTitle"><?= __("Title"); ?></label>
<input type="text" name="title" class="form-control" value="<?php echo isset($suggested_title) && $suggested_title ? $suggested_title : set_value('title', $row->title); ?>" id="inputTitle">
</div>
<div class="mb-3">
<label for="catSelect">
<?= __("Category"); ?>
<?php if ($row->cat == 'Contacts'): ?>
<span class="ms-1" data-bs-toggle="tooltip" title="<?= __("Contacts is a special note category used in various places of Wavelog to store information about QSO partners. This notes are private and are not shared with other users nor exported to external services.") ?>">
<i class="fa fa-question-circle text-info"></i>
</span>
<?php endif; ?>
</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', $row->cat) == $category_key ? ' selected="selected"' : '') ?>><?= $category_label ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label for="inputTitle"><?= __("Note Contents"); ?></label>
<textarea name="content" style="display:none" id="notes"><?php echo set_value('content', $row->note); ?></textarea>
</div>
<input type="hidden" name="id" value="<?php echo $id; ?>" />
<div class="row">
<div class="col text-end">
<button type="submit" value="Submit" class="btn btn-primary">
<i class="fa fa-pencil-square-o btn-sm"></i> <?= __("Save Note"); ?>
</button>
</div>
</div>
</form>
</div>
<?php endif; ?>
<form method="post" action="<?php echo site_url('notes/edit'); ?>/<?php echo $id; ?>" name="notes_add" id="notes_add">
<div class="mb-3">
<label for="inputTitle"><?= __("Title"); ?></label>
<input type="text" name="title" class="form-control" value="<?php echo $row->title; ?>" id="inputTitle">
</div>
<div class="mb-3">
<label for="catSelect"><?= __("Category"); ?></label>
<select name="category" class="form-select" id="catSelect">
<option value="General" selected="selected"><?= __("General"); ?></option>
<option value="Antennas"><?= __("Antennas"); ?></option>
<option value="Satellites"><?= __("Satellites"); ?></option>
</select>
</div>
<div class="mb-3">
<label for="inputTitle"><?= __("Note Contents"); ?></label>
<textarea name="content" style="display:none" id="notes"><?php echo $row->note; ?></textarea>
</div>
<input type="hidden" name="id" value="<?php echo $id; ?>" />
<button type="submit" value="Submit" class="btn btn-primary"><?= __("Save Note"); ?></button>
</form>
</div>
<?php } ?>
</div>
<?php } ?>
</div>

View File

@@ -1,38 +1,84 @@
<!-- Notes main view: lists notes, categories, and search UI -->
<div class="container notes">
<div class="card">
<div class="card-header">
<h2 class="card-title"><?= __("Notes"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link active" href="<?php echo site_url('notes'); ?>"><?= __("Notes"); ?></a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('notes/add'); ?>"><?= __("Create Note"); ?></a>
</li>
</ul>
</div>
<div class="card-body">
<?php
if ($notes->num_rows() > 0)
{
echo "<h3>".__("Your Notes")."</h3>";
echo "<ul class=\"list-group\">";
foreach ($notes->result() as $row)
{
echo "<li class=\"list-group-item\">";
echo "<a href=\"".site_url()."/notes/view/".$row->id."\">".$row->title."</a>";
echo "</li>";
}
echo "</ul>";
} else {
echo "<p>".__("You don't currently have any notes, these are a fantastic way of storing data like ATU settings, beacons and general station notes and its better than paper as you can't lose them!")."</p>";
}
?>
</div>
<div class="card-header">
<h2 class="card-title"><?= __("Notes"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link active" href="<?= site_url('notes'); ?>">
<i class="fa fa-sticky-note"></i> <?= __("Your Notes"); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= site_url('notes/add'); ?>">
<i class="fa fa-plus-square"></i> <?= __("Create Note"); ?>
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="row pt-2 align-items-center flex-wrap">
<div class="col-md-7 col-12 mb-2 mb-md-0">
<!-- Category filter buttons -->
<div id="categoryButtons" class="btn-group me-2 flex-wrap" role="group" aria-label="Category Filter">
<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
// Decode HTML entities for proper display
$decoded_categories = array();
foreach ($categories as $key => $value) {
$decoded_categories[$key] = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
?>
<?php foreach ($decoded_categories as $category_key => $category_label): ?>
<button type="button" class="btn btn-sm btn-outline-secondary btn-light category-btn" data-category="<?= htmlspecialchars($category_key) ?>">
<?= htmlspecialchars($category_label); ?>
<?php if ($category_key === 'Contacts'): ?>
<span class="ms-1" data-bs-toggle="tooltip" title="<?= __("Contacts is a special note category used in various parts of Wavelog to store information about QSO partners. These notes are private and are neither shared with other users nor exported to external services.") ?>">
<i class="fa fa-question-circle"></i>
</span>
<?php endif; ?>
<span class="badge bg-secondary"><?= $category_counts[$category_key] ?? 0 ?></span>
</button>
<?php endforeach; ?>
<a href="<?php echo site_url('notes/add'); ?>" class="btn btn-sm btn-success">
<i class="fas fa-plus"></i> <?= __("Create Note"); ?>
</a>
</div>
</div>
<div class="col-md-5 col-12">
<!-- 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="notesSearchReset" type="button" title="<?= __("Reset search") ?>">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
<div class="pt-3" id="notesTableContainer">
<!-- Notes table -->
<script>
// Pass data from PHP to JavaScript
window.browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
window.categoryTranslations = <?= json_encode($decoded_categories, JSON_UNESCAPED_UNICODE) ?>;
</script>
<table id="notesTable" style="width:100%" class="table-sm table table-hover table-striped table-bordered table-condensed text-center">
<thead>
<tr>
<th style="width:15%" class="text-center"><?= __("Category"); ?></th>
<th style="width:45%" class="text-start"><?= __("Title"); ?></th>
<th style="width:15%" class="text-center"><?= __("Creation"); ?></th>
<th style="width:15%" class="text-center"><?= __("Last Modification"); ?></th>
<th style="width:10%" class="text-center"><?= __("Actions"); ?></th>
</tr>
</thead>
<tbody>
<!-- Notes rows will be loaded and rendered by JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@@ -1,26 +1,55 @@
<!-- Notes view: displays a single note -->
<div class="container notes">
<div class="card">
<?php foreach ($note->result() as $row) { ?>
<div class="card-header">
<h2 class="card-title"><?= __("Notes"); ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" href="<?= site_url('notes'); ?>">
<i class="fa fa-sticky-note-o"></i> <?= __("Your Notes"); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?= site_url('notes/add'); ?>">
<i class="fa fa-plus-square"></i> <?= __("Create Note"); ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="<?= site_url('notes/add'); ?>">
<i class="fa fa-sticky-note"></i> <?= __("View Note"); ?>
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-8">
<div class="mb-2">
<span style="font-size:1em;">
<?= __("Category"); ?>: <?= __($row->cat); ?>
<?php if ($row->cat == 'Contacts'): ?>
<span class="ms-1" data-bs-toggle="tooltip" title="<?= __("Contacts is a special note category used in various places of Wavelog to store information about QSO partners. This notes are private and are not shared with other users nor exported to external services.") ?>">
<i class="fa fa-question-circle text-info"></i>
</span>
<?php endif; ?>
</span>
</div>
<h4 class="fw-bold mb-0"><?php echo htmlspecialchars($row->title); ?></h4>
</div>
</div>
<!-- Note contents -->
<div class="mb-4">
<textarea name="content" style="display:none" id="notes_view"><?php echo $row->note; ?></textarea>
</div>
<div class="row">
<div class="col text-end">
<a href="<?php echo site_url('notes/edit'); ?>/<?php echo $row->id; ?>" class="btn btn-primary btn-sm"><i class="fas fa-pencil-square-o"></i> <?= __("Edit Note"); ?></a>
<a href="<?php echo site_url('notes/delete'); ?>/<?php echo $row->id; ?>" class="btn btn-danger btn-sm"><i class="fas fa-trash-alt"></i> <?= __("Delete Note"); ?></a>
<div class="card">
<?php foreach ($note->result() as $row) { ?>
<div class="card-header">
<h2 class="card-title"><?= __("Notes"); ?> - <?php echo $row->title; ?></h2>
<ul class="nav nav-tabs card-header-tabs">
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('notes'); ?>"><?= __("Notes"); ?></a>
</li>
<li class="nav-item">
<a class="nav-link" href="<?php echo site_url('notes/add'); ?>"><?= __("Create Note"); ?></a>
</li>
</ul>
</div>
<div class="card-body">
<textarea name="content" style="display:none" id="notes_view"><?php echo $row->note; ?></textarea>
<a href="<?php echo site_url('notes/edit'); ?>/<?php echo $row->id; ?>" class="btn btn-primary"><i class="fas fa-edit"></i> <?= __("Edit Note"); ?></a>
<a href="<?php echo site_url('notes/delete'); ?>/<?php echo $row->id; ?>" class="btn btn-danger"><i class="fas fa-trash-alt"></i> <?= __("Delete Note"); ?></a>
<?php } ?>
</div
> </div>
</div>
</div>
</div>
</div>
<?php } ?>
</div>
</div>

View File

@@ -1,68 +1,560 @@
const notes = new EasyMDE({element: document.getElementById('notes'), forceSync: true, spellChecker: false, placeholder: 'Create note...', maxHeight: '350px',
toolbar: [
{
name: "bold",
action: EasyMDE.toggleBold,
className: "fa fa-bold",
title: "bold",
},
{
name: "italic",
action: EasyMDE.toggleItalic,
className: "fa fa-italic",
title: "italic",
},
{
name: "heading",
action: EasyMDE.toggleHeadingSmaller,
className: "fa fa-header",
title: "heading",
},
"|",
{
name: "quote",
action: EasyMDE.toggleBlockquote,
className: "fa fa-quote-left",
title: "quote",
},
{
name: "unordered-list",
action: EasyMDE.toggleUnorderedList,
className: "fa fa-list-ul",
title: "unordered list",
},
{
name: "ordered-list",
action: EasyMDE.toggleOrderedList,
className: "fa fa-list-ol",
title: "ordered list",
},
"|",
{
name: "link",
action: EasyMDE.drawLink,
className: "fa fa-link",
title: "link"
},
{
name: "image",
action: EasyMDE.drawImage,
className: "fa fa-image",
title: "image"
},
"|",
{
name: "preview",
action: EasyMDE.togglePreview,
className: "fa fa-eye no-disable",
title: "preview"
},
"|",
{
name: "guide",
action: "https://www.markdownguide.org/basic-syntax/", // link
className: "fa fa-question-circle",
title: "Markdown Guide"
// Initialize EasyMDE for the notes textarea if on add/edit page
if (typeof EasyMDE !== 'undefined') {
const notes = new EasyMDE({
element: document.getElementById('notes'),
forceSync: true,
spellChecker: false,
placeholder: 'Create note...',
maxHeight: '350px',
toolbar: [
{
name: "bold",
action: EasyMDE.toggleBold,
className: "fa fa-bold",
title: "bold",
},
{
name: "italic",
action: EasyMDE.toggleItalic,
className: "fa fa-italic",
title: "italic",
},
{
name: "heading",
action: EasyMDE.toggleHeadingSmaller,
className: "fa fa-header",
title: "heading",
},
"|",
{
name: "quote",
action: EasyMDE.toggleBlockquote,
className: "fa fa-quote-left",
title: "quote",
},
{
name: "unordered-list",
action: EasyMDE.toggleUnorderedList,
className: "fa fa-list-ul",
title: "unordered list",
},
{
name: "ordered-list",
action: EasyMDE.toggleOrderedList,
className: "fa fa-list-ol",
title: "ordered list",
},
"|",
{
name: "link",
action: EasyMDE.drawLink,
className: "fa fa-link",
title: "link"
},
{
name: "image",
action: EasyMDE.drawImage,
className: "fa fa-image",
title: "image"
},
"|",
{
name: "preview",
action: EasyMDE.togglePreview,
className: "fa fa-eye no-disable",
title: "preview"
},
"|",
{
name: "guide",
action: "https://www.markdownguide.org/basic-syntax/", // link
className: "fa fa-question-circle",
title: "Markdown Guide"
}
]
});
// Always sync EasyMDE content to textarea before form submit
var notesForm = document.getElementById('notes_add');
if (notesForm) {
notesForm.addEventListener('submit', function(e) {
if (notes && notes.codemirror) {
notes.codemirror.save(); // forceSync
}
});
}
// If validation fails and textarea has value, restore it to EasyMDE
var notesTextarea = document.getElementById('notes');
if (notesTextarea && notesTextarea.value && notes.value() !== notesTextarea.value) {
notes.value(notesTextarea.value);
}
}
// Main notes page functionality
document.addEventListener('DOMContentLoaded', function() {
// Early exit if we're not on a notes page
var notesTableBody = document.querySelector('#notesTable tbody');
var isNotesMainPage = notesTableBody !== null;
var base_url = window.base_url || document.body.getAttribute('data-baseurl') || '/';
// Constants
const NOTES_PER_PAGE = 15;
const SEARCH_MIN_LENGTH = 3;
const SORT_COLUMN_MAP = ['cat', 'title', 'creation_date', 'last_modified', null];
// Cache frequently used DOM elements to avoid repeated queries
var domCache = {
notesTableBody: notesTableBody,
notesTable: document.getElementById('notesTable'),
categoryButtons: document.querySelectorAll('.category-btn'),
searchBox: document.getElementById('notesSearchBox'),
resetBtn: document.getElementById('notesSearchReset'),
titleInput: document.getElementById('inputTitle'),
catSelect: document.getElementById('catSelect'),
saveBtn: document.querySelector('button[type="submit"]'),
form: document.getElementById('notes_add'),
paginationContainer: document.getElementById('notesPagination')
};
// Create pagination container if it doesn't exist
if (!domCache.paginationContainer) {
domCache.paginationContainer = document.createElement('div');
domCache.paginationContainer.id = 'notesPagination';
domCache.paginationContainer.className = 'd-flex justify-content-center my-3';
var notesTableContainer = document.getElementById('notesTableContainer');
if (notesTableContainer) {
notesTableContainer.appendChild(domCache.paginationContainer);
}
]
}
// Initialize existing UTC time cells and tooltips on page load
// Helper function to initialize table elements after rendering
function initializeTableElements() {
// Convert UTC times to local time
document.querySelectorAll('#notesTable td[data-utc]').forEach(function(td) {
var utc = td.getAttribute('data-utc');
td.textContent = utcToLocal(utc);
});
// Initialize Bootstrap tooltips for note titles
var tooltipTriggerList = [].slice.call(document.querySelectorAll('#notesTable a[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function(el) {
new bootstrap.Tooltip(el);
});
}
// Run initialization for existing table content
initializeTableElements();
// Duplicate Contacts note check for add/edit pages
var modal;
function showModal(msg) {
if (!modal) {
modal = document.createElement('div');
modal.className = 'modal fade';
modal.innerHTML = '<div class="modal-dialog"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Duplicate Contacts Note</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p>' + msg + '</p></div></div></div>';
document.body.appendChild(modal);
}
modal.querySelector('.modal-body p').textContent = msg;
$(modal).modal('show');
}
// Reload category counters via AJAX
function reloadCategoryCounters() {
fetch(base_url + 'index.php/notes/get_category_counts', { method: 'POST' })
.then(response => response.json())
.then(counts => {
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];
}
});
});
} // Helper: Convert UTC string to browser local time
function utcToLocal(utcString) {
if (!utcString) return '';
// Parse as UTC
var utcDate = new Date(utcString + ' UTC');
if (isNaN(utcDate.getTime())) return utcString;
return utcDate.toLocaleString();
}
// Get currently active category
function getActiveCategory() {
var activeBtn = document.querySelector('.category-btn.active');
if (activeBtn) {
var cat = activeBtn.getAttribute('data-category');
return cat === '__all__' ? '' : cat;
}
return '';
}
// Perform search and update table
function performNotesSearch() {
var searchTerm = searchBox ? searchBox.value.trim() : '';
var selectedCat = getActiveCategory();
var formData = new FormData();
formData.append('cat', selectedCat);
// Only send search if 3+ chars, else send empty search
if (searchTerm.length >= 3) {
formData.append('search', searchTerm);
} else {
formData.append('search', '');
}
fetch(base_url + 'index.php/notes/search', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => {
var tbody = '';
if (data.length === 0) {
tbody = '<tr><td colspan="5" class="text-center text-muted">' + lang_notes_not_found + '</td></tr>';
} else {
data.forEach(function(note) {
tbody += '<tr>' +
'<td>' + (note.cat ? note.cat : '') + '</td>' +
'<td><a href="' + base_url + 'index.php/notes/view/' + (note.id ? note.id : '') + '">' + (note.title ? note.title : '') + '</a></td>' +
'<td>' + (note.last_modified ? note.last_modified : '') + '</td>' +
'</tr>';
});
}
if (notesTableBody) {
notesTableBody.innerHTML = tbody;
}
})
.catch(error => {
if (notesTableBody) {
notesTableBody.innerHTML = '<tr><td colspan="5">' + lang_notes_error_loading + ': ' + error.message + '</td></tr>';
}
});
}
// Sorting logic for notes table
var sortState = {
column: 3, // Default to 'Last Modification' column
direction: 'desc' // Show latest modified at top
};
var columnHeaders = domCache.notesTable ? domCache.notesTable.querySelectorAll('thead th') : [];
// Add sorting indicators and click handlers (only for supported columns)
columnHeaders.forEach(function(th, idx) {
var span = document.createElement('span');
span.className = 'dt-column-order';
th.appendChild(span);
if (SORT_COLUMN_MAP[idx]) {
span.setAttribute('role', 'button');
span.setAttribute('aria-label', th.textContent + ': ' + lang_notes_sort);
span.setAttribute('tabindex', '0');
th.style.cursor = 'pointer';
th.addEventListener('click', function() {
if (sortState.column !== idx) {
sortState.column = idx;
sortState.direction = 'asc';
} else if (sortState.direction === 'asc') {
sortState.direction = 'desc';
} else if (sortState.direction === 'desc') {
sortState.direction = null;
sortState.column = null;
} else {
sortState.direction = 'asc';
}
updateSortIndicators();
performNotesSearch();
});
} else {
th.style.cursor = 'default';
}
});
// Update sort indicators in the header
function updateSortIndicators() {
columnHeaders.forEach(function(th, idx) {
var span = th.querySelector('.dt-column-order');
if (!span) return;
span.textContent = '';
if (sortState.column === idx) {
if (sortState.direction === 'asc') {
span.textContent = '▲';
} else if (sortState.direction === 'desc') {
span.textContent = '▼';
}
}
});
}
// Server-side pagination, sorting, and search integration
var currentPage = 1;
var totalPages = 1;
var lastResponseTotal = 0;
window.lastNotesData = [];
// Render pagination controls
function renderPagination() {
if (!domCache.paginationContainer) return;
domCache.paginationContainer.innerHTML = '';
if (totalPages <= 1) return;
var ul = document.createElement('ul');
ul.className = 'pagination pagination-sm';
for (var i = 1; i <= totalPages; i++) {
var li = document.createElement('li');
li.className = 'page-item' + (i === currentPage ? ' active' : '');
var a = document.createElement('a');
a.className = 'page-link';
a.href = '#';
a.textContent = i;
a.addEventListener('click', function(e) {
e.preventDefault();
var page = parseInt(this.textContent);
if (page !== currentPage) {
currentPage = page;
performNotesSearch();
}
});
li.appendChild(a);
ul.appendChild(li);
}
domCache.paginationContainer.appendChild(ul);
}
// Simple Markdown to plain text conversion for tooltip preview
function markdownToText(md) {
// Remove code blocks
md = md.replace(/```[\s\S]*?```/g, '');
// Remove inline code
md = md.replace(/`[^`]*`/g, '');
// Remove images
md = md.replace(/!\[.*?\]\(.*?\)/g, '');
// Remove links but keep text
md = md.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1');
// Remove headings
md = md.replace(/^#{1,6}\s*/gm, '');
// Remove blockquotes
md = md.replace(/^>\s?/gm, '');
// Remove emphasis
md = md.replace(/(\*\*|__)(.*?)\1/g, '$2');
md = md.replace(/(\*|_)(.*?)\1/g, '$2');
// Remove lists
md = md.replace(/^\s*([-*+]|\d+\.)\s+/gm, '');
// Remove horizontal rules
md = md.replace(/^(-{3,}|\*{3,}|_{3,})$/gm, '');
// Remove HTML tags
md = md.replace(/<[^>]+>/g, '');
// Collapse whitespace
md = md.replace(/\s+/g, ' ').trim();
return md;
}
// Render notes table with data
function renderNotesTable(data) {
// Helper function to get translated category name
function getTranslatedCategory(categoryKey) {
if (window.categoryTranslations && window.categoryTranslations[categoryKey]) {
return window.categoryTranslations[categoryKey];
}
return categoryKey || '';
}
var tbody = '';
if (data.length === 0) {
tbody = '<tr><td colspan="5" class="text-center text-muted">' + lang_notes_not_found + '</td></tr>';
} else {
data.forEach(function(note) {
// Strip HTML/Markdown and truncate to 100 chars for tooltip
var rawContent = note.note ? note.note : '';
// Use a more robust Markdown-to-text conversion
var plainContent = markdownToText(rawContent);
// Truncate to 100 chars
var preview = plainContent.length > 100 ? plainContent.substring(0, 100) + '…' : plainContent;
tbody += '<tr>' +
'<td class="text-center">' + getTranslatedCategory(note.cat) + '</td>' +
'<td class="text-start"><a href="' + base_url + 'index.php/notes/view/' + (note.id ? note.id : '') + '" title="' + preview.replace(/&/g, '&amp;').replace(/"/g, '&quot;') + '" data-bs-toggle="tooltip">' + (note.title ? note.title : '') + '</a></td>' +
'<td class="text-center" data-utc="' + (note.creation_date ? note.creation_date : '') + '"></td>' +
'<td class="text-center" data-utc="' + (note.last_modified ? note.last_modified : '') + '"></td>' +
'<td class="text-center">' +
'<div class="btn-group btn-group-sm" role="group">' +
'<a href="' + base_url + 'index.php/notes/view/' + (note.id ? note.id : '') + '" class="btn btn-info" title="View"><i class="fa fa-eye"></i></a>' +
'<a href="' + base_url + 'index.php/notes/edit/' + (note.id ? note.id : '') + '" class="btn btn-primary" title="Edit"><i class="fa fa-pencil"></i></a>' +
(note.cat === 'Contacts'
? '<button type="button" class="btn btn-secondary" title="' + lang_notes_duplication_disabled + '" disabled data-bs-toggle="tooltip"><i class="fa fa-copy"></i></button>'
: '<button type="button" class="btn btn-secondary" title="' + lang_notes_duplicate + '" onclick="confirmDuplicateNote(\'' + (note.id ? note.id : '') + '\')"><i class="fa fa-copy"></i></button>'
) +
'<button type="button" class="btn btn-danger" title="' + lang_general_word_delete + '" onclick="confirmDeleteNote(\'' + (note.id ? note.id : '') + '\')"><i class="fa fa-trash"></i></button>' +
'</div>' +
'</td>' +
'</tr>';
});
}
if (domCache.notesTableBody) {
domCache.notesTableBody.innerHTML = tbody;
// After rendering, initialize table elements
initializeTableElements();
}
updateSortIndicators();
}
// Modal confirmation for delete and duplicate
window.confirmDeleteNote = function(noteId) {
showBootstrapModal(lang_notes_delete, lang_notes_delete_confirmation, function() {
deleteNote(noteId);
});
};
window.confirmDuplicateNote = function(noteId) {
var note = (window.lastNotesData || []).find(function(n) { return n.id == noteId; });
if (note && note.cat === 'Contacts') {
showBootstrapModal(lang_notes_duplication_disabled_short, lang_notes_duplication_disabled, function(){});
return;
}
showBootstrapModal(lang_notes_duplicate, lang_notes_duplicate_confirmation, function() {
duplicateNote(noteId);
});
};
// Actions for delete and duplicate
function deleteNote(noteId) {
fetch(base_url + 'index.php/notes/delete/' + noteId, { method: 'POST' })
.then(() => {
// Check if we need to go to previous page after deletion
// If we're on the last page and it only has 1 item, go back one page
var currentPageItemCount = window.lastNotesData ? window.lastNotesData.length : 0;
if (currentPage > 1 && currentPageItemCount === 1) {
currentPage = currentPage - 1;
}
performNotesSearch();
reloadCategoryCounters();
});
}
// Duplicate note via POST with timestamp
function duplicateNote(noteId) {
// Get local timestamp
var now = new Date();
var timestamp = now.toLocaleString();
var formData = new FormData();
formData.append('timestamp', timestamp);
fetch(base_url + 'index.php/notes/duplicate/' + noteId, {
method: 'POST',
body: formData
})
.then(() => {
performNotesSearch();
reloadCategoryCounters();
});
}
// Bootstrap modal helper
function showBootstrapModal(title, message, onConfirm) {
var modalId = 'confirmModal_' + Math.random().toString(36).substr(2, 9);
var modalHtml = '<div class="modal fade" id="' + modalId + '" tabindex="-1" role="dialog" data-bs-backdrop="static">' +
'<div class="modal-dialog" role="document">' +
'<div class="modal-content">' +
'<div class="modal-header"><h5 class="modal-title">' + title + '</h5></div>' +
'<div class="modal-body"><p>' + message + '</p></div>' +
'<div class="modal-footer justify-content-end">' +
'<button type="button" class="btn btn-primary me-2" id="confirmModalBtn_' + modalId + '">' + lang_general_word_ok + '</button>' +
'<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">' + lang_general_word_cancel + '</button>' +
'</div></div></div></div>';
var modalDiv = document.createElement('div');
modalDiv.innerHTML = modalHtml;
document.body.appendChild(modalDiv);
var modalEl = modalDiv.querySelector('.modal');
var modal;
try {
modal = new bootstrap.Modal(modalEl, { backdrop: 'static' });
modal.show();
} catch (e) {
document.body.removeChild(modalDiv);
if (confirm(message)) {
onConfirm();
}
return;
}
modalDiv.querySelector('#confirmModalBtn_' + modalId).onclick = function() {
modal.hide();
setTimeout(function() { document.body.removeChild(modalDiv); }, 300);
onConfirm();
};
modalDiv.querySelector('[data-bs-dismiss="modal"]').onclick = function() {
modal.hide();
setTimeout(function() { document.body.removeChild(modalDiv); }, 300);
};
}
// Patch performNotesSearch to use server-side pagination and sorting
performNotesSearch = function() {
var searchTerm = domCache.searchBox ? domCache.searchBox.value.trim() : '';
var selectedCat = getActiveCategory();
var sortColIdx = sortState.column;
var sortDir = sortState.direction;
var sortCol = (sortColIdx !== null && SORT_COLUMN_MAP[sortColIdx]) ? SORT_COLUMN_MAP[sortColIdx] : null;
var formData = new FormData();
formData.append('cat', selectedCat);
formData.append('search', searchTerm.length >= SEARCH_MIN_LENGTH ? searchTerm : '');
formData.append('page', currentPage);
formData.append('per_page', NOTES_PER_PAGE);
formData.append('sort_col', sortCol || '');
formData.append('sort_dir', sortDir || '');
fetch(base_url + 'index.php/notes/search', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(resp => {
var data = (resp && Array.isArray(resp.notes)) ? resp.notes : [];
window.lastNotesData = data;
lastResponseTotal = resp.total || 0;
totalPages = Math.max(1, Math.ceil(lastResponseTotal / NOTES_PER_PAGE));
if (currentPage > totalPages) currentPage = totalPages;
renderNotesTable(data);
renderPagination();
reloadCategoryCounters();
})
.catch(error => {
if (domCache.notesTableBody) {
domCache.notesTableBody.innerHTML = '<tr><td colspan="5">' + lang_notes_error_loading + ':' + error.message + '</td></tr>';
}
if (domCache.paginationContainer) {
domCache.paginationContainer.innerHTML = '';
}
});
};
// Reset to first page on search, sort, or category change
if (domCache.categoryButtons && domCache.notesTableBody) {
domCache.categoryButtons.forEach(function(btn) {
btn.addEventListener('click', function() {
domCache.categoryButtons.forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
currentPage = 1;
performNotesSearch();
});
});
}
if (domCache.searchBox) {
domCache.searchBox.addEventListener('input', function() {
currentPage = 1;
performNotesSearch();
});
}
if (domCache.resetBtn) {
domCache.resetBtn.addEventListener('click', function() {
if (domCache.searchBox) domCache.searchBox.value = '';
currentPage = 1;
performNotesSearch();
});
}
// Initial render - only if we have the necessary elements
if (domCache.notesTableBody) {
performNotesSearch();
}
});