diff --git a/application/controllers/Notes.php b/application/controllers/Notes.php index ba27c2a00..991b2e591 100644 --- a/application/controllers/Notes.php +++ b/application/controllers/Notes.php @@ -1,119 +1,320 @@ 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'); - } -} \ No newline at end of file +} diff --git a/application/models/Note.php b/application/models/Note.php index 8027ab0d5..4939ba561 100644 --- a/application/models/Note.php +++ b/application/models/Note.php @@ -1,9 +1,25 @@ __("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 + ]; } } diff --git a/application/views/interface_assets/footer.php b/application/views/interface_assets/footer.php index 29d064690..a98bc81d4 100644 --- a/application/views/interface_assets/footer.php +++ b/application/views/interface_assets/footer.php @@ -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"); ?>"; + @@ -278,9 +291,11 @@ function stopImpersonate_modal() { -uri->segment(1) == "notes" && ($this->uri->segment(2) == "add" || $this->uri->segment(2) == "edit") ) { ?> +uri->segment(1) == "notes" ) { ?> + uri->segment(2) == "add" || $this->uri->segment(2) == "edit") { ?> + diff --git a/application/views/notes/add.php b/application/views/notes/add.php index 767b838dd..23df9cf51 100644 --- a/application/views/notes/add.php +++ b/application/views/notes/add.php @@ -1,52 +1,61 @@ - +
".__("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!")."
"; - } - - ?> -| = __("Category"); ?> | += __("Title"); ?> | += __("Creation"); ?> | += __("Last Modification"); ?> | += __("Actions"); ?> | +
|---|