mirror of
https://gh.wpcy.net/https://github.com/stingray82/MainWP-Client-Notes-For-Pro-Report.git
synced 2026-04-29 11:10:23 +08:00
- Added strict Y-m-d validation to save handler to prevent invalid manual date entries. - Improved DB query safety: • ajax_load_work_note_action() now uses a single, clean $wpdb->prepare(). • Switched delete handler to use $wpdb->delete(). - Polished migration toolbar JS injection for reliability. - General code cleanup for consistency and maintainability. Changelog: * Added: Strict date format validation when saving notes, blocking invalid manual entries. * Improved: Database query safety for loading and deleting notes. * Improved: Migration toolbar JS injection reliability. * Cleaned: General code formatting and minor logic refinements.
753 lines
30 KiB
PHP
753 lines
30 KiB
PHP
<?php
|
||
namespace MainWP\Dashboard;
|
||
|
||
/**
|
||
* Handles Work Notes functionality: UI, DB storage, AJAX, and migration.
|
||
*/
|
||
class MainWP_Work_Notes {
|
||
|
||
const CLEANUP_REMOVE_MIGRATION_LOGIC_VERSION = '1.3.2';
|
||
const CLEANUP_DELETE_LEGACY_OPTIONS_VERSION = '1.3.4';
|
||
|
||
/**
|
||
* Initialize plugin hooks.
|
||
*/
|
||
public static function init() {
|
||
// Add subpage for each child site
|
||
add_filter('mainwp_getsubpages_sites', [__CLASS__, 'add_sub_menu'], 10, 1);
|
||
|
||
// Enqueue assets
|
||
add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue_assets']);
|
||
|
||
// Core AJAX handlers
|
||
add_action('wp_ajax_save_work_note', [__CLASS__, 'ajax_save_work_note_action']);
|
||
add_action('wp_ajax_delete_work_note', [__CLASS__, 'ajax_delete_work_note_action']);
|
||
add_action('wp_ajax_load_work_note', [__CLASS__, 'ajax_load_work_note_action']);
|
||
add_action('wp_ajax_load_work_notes_form', [__CLASS__, 'ajax_load_work_notes_form']);
|
||
|
||
// === Migration Hooks (Temporary, can be removed in a future version) ===
|
||
add_action('admin_init', function () {
|
||
// Only run if we're in admin and can manage
|
||
if ( ! is_admin() || ! current_user_can('manage_options') ) {
|
||
return;
|
||
}
|
||
|
||
$plugin_ver = defined('RUP_MAINWP_CLIENT_NOTES_VERSION')
|
||
? RUP_MAINWP_CLIENT_NOTES_VERSION
|
||
: '0.0.0';
|
||
|
||
// Handle first-run migration (until 1.3.2)
|
||
if (
|
||
version_compare($plugin_ver, self::CLEANUP_REMOVE_MIGRATION_LOGIC_VERSION, '<') &&
|
||
! get_option('mainwp_work_notes_migrated')
|
||
) {
|
||
self::maybe_auto_migrate_legacy_notes();
|
||
}
|
||
|
||
// Cleanup legacy options in 1.3.4+
|
||
if (
|
||
version_compare($plugin_ver, self::CLEANUP_DELETE_LEGACY_OPTIONS_VERSION, '>=') &&
|
||
get_option('mainwp_work_notes_migrated')
|
||
) {
|
||
self::maybe_delete_legacy_options();
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* === Migration Logic (Temporary, can be removed in a future version) ===
|
||
*/
|
||
|
||
|
||
private static function maybe_auto_migrate_legacy_notes() {
|
||
self::create_work_notes_table();
|
||
global $wpdb;
|
||
$legacy_notes_exist = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE 'mainwp_work_notes_%'");
|
||
if ($legacy_notes_exist > 0) {
|
||
self::migrate_work_notes_to_db();
|
||
}
|
||
update_option('mainwp_work_notes_migrated', true);
|
||
|
||
add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue_migration_js']);
|
||
add_action('admin_bar_menu', [__CLASS__, 'maybe_add_migration_toolbar_link'], 100);
|
||
add_action('wp_ajax_mainwp_migrate_work_notes', [__CLASS__, 'ajax_migrate_work_notes']);
|
||
}
|
||
|
||
|
||
/**
|
||
* Show migration button in admin bar if migration hasn't run.
|
||
*/
|
||
public static function maybe_add_migration_toolbar_link($wp_admin_bar) {
|
||
if (!current_user_can('manage_options') || get_option('mainwp_work_notes_migrated')) return;
|
||
|
||
$wp_admin_bar->add_node([
|
||
'id' => 'mainwp_migrate_notes',
|
||
'title' => 'Migrate Work Notes',
|
||
'href' => '#',
|
||
'meta' => ['title' => 'Migrate Work Notes', 'class' => 'mainwp-migrate-notes-toolbar']
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Enqueue JS to handle migration button click.
|
||
*/
|
||
public static function enqueue_migration_js() {
|
||
if (!current_user_can('manage_options') || get_option('mainwp_work_notes_migrated')) return;
|
||
|
||
wp_add_inline_script('jquery-core', "
|
||
jQuery(document).ready(function($) {
|
||
$('.mainwp-migrate-notes-toolbar a').on('click', function(e) {
|
||
e.preventDefault();
|
||
if (!confirm('Are you sure you want to migrate existing work notes to the database?')) return;
|
||
|
||
$.post(ajaxurl, {
|
||
action: 'mainwp_migrate_work_notes',
|
||
nonce: '" . wp_create_nonce('work_notes_migrate') . "'
|
||
}, function(response) {
|
||
if (response.success) {
|
||
alert(response.data.message);
|
||
location.reload();
|
||
} else {
|
||
alert('Migration failed: ' + response.data.message);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
");
|
||
}
|
||
|
||
/**
|
||
* Handle AJAX-based work note migration.
|
||
*/
|
||
public static function ajax_migrate_work_notes() {
|
||
check_ajax_referer('work_notes_migrate', 'nonce');
|
||
|
||
if (!current_user_can('manage_options')) {
|
||
wp_send_json_error(['message' => 'Permission denied']);
|
||
}
|
||
|
||
// Call static migrate method from work notes class
|
||
if (class_exists('\MainWP\Dashboard\MainWP_Work_Notes')) {
|
||
\MainWP\Dashboard\MainWP_Work_Notes::migrate_work_notes_to_db();
|
||
update_option('mainwp_work_notes_migrated', true);
|
||
wp_send_json_success(['message' => 'Work notes migrated successfully.']);
|
||
}
|
||
|
||
wp_send_json_error(['message' => 'Migration class not found.']);
|
||
}
|
||
|
||
//Legacy Clean up
|
||
public static function maybe_delete_legacy_options() {
|
||
global $wpdb;
|
||
|
||
$prefix = 'mainwp_work_notes_%';
|
||
$wpdb->query(
|
||
$wpdb->prepare(
|
||
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||
$prefix
|
||
)
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* === End of Migration Logic ===
|
||
*/
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Create the custom database table for storing work notes.
|
||
*/
|
||
public static function create_work_notes_table() {
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'mainwp_work_notes';
|
||
$charset = $wpdb->get_charset_collate();
|
||
|
||
$sql = "CREATE TABLE IF NOT EXISTS $table (
|
||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||
site_id BIGINT UNSIGNED NOT NULL,
|
||
work_date DATE NOT NULL,
|
||
content LONGTEXT NOT NULL,
|
||
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id),
|
||
INDEX (site_id),
|
||
INDEX (work_date)
|
||
) $charset;";
|
||
|
||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||
dbDelta($sql);
|
||
}
|
||
|
||
/**
|
||
* Migrate old work notes from wp_options into the dedicated database table.
|
||
*/
|
||
public static function migrate_work_notes_to_db() {
|
||
global $wpdb;
|
||
|
||
// Ensure table exists before inserting data
|
||
self::create_work_notes_table();
|
||
|
||
$prefix = 'mainwp_work_notes_';
|
||
$table = $wpdb->prefix . 'mainwp_work_notes';
|
||
$options = $wpdb->get_results("SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE '{$prefix}%'");
|
||
|
||
foreach ($options as $option) {
|
||
$site_id = (int) str_replace($prefix, '', $option->option_name);
|
||
$notes = maybe_unserialize($option->option_value);
|
||
|
||
if (!is_array($notes)) continue;
|
||
|
||
foreach ($notes as $note) {
|
||
if (empty($note['date']) || empty($note['content'])) continue;
|
||
|
||
// Prevent duplicate migrations
|
||
$exists = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT COUNT(*) FROM $table WHERE site_id = %d AND work_date = %s AND content = %s",
|
||
$site_id, $note['date'], $note['content']
|
||
));
|
||
|
||
if (!$exists) {
|
||
$wpdb->insert($table, [
|
||
'site_id' => $site_id,
|
||
'work_date' => sanitize_text_field($note['date']),
|
||
'content' => wp_kses_post($note['content']),
|
||
'timestamp' => isset($note['timestamp']) ? date('Y-m-d H:i:s', $note['timestamp']) : current_time('mysql'),
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Add "Work Notes" tab to each child site.
|
||
*/
|
||
public static function add_sub_menu($subArray) {
|
||
$subArray[] = [
|
||
'title' => 'Work Notes',
|
||
'slug' => 'WorkNotes',
|
||
'sitetab' => true,
|
||
'menu_hidden' => true,
|
||
'callback' => [__CLASS__, 'render']
|
||
];
|
||
return $subArray;
|
||
}
|
||
|
||
/**
|
||
* Enqueue editor, Flatpickr, and JS assets.
|
||
*/
|
||
public static function enqueue_assets() {
|
||
wp_enqueue_editor();
|
||
wp_enqueue_style('flatpickr-css', 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css');
|
||
wp_enqueue_script('flatpickr-js', 'https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.js', [], null, true);
|
||
wp_enqueue_script('mainwp-work-notes-js', plugins_url('mainwp-work-notes.js', __FILE__), ['jquery', 'flatpickr-js'], null, true);
|
||
wp_localize_script('mainwp-work-notes-js', 'mainwpWorkNotes', [
|
||
'ajax_url' => admin_url('admin-ajax.php'),
|
||
'nonce' => wp_create_nonce('work_notes_nonce'),
|
||
'date_format' => self::get_js_date_format(),
|
||
'today' => current_time('Y-m-d')
|
||
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Convert WP date format to Flatpickr-compatible format.
|
||
*/
|
||
private static function get_js_date_format() {
|
||
// Map a reasonable subset of WP date tokens to Flatpickr
|
||
$php = get_option('date_format');
|
||
$map = [
|
||
// Months
|
||
'F' => 'F', 'M' => 'M', 'm' => 'm', 'n' => 'n',
|
||
// Days
|
||
'd' => 'd', 'j' => 'j',
|
||
// Years
|
||
'Y' => 'Y', 'y' => 'y',
|
||
// Ordinal day (WP’s jS) -> Flatpickr doesn’t support ordinals; fall back to j
|
||
'S' => '', // strip the ordinal suffix
|
||
];
|
||
// naive transliteration: replace jS with j first, then map rest
|
||
$php = preg_replace('/jS/', 'j', $php);
|
||
return strtr($php, $map);
|
||
}
|
||
|
||
|
||
/**
|
||
* AJAX: Save or update a work note.
|
||
*/
|
||
public static function ajax_save_work_note_action() {
|
||
check_ajax_referer('work_notes_nonce', 'nonce');
|
||
if (!current_user_can('manage_options')) wp_send_json_error(['message' => 'Insufficient permissions.']);
|
||
|
||
global $wpdb;
|
||
$table = $wpdb->prefix . 'mainwp_work_notes';
|
||
|
||
$site_id = isset($_POST['wpid']) ? intval($_POST['wpid']) : 0;
|
||
$note_id = isset($_POST['note_id']) ? intval($_POST['note_id']) : -1;
|
||
$date = sanitize_text_field($_POST['work_notes_date']);
|
||
// Validate strict Y-m-d aiming for consisant storage here.
|
||
$dt = \DateTime::createFromFormat('Y-m-d', $date);
|
||
$valid = $dt && $dt->format('Y-m-d') === $date;
|
||
if ( ! $valid ) {
|
||
wp_send_json_error(['message' => 'Invalid date format. Please use the date picker.']);
|
||
}
|
||
$content = wp_kses_post($_POST['work_notes_content']);
|
||
|
||
if (!$site_id || !$date) wp_send_json_error(['message' => 'Missing data.']);
|
||
|
||
if ($note_id > 0) {
|
||
$wpdb->update($table, [
|
||
'work_date' => $date,
|
||
'content' => $content
|
||
], ['id' => $note_id]);
|
||
} else {
|
||
$wpdb->insert($table, [
|
||
'site_id' => $site_id,
|
||
'work_date' => $date,
|
||
'content' => $content,
|
||
'timestamp' => current_time('mysql')
|
||
]);
|
||
$note_id = $wpdb->insert_id;
|
||
}
|
||
|
||
wp_send_json_success(['message' => 'Note saved successfully.', 'note_id' => $note_id]);
|
||
}
|
||
|
||
/**
|
||
* AJAX: Delete a work note.
|
||
*/
|
||
public static function ajax_delete_work_note_action() {
|
||
check_ajax_referer('work_notes_nonce', 'nonce');
|
||
if (!current_user_can('manage_options')) wp_send_json_error(['message' => 'Insufficient permissions.']);
|
||
|
||
global $wpdb;
|
||
$id = isset($_POST['note_id']) ? intval($_POST['note_id']) : 0;
|
||
$table = $wpdb->prefix . 'mainwp_work_notes';
|
||
if ($id > 0) {
|
||
$wpdb->delete($table, ['id' => $id]);
|
||
wp_send_json_success(['message' => 'Note deleted successfully.']);
|
||
}
|
||
|
||
wp_send_json_error(['message' => 'Invalid note ID.']);
|
||
}
|
||
|
||
/**
|
||
* AJAX: Load a specific note for editing.
|
||
*/
|
||
public static function ajax_load_work_note_action() {
|
||
check_ajax_referer('work_notes_nonce', 'nonce');
|
||
if ( ! current_user_can('manage_options') ) {
|
||
wp_send_json_error(['message' => 'Insufficient permissions.']);
|
||
}
|
||
|
||
global $wpdb;
|
||
$id = isset($_POST['note_id']) ? absint($_POST['note_id']) : 0;
|
||
|
||
$table = $wpdb->prefix . 'mainwp_work_notes';
|
||
$note = $wpdb->get_row(
|
||
$wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id),
|
||
ARRAY_A
|
||
);
|
||
|
||
if ( $note ) {
|
||
wp_send_json_success([
|
||
'date' => $note['work_date'],
|
||
'content' => $note['content'],
|
||
]);
|
||
} else {
|
||
wp_send_json_error(['message' => 'Note not found.']);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* AJAX: Load the full table of notes (after save/delete).
|
||
*/
|
||
public static function ajax_load_work_notes_form() {
|
||
check_ajax_referer('work_notes_nonce', 'nonce');
|
||
if (!current_user_can('manage_options')) wp_send_json_error(['message' => 'Insufficient permissions.']);
|
||
|
||
global $wpdb;
|
||
$site_id = isset($_POST['site_id']) ? intval($_POST['site_id']) : 0;
|
||
|
||
$notes = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}mainwp_work_notes WHERE site_id = %d ORDER BY work_date DESC",
|
||
$site_id
|
||
));
|
||
|
||
ob_start();
|
||
echo '<tbody>';
|
||
foreach ($notes as $note) {
|
||
$formatted_date = date_i18n(get_option('date_format'), strtotime($note->work_date));
|
||
echo '<tr data-note-id="' . esc_attr($note->id) . '">';
|
||
echo '<td>' . esc_html($formatted_date) . '</td>';
|
||
echo '<td>' . wp_kses_post($note->content) . '</td>';
|
||
echo '<td>';
|
||
echo '<button class="ui button blue edit-note" data-note-id="' . esc_attr($note->id) . '">Edit</button> ';
|
||
echo '<button class="ui button red delete-note" data-note-id="' . esc_attr($note->id) . '">Delete</button>';
|
||
echo '</td>';
|
||
echo '</tr>';
|
||
}
|
||
echo '</tbody>';
|
||
|
||
wp_send_json_success(['html' => ob_get_clean()]);
|
||
}
|
||
|
||
/**
|
||
* Render the full Work Notes UI.
|
||
*/
|
||
public static function render() {
|
||
do_action('mainwp_pageheader_sites');
|
||
|
||
$current_wpid = MainWP_System_Utility::get_current_wpid();
|
||
if (!MainWP_Utility::ctype_digit($current_wpid)) return;
|
||
|
||
global $wpdb;
|
||
$notes = $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}mainwp_work_notes WHERE site_id = %d ORDER BY work_date DESC",
|
||
$current_wpid
|
||
));
|
||
|
||
echo '<div id="mainwp_tab_WorkNotes_container" class="ui segment">';
|
||
echo '<div class="mainwp-work-note-message" style="display:none;"></div>';
|
||
echo '<form id="work-notes-form" class="ui form" style="padding: 20px; max-width: 95%; margin: 0 auto;">';
|
||
echo '<input type="hidden" name="wpid" value="' . esc_attr($current_wpid) . '">';
|
||
echo '<input type="hidden" name="note_id" value="-1">';
|
||
|
||
$current_date = current_time('Y-m-d');
|
||
echo '<div class="field"><label for="work_notes_date">Work Date:</label>';
|
||
echo '<input type="text" id="work_notes_date" name="work_notes_date" value="' . esc_attr($current_date) . '" required style="width: 100%;"></div>';
|
||
|
||
echo '<div class="field"><label for="work_notes_content">Work Details:</label>';
|
||
ob_start();
|
||
wp_editor('', 'work_notes_content', [
|
||
'textarea_name' => 'work_notes_content',
|
||
'textarea_rows' => 10,
|
||
'media_buttons' => true,
|
||
'tinymce' => true,
|
||
'quicktags' => true,
|
||
]);
|
||
echo ob_get_clean();
|
||
echo '</div>';
|
||
echo '<button type="button" id="save-work-note" class="ui button green">Save Work Note</button>';
|
||
echo '</form>';
|
||
|
||
echo '<h3 class="ui dividing header">Existing Work Notes</h3>';
|
||
echo '<table class="ui celled table"><thead><tr><th>Date</th><th>Details</th><th>Actions</th></tr></thead><tbody>';
|
||
foreach ($notes as $note) {
|
||
$formatted_date = date_i18n(get_option('date_format'), strtotime($note->work_date));
|
||
echo '<tr data-note-id="' . esc_attr($note->id) . '">';
|
||
echo '<td>' . esc_html($formatted_date) . '</td>';
|
||
echo '<td>' . wp_kses_post($note->content) . '</td>';
|
||
echo '<td>';
|
||
echo '<button class="ui button blue edit-note" data-note-id="' . esc_attr($note->id) . '">Edit</button> ';
|
||
echo '<button class="ui button red delete-note" data-note-id="' . esc_attr($note->id) . '">Delete</button>';
|
||
echo '</td>';
|
||
echo '</tr>';
|
||
}
|
||
echo '</tbody></table></div>';
|
||
|
||
do_action('mainwp_pagefooter_sites');
|
||
}
|
||
}
|
||
|
||
|
||
//MainWP_Work_Notes::init();
|
||
\MainWP\Dashboard\MainWP_Work_Notes::init();
|
||
|
||
|
||
/**
|
||
* Handles Pro Reports integration for work notes.
|
||
*/
|
||
class MainWP_Work_Notes_Pro_Reports {
|
||
|
||
/**
|
||
* Register custom token filters.
|
||
*/
|
||
public static function init() {
|
||
add_filter('mainwp_pro_reports_custom_tokens', [__CLASS__, 'generate_work_notes_tokens'], 10, 4);
|
||
add_filter('mainwp_client_reports_custom_tokens', [__CLASS__, 'client_reports_custom_tokens'], 10, 3);
|
||
}
|
||
|
||
/**
|
||
* Hook entry: enrich $tokensValues with all work-notes tokens.
|
||
*
|
||
* This simply delegates to generate_work_notes_tokens(), which is the single
|
||
* source of truth for adding tokens like:
|
||
* - [client.customwork.notes] (legacy)
|
||
* - [client.customwork.notes_table] (class-based)
|
||
* - [client.customwork.notes_email] (inline/email)
|
||
* …and any future tokens you add later.
|
||
*
|
||
* @param array $tokensValues
|
||
* @param object $report
|
||
* @param array $site
|
||
* @return array Updated $tokensValues containing all work-notes tokens.
|
||
*/
|
||
public static function client_reports_custom_tokens( $tokensValues, $report, $site ) {
|
||
// Delegate: this will add ALL related tokens now and in the future.
|
||
$tokensValues = self::generate_work_notes_tokens( $tokensValues, $report, $site, null );
|
||
return $tokensValues;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Fallback wrapper for string-returning hooks. (Not Currently used not sure about non pro report tokens so have added just incase)
|
||
* Prefers the class-based token, then email, then legacy.
|
||
*/
|
||
public static function client_reports_custom_tokens_string( $tokensValues, $report, $site ) {
|
||
$tokensValues = self::generate_work_notes_tokens( $tokensValues, $report, $site, null );
|
||
|
||
// Pick best-available token without hardcoding existence.
|
||
$preferred = array(
|
||
'[client.customwork.notes_table]',
|
||
'[client.customwork.notes_email]',
|
||
'[client.customwork.notes]',
|
||
);
|
||
foreach ( $preferred as $token ) {
|
||
if ( isset( $tokensValues[ $token ] ) ) {
|
||
return $tokensValues[ $token ];
|
||
}
|
||
}
|
||
// Nothing matched; return empty string to be safe.
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Generate work notes tokens.
|
||
*
|
||
* Tokens produced:
|
||
* - [client.customwork.notes] (legacy, inline <table> w/ minimal styles — unchanged)
|
||
* - [client.customwork.notes_table] (class-based, CSS-stylable wrapper/table)
|
||
* - [client.customwork.notes_email] (email-safe inline styles, now with modes)
|
||
*
|
||
* Email modes:
|
||
* Core (built-in): default | compact | bordered
|
||
* Custom (via filter): add/override with `mainwp_client_notes_email_modes`
|
||
*
|
||
* Usage in email templates:
|
||
* [client.customwork.notes_email] -> default mode
|
||
* [client.customwork.notes_email mode="compact"] -> compact mode
|
||
* [client.customwork.notes_email mode="bordered"] -> bordered mode
|
||
* [client.customwork.notes_email mode="my_brand_mode"] -> (if provided by filter)
|
||
*/
|
||
public static function generate_work_notes_tokens( $tokensValues, $report, $site, $templ_email ) {
|
||
// Site/date guards
|
||
$site_id = isset( $site['id'] ) ? (int) $site['id'] : 0;
|
||
if ( ! $site_id ) return $tokensValues;
|
||
|
||
$from_date = isset( $report->date_from ) ? date( 'Y-m-d', $report->date_from ) : '';
|
||
$to_date = isset( $report->date_to ) ? date( 'Y-m-d', $report->date_to ) : '';
|
||
if ( ! $from_date || ! $to_date ) return $tokensValues;
|
||
|
||
// Fetch notes
|
||
$notes = self::get_work_notes( $site_id, $from_date, $to_date );
|
||
|
||
// Labels / visibility (keys: date, content)
|
||
$columns = apply_filters( 'mainwp_client_notes_columns', array(
|
||
'date' => esc_html__( 'Date', 'mainwp-client-notes-pro-reports-extention' ),
|
||
'content' => esc_html__( 'Work Details', 'mainwp-client-notes-pro-reports-extention' ),
|
||
) );
|
||
|
||
// Helpers
|
||
$format_note_date = static function( $raw_date ) {
|
||
return date_i18n( get_option( 'date_format' ), strtotime( $raw_date ) );
|
||
};
|
||
$cell_html = static function( $key, $note, $formatted_date ) {
|
||
$value = ( 'date' === $key ) ? esc_html( $formatted_date ) : wp_kses_post( $note->content );
|
||
return apply_filters( 'mainwp_client_notes_cell_content', $value, $key, $note );
|
||
};
|
||
|
||
// No notes -> fill all tokens with message
|
||
if ( empty( $notes ) ) {
|
||
$no_notes = __( 'No work notes found within the selected date range.', 'mainwp-client-notes-pro-reports-extention' );
|
||
$tokensValues['[client.customwork.notes]'] = $no_notes;
|
||
$tokensValues['[client.customwork.notes_table]'] = $no_notes;
|
||
$tokensValues['[client.customwork.notes_email]'] = esc_html( $no_notes );
|
||
return $tokensValues;
|
||
}
|
||
|
||
// =========================
|
||
// Legacy (unchanged)
|
||
// =========================
|
||
$legacy = '<table style="width: 100%; border-collapse: collapse;" border="1">';
|
||
$legacy .= '<thead><tr>';
|
||
if ( isset( $columns['date'] ) ) { $legacy .= '<th>' . esc_html( $columns['date'] ) . '</th>'; }
|
||
if ( isset( $columns['content'] ) ) { $legacy .= '<th>' . esc_html( $columns['content'] ) . '</th>'; }
|
||
$legacy .= '</tr></thead><tbody>';
|
||
foreach ( $notes as $note ) {
|
||
$formatted_date = $format_note_date( $note->work_date );
|
||
$legacy .= '<tr>';
|
||
if ( isset( $columns['date'] ) ) { $legacy .= '<td>' . $cell_html( 'date', $note, $formatted_date ) . '</td>'; }
|
||
if ( isset( $columns['content'] ) ) { $legacy .= '<td>' . $cell_html( 'content', $note, $formatted_date ) . '</td>'; }
|
||
$legacy .= '</tr>';
|
||
}
|
||
$legacy .= '</tbody></table>';
|
||
$tokensValues['[client.customwork.notes]'] = $legacy;
|
||
|
||
// =========================
|
||
// Class-based (CSS-stylable)
|
||
// =========================
|
||
$classes = apply_filters( 'mainwp_client_notes_table_classes', array(
|
||
'wrapper' => 'client-notes',
|
||
'table' => 'client-notes__table',
|
||
'date' => 'client-notes__date',
|
||
'content' => 'client-notes__content',
|
||
) );
|
||
$attrs = apply_filters( 'mainwp_client_notes_table_attributes', array(
|
||
'wrapper' => array(),
|
||
'table' => array(),
|
||
) );
|
||
$attr_to_html = static function( $arr ) {
|
||
if ( empty( $arr ) || ! is_array( $arr ) ) return '';
|
||
$buf = '';
|
||
foreach ( $arr as $k => $v ) {
|
||
if ( $v === '' || $v === null ) continue;
|
||
$buf .= ' ' . esc_attr( $k ) . '="' . esc_attr( $v ) . '"';
|
||
}
|
||
return $buf;
|
||
};
|
||
|
||
$modern = '<div class="' . esc_attr( $classes['wrapper'] ) . '"' . $attr_to_html( $attrs['wrapper'] ) . '>';
|
||
$modern .= '<table class="' . esc_attr( $classes['table'] ) . '"' . $attr_to_html( $attrs['table'] ) . '>';
|
||
$modern .= '<thead><tr>';
|
||
if ( isset( $columns['date'] ) ) { $modern .= '<th>' . esc_html( $columns['date'] ) . '</th>'; }
|
||
if ( isset( $columns['content'] ) ) { $modern .= '<th>' . esc_html( $columns['content'] ) . '</th>'; }
|
||
$modern .= '</tr></thead><tbody>';
|
||
foreach ( $notes as $note ) {
|
||
$formatted_date = $format_note_date( $note->work_date );
|
||
$modern .= '<tr>';
|
||
if ( isset( $columns['date'] ) ) { $modern .= '<td class="' . esc_attr( $classes['date'] ) . '">' . $cell_html( 'date', $note, $formatted_date ) . '</td>'; }
|
||
if ( isset( $columns['content'] ) ) { $modern .= '<td class="' . esc_attr( $classes['content'] ) . '">' . $cell_html( 'content', $note, $formatted_date ) . '</td>'; }
|
||
$modern .= '</tr>';
|
||
}
|
||
$modern .= '</tbody></table></div>';
|
||
$tokensValues['[client.customwork.notes_table]'] = $modern;
|
||
|
||
// =========================
|
||
// Email-safe token (inline)
|
||
// Modes are selected via filters (no token attributes).
|
||
// Core modes: default | compact | bordered
|
||
// Add/override modes: mainwp_client_notes_email_modes
|
||
// Choose active mode: mainwp_client_notes_email_active_mode
|
||
// Per-row tweaks: mainwp_client_notes_email_row_styles
|
||
// Legacy overrides: mainwp_client_notes_email_styles (merged)
|
||
// =========================
|
||
|
||
$core_modes = array(
|
||
'default' => array(
|
||
'table' => 'border-collapse:collapse;border:1px solid #e5e7eb;width:100%;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;',
|
||
'th' => 'background:#f3f4f6;text-align:left;padding:10px 12px;border-bottom:1px solid #e5e7eb;',
|
||
'td' => 'padding:10px 12px;vertical-align:top;border-top:1px solid #f1f5f9;',
|
||
'date_td' => '',
|
||
'content_td' => '',
|
||
'odd_bg' => '#fafafa',
|
||
'even_bg' => '#ffffff',
|
||
'table_role' => 'presentation',
|
||
'hide_header'=> false,
|
||
),
|
||
'compact' => array(
|
||
'table' => 'border-collapse:collapse;border:1px solid #dddddd;width:100%;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:1.4;',
|
||
'th' => 'background:#f8f8f8;text-align:left;padding:6px 8px;border-bottom:1px solid #dddddd;',
|
||
'td' => 'padding:6px 8px;vertical-align:top;border-top:1px solid #eeeeee;',
|
||
'date_td' => 'width:120px;white-space:nowrap;',
|
||
'content_td' => '',
|
||
'odd_bg' => '#ffffff',
|
||
'even_bg' => '#fdfdfd',
|
||
'table_role' => 'presentation',
|
||
'hide_header'=> false,
|
||
),
|
||
'bordered' => array(
|
||
'table' => 'border-collapse:collapse;border:1px solid #cccccc;width:100%;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;',
|
||
'th' => '',
|
||
'td' => 'padding:10px 12px;vertical-align:top;border-top:1px solid #cccccc;',
|
||
'date_td' => 'font-weight:bold;white-space:nowrap;',
|
||
'content_td' => 'word-break:break-word;',
|
||
'odd_bg' => '#ffffff',
|
||
'even_bg' => '#ffffff',
|
||
'table_role' => 'presentation',
|
||
'hide_header'=> true,
|
||
),
|
||
);
|
||
|
||
// Add/override modes
|
||
$style_modes = apply_filters( 'mainwp_client_notes_email_modes', $core_modes );
|
||
|
||
// Pick active mode (per site/report if desired)
|
||
$active_mode = apply_filters( 'mainwp_client_notes_email_active_mode', 'default', $report, $site );
|
||
if ( empty( $active_mode ) || ! isset( $style_modes[ $active_mode ] ) ) {
|
||
$active_mode = 'default';
|
||
}
|
||
|
||
$email_styles = $style_modes[ $active_mode ];
|
||
|
||
// Legacy single-style override: merge over chosen mode
|
||
$legacy_override = apply_filters( 'mainwp_client_notes_email_styles', array() );
|
||
if ( is_array( $legacy_override ) && ! empty( $legacy_override ) ) {
|
||
$email_styles = array_merge( $email_styles, $legacy_override );
|
||
}
|
||
|
||
$hide_header = ! empty( $email_styles['hide_header'] );
|
||
|
||
// Build email table
|
||
$row = 0;
|
||
$email = '<table role="' . esc_attr( $email_styles['table_role'] ) . '" width="100%" cellpadding="0" cellspacing="0" style="' . esc_attr( $email_styles['table'] ) . '">';
|
||
if ( ! $hide_header ) {
|
||
$email .= '<thead><tr>';
|
||
if ( isset( $columns['date'] ) ) { $email .= '<th align="left" style="' . esc_attr( $email_styles['th'] ) . '">' . esc_html( $columns['date'] ) . '</th>'; }
|
||
if ( isset( $columns['content'] ) ) { $email .= '<th align="left" style="' . esc_attr( $email_styles['th'] ) . '">' . esc_html( $columns['content'] ) . '</th>'; }
|
||
$email .= '</tr></thead>';
|
||
}
|
||
$email .= '<tbody>';
|
||
foreach ( $notes as $note ) {
|
||
$row++;
|
||
$formatted_date = $format_note_date( $note->work_date );
|
||
|
||
$row_styles = apply_filters( 'mainwp_client_notes_email_row_styles', array(
|
||
'bg' => ( $row % 2 === 1 ) ? $email_styles['odd_bg'] : $email_styles['even_bg'],
|
||
), $row, $note );
|
||
|
||
$date_td_styles = trim( $email_styles['td'] . ( ! empty( $email_styles['date_td'] ) ? $email_styles['date_td'] : '' ) );
|
||
$content_td_styles = trim( $email_styles['td'] . ( ! empty( $email_styles['content_td'] ) ? $email_styles['content_td'] : '' ) );
|
||
|
||
$email .= '<tr>';
|
||
if ( isset( $columns['date'] ) ) { $email .= '<td style="background:' . esc_attr( $row_styles['bg'] ) . ';' . esc_attr( $date_td_styles ) . '">' . $cell_html( 'date', $note, $formatted_date ) . '</td>'; }
|
||
if ( isset( $columns['content'] ) ) { $email .= '<td style="background:' . esc_attr( $row_styles['bg'] ) . ';' . esc_attr( $content_td_styles ) . '">' . $cell_html( 'content', $note, $formatted_date ) . '</td>'; }
|
||
$email .= '</tr>';
|
||
}
|
||
$email .= '</tbody></table>';
|
||
|
||
$tokensValues['[client.customwork.notes_email]'] = $email;
|
||
|
||
return $tokensValues;
|
||
}
|
||
|
||
|
||
|
||
|
||
/**
|
||
* Get work notes from DB in a date range.
|
||
*/
|
||
public static function get_work_notes($site_id, $date_from, $date_to) {
|
||
global $wpdb;
|
||
|
||
return $wpdb->get_results($wpdb->prepare(
|
||
"SELECT * FROM {$wpdb->prefix}mainwp_work_notes WHERE site_id = %d AND work_date BETWEEN %s AND %s ORDER BY work_date ASC",
|
||
$site_id, $date_from, $date_to
|
||
));
|
||
}
|
||
}
|
||
|
||
MainWP_Work_Notes_Pro_Reports::init();
|
||
|