mirror of
https://github.com/wavelog/wavelog.git
synced 2026-03-22 10:24:14 +00:00
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:
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 } ?>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, '&').replace(/"/g, '"') + '" 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();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user