last Code 12:14 1/5/2025 ইং
<?php
/*
Plugin Name: Business Seller Management
Plugin URI: https://www.gsmalo.com
Description: A plugin to manage your business sales, sellers, and transactions efficiently.
Version: 1.0
Author: GSM Abdullah Al Masud
Author URI: https://www.gsmalo.com
Text Domain: business-seller-management
*/
// Prevent direct access
if ( ! defined('ABSPATH') ) {
exit;
}
// Define plugin constants
define('BSM_PLUGIN_PATH', plugin_dir_path(__FILE__));
define('BSM_PLUGIN_URL', plugin_dir_url(__FILE__));
// ----- Include necessary files ----- //
// ---- Seller-related files FIRST (to define functions used by admin hooks) ----
require_once BSM_PLUGIN_PATH . 'includes/seller/status-update.php'; // Defines bsm_handle_gsmalo_balance_after_status_update & bsm_make_status_log_text
require_once BSM_PLUGIN_PATH . 'includes/seller/submit-sale.php';
require_once BSM_PLUGIN_PATH . 'includes/seller/notes.php'; // For AJAX note handling (used?)
require_once BSM_PLUGIN_PATH . 'includes/seller/statas-log-notes.php'; // Defines bsm_calculate_balance_change_for_status_transition & AJAX handlers
require_once BSM_PLUGIN_PATH . 'includes/point-system.php';
// Include Seller AJAX functions only on AJAX requests AFTER main seller files are included
if ( defined('DOING_AJAX') && DOING_AJAX ) {
require_once BSM_PLUGIN_PATH . 'includes/seller/ajax-functions.php';
}
// ---- Admin-related files SECOND ----
require_once BSM_PLUGIN_PATH . 'includes/admin/admin-dashboard.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/all-sales.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/all-logs.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/status.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/reports.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/settings.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/clear-tables.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/fix-database.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/role-permissions.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/update-status.php'; // Uses functions from seller/status-update.php via init hook
require_once BSM_PLUGIN_PATH . 'includes/admin/edit-sale.php'; // Uses functions from seller/status-update.php via init hook
require_once BSM_PLUGIN_PATH . 'includes/admin/advanced-error-guard.php';
require_once BSM_PLUGIN_PATH . 'includes/admin/balance-adjustment-handler.php'; // AJAX handler for manual balance adjustments
// ---- Seller Dashboard files (shortcodes for separate pages) ----
require_once BSM_PLUGIN_PATH . 'includes/seller/dashboard.php'; // [bsm_seller_dashboard_main]
require_once BSM_PLUGIN_PATH . 'includes/seller/sale.php'; // [bsm_seller_dashboard_sale]
require_once BSM_PLUGIN_PATH . 'includes/seller/report.php'; // [bsm_seller_dashboard_report]
require_once BSM_PLUGIN_PATH . 'includes/seller/settings.php'; // [bsm_seller_dashboard_settings]
// Note: Point report shortcode is registered below
// ----- Include the install-tables file (for automatic table/column creation) ----- //
require_once BSM_PLUGIN_PATH . 'includes/install-tables.php';
/**
* Hook to include the Balance Adjustments template in the admin context,
* after all pluggable functions (e.g. wp_create_nonce) are available.
*
* **Changed:** do NOT include on AJAX requests, to keep admin-ajax.php JSON‑only.
*/
function bsm_load_balance_adjustments_template() {
if (
is_admin()
&& ! ( defined('DOING_AJAX') && DOING_AJAX )
&& file_exists( BSM_PLUGIN_PATH . 'templates/admin/balance-adjustments.php' )
) {
require_once BSM_PLUGIN_PATH . 'templates/admin/balance-adjustments.php';
}
}
add_action( 'admin_init', 'bsm_load_balance_adjustments_template' );
/**
* Create the 'Seller' role with minimal capabilities.
*/
function bsm_add_seller_role() {
add_role(
'seller',
'Seller',
array(
'read' => true,
// অতিরিক্ত capability প্রয়োজনে এখানে যুক্ত করা যেতে পারে।
)
);
}
/**
* Remove the 'Seller' role.
*/
function bsm_remove_seller_role() {
remove_role('seller');
}
// ----- Activation & Deactivation Hooks ----- //
register_activation_hook(__FILE__, 'bsm_activate_plugin');
register_deactivation_hook(__FILE__, 'bsm_deactivate_plugin');
/**
* Activation hook: create/update tables, add seller role
*/
function bsm_activate_plugin() {
// Create or update database tables (now from install-tables.php)
bsm_create_tables();
// Add the 'Seller' role if it doesn't exist
if ( ! get_role('seller') ) {
bsm_add_seller_role();
}
}
/**
* Deactivation hook: optionally remove the 'Seller' role
*/
function bsm_deactivate_plugin() {
// Remove the 'Seller' role on deactivation (optional - consider if needed)
// bsm_remove_seller_role();
// অন্যান্য ডিঅ্যাক্টিভেশন routines এখানে যোগ করা যেতে পারে।
}
/**
* Admin Menu Setup
*/
add_action('admin_menu', 'bsm_admin_menu');
function bsm_admin_menu() {
add_menu_page(
'Seller Management',
'Seller Management',
'manage_options', // Main capability for the menu
'bsm-admin-dashboard',
'bsm_admin_dashboard_page',
'dashicons-chart-area',
6
);
add_submenu_page(
'bsm-admin-dashboard',
'Admin Dashboard',
'Admin Dashboard',
'manage_options', // Capability for this specific page
'bsm-admin-dashboard', // Menu slug (same as parent for first item)
'bsm_admin_dashboard_page' // Function to display page
);
add_submenu_page(
'bsm-admin-dashboard',
'All Sales',
'All Sales',
'manage_options', // TODO: Consider a more specific capability like 'bsm_view_all_sales'
'bsm-all-sales',
'bsm_all_sales_page'
);
add_submenu_page(
'bsm-admin-dashboard',
'All Logs',
'All Logs',
'manage_options', // TODO: Consider 'bsm_view_all_logs'
'bsm-all-logs',
'bsm_all_logs_page'
);
add_submenu_page(
'bsm-admin-dashboard',
'Reports',
'Reports',
'manage_options', // TODO: Consider 'bsm_view_reports'
'bsm-reports',
'bsm_reports_page'
);
add_submenu_page(
'bsm-admin-dashboard',
'Status Overview', // Renamed for clarity
'Status Overview',
'manage_options', // TODO: Consider 'bsm_view_status_overview'
'bsm-status',
'bsm_status_page'
);
add_submenu_page(
'bsm-admin-dashboard',
'Settings',
'Settings',
'manage_options', // Settings usually require high capability
'bsm-settings',
'bsm_settings_page'
);
add_submenu_page(
'bsm-admin-dashboard',
'Status Control',
'Status Control',
'manage_options', // Modifying status requires high capability, consider 'bsm_manage_sale_status'
'bsm-status-control',
'bsm_status_control_page'
);
add_submenu_page(
'bsm-admin-dashboard',
'gsmalo.org Transaction',
'gsmalo.org Transaction',
'manage_options', // Viewing financial data requires high capability, consider 'bsm_view_transactions'
'gsmalo-org-transaction',
'bsm_gsmalo_org_transaction_page'
);
}
/**
* Function to render the "Status Control" page.
*/
function bsm_status_control_page() {
$template_path = BSM_PLUGIN_PATH . 'templates/admin/settings/status-control.php';
if (file_exists($template_path)) {
include $template_path;
} else {
echo '<div class="wrap"><h1>Status Control</h1><p>Error: Template file not found at ' . esc_html($template_path) . '</p></div>';
}
}
/**
* Function: Render the gsmalo.org Transaction page.
*/
function bsm_gsmalo_org_transaction_page() {
$template_path = BSM_PLUGIN_PATH . 'templates/admin/gsmalo-org-transaction.php';
if (file_exists($template_path)) {
include $template_path;
} else {
echo '<div class="wrap"><h1>gsmalo.org Transaction</h1><p>Error: Template file not found.</p></div>';
}
}
/**
* Shortcode for Seller Point Report Page
*/
function bsm_point_report_shortcode() {
ob_start();
if (is_user_logged_in()) {
// Optional: Check if the user has the 'seller' role
// $user = wp_get_current_user();
// if ( !in_array('seller', (array) $user->roles) && !current_user_can('manage_options') ) {
// return '<p style="color:red;font-weight:bold;">Access Denied.</p>';
// }
$template_path = BSM_PLUGIN_PATH . 'templates/seller/point-report.php';
if (file_exists($template_path)) {
include $template_path;
} else {
echo '<p>Error: Point report template not found.</p>';
}
} else {
echo '<p style="color:red; font-weight:bold;">Please log in to view your point report.</p>';
}
return ob_get_clean();
}
add_shortcode('bsm_point_report', 'bsm_point_report_shortcode');
// ----- Shortcode for Seller gsmalo.org Transaction History Page -----
/**
* Shortcode callback for displaying the current seller's gsmalo.org transaction history.
* Usage: [bsm_seller_transaction_history]
*/
function bsm_seller_transaction_history_shortcode() {
ob_start();
if (is_user_logged_in()) {
// Optional: Role check?
$seller_id = get_current_user_id(); // Make $seller_id available
$template_path = BSM_PLUGIN_PATH . 'templates/seller/transaction-history-page.php';
if (file_exists($template_path)) {
include $template_path; // $seller_id is available inside this template
} else {
echo '<p>Error: Seller transaction history template not found.</p>';
}
} else {
echo '<p style="color:red; font-weight:bold;">Please log in to view your transaction history.</p>';
}
return ob_get_clean();
}
add_shortcode('bsm_seller_transaction_history', 'bsm_seller_transaction_history_shortcode');
// ------------------------------------------------------------
// +++++ Shortcode for Seller ALL Transaction History Page +++++
/**
* Shortcode callback for displaying the current seller's COMPLETE transaction history (all sale types).
* Usage: [bsm_seller_all_transaction_history]
*/
function bsm_seller_all_transaction_history_shortcode() {
ob_start();
if (is_user_logged_in()) {
$seller_id = get_current_user_id(); // Make $seller_id available for the template
$template_path = BSM_PLUGIN_PATH . 'templates/seller/all-transaction-history-page.php'; // ** Include the NEW template **
if (file_exists($template_path)) {
include $template_path; // $seller_id is available inside this template
} else {
// Error message if the new template file is missing
echo '<p>Error: All Transaction History template file not found at ' . esc_html($template_path) . '</p>';
}
} else {
echo '<p style="color:red; font-weight:bold;">Please log in to view your complete transaction history.</p>';
}
return ob_get_clean();
}
add_shortcode('bsm_seller_all_transaction_history', 'bsm_seller_all_transaction_history_shortcode');
// ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// <<<<< NEW: Shortcode for Last Status Activities Page >>>>>
/**
* Shortcode callback for displaying the "Last Status Activities" page.
* Usage: [bsm_last_status_activities]
*/
function bsm_last_status_activities_shortcode() {
ob_start();
// Check if user is logged in (Can be restricted to sellers or admins if needed)
if (is_user_logged_in()) {
$template_path = BSM_PLUGIN_PATH . 'templates/seller/last-status-activities-page.php'; // Point to the new template file
if (file_exists($template_path)) {
include $template_path;
} else {
// Error message if the template file is missing
echo '<p>Error: Last Status Activities template file not found at ' . esc_html($template_path) . '</p>';
}
} else {
echo '<p style="color:red; font-weight:bold;">Please log in to view status activities.</p>';
}
return ob_get_clean();
}
add_shortcode('bsm_last_status_activities', 'bsm_last_status_activities_shortcode');
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
/**
* AJAX handler for fixing a single Warning tag in a transaction.
* (Code unchanged from previous version)
*/
add_action('wp_ajax_bsm_fix_warning', 'bsm_fix_warning_ajax_handler');
function bsm_fix_warning_ajax_handler() {
check_ajax_referer('bsm_fix_warning_nonce', '_ajax_nonce');
if ( ! current_user_can('manage_options') ) {
wp_send_json_error( array('message' => 'You do not have permission.') );
}
$txn_id = isset($_POST['transaction_id']) ? intval($_POST['transaction_id']) : 0;
if ( $txn_id < 1 ) {
wp_send_json_error( array('message' => 'Invalid transaction ID.') );
}
global $wpdb;
$table = $wpdb->prefix . 'bsm_balance_adjustments';
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT reason, fixed_by FROM {$table} WHERE id = %d",
$txn_id
), ARRAY_A );
if ( ! $row ) {
wp_send_json_error( array('message' => 'Transaction not found.') );
}
$current_reason = $row['reason'];
if ( stripos($current_reason, '[warning]') === false ) {
if (!empty($row['fixed_by'])) {
wp_send_json_error( array('message' => 'Warning already fixed.') );
} else {
wp_send_json_error( array('message' => 'Warning tag not found.') );
}
}
$final_reason = trim(preg_replace('/\s*\[warning\]/i', '', $current_reason));
$fixer_id = get_current_user_id();
$fixer_user = get_userdata($fixer_id);
$fixer_name = $fixer_user ? $fixer_user->display_name : ('User ID: '.$fixer_id);
$fix_timestamp_mysql = current_time('mysql');
$fix_timestamp_display = date('h:i A | d-m-Y', current_time('timestamp'));
$updated = $wpdb->update(
$table,
array(
'reason' => $final_reason,
'fixed_by' => $fixer_id,
'fixed_at' => $fix_timestamp_mysql
),
array( 'id' => $txn_id ),
array( '%s', '%d', '%s' ),
array( '%d' )
);
if ( $updated === false ) {
$db_error = $wpdb->last_error;
error_log("Fix Warning AJAX: Database update failed - " . $db_error);
wp_send_json_error( array('message' => 'Database update failed: ' . $db_error) );
}
$fix_note_html = esc_html($fixer_name) . '<br><small>' . esc_html($fix_timestamp_display) . '</small>';
wp_send_json_success( array(
'message' => 'Warning removed and fix recorded.',
'fix_note_html' => $fix_note_html
) );
}
/**
* Helper function to handle error logging based on WP_DEBUG setting.
*/
if (!function_exists('bsm_custom_log_error')) {
function bsm_custom_log_error($message) {
if (defined('WP_DEBUG') && WP_DEBUG === true) {
error_log('[BSM Plugin Error] ' . print_r($message, true));
}
}
}
?>
<?php
/**
* File: business-seller-management/includes/install-tables.php
*
* Description:
* - Creates/Updates necessary database tables on plugin activation.
* - **Update:** Removed `fixed_by` and `fixed_at` columns from the initial
* CREATE TABLE definition for `bsm_balance_adjustments` used by `dbDelta`.
* - Added a separate routine after `dbDelta` to check for the existence of
* `fixed_by` and `fixed_at` columns using `ALTER TABLE` if they are missing.
* This ensures column creation even if `dbDelta` fails to add them.
*
* Table List (Created/Updated via dbDelta or ALTER TABLE):
* 1) bsm_sales: Sale information.
* 2) bsm_sellers: Seller details.
* 3) bsm_transactions: Transaction logs.
* 4) bsm_sale_notes: Sale/Work notes.
* 5) bsm_statas_log_notes: Status log notes.
* 6) bsm_points: Point transaction details.
* 7) bsm_balance_adjustments: Balance adjustments (with `fixed_by`, `fixed_at` ensured).
*/
if ( ! defined('ABSPATH') ) {
exit;
}
function bsm_create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); // Include upgrade functions (like dbDelta)
// Table names
$table_sales = $wpdb->prefix . 'bsm_sales';
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$table_transactions = $wpdb->prefix . 'bsm_transactions';
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$table_statas_log_notes = $wpdb->prefix . 'bsm_statas_log_notes';
$table_points = $wpdb->prefix . 'bsm_points';
$table_balance_adjustments = $wpdb->prefix . 'bsm_balance_adjustments';
// ----- SQL Definitions for dbDelta -----
// (All table definitions are unchanged EXCEPT bsm_balance_adjustments initially)
// 1) bsm_sales Table
$sql_sales = "CREATE TABLE $table_sales (
id mediumint(9) NOT NULL AUTO_INCREMENT,
seller_id mediumint(9) NOT NULL,
product_name varchar(255) NOT NULL,
sale_type varchar(50) NOT NULL,
order_id varchar(100) NOT NULL DEFAULT '',
payment_method varchar(50) NOT NULL,
transaction_id varchar(100) NOT NULL,
purchase_price decimal(10,2) NOT NULL DEFAULT 0.00,
selling_price decimal(10,2) NOT NULL DEFAULT 0.00,
profit decimal(10,2) NOT NULL DEFAULT 0.00,
loss decimal(10,2) NOT NULL DEFAULT 0.00,
status varchar(50) NOT NULL DEFAULT 'pending',
customer_payment varchar(255) NOT NULL DEFAULT '',
work_proof_link text NOT NULL,
video_duration decimal(10,2) NOT NULL DEFAULT 0.00,
proof_text text NOT NULL,
proof_screen_short text NOT NULL,
qty int(11) NOT NULL DEFAULT 0,
video_phase varchar(50) NOT NULL DEFAULT '',
edit_upload_id int(11) DEFAULT NULL,
note text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY seller_id (seller_id),
KEY status (status),
KEY created_at (created_at)
) $charset_collate;";
// 2) bsm_sellers Table
$sql_sellers = "CREATE TABLE $table_sellers (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id mediumint(9) NOT NULL,
gsmalo_org_balance decimal(10,2) NOT NULL DEFAULT 0.00,
points_balance decimal(10,2) NOT NULL DEFAULT 0.00,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY user_id (user_id)
) $charset_collate;";
// 3) bsm_transactions Table
$sql_transactions = "CREATE TABLE $table_transactions (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
transaction_type varchar(50) NOT NULL,
amount decimal(10,2) NOT NULL DEFAULT 0.00,
status varchar(50) NOT NULL DEFAULT 'pending',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY sale_id (sale_id)
) $charset_collate;";
// 4) bsm_sale_notes Table
$sql_sale_notes = "CREATE TABLE $table_sale_notes (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
user_id mediumint(9) NOT NULL,
note_type varchar(20) NOT NULL DEFAULT 'private',
note_text text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY sale_id (sale_id),
KEY user_id (user_id)
) $charset_collate;";
// 5) bsm_statas_log_notes Table
$sql_statas_log_notes = "CREATE TABLE $table_statas_log_notes (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
user_id mediumint(9) NOT NULL,
note_type varchar(20) NOT NULL DEFAULT 'private',
note_text text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY sale_id (sale_id)
) $charset_collate;";
// 6) bsm_points Table
$sql_points = "CREATE TABLE $table_points (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id mediumint(9) NOT NULL,
sale_id mediumint(9) DEFAULT NULL,
points decimal(10,2) NOT NULL DEFAULT 0.00,
type varchar(20) NOT NULL DEFAULT 'regular',
description text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY sale_id (sale_id),
KEY type (type)
) $charset_collate;";
// 7) bsm_balance_adjustments Table (**Initial Definition for dbDelta - WITHOUT new columns**)
$sql_balance_adjustments = "CREATE TABLE $table_balance_adjustments (
id mediumint(9) NOT NULL AUTO_INCREMENT,
seller_id mediumint(9) NOT NULL,
adjusted_amount decimal(10,2) NOT NULL,
reason text NOT NULL,
adjusted_by mediumint(9) NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY seller_id (seller_id),
KEY adjusted_by (adjusted_by)
) $charset_collate;";
// ----- Execute dbDelta for all tables -----
// dbDelta will create tables if they don't exist or update existing ones based on the PRIMARY KEY and Indexes.
// It *might* add new columns if defined here, but it's unreliable for that.
dbDelta($sql_sales);
dbDelta($sql_sellers);
dbDelta($sql_transactions);
dbDelta($sql_sale_notes);
dbDelta($sql_statas_log_notes);
dbDelta($sql_points);
dbDelta($sql_balance_adjustments); // Create/update the base table first
// ----- **NEW:** Manually Check and Add Missing Columns -----
// This part ensures 'fixed_by' and 'fixed_at' are added if dbDelta missed them.
// Get existing columns for the balance adjustments table
// Note: Using DESCRIBE instead of get_col for better reliability across MySQL versions
$table_columns = $wpdb->get_results("DESCRIBE $table_balance_adjustments;");
$existing_column_names = array();
if ($table_columns) {
foreach($table_columns as $col) {
$existing_column_names[] = strtolower($col->Field); // Store column names in lowercase
}
}
// Check and add 'fixed_by' column if it doesn't exist
if (!in_array('fixed_by', $existing_column_names)) {
$alter_sql_fixed_by = "ALTER TABLE $table_balance_adjustments ADD COLUMN fixed_by mediumint(9) DEFAULT NULL AFTER created_at;";
$wpdb->query($alter_sql_fixed_by);
// Add index for the new column
$index_sql_fixed_by = "ALTER TABLE $table_balance_adjustments ADD INDEX fixed_by (fixed_by);";
// Ignore errors for index creation (it might exist from a partial previous attempt)
$current_error_reporting = $wpdb->hide_errors();
$wpdb->query($index_sql_fixed_by);
$wpdb->show_errors($current_error_reporting);
}
// Check and add 'fixed_at' column if it doesn't exist
if (!in_array('fixed_at', $existing_column_names)) {
$alter_sql_fixed_at = "ALTER TABLE $table_balance_adjustments ADD COLUMN fixed_at datetime DEFAULT NULL AFTER fixed_by;"; // Add after fixed_by
$wpdb->query($alter_sql_fixed_at);
}
}
// Hook this function to plugin activation OR consider running it on 'admin_init'
// register_activation_hook( BSM_PLUGIN_FILE, 'bsm_create_tables' ); // Assumes BSM_PLUGIN_FILE is defined in your main file
// Optionally, run on admin_init to ensure updates (can be heavy, use transients or version checks to limit frequency)
/*
add_action( 'admin_init', 'bsm_maybe_update_db_schema' );
function bsm_maybe_update_db_schema() {
$current_version = '1.1'; // Increment this when you change the schema
$installed_version = get_option( 'bsm_db_version' );
if ( $installed_version != $current_version ) {
bsm_create_tables(); // Run the table creation/update function
update_option( 'bsm_db_version', $current_version );
}
}
*/
?>
<?php
/**
* File: /business-seller-management/includes/point-system.php
*
* Description:
* - This file handles all point system logic for Business Seller Management plugin.
* - We'll have two main approaches:
* (A) Profit-based monthly percentage points
* (B) Daily (or monthly) target-based bonus
* - Future expansions:
* - Admin Dashboard Settings page to customize these percentage ranges, target thresholds, etc.
* - Different sellers or roles could have different point rules if needed.
*
* Usage:
* 1) Include/require this file from your main plugin file or other relevant files.
* 2) Then call the relevant function(s) whenever you want to calculate or update points for a seller.
*/
// -----------------------------------------------------------
// A) Profit-based monthly percentage points (Default Ranges)
// -----------------------------------------------------------
/**
* bsm_calculate_monthly_points( $monthly_profit )
*
* Given the total monthly profit for a seller, return how many points
* they should get, based on the default percentage ranges described.
*
* These default ranges are (in BDT):
*
* 1) 0 to 20,000 => 9%
* 2) 20,000+ to 50,000 => 11%
* 3) 50,000+ to 80,000 => 12%
* 4) 80,000+ to 100,000 => 14%
* 5) 100,000+ to 150,000 => 15%
* 6) 150,000+ to 200,000 => 17%
* 7) 200,000+ to 300,000 => 18%
* 8) 300,000+ to 400,000 => 19%
* 9) 400,000+ to 500,000 => 20%
* 10) 500,000+ to 1,000,000 => 22%
*
* If profit is beyond 1,000,000, we can still apply 22% or do something else.
*
* Return value: integer or float points
*/
function bsm_calculate_monthly_points( $monthly_profit ) {
// Ensure numeric
$profit = floatval($monthly_profit);
if($profit <= 0){
return 0; // no profit => no points
}
// Default Ranges
// Ranges are inclusive from boundary upwards
// You can adjust logic as you see fit
if( $profit > 500000 && $profit <= 1000000 ){
$percent = 22;
}
elseif( $profit > 400000 ){
$percent = 20;
}
elseif( $profit > 300000 ){
$percent = 19;
}
elseif( $profit > 200000 ){
$percent = 18;
}
elseif( $profit > 150000 ){
$percent = 17;
}
elseif( $profit > 100000 ){
$percent = 15;
}
elseif( $profit > 80000 ){
$percent = 14;
}
elseif( $profit > 50000 ){
$percent = 12;
}
elseif( $profit > 20000 ){
$percent = 11;
}
else {
// up to 20000
$percent = 9;
}
// Calculate points as "profit * (percent/100)"
$points = ($profit * ($percent/100.0));
// Round or not is up to your policy
return round($points);
}
// -----------------------------------------------------------
// B) Daily / Monthly target-based bonus approach
// -----------------------------------------------------------
/**
* bsm_calculate_daily_target_bonus( $points_today )
*
* Given how many points a seller earned "today", check if they fill up a daily target,
* awarding extra "free" points.
*
* Example thresholds (Default):
* 1) 100 point target => 20 extra
* 2) 300 point target => 60 extra
* 3) 500 point target => 100 extra
* 4) 700 point target => 50 extra
* 5) 1000 point target => 70 extra
* 6) 1300 point target => 80 extra
* 7) 1600 point target => 100 extra
* 8) 2000 point target => 200 extra
* 9) 2500 point target => 250 extra
* 10) 3000 point target => 300 extra
*
* Return: int bonus points. If multiple thresholds are reached in a single day, you can
* either only award the highest or sum them up. For simplicity, let's just pick the highest threshold reached.
*/
function bsm_calculate_daily_target_bonus( $points_today ){
$p = floatval($points_today);
if($p <= 0){
return 0;
}
// We'll store thresholds in ascending order
$thresholds = [
3000 => 300,
2500 => 250,
2000 => 200,
1600 => 100,
1300 => 80,
1000 => 70,
700 => 50,
500 => 100,
300 => 60,
100 => 20,
];
// Notice the logic for "700 => 50" and "500 => 100" might be questionable, but we're just replicating the user data
// We'll check from top to bottom to see the highest threshold crossed
foreach($thresholds as $min_points => $bonus){
if($p >= $min_points){
return $bonus; // first match => largest threshold
}
}
return 0; // if none matched
}
/**
* bsm_calculate_monthly_target_bonus( $points_this_month )
*
* Another target system (monthly). Example thresholds:
* (basic) 15,000 => 500
* (silver) 22,000 => 1500
* (gold) 30,000 => 3000
* (premium)45,000 => 4000
*
* We assume we pick the highest threshold achieved.
*/
function bsm_calculate_monthly_target_bonus( $points_this_month ){
$p = floatval($points_this_month);
if($p <= 0){
return 0;
}
// Highest threshold match
$thresholds = [
45000 => 4000, // premium
30000 => 3000, // gold
22000 => 1500, // silver
15000 => 500, // basic
];
foreach($thresholds as $t => $b){
if($p >= $t){
return $b;
}
}
return 0;
}
// -----------------------------------------------------------
// C) Helper to fix "Undefined property: stdClass::$point"
// -----------------------------------------------------------
/**
* bsm_fix_sale_point_property(&$sale)
*
* If the $sale object does not have a 'point' property,
* define it as 0. This prevents "Undefined property" notice.
* Call this before referencing $sale->point in sale-page.php
*/
function bsm_fix_sale_point_property(&$sale){
if(!property_exists($sale, 'point')){
$sale->point = 0;
}
}
business-seller-management/includes/admin/admin-dashboard.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
/**
* bsm_admin_dashboard_page()
*
* Displays data on the Admin Dashboard:
* - gsmalo.org balances (now including user display_name)
* - Today's summary
* - Yesterday's summary
* - This month's summary
* - Status/log placeholders
* - Today's detailed sales
* - Last 50 sales
*/
function bsm_admin_dashboard_page() {
global $wpdb;
// Table names
$table_sales = $wpdb->prefix . 'bsm_sales';
$table_sellers = $wpdb->prefix . 'bsm_sellers';
// ভবিষ্যতে logs বা status change টেবিল থাকলে সেখানে JOIN করা যাবে।
// =====================================================
// 1) gsmalo.org Balance (Summary of all sellers)
// =====================================================
// user_display_name-সহ সকল সেলার এর ব্যালেন্স নিচ্ছি।
// যাতে টেমপ্লেটে "নাম + ব্যালেন্স" বক্স দেখানো যায়।
$all_seller_balances = $wpdb->get_results("
SELECT s.user_id, s.gsmalo_org_balance, u.display_name
FROM $table_sellers AS s
LEFT JOIN {$wpdb->users} AS u ON s.user_id = u.ID
ORDER BY s.user_id ASC
");
// =====================================================
// 2) Today's Summary
// =====================================================
$today = date('Y-m-d');
$today_sales = $wpdb->get_results(
$wpdb->prepare("
SELECT *
FROM $table_sales
WHERE DATE(created_at) = %s
", $today)
);
$today_total_sales = 0;
$today_total_profit = 0;
$today_total_loss = 0;
// Breakdown counters
$today_web_sale_count = 0; // gsmalo.org, gsmcourse.com, gsmalo.com
$today_tnx_count = 0;
$today_product_sale_count= 0; // (product + service) বা শুধুই product? আপনি যেভাবে চান
$today_product_sales = 0; $today_product_profit = 0;
$today_service_sales = 0; $today_service_profit = 0;
$today_gsmalo_org_sales = 0; $today_gsmalo_org_profit = 0;
$today_gsmalo_com_sales = 0; $today_gsmalo_com_profit = 0;
$today_gsmcourse_com_sales= 0; $today_gsmcourse_com_profit= 0;
// সেলার-ভিত্তিক সারাংশ
$today_seller_summary = [];
foreach ($today_sales as $sale) {
$today_total_sales += $sale->selling_price;
$today_total_profit += $sale->profit;
$today_total_loss += $sale->loss;
if (!empty($sale->transaction_id)) {
$today_tnx_count++;
}
switch ($sale->sale_type) {
case 'product':
$today_product_sales += $sale->selling_price;
$today_product_profit += $sale->profit;
$today_product_sale_count++;
break;
case 'service':
$today_service_sales += $sale->selling_price;
$today_service_profit += $sale->profit;
// অনেক সময় service-কে product_sale_count-এ যোগ না করতে পারেন, ইচ্ছা।
break;
case 'gsmalo.org':
$today_gsmalo_org_sales += $sale->selling_price;
$today_gsmalo_org_profit += $sale->profit;
$today_web_sale_count++;
break;
case 'gsmalo.com':
$today_gsmalo_com_sales += $sale->selling_price;
$today_gsmalo_com_profit += $sale->profit;
$today_web_sale_count++;
break;
case 'gsmcourse.com':
$today_gsmcourse_com_sales += $sale->selling_price;
$today_gsmcourse_com_profit += $sale->profit;
$today_web_sale_count++;
break;
}
// seller summary
if (!isset($today_seller_summary[$sale->seller_id])) {
$today_seller_summary[$sale->seller_id] = [
'count_sales' => 0,
'total_sales' => 0,
'total_profit' => 0,
'product_count' => 0,
];
}
$today_seller_summary[$sale->seller_id]['count_sales']++;
$today_seller_summary[$sale->seller_id]['total_sales'] += $sale->selling_price;
$today_seller_summary[$sale->seller_id]['total_profit'] += $sale->profit;
if ($sale->sale_type === 'product') {
$today_seller_summary[$sale->seller_id]['product_count']++;
}
}
// =====================================================
// 3) Yesterday's Summary
// =====================================================
$yesterday = date('Y-m-d', strtotime('-1 day'));
$yesterday_sales = $wpdb->get_results(
$wpdb->prepare("
SELECT *
FROM $table_sales
WHERE DATE(created_at) = %s
", $yesterday)
);
$yesterday_total_sales = 0;
$yesterday_total_profit = 0;
$yesterday_total_loss = 0;
$yesterday_web_sale_count = 0;
$yesterday_tnx_count = 0;
$yesterday_product_sale_count= 0;
$yesterday_product_sales = 0; $yesterday_product_profit = 0;
$yesterday_service_sales = 0; $yesterday_service_profit = 0;
$yesterday_gsmalo_org_sales = 0; $yesterday_gsmalo_org_profit = 0;
$yesterday_gsmalo_com_sales = 0; $yesterday_gsmalo_com_profit = 0;
$yesterday_gsmcourse_com_sales= 0; $yesterday_gsmcourse_com_profit= 0;
$yesterday_seller_summary = [];
foreach ($yesterday_sales as $sale) {
$yesterday_total_sales += $sale->selling_price;
$yesterday_total_profit += $sale->profit;
$yesterday_total_loss += $sale->loss;
if (!empty($sale->transaction_id)) {
$yesterday_tnx_count++;
}
switch ($sale->sale_type) {
case 'product':
$yesterday_product_sales += $sale->selling_price;
$yesterday_product_profit += $sale->profit;
$yesterday_product_sale_count++;
break;
case 'service':
$yesterday_service_sales += $sale->selling_price;
$yesterday_service_profit += $sale->profit;
break;
case 'gsmalo.org':
$yesterday_gsmalo_org_sales += $sale->selling_price;
$yesterday_gsmalo_org_profit += $sale->profit;
$yesterday_web_sale_count++;
break;
case 'gsmalo.com':
$yesterday_gsmalo_com_sales += $sale->selling_price;
$yesterday_gsmalo_com_profit += $sale->profit;
$yesterday_web_sale_count++;
break;
case 'gsmcourse.com':
$yesterday_gsmcourse_com_sales += $sale->selling_price;
$yesterday_gsmcourse_com_profit += $sale->profit;
$yesterday_web_sale_count++;
break;
}
if (!isset($yesterday_seller_summary[$sale->seller_id])) {
$yesterday_seller_summary[$sale->seller_id] = [
'count_sales' => 0,
'total_sales' => 0,
'total_profit' => 0,
'product_count' => 0,
];
}
$yesterday_seller_summary[$sale->seller_id]['count_sales']++;
$yesterday_seller_summary[$sale->seller_id]['total_sales'] += $sale->selling_price;
$yesterday_seller_summary[$sale->seller_id]['total_profit'] += $sale->profit;
if ($sale->sale_type === 'product') {
$yesterday_seller_summary[$sale->seller_id]['product_count']++;
}
}
// =====================================================
// 4) This Month's Summary
// =====================================================
$current_month = date('Y-m');
$monthly_sales = $wpdb->get_results(
$wpdb->prepare("
SELECT *
FROM $table_sales
WHERE DATE_FORMAT(created_at, '%%Y-%%m') = %s
", $current_month)
);
$month_total_sales = 0;
$month_total_profit = 0;
$month_total_loss = 0;
$month_web_sale_count = 0;
$month_tnx_count = 0;
$month_product_sale_count= 0;
$month_product_sales = 0; $month_product_profit = 0;
$month_service_sales = 0; $month_service_profit = 0;
$month_gsmalo_org_sales = 0; $month_gsmalo_org_profit = 0;
$month_gsmalo_com_sales = 0; $month_gsmalo_com_profit = 0;
$month_gsmcourse_com_sales= 0; $month_gsmcourse_com_profit= 0;
foreach ($monthly_sales as $sale) {
$month_total_sales += $sale->selling_price;
$month_total_profit += $sale->profit;
$month_total_loss += $sale->loss;
if (!empty($sale->transaction_id)) {
$month_tnx_count++;
}
switch ($sale->sale_type) {
case 'product':
$month_product_sales += $sale->selling_price;
$month_product_profit += $sale->profit;
$month_product_sale_count++;
break;
case 'service':
$month_service_sales += $sale->selling_price;
$month_service_profit += $sale->profit;
break;
case 'gsmalo.org':
$month_gsmalo_org_sales += $sale->selling_price;
$month_gsmalo_org_profit += $sale->profit;
$month_web_sale_count++;
break;
case 'gsmalo.com':
$month_gsmalo_com_sales += $sale->selling_price;
$month_gsmalo_com_profit += $sale->profit;
$month_web_sale_count++;
break;
case 'gsmcourse.com':
$month_gsmcourse_com_sales += $sale->selling_price;
$month_gsmcourse_com_profit += $sale->profit;
$month_web_sale_count++;
break;
}
}
// =====================================================
// 5) Placeholder: Status changes + log summary
// =====================================================
$today_status_changes_count = 0; // Placeholder
$today_log_summary = []; // Placeholder
// =====================================================
// 6) Table of today's sales (already in $today_sales)
// =====================================================
// =====================================================
// 7) Last 50 sales
// =====================================================
$last_50_sales = $wpdb->get_results("
SELECT *
FROM $table_sales
ORDER BY created_at DESC
LIMIT 50
");
// =====================================================
// 8) Placeholder: All menu short summary
// =====================================================
$all_sales_count = $wpdb->get_var("SELECT COUNT(*) FROM $table_sales");
$all_logs_count = 0; // no logs table yet
// =====================================================
// Finally, include the template
// =====================================================
include BSM_PLUGIN_PATH . 'templates/admin/admin-dashboard.php';
}
business-seller-management/includes/admin/advanced-error-guard.php
খালি
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
/**
* bsm_all_logs_page()
*
* Callback function for the "All Logs" submenu page.
* Currently shows a placeholder page.
* Later, we will add actual code (log list, filters, pagination, etc.) based on your requirements.
*/
function bsm_all_logs_page() {
// Placeholder logic for "All Logs" menu
// We will include a template file here
// wp-content/plugins/business-seller-management/templates/admin/all-logs-page.php
include BSM_PLUGIN_PATH . 'templates/admin/all-logs-page.php';
}
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
/**
* bsm_all_sales_page()
*
* Callback function for the "All Sales" submenu page.
* Currently shows a placeholder page.
* Later, we will add actual code (CRUD, filtering, pagination, etc.) based on your requirements.
*/
function bsm_all_sales_page() {
// Placeholder logic for "All Sales" menu
// We will include a template file here
// In the next step, we will create that template file at:
// wp-content/plugins/business-seller-management/templates/admin/all-sales-page.php
include BSM_PLUGIN_PATH . 'templates/admin/all-sales-page.php';
}
<?php
/**
* File: business-seller-management/includes/admin/balance-adjustment-handler.php
*
* Description:
* - Handles AJAX requests for adjusting gsmalo.org balances and fetching history.
* - Ensures Nonce verification, data validation, balance updates, and history recording (including `adjusted_by` user ID).
* - Adds '[Warning]' tag to reason if 'Enable Warning' checkbox is checked.
* - Returns adjustment history as an HTML table via AJAX for display in modals.
* - **Verification:** Confirmed that `$current_user_id` is correctly retrieved using `get_current_user_id()` and inserted into the `adjusted_by` column in the database during balance adjustment.
* - **UPDATED:** Modified seller check and update logic to handle users who may not yet have an entry in the `bsm_sellers` table.
*/
if ( ! defined('ABSPATH') ) {
exit; // Exit if accessed directly
}
/**
* Helper function: Debug Logger
*/
function bsm_balance_debug_log($message) {
if (defined('WP_DEBUG') && WP_DEBUG === true) { // Log only if WP_DEBUG is enabled
error_log('[BSM Balance Adjustment] ' . print_r($message, true));
}
}
/**
* Ajax Handler: Balance Adjustment Submit
*/
add_action('wp_ajax_bsm_adjust_gsmalo_balance', 'bsm_balance_adjustment_submit_cb');
function bsm_balance_adjustment_submit_cb() {
// Nonce check
if ( ! isset($_POST['bsm_adjust_balance_nonce']) ||
! check_ajax_referer('bsm_adjust_balance_action', 'bsm_adjust_balance_nonce', false) ) {
bsm_balance_debug_log("Nonce verification failed in submit handler.");
wp_send_json_error( array('message' => 'Security check failed (nonce error).') , 400 );
}
// Permission check (ensure only users who can manage options can do this)
if ( ! current_user_can('manage_options') ) {
bsm_balance_debug_log("Permission denied for balance adjustment.");
wp_send_json_error( array('message' => 'You do not have permission to perform this action.') , 403 );
}
// Clean output buffer
while (ob_get_level() > 0) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
// Collect input
$seller_id = isset($_POST['seller_id']) ? intval($_POST['seller_id']) : 0; // This is the user_id
$adjust_amount = isset($_POST['adjust_amount']) ? floatval($_POST['adjust_amount']) : 0;
$adjust_reason = isset($_POST['adjust_reason']) ? sanitize_text_field($_POST['adjust_reason']) : '';
// Append '[Warning]' if checkbox checked and reason doesn't already have it or a fix note
if ( isset($_POST['warning_on']) && $_POST['warning_on'] == "1" ) {
if ( stripos($adjust_reason, "[warning]") === false && stripos($adjust_reason, "(Fixed by:") === false ) {
$adjust_reason = trim($adjust_reason); // Trim first
$adjust_reason .= ($adjust_reason ? " " : "") . "[Warning]"; // Add warning tag
}
}
// Validation
if ( !$seller_id ) {
bsm_balance_debug_log("Invalid seller ID (user_id) provided.");
wp_send_json_error( array('message' => 'Invalid seller ID.'), 400 );
}
// Allow zero adjustment? Maybe for adding only a note? Let's allow it for now.
// if ( 0 == $adjust_amount ) {
// bsm_balance_debug_log("Adjustment amount is zero.");
// wp_send_json_error( array('message' => 'Adjust amount cannot be zero.'), 400 );
// }
if ( empty($adjust_reason) && $adjust_amount == 0 ) {
bsm_balance_debug_log("Reason cannot be empty if amount is zero.");
wp_send_json_error( array('message' => 'Reason cannot be empty if adjustment amount is zero.'), 400 );
}
global $wpdb;
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$table_history = $wpdb->prefix . 'bsm_balance_adjustments';
$table_users = $wpdb->users; // WordPress users table
// --- UPDATED Seller Check and Update Logic ---
// First, check if the user exists in WordPress
$user_exists = $wpdb->get_var($wpdb->prepare("SELECT ID FROM {$table_users} WHERE ID = %d", $seller_id));
if (!$user_exists) {
bsm_balance_debug_log("WordPress user with ID $seller_id not found.");
wp_send_json_error( array('message' => 'WordPress user not found.'), 400 );
}
// Now, check if the seller exists in the bsm_sellers table
$seller_in_bsm = $wpdb->get_row( $wpdb->prepare("SELECT id, gsmalo_org_balance FROM $table_sellers WHERE user_id = %d", $seller_id) );
$new_balance = 0.0;
$bsm_seller_id = null;
if ( !$seller_in_bsm ) {
// Seller does NOT exist in bsm_sellers, create a new entry
bsm_balance_debug_log("Seller with user_id $seller_id not found in bsm_sellers. Creating new entry.");
$insert_bsm = $wpdb->insert(
$table_sellers,
array(
'user_id' => $seller_id,
'gsmalo_org_balance' => $adjust_amount, // Initial balance is the adjustment amount
'points_balance' => 0.00, // Assuming points balance starts at 0 for new sellers
'created_at' => current_time('mysql')
),
array('%d', '%f', '%f', '%s')
);
if (false === $insert_bsm) {
bsm_balance_debug_log("Database insert error creating new bsm_sellers entry: " . $wpdb->last_error);
wp_send_json_error( array('message' => 'Database error creating seller record: '.$wpdb->last_error), 500 );
}
$bsm_seller_id = $wpdb->insert_id;
$new_balance = $adjust_amount; // New balance is the adjustment amount
bsm_balance_debug_log("Created new bsm_sellers entry with ID $bsm_seller_id. New balance: $new_balance");
} else {
// Seller already exists in bsm_sellers, update the entry if amount is not zero
$bsm_seller_id = $seller_in_bsm->id;
$current_balance = floatval($seller_in_bsm->gsmalo_org_balance);
$new_balance = $current_balance + $adjust_amount;
bsm_balance_debug_log("Seller with bsm_seller_id $bsm_seller_id found. Current balance: $current_balance, adjustment: $adjust_amount. New balance: $new_balance");
if ($adjust_amount != 0) {
$update = $wpdb->update(
$table_sellers,
array('gsmalo_org_balance' => $new_balance),
array('id' => $bsm_seller_id), // Use primary key 'id' from bsm_sellers
array('%f'),
array('%d')
);
if ( false === $update ) {
bsm_balance_debug_log("Database update error updating seller balance: " . $wpdb->last_error);
wp_send_json_error( array('message' => 'Database update error updating seller balance: '.$wpdb->last_error), 500 );
}
bsm_balance_debug_log("Updated bsm_sellers entry with ID $bsm_seller_id.");
} else {
bsm_balance_debug_log("Adjustment amount is zero. Skipping balance update in bsm_sellers.");
}
}
// **Confirmed:** Record history with the current logged-in user's ID
$current_user_id = get_current_user_id();
if ( !$current_user_id ) {
bsm_balance_debug_log("Could not get current user ID for history logging.");
// Decide how to handle this - maybe use a system ID or default admin ID? Or fail? Let's fail for now.
wp_send_json_error( array('message' => 'Could not identify adjusting user.'), 500 );
}
$insert = $wpdb->insert(
$table_history,
array(
'seller_id' => $seller_id, // Log the WordPress user_id
'adjusted_amount' => $adjust_amount,
'reason' => $adjust_reason,
'adjusted_by' => $current_user_id, // Store the current user's ID (admin who made the adjustment)
'created_at' => current_time('mysql')
),
array('%d','%f','%s','%d','%s') // Ensure %d format for adjusted_by
);
if ( false === $insert ) {
bsm_balance_debug_log("History insert error: " . $wpdb->last_error);
// Attempt to revert the balance update if history logging failed and balance was changed
if ($adjust_amount != 0 && $seller_in_bsm) { // Only attempt revert if update happened and seller existed before
$wpdb->update(
$table_sellers,
array('gsmalo_org_balance' => $current_balance), // Revert to original balance
array('id' => $bsm_seller_id),
array('%f'),
array('%d')
);
bsm_balance_debug_log("Attempted to revert seller balance update for bsm_seller_id $bsm_seller_id due to history insert failure.");
}
wp_send_json_error( array('message' => 'Failed to record adjustment history: '.$wpdb->last_error), 500 );
}
wp_send_json_success( array(
'message' => 'Balance updated successfully.',
'new_balance' => $new_balance // Return the new balance
) );
}
/**
* Ajax Handler: Load Balance Adjustment History
* (This function is kept AS IS from the previous version)
*/
add_action('wp_ajax_bsm_get_balance_adjust_history', 'bsm_load_balance_adjustment_history_cb');
function bsm_load_balance_adjustment_history_cb() {
// Nonce check
if ( ! isset($_POST['bsm_get_balance_adjust_history_nonce']) ||
! check_ajax_referer('bsm_get_balance_adjust_history', 'bsm_get_balance_adjust_history_nonce', false) ) {
bsm_balance_debug_log("Nonce verification failed in history handler.");
wp_send_json_error( array('message' => 'Security check failed (nonce error).'), 400 );
}
// Permission check
if ( ! current_user_can('manage_options') ) {
bsm_balance_debug_log("Permission denied for viewing history.");
wp_send_json_error( array('message' => 'You do not have permission.') , 403 );
}
// Clean output buffer
while (ob_get_level() > 0) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
$seller_id = isset($_POST['seller_id']) ? intval($_POST['seller_id']) : 0; // This is the user_id
if ( !$seller_id ) {
bsm_balance_debug_log("Seller ID (user_id) missing in history load.");
wp_send_json_error( array('message' => 'Seller ID missing.'), 400 );
}
global $wpdb;
$table_history = $wpdb->prefix . 'bsm_balance_adjustments';
// Fetch latest 20 history records, include adjusted_by ID
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT id, adjusted_amount, reason, created_at, adjusted_by, fixed_by, fixed_at FROM $table_history WHERE seller_id = %d ORDER BY created_at DESC LIMIT 20",
$seller_id // Query using user_id stored in seller_id column
), ARRAY_A );
// Prepare HTML table for the modal popup
ob_start();
if ( $results ) {
echo '<table style="width:100%; border-collapse:collapse; font-size:12px;">'; // Smaller font size
echo '<thead><tr style="background:#f5f5f5;"><th style="padding:5px;border:1px solid #ccc;">Time & Date</th><th style="padding:5px;border:1px solid #ccc;">Amount $</th><th style="padding:5px;border:1px solid #ccc;text-align:left;">Reason</th><th style="padding:5px;border:1px solid #ccc;">Adjusted By</th></tr></thead><tbody>';
foreach ( $results as $hd ) {
// Fetch adjuster name
$adjuster_name = 'Unknown';
if ($hd['adjusted_by'] > 0) {
$udata = get_userdata($hd['adjusted_by']);
if ($udata) {
$adjuster_name = $udata->display_name ? $udata->display_name : ('User ID: '.$hd['adjusted_by']);
}
}
echo '<tr>';
echo '<td style="padding:5px;border:1px solid #ccc;text-align:center;">' . esc_html( date('d-m-y h:i A', strtotime($hd['created_at'])) ) . '</td>'; // Shorter date format
echo '<td style="padding:5px;border:1px solid #ccc;text-align:right;">' . esc_html( number_format((float)$hd['adjusted_amount'], 2) ) . '</td>'; // Right align amount
echo '<td style="padding:5px;border:1px solid #ccc;text-align:left;">' . esc_html( $hd['reason'] ) . '</td>';
echo '<td style="padding:5px;border:1px solid #ccc;text-align:center;">' . esc_html( $adjuster_name ) . '</td>'; // Display adjuster name
echo '</tr>';
}
echo '</tbody></table>';
} else {
echo '<p>No adjustment history found for this seller.</p>';
}
$history_html = ob_get_clean();
wp_send_json_success( array(
'history_html' => $history_html
) );
}
?>
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Function to clear plugin database tables.
*/
function bsm_clear_tables() {
// Security check
if (!isset($_POST['bsm_clear_tables_nonce']) || !wp_verify_nonce($_POST['bsm_clear_tables_nonce'], 'bsm_clear_tables')) {
wp_die("Security check failed.");
}
if (!current_user_can('manage_options')) {
wp_die("You do not have sufficient permissions to access this page.");
}
global $wpdb;
$tables = [
$wpdb->prefix . 'bsm_sales',
$wpdb->prefix . 'bsm_sellers',
$wpdb->prefix . 'bsm_transactions',
$wpdb->prefix . 'bsm_sale_notes'
];
foreach ($tables as $table) {
$wpdb->query("TRUNCATE TABLE $table");
}
wp_redirect(add_query_arg('settings_updated', 'true', wp_get_referer()));
exit;
}
add_action('admin_post_bsm_clear_tables', 'bsm_clear_tables');
add_action('admin_post_nopriv_bsm_clear_tables', 'bsm_clear_tables');
<?php
/**
* File path: business-seller-management/includes/install/create-tables.php
*
* This file ensures that upon plugin activation,
* all required tables and columns get created (or updated),
* without deleting existing data.
*/
if ( ! defined('ABSPATH') ) {
exit; // prevent direct access
}
/**
* bsm_create_tables()
*
* Creates or updates (via dbDelta) the necessary tables for this plugin.
* - wpex_bsm_sales
* - wpex_bsm_sellers
* - wpex_bsm_transactions
* - wpex_bsm_sale_notes
*
* If a table/column already exists, dbDelta() will leave it as is
* so data remains intact. If a column is missing, dbDelta() will add it.
*/
function bsm_create_tables() {
global $wpdb;
// IMPORTANT: By default, $wpdb->prefix might be "wp_".
// But you specifically want "wpex_bsm_sales" etc.
// We'll hardcode "wpex_" in this example.
// If you want to rely on $wpdb->prefix, replace "wpex_" with "{$wpdb->prefix}"
// Make sure we can run dbDelta()
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$charset_collate = $wpdb->get_charset_collate();
// ---- 1) wpex_bsm_sales ----
// এখানে আপনি ওই কলামগুলো রাখবেন যেগুলো submit-sale.php ইনসার্ট করে:
// id, seller_id, product_name, sale_type, order_id, payment_method, transaction_id,
// purchase_price, selling_price, profit, loss, status, created_at,
// customer_payment, work_proof_link, video_duration, proof_text, proof_screen_short, note
$sql_sales = "CREATE TABLE IF NOT EXISTS wpex_bsm_sales (
id mediumint(9) NOT NULL AUTO_INCREMENT,
seller_id mediumint(9) NOT NULL,
product_name varchar(255) NOT NULL,
sale_type varchar(50) NOT NULL,
order_id varchar(100) NOT NULL DEFAULT '',
payment_method varchar(50) NOT NULL,
transaction_id varchar(100) NOT NULL,
purchase_price decimal(10,2) NOT NULL DEFAULT 0,
selling_price decimal(10,2) NOT NULL DEFAULT 0,
profit decimal(10,2) NOT NULL DEFAULT 0,
loss decimal(10,2) NOT NULL DEFAULT 0,
status varchar(50) NOT NULL DEFAULT 'pending',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
customer_payment varchar(255) NOT NULL DEFAULT '',
work_proof_link text NOT NULL,
video_duration decimal(10,2) NOT NULL DEFAULT 0,
proof_text text NOT NULL,
proof_screen_short text NOT NULL,
note text NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
// ---- 2) wpex_bsm_sellers ----
$sql_sellers = "CREATE TABLE IF NOT EXISTS wpex_bsm_sellers (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id mediumint(9) NOT NULL,
gsmalo_org_balance decimal(10,2) NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// ---- 3) wpex_bsm_transactions ----
$sql_transactions = "CREATE TABLE IF NOT EXISTS wpex_bsm_transactions (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
transaction_type varchar(50) NOT NULL,
amount decimal(10,2) NOT NULL DEFAULT 0,
status varchar(50) NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// ---- 4) wpex_bsm_sale_notes ----
$sql_sale_notes = "CREATE TABLE IF NOT EXISTS wpex_bsm_sale_notes (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
user_id mediumint(9) NOT NULL,
note_type varchar(20) NOT NULL DEFAULT 'private',
note_text text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// dbDelta can process multiple CREATE statements if they're separated by `;`
// But it's often simpler to call dbDelta per statement:
dbDelta( $sql_sales );
dbDelta( $sql_sellers );
dbDelta( $sql_transactions );
dbDelta( $sql_sale_notes );
}
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
// Handle sale edit from Admin side
function bsm_handle_sale_edit() {
// Check if the form was submitted, nonce is set and valid
if (isset($_POST['edit_sale']) && isset($_POST['bsm_edit_sale_nonce']) && wp_verify_nonce($_POST['bsm_edit_sale_nonce'], 'bsm_edit_sale')) {
global $wpdb;
// Optional: Add capability check if needed
if (!current_user_can('edit_posts')) { // Example capability check
wp_die("You don't have permission to edit sales.");
}
$current_user_id = get_current_user_id();
$current_user_display_name = get_userdata($current_user_id)->display_name; // Get display name for log
// Sanitize and validate form data
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
if ($sale_id <= 0) {
wp_redirect(add_query_arg('sale_updated', 'invalid_id', wp_get_referer()));
exit;
}
// Fetch the sale data *before* the update to get the old status and other necessary details
$table_name_sales = $wpdb->prefix . 'bsm_sales';
$sale_before_update = $wpdb->get_row($wpdb->prepare(
"SELECT seller_id, status, sale_type, purchase_price FROM $table_name_sales WHERE id = %d",
$sale_id
));
if (!$sale_before_update) {
wp_redirect(add_query_arg('sale_updated', 'not_found', wp_get_referer()));
exit;
}
$old_status = $sale_before_update->status;
$seller_id_for_balance = $sale_before_update->seller_id; // Get seller ID for balance update
$original_sale_type = $sale_before_update->sale_type; // Get original sale type for balance logic
$original_purchase_price = $sale_before_update->purchase_price; // Get original purchase price for balance logic
// Get new data from POST
$product_name = isset($_POST['product_name']) ? sanitize_text_field($_POST['product_name']) : '';
// Get sale_type from POST, but use the original one for balance logic if type is not editable
$sale_type_from_post = isset($_POST['sale_type']) ? sanitize_text_field($_POST['sale_type']) : $original_sale_type;
$payment_method = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : '';
$selling_price = isset($_POST['selling_price']) ? floatval($_POST['selling_price']) : 0.0;
// Get purchase price from POST, but use the original one if needed for balance calc
$purchase_price_from_post = isset($_POST['purchase_price']) ? floatval($_POST['purchase_price']) : $original_purchase_price;
$new_status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : $old_status;
// --- TODO: Add validation for required fields if necessary ---
// Calculate profit and loss based on potentially updated prices
$profit = 0;
$loss = 0;
if (strtolower($original_sale_type) === 'gsmalo.org') { // Use original sale type for this logic
$dollarRate = get_option('bsm_usd_value_in_taka', 85);
// Use the purchase price submitted from the form for profit calc IF it was editable,
// otherwise use the original. Assuming it IS editable based on the form fields.
$purchase_price_for_profit_calc_usd = $purchase_price_from_post;
$purchase_price_bdt = $purchase_price_for_profit_calc_usd * $dollarRate;
$profit = max(0, $selling_price - $purchase_price_bdt);
$loss = max(0, $purchase_price_bdt - $selling_price);
} else {
// Assume BDT for other types for profit/loss calculation
$profit = max(0, $selling_price - $purchase_price_from_post); // Use potentially updated purchase price
$loss = max(0, $purchase_price_from_post - $selling_price);
}
// Prepare data for update
$update_data = array(
'product_name' => $product_name,
'sale_type' => $sale_type_from_post, // Update sale type if changed in form
'payment_method' => $payment_method,
'selling_price' => $selling_price,
'purchase_price' => $purchase_price_from_post, // Update purchase price
'profit' => $profit,
'loss' => $loss,
'status' => $new_status // Update status
// Add other editable fields here if the edit form includes them
);
$update_format = array(
'%s', '%s', '%s', '%f', '%f', '%f', '%f', '%s'
// Add formats for other fields
);
// Update sale data in the database
$updated = $wpdb->update(
$table_name_sales,
$update_data,
array('id' => $sale_id),
$update_format,
array('%d')
);
$redirect_message = '';
if ($updated !== false) {
$redirect_message = 'true'; // Base success message parameter
// --- Check if status actually changed ---
if ($old_status !== $new_status) {
// --- Log the status change in bsm_sale_notes ---
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$status_colors_for_log = [ /* Duplicating colors for safety */
"Rejected" => "#8B0000", "Refund Done" => "#800080", "Successful All" => "#006400", "Check Admin" => "#808080",
"Refund Required" => "#FF00FF", "Reject Delivery Done" => "#A52A2A", "Cost" => "#333333", "Cancel" => "#8B0000",
"Block" => "#000000", "Success But Not Delivery" => "#008000", "Parts Brought" => "#008080", "On Hold" => "#FFA500",
"In Process" => "#1E90FF", "Failed" => "#FF0000", "Need Parts" => "#FF69B4", "pending" => "#cccccc", "Review Apply" => "#666666",
'Addition' => '#00008B', 'Revert' => '#FF6347'
];
// Manual log text generation
$old_col = isset($status_colors_for_log[$old_status]) ? $status_colors_for_log[$old_status] : "#999";
$new_col = isset($status_colors_for_log[$new_status]) ? $status_colors_for_log[$new_status] : "#999";
$log_note_text = "Status changed from <span style='color:{$old_col}; font-weight:bold;'>{$old_status}</span> to <span style='color:{$new_col}; font-weight:bold;'>{$new_status}</span> by {$current_user_display_name} (Edit Sale).";
$wpdb->insert($table_sale_notes, [
'sale_id' => $sale_id,
'user_id' => $current_user_id,
'note_type' => 'status_change',
'note_text' => $log_note_text,
'created_at' => current_time('mysql')
],
['%d','%d','%s','%s','%s']
);
// --- Direct Balance Adjustment Logic (using new refund group) ---
if (strtolower($original_sale_type) === 'gsmalo.org' && floatval($original_purchase_price) > 0) {
$refund_trigger_statuses = ['Reject Delivery Done', 'Refund Done', 'Rejected']; // Use the correct refund group
$purchase_price_usd_for_refund = floatval($original_purchase_price); // Use original price for refund calc
$was_in_refund_group = in_array($old_status, $refund_trigger_statuses);
$is_in_refund_group = in_array($new_status, $refund_trigger_statuses);
$balance_change = 0;
// Refund condition
if (!$was_in_refund_group && $is_in_refund_group) {
$balance_change = $purchase_price_usd_for_refund; // Add back
}
// Re-deduct condition
elseif ($was_in_refund_group && !$is_in_refund_group) {
$balance_change = -$purchase_price_usd_for_refund; // Deduct again
}
// Apply balance change if needed
if ($balance_change != 0) {
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$seller_info = $wpdb->get_row($wpdb->prepare("SELECT id, gsmalo_org_balance FROM $table_sellers WHERE user_id = %d", $seller_id_for_balance));
if ($seller_info) {
$new_seller_balance = floatval($seller_info->gsmalo_org_balance) + $balance_change;
$balance_updated = $wpdb->update(
$table_sellers,
array('gsmalo_org_balance' => $new_seller_balance),
array('id' => $seller_info->id),
array('%f'),
array('%d')
);
if ($balance_updated === false) {
error_log("BSM Admin Sale Edit Error: Failed to update balance for seller ID {$seller_id_for_balance} on sale ID {$sale_id}. DB Error: " . $wpdb->last_error);
// Optionally add a specific redirect message part
$redirect_message = 'updated_balance_failed';
}
// No separate balance log entry needed
} else {
error_log("BSM Admin Sale Edit Error: Could not find seller record for seller ID {$seller_id_for_balance} to adjust balance.");
$redirect_message = 'updated_seller_not_found';
}
}
} // end gsmalo.org check
} // end if status changed
} else {
// Database update failed or no rows affected
if($wpdb->last_error) {
$redirect_message = 'update_failed_db_error';
error_log("BSM Admin Sale Edit Error: Failed for Sale ID {$sale_id}. DB Error: " . $wpdb->last_error);
} else {
// If $updated is 0 but no error, it means no data actually changed. Check if status changed before update.
if ($old_status !== $new_status) {
// This case is less likely if update returns 0 without error, but handle for completeness
$redirect_message = 'no_actual_changes_but_status_diff';
// We might still need to run balance logic if status *intended* to change but didn't update DB? Unlikely.
} else {
$redirect_message = 'no_changes_made'; // No data changed
}
}
}
// Redirect with success/error message
wp_redirect(add_query_arg('sale_updated', $redirect_message, wp_get_referer()));
exit;
}
}
add_action('init', 'bsm_handle_sale_edit');
// Handle sale deletion (Unchanged from original)
function bsm_handle_sale_delete() {
// Check if delete action is triggered and nonce is valid
// Make nonce sale-specific for better security
if (isset($_GET['delete_sale']) && isset($_GET['_wpnonce']) && wp_verify_nonce($_GET['_wpnonce'], 'bsm_delete_sale_' . intval($_GET['delete_sale']))) {
global $wpdb;
// Optional: Add capability check
if (!current_user_can('delete_posts')) { // Example capability
wp_die("You don't have permission to delete sales.");
}
// Sanitize sale ID
$sale_id = intval($_GET['delete_sale']);
if ($sale_id > 0) {
// --- TODO: Before deleting, consider related data ---
// - Delete related notes from bsm_sale_notes?
// - Delete related status logs from bsm_statas_log_notes?
// - Delete related points from bsm_points?
// - Adjust seller balance if deleting a gsmalo.org sale? (Complicated - depends on current status)
// For now, just deleting the sale record itself.
// Delete sale from the database
$table_name_sales = $wpdb->prefix . 'bsm_sales';
$deleted = $wpdb->delete(
$table_name_sales,
array('id' => $sale_id),
array('%d')
);
// Also delete related notes and logs if desired
// $wpdb->delete($wpdb->prefix . 'bsm_sale_notes', array('sale_id' => $sale_id), array('%d'));
// $wpdb->delete($wpdb->prefix . 'bsm_statas_log_notes', array('sale_id' => $sale_id), array('%d'));
// $wpdb->delete($wpdb->prefix . 'bsm_points', array('sale_id' => $sale_id), array('%d'));
$redirect_message = ($deleted) ? 'true' : 'failed';
} else {
$redirect_message = 'invalid_id';
}
// Redirect with success/error message
$redirect_url = wp_get_referer();
if (!$redirect_url) {
$redirect_url = admin_url('admin.php?page=bsm-all-sales'); // Fallback to all sales page
}
// Ensure the nonce and delete_sale param are removed from the URL after redirect
$redirect_url = remove_query_arg(array('delete_sale', '_wpnonce'), $redirect_url);
wp_redirect(add_query_arg('sale_deleted', $redirect_message, $redirect_url));
exit;
}
}
add_action('init', 'bsm_handle_sale_delete');
?>
<?php
if ( ! defined('ABSPATH') ) {
exit; // Exit if accessed directly
}
/**
* bsm_fix_database()
*
* Repairs and optimizes the plugin tables (without dropping them).
*/
function bsm_fix_database() {
// Security check: Nonce and capability
if ( ! isset($_POST['bsm_fix_db_nonce']) || ! wp_verify_nonce($_POST['bsm_fix_db_nonce'], 'bsm_fix_db') ) {
wp_die("Security check failed!");
}
if ( ! current_user_can('manage_options') ) {
wp_die("You do not have sufficient permissions to perform this action.");
}
global $wpdb;
$tables = array(
$wpdb->prefix . 'bsm_sales',
$wpdb->prefix . 'bsm_sellers',
$wpdb->prefix . 'bsm_transactions',
$wpdb->prefix . 'bsm_sale_notes',
$wpdb->prefix . 'bsm_payment_methods',
$wpdb->prefix . 'bsm_statas_log_notes'
);
foreach ( $tables as $table ) {
// Repair the table
$wpdb->query("REPAIR TABLE $table");
// Optimize the table
$wpdb->query("OPTIMIZE TABLE $table");
}
wp_redirect(add_query_arg('fix_db', 'true', wp_get_referer()));
exit;
}
add_action('admin_post_bsm_fix_database', 'bsm_fix_database');
add_action('admin_post_nopriv_bsm_fix_database', 'bsm_fix_database');
/**
* bsm_reset_database()
*
* Drops all plugin custom tables and recreates them from scratch.
* This provides a one-click solution for fully resetting the database
* so that new tables (with correct structure) are created again.
*/
function bsm_reset_database() {
// Security check: Nonce and capability
if ( ! isset($_POST['bsm_reset_db_nonce']) || ! wp_verify_nonce($_POST['bsm_reset_db_nonce'], 'bsm_reset_db') ) {
wp_die("Security check failed!");
}
if ( ! current_user_can('manage_options') ) {
wp_die("You do not have sufficient permissions to perform this action.");
}
global $wpdb;
// List all custom plugin tables here
$tables = array(
$wpdb->prefix . 'bsm_sales',
$wpdb->prefix . 'bsm_sellers',
$wpdb->prefix . 'bsm_transactions',
$wpdb->prefix . 'bsm_sale_notes',
$wpdb->prefix . 'bsm_payment_methods',
$wpdb->prefix . 'bsm_statas_log_notes'
);
// Drop each table if it exists
foreach ( $tables as $table ) {
$wpdb->query("DROP TABLE IF EXISTS $table");
}
// Now call the main table-creation routine
if ( function_exists('bsm_create_tables') ) {
bsm_create_tables(); // From your main plugin file
} else {
wp_die("Table creation function (bsm_create_tables) not found!");
}
wp_redirect(add_query_arg('reset_db', 'true', wp_get_referer()));
exit;
}
add_action('admin_post_bsm_reset_database', 'bsm_reset_database');
add_action('admin_post_nopriv_bsm_reset_database', 'bsm_reset_database');
business-seller-management/includes/admin/payment-methods.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
// Handle payment method addition
function bsm_handle_payment_method_add() {
if (isset($_POST['add_payment_method']) && wp_verify_nonce($_POST['bsm_add_payment_method_nonce'], 'bsm_add_payment_method')) {
global $wpdb;
// Sanitize and validate form data
$method_name = sanitize_text_field($_POST['method_name']);
$method_number = sanitize_text_field($_POST['method_number']);
$method_type = sanitize_text_field($_POST['method_type']);
// Insert payment method into the database
$table_name_payment_methods = $wpdb->prefix . 'bsm_payment_methods';
$wpdb->insert(
$table_name_payment_methods,
array(
'method_name' => $method_name,
'method_number' => $method_number,
'method_type' => $method_type,
'created_at' => current_time('mysql')
),
array('%s', '%s', '%s', '%s')
);
// Redirect with success message
wp_redirect(add_query_arg('payment_method_added', 'true', wp_get_referer()));
exit;
}
}
add_action('init', 'bsm_handle_payment_method_add');
// Handle payment method edit
function bsm_handle_payment_method_edit() {
if (isset($_POST['edit_payment_method']) && wp_verify_nonce($_POST['bsm_edit_payment_method_nonce'], 'bsm_edit_payment_method')) {
global $wpdb;
// Sanitize and validate form data
$method_id = intval($_POST['method_id']);
$method_name = sanitize_text_field($_POST['method_name']);
$method_number = sanitize_text_field($_POST['method_number']);
$method_type = sanitize_text_field($_POST['method_type']);
// Update payment method in the database
$table_name_payment_methods = $wpdb->prefix . 'bsm_payment_methods';
$wpdb->update(
$table_name_payment_methods,
array(
'method_name' => $method_name,
'method_number' => $method_number,
'method_type' => $method_type
),
array('id' => $method_id),
array('%s', '%s', '%s'),
array('%d')
);
// Redirect with success message
wp_redirect(add_query_arg('payment_method_updated', 'true', wp_get_referer()));
exit;
}
}
add_action('init', 'bsm_handle_payment_method_edit');
// Handle payment method deletion
function bsm_handle_payment_method_delete() {
if (isset($_GET['delete_payment_method']) && wp_verify_nonce($_GET['_wpnonce'], 'bsm_delete_payment_method')) {
global $wpdb;
// Sanitize method ID
$method_id = intval($_GET['delete_payment_method']);
// Delete payment method from the database
$table_name_payment_methods = $wpdb->prefix . 'bsm_payment_methods';
$wpdb->delete(
$table_name_payment_methods,
array('id' => $method_id),
array('%d')
);
// Redirect with success message
wp_redirect(add_query_arg('payment_method_deleted', 'true', wp_get_referer()));
exit;
}
}
add_action('init', 'bsm_handle_payment_method_delete');
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
/**
* bsm_reports_page()
*
* Callback function for the "Reports" submenu page.
* Currently shows a placeholder page.
* Later, we will add actual code (charts, filtering, date range, etc.) based on your requirements.
*/
function bsm_reports_page() {
// Placeholder logic for "Reports" menu
// We will include a template file
// wp-content/plugins/business-seller-management/templates/admin/reports-page.php
include BSM_PLUGIN_PATH . 'templates/admin/reports-page.php';
}
business-seller-management/includes/admin/reset-database.php
/**
* Create or update the necessary plugin tables if they do not exist.
*/
function bsm_create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
// টেবিলগুলোর নাম নির্ধারণ
$table_sales = $wpdb->prefix . 'bsm_sales';
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$table_transactions = $wpdb->prefix . 'bsm_transactions';
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$table_statas_log_notes = $wpdb->prefix . 'bsm_statas_log_notes'; // যদি আপনি statas_log_notes টেবিলও ব্যবহার করেন
// ----- bsm_sales টেবিল -----
// এখানে 'order_id', 'customer_payment', 'work_proof_link' ইত্যাদি নতুন কলাম অ্যাড করা হয়েছে।
$sql_sales = "CREATE TABLE $table_sales (
id mediumint(9) NOT NULL AUTO_INCREMENT,
seller_id mediumint(9) NOT NULL,
product_name varchar(255) NOT NULL,
sale_type varchar(50) NOT NULL,
order_id varchar(100) NOT NULL DEFAULT '',
payment_method varchar(50) NOT NULL,
transaction_id varchar(100) NOT NULL,
purchase_price decimal(10,2) NOT NULL DEFAULT 0,
selling_price decimal(10,2) NOT NULL DEFAULT 0,
profit decimal(10,2) NOT NULL DEFAULT 0,
loss decimal(10,2) NOT NULL DEFAULT 0,
status varchar(50) NOT NULL DEFAULT 'pending',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
customer_payment varchar(255) NOT NULL DEFAULT '',
work_proof_link text NOT NULL,
video_duration decimal(10,2) NOT NULL DEFAULT 0,
proof_text text NOT NULL,
proof_screen_short text NOT NULL,
note text NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
// ----- bsm_sellers টেবিল -----
$sql_sellers = "CREATE TABLE $table_sellers (
id mediumint(9) NOT NULL AUTO_INCREMENT,
user_id mediumint(9) NOT NULL,
gsmalo_org_balance decimal(10,2) NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// ----- bsm_transactions টেবিল -----
$sql_transactions = "CREATE TABLE $table_transactions (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
transaction_type varchar(50) NOT NULL,
amount decimal(10,2) NOT NULL DEFAULT 0,
status varchar(50) NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// ----- bsm_sale_notes টেবিল -----
$sql_sale_notes = "CREATE TABLE $table_sale_notes (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
user_id mediumint(9) NOT NULL,
note_type varchar(20) NOT NULL DEFAULT 'private',
note_text text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// ----- bsm_statas_log_notes টেবিল (যদি আপনার প্রয়োজন হয়) -----
// statas-log-notes.php ফাইলে আপনি যদি এই টেবিল ব্যবহার করে থাকেন, তাহলে এখানে যুক্ত করুন
$sql_statas_log_notes = "CREATE TABLE $table_statas_log_notes (
id mediumint(9) NOT NULL AUTO_INCREMENT,
sale_id mediumint(9) NOT NULL,
user_id mediumint(9) NOT NULL,
note_type varchar(20) NOT NULL DEFAULT 'private',
note_text text NOT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
// dbDelta কল করে টেবিলগুলো তৈরি/আপডেট করা
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql_sales);
dbDelta($sql_sellers);
dbDelta($sql_transactions);
dbDelta($sql_sale_notes);
// statas_log_notes টেবিল লাগলে
dbDelta($sql_statas_log_notes);
}
business-seller-management/includes/admin/role-permissions.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* This file handles the "Role Permissions" settings form.
*/
/**
* Handle form submission from the Settings page.
*/
function bsm_update_role_permissions() {
// Security check
if (!isset($_POST['bsm_role_permissions_nonce']) ||
!wp_verify_nonce($_POST['bsm_role_permissions_nonce'], 'bsm_role_permissions')) {
wp_die("Security check failed!");
}
if (!current_user_can('manage_options')) {
wp_die("You do not have permission to do this.");
}
// Check posted data
$enable_editor_sales = isset($_POST['enable_editor_sales']) ? 1 : 0;
// Update option so we can remember user preference
update_option('bsm_enable_editor_sales', $enable_editor_sales);
// Now add or remove capability from Editor role
$editor = get_role('editor');
if ($editor) {
if ($enable_editor_sales) {
// Add capability
$editor->add_cap('manage_bsm_sales', true);
} else {
// Remove capability
$editor->remove_cap('manage_bsm_sales');
}
}
// Redirect back to settings with success message
wp_redirect(add_query_arg('settings_updated', 'true', wp_get_referer()));
exit;
}
add_action('admin_post_bsm_update_role_permissions', 'bsm_update_role_permissions');
/**
* On plugin activation, we can optionally set default or do nothing.
* But we will rely on user toggling in the Settings page.
*/
<?php
/**
* File: business-seller-management/includes/admin/settings-general-save.php
*
* Description:
* - এই ফাইলটি General Settings ফর্মের ডেটা প্রক্রিয়াকরণ ও সেভ করার জন্য ব্যবহৃত হয়।
* - ইউজারের ইনপুট (USD to BDT রেট, Debug Mode, Notification Email) স্যানিটাইজ করে
* ডাটাবেজে আপডেট করে এবং সফলভাবে সেভ হলে পূর্বের পেজে রিডাইরেক্ট করে।
*/
if ( ! defined('ABSPATH') ) {
exit;
}
/**
* bsm_save_general_settings()
*
* General Settings ফর্ম সাবমিট হলে, এই ফাংশনটি ইনপুট যাচাই করে এবং সেটিংস গুলো
* ডাটাবেজে আপডেট করে।
*/
function bsm_save_general_settings() {
// ইউজার অনুমতি যাচাই
if ( ! current_user_can('manage_options') ) {
wp_die("You do not have sufficient permissions to perform this action.");
}
// ননস যাচাই
if ( ! isset($_POST['bsm_general_settings_nonce']) || ! wp_verify_nonce($_POST['bsm_general_settings_nonce'], 'bsm_save_general_settings') ) {
wp_die("Security check failed.");
}
// ইনপুট স্যানিটাইজ ও আপডেট
$usd_rate = isset($_POST['bsm_usd_rate']) ? floatval($_POST['bsm_usd_rate']) : 85;
$debug_mode = isset($_POST['bsm_debug_mode']) ? 1 : 0;
$notification_email = isset($_POST['bsm_notification_email']) ? sanitize_email($_POST['bsm_notification_email']) : get_option('admin_email');
update_option('bsm_usd_rate', $usd_rate);
update_option('bsm_debug_mode', $debug_mode);
update_option('bsm_notification_email', $notification_email);
// সফলভাবে সেভ হলে পূর্বের পেজে রিডাইরেক্ট করুন
wp_redirect(add_query_arg('general_settings_saved', 'true', wp_get_referer()));
exit;
}
add_action('admin_post_bsm_save_general_settings', 'bsm_save_general_settings');
business-seller-management/includes/admin/settings.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Callback function for the Admin Settings page.
*/
function bsm_settings_page() {
// যদি কোনো POST রিকোয়েস্ট থাকে, তাহলে প্রক্রিয়া করুন (যদি প্রয়োজন হয়)
// তবে এখানে আমরা শুধু সেটিং পেজের UI দেখাচ্ছি।
include BSM_PLUGIN_PATH . 'templates/admin/settings-page.php';
}
/**
* Shortcode registration for Settings page (if needed)
* You can also access this page from the Admin Menu.
*/
function bsm_settings_shortcode() {
ob_start();
bsm_settings_page();
return ob_get_clean();
}
add_shortcode('bsm_settings_page', 'bsm_settings_shortcode');
business-seller-management/includes/admin/status.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
/**
* bsm_status_page()
*
* Callback function for the "Status" submenu page.
* Currently shows a placeholder page.
* Later, we will add actual code (status overview, status change logs, etc.) based on your requirements.
*/
function bsm_status_page() {
// Placeholder logic for "Status" menu
// We will include a template file here
// wp-content/plugins/business-seller-management/templates/admin/status-page.php
include BSM_PLUGIN_PATH . 'templates/admin/status-page.php';
}
business-seller-management/includes/admin/update-status.php
<?php
/**
* File Path: plugins/business-seller-management/includes/seller/status-update.php
*
* AJAX handler for updating sale status with step logic.
* Contains the central function `bsm_handle_gsmalo_balance_after_status_update`.
*
* **Updated:** The central balance function `bsm_handle_gsmalo_balance_after_status_update`
* now retrieves the list of refund trigger statuses from the WordPress option
* 'bsm_refund_trigger_statuses' instead of using a hardcoded array.
*
* - Refunds purchase_price (USD) if status changes *into* the saved refund group.
* - Re-deducts purchase_price (USD) if status changes *out of* the saved refund group.
* - Updates seller's gsmalo_org_balance in bsm_sellers table.
* - **Does NOT** add entries to bsm_balance_adjustments table for these automatic adjustments.
* - Other logic (nonce, permissions, status logging in bsm_sale_notes, step validation) remains unchanged.
*/
if ( ! defined('ABSPATH') ) {
exit;
}
/**
* Central function to handle balance adjustments after status update for gsmalo.org sales.
* Reads refund trigger statuses from WP Options.
*
* @param int $sale_id The ID of the sale being updated.
* @param string $old_status The status before the update.
* @param string $new_status The status after the update.
*/
if ( ! function_exists('bsm_handle_gsmalo_balance_after_status_update') ) {
function bsm_handle_gsmalo_balance_after_status_update($sale_id, $old_status, $new_status) {
global $wpdb;
// --- Get refund trigger statuses from options ---
$default_refund_statuses = ['Reject Delivery Done', 'Refund Done', 'Rejected']; // Default if option not set
$refund_trigger_statuses = get_option('bsm_refund_trigger_statuses', $default_refund_statuses);
// Ensure it's always an array
if (!is_array($refund_trigger_statuses)) {
$refund_trigger_statuses = $default_refund_statuses;
}
// --- End Get refund trigger statuses ---
// Fetch sale details needed for logic
$sale = $wpdb->get_row($wpdb->prepare(
"SELECT seller_id, sale_type, purchase_price FROM {$wpdb->prefix}bsm_sales WHERE id = %d",
$sale_id
));
// Proceed only if it's a gsmalo.org sale with a positive purchase price
if ( $sale && strtolower($sale->sale_type) === 'gsmalo.org' && floatval($sale->purchase_price) > 0 ) {
$purchase_price_usd = floatval($sale->purchase_price);
$seller_id = $sale->seller_id;
$was_in_refund_group = in_array($old_status, $refund_trigger_statuses);
$is_in_refund_group = in_array($new_status, $refund_trigger_statuses);
$balance_change = 0;
// Condition 1: Refund - Entering the refund group from a non-refund status
if (!$was_in_refund_group && $is_in_refund_group) {
$balance_change = $purchase_price_usd; // Add back
}
// Condition 2: Re-Deduct - Leaving the refund group to a non-refund status
elseif ($was_in_refund_group && !$is_in_refund_group) {
$balance_change = -$purchase_price_usd; // Deduct again
}
// Apply the balance change if it's non-zero
if ($balance_change != 0) {
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$seller_info = $wpdb->get_row($wpdb->prepare("SELECT id, gsmalo_org_balance FROM $table_sellers WHERE user_id = %d", $seller_id));
if ($seller_info) {
$new_seller_balance = floatval($seller_info->gsmalo_org_balance) + $balance_change;
// Update seller's balance
$balance_updated = $wpdb->update(
$table_sellers,
array('gsmalo_org_balance' => $new_seller_balance),
array('id' => $seller_info->id),
array('%f'),
array('%d')
);
if ($balance_updated === false) {
error_log("BSM Balance Adjustment Error: Failed to update balance for seller ID {$seller_id} on sale ID {$sale_id} during status change from {$old_status} to {$new_status}. DB Error: " . $wpdb->last_error);
}
// **NO LOGGING TO bsm_balance_adjustments here**
} else {
error_log("BSM Balance Adjustment Error: Could not find seller record for seller ID {$seller_id} to adjust balance for sale ID {$sale_id}.");
}
}
} // End if gsmalo.org sale
} // End function bsm_handle_gsmalo_balance_after_status_update
}
if ( ! function_exists('bsm_ajax_status_update') ) {
function bsm_ajax_status_update() {
global $wpdb; // Removed $positive_statuses_for_balance
// 1) Nonce check (Unchanged)
if ( ! isset($_POST['nonce']) || ! wp_verify_nonce($_POST['nonce'], 'bsm_status_update') ) {
wp_send_json_error( array('message' => 'Invalid nonce') );
}
// 2) Must be logged in (Unchanged)
if ( ! is_user_logged_in() ) {
wp_send_json_error( array('message' => 'User not logged in') );
}
// 3) Identify user roles & capabilities (Unchanged)
$current_user = wp_get_current_user();
$updating_user_id = $current_user->ID;
$roles = (array) $current_user->roles;
$is_admin = in_array('administrator', $roles);
$is_editor = in_array('editor', $roles);
$has_bsm_manage_all = user_can($current_user, 'bsm_manage_all_statuses');
// 4) Get sale_id and new_status (Unchanged)
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
$new_status = isset($_POST['new_status']) ? sanitize_text_field($_POST['new_status']) : '';
if ( $sale_id <= 0 || empty($new_status) ) {
wp_send_json_error( array('message' => 'Invalid sale ID or status') );
}
$table_sales = $wpdb->prefix . 'bsm_sales';
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$table_sellers = $wpdb->prefix . 'bsm_sellers';
// 5) Fetch existing sale details (Unchanged)
$sale = $wpdb->get_row($wpdb->prepare(
"SELECT id, seller_id, status, sale_type, purchase_price FROM $table_sales WHERE id=%d",
$sale_id
));
if ( ! $sale ) {
wp_send_json_error( array('message' => 'Sale record not found') );
}
$current_status = $sale->status;
$seller_id = $sale->seller_id;
// Prevent updating if status is the same (Unchanged)
if ($current_status === $new_status) {
wp_send_json_error( array('message' => 'Status is already ' . $new_status) );
}
// Status color map (Unchanged)
$status_colors = [ /* ... colors remain the same ... */
"Rejected" => "#8B0000", "Refund Done" => "#800080", "Successful All" => "#006400", "Check Admin" => "#808080",
"Refund Required" => "#FF00FF", "Reject Delivery Done" => "#A52A2A", "Cost" => "#333333", "Cancel" => "#8B0000",
"Block" => "#000000", "Success But Not Delivery" => "#008000", "Parts Brought" => "#008080", "On Hold" => "#FFA500",
"In Process" => "#1E90FF", "Failed" => "#FF0000", "Need Parts" => "#FF69B4", "pending" => "#cccccc", "Review Apply" => "#666666",
'Addition' => '#00008B',
'Revert' => '#FF6347'
];
// Helper functions (Unchanged)
if ( ! function_exists('bsm_make_status_log_text') ) {
function bsm_make_status_log_text($old_status, $new_status, $status_colors) {
// Fetch display names for statuses
$all_statuses_display = get_option('bsm_all_statuses', []);
$old_status_display = $all_statuses_display[$old_status] ?? $old_status;
$new_status_display = $all_statuses_display[$new_status] ?? $new_status;
$old_col = isset($status_colors[$old_status]) ? $status_colors[$old_status] : "#999";
$new_col = isset($status_colors[$new_status]) ? $status_colors[$new_status] : "#999";
// Use display names in the log text
return "Status changed from <span style='color:{$old_col}; font-weight:bold;'>{$old_status_display}</span> to <span style='color:{$new_col}; font-weight:bold;'>{$new_status_display}</span>";
}
}
if ( ! function_exists('bsm_perform_status_update_db') ) {
function bsm_perform_status_update_db($sale_id, $old_status, $new_status, $user_id, $status_colors) {
global $wpdb; $table_sales = $wpdb->prefix . 'bsm_sales'; $table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$updated = $wpdb->update( $table_sales, ['status' => $new_status], ['id' => $sale_id], ['%s'], ['%d'] );
if ( $updated === false ) { return "Database update failed: " . $wpdb->last_error; }
$note_text = bsm_make_status_log_text($old_status, $new_status, $status_colors); // This now uses display names if helper is defined correctly
$wpdb->insert($table_sale_notes, [ 'sale_id' => $sale_id, 'user_id' => $user_id, 'note_type' => 'status_change', 'note_text' => $note_text, 'created_at' => current_time('mysql') ], ['%d','%d','%s','%s','%s']);
return true;
}
}
// Step-based transition logic (Unchanged)
$can_update = false;
if ( $is_admin || ($is_editor && $has_bsm_manage_all) ) {
$can_update = true;
} else {
// Fetch current list of all statuses from options for validation
$all_statuses = array_keys(get_option('bsm_all_statuses', $status_colors)); // Use keys from option
$step2_map = [ "Success But Not Delivery" => ["Successful All"], "Parts Brought" => ["Successful All"], "Refund Required" => ["Cancel","Refund Done"] ];
$step3_locked = [ "Refund Done","Successful All","Check Admin","Reject Delivery Done","Cost","Cancel", "Block","Rejected","Failed","Review Apply" ];
$allowed_from_need_parts = [ "Cancel","Parts Brought","Success But Not Delivery", "Successful All","Rejected","In Process","Failed" ];
$transition_map = [
"pending" => array_diff($all_statuses, ["pending"]), "On Hold" => array_diff($all_statuses, ["On Hold"]),
"In Process"=> array_diff($all_statuses, ["In Process"]), "Need Parts"=> $allowed_from_need_parts,
"Success But Not Delivery" => $step2_map["Success But Not Delivery"] ?? [], // Add null coalesce
"Parts Brought" => $step2_map["Parts Brought"] ?? [],
"Refund Required" => $step2_map["Refund Required"] ?? [],
];
foreach ($step3_locked as $st3) { if (in_array($st3, $all_statuses)) $transition_map[$st3] = []; } // Only lock existing statuses
$possible_other_types = ['video','marketing','office_management','other_option','other'];
$is_other_sale_type = in_array(strtolower($sale->sale_type), $possible_other_types);
$allowed_next = isset($transition_map[$current_status]) ? $transition_map[$current_status] : [];
if (empty($allowed_next)) { wp_send_json_error( array('message' => "Current status '{$current_status}' is locked.") ); }
if ($is_other_sale_type) {
$allowed_for_other_seller = ["pending","On Hold","In Process","Review Apply"];
$allowed_next = array_intersect($allowed_next, $allowed_for_other_seller);
}
// Ensure $new_status is a valid status from options before checking transitions
if (!in_array($new_status, $all_statuses)) { wp_send_json_error( array('message' => "Invalid target status '{$new_status}'.") ); }
$admin_only_statuses = []; // Define admin-only statuses if needed, e.g., from another option
if (in_array($new_status, $admin_only_statuses) && !$is_admin && !$is_editor) { wp_send_json_error( array('message' => "You cannot set admin-only status '{$new_status}'.") ); }
if (!in_array($new_status, $allowed_next)) { wp_send_json_error( array('message' => "Cannot go from '{$current_status}' to '{$new_status}'.") ); }
$can_update = true;
}
// --- Perform DB Update and Balance Adjustment ---
if ($can_update) {
// 1. Update Status in bsm_sales and log it in bsm_sale_notes
$update_result = bsm_perform_status_update_db($sale_id, $current_status, $new_status, $updating_user_id, $status_colors);
if ($update_result !== true) {
wp_send_json_error( array('message' => $update_result) );
}
// 2. Call the centralized balance adjustment function (which now uses the saved option)
bsm_handle_gsmalo_balance_after_status_update($sale_id, $current_status, $new_status);
// Send success response (Unchanged)
wp_send_json_success([
'message' => "Status updated successfully from '{$current_status}' to '{$new_status}'.",
'new_status' => $new_status,
'sale_id' => $sale_id
]);
} else {
wp_send_json_error( array('message' => 'Update permission denied or validation failed.') );
}
} // end bsm_ajax_status_update()
add_action('wp_ajax_bsm_status_update','bsm_ajax_status_update');
add_action('wp_ajax_nopriv_bsm_status_update','bsm_ajax_status_update');
} // end if function_exists check
?>
<?php
/**
* File: wp-content/plugins/business-seller-management/includes/seller/submit-sale.php
*
* Process the form data for creating a new sale or work entry.
* Tied to action = bsm_submit_sale in admin-post.php
*
* Updated:
* - "Other" গ্রুপের বিক্রয় এন্ট্রিতে Admin Settings এর "পয়েন্ট" ট্যাবের সেটআপ অনুযায়ী পয়েন্ট নির্ধারণ করা হবে।
* - Video Group (Record, Edit & Upload এবং A to Z Complete) এর ক্ষেত্রে ভিডিওর duration (মিনিট/ঘন্টা) ভিত্তিক পয়েন্ট
* গণনা হবে – যদি Duration আপডেট করা হয়, তাহলে সেই অনুযায়ী পয়েন্টও স্বয়ংক্রিয়ভাবে আপডেট হবে।
* - Marketing ও Office Management এর ক্ষেত্রে Quantity (Qty) এর ভিত্তিতে পয়েন্ট গণনা হবে (Admin Settings‑এ নির্ধারিত "Per quantity" অনুযায়ী)।
* - সকল বিক্রয় এন্ট্রিতে level/bonus গণনার লজিক বাদ দিয়ে শুধুমাত্র base points সংরক্ষণ করা হবে; level/bonus গণনা
* পরবর্তীতে Point Report পেজ থেকে সম্পন্ন হবে।
* - **NEW:** If sale_type is 'gsmalo.org', deduct purchase_price (USD) from seller's gsmalo_org_balance immediately upon sale creation.
*/
if ( ! defined('ABSPATH') ) {
exit;
}
function bsm_submit_sale() {
// 1) Must be a POST request
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
wp_die("Invalid request method.");
}
// 2) Check if user is logged in
if ( ! is_user_logged_in() ) {
if ( function_exists('bsm_custom_log_error') ) {
bsm_custom_log_error("bsm_submit_sale: User not logged in.");
}
wp_redirect( add_query_arg('sale_submitted', 'not_logged_in', wp_get_referer()) );
exit;
}
$user_id = get_current_user_id();
// 3) Nonce check
if ( ! isset($_POST['bsm_sale_nonce']) || ! wp_verify_nonce($_POST['bsm_sale_nonce'], 'bsm_submit_sale') ) {
if ( function_exists('bsm_custom_log_error') ) {
bsm_custom_log_error("bsm_submit_sale: Nonce verification failed.");
}
wp_redirect( add_query_arg('sale_submitted', 'nonce_error', wp_get_referer()) );
exit;
}
// 4) Collect common fields
$product_name = isset($_POST['product_name']) ? sanitize_text_field($_POST['product_name']) : '';
$sale_type = isset($_POST['sale_type']) ? sanitize_text_field($_POST['sale_type']) : '';
// Initialize additional variables
$payment_method = '';
$tnx_id = '';
$purchase_price = 0.0;
$selling_price = 0.0;
$customer_payment = '';
$order_id = '';
$note = '';
// For "other" group and for custom sale types
$parts_collection = '';
$work_proof_link = '';
$video_duration = 0.0;
$proof_text = '';
$proof_screen_short = '';
$video_phase = '';
$qty = 0;
$errors = array();
if ( empty($product_name) ) {
$errors[] = "Product/Work Description is required.";
}
if ( empty($sale_type) ) {
$errors[] = "Sale Type is required.";
}
global $wpdb;
// Load the custom sale types (if any)
$custom_sale_types = get_option('bsm_custom_sale_types', array());
if(!is_array($custom_sale_types)) {
$custom_sale_types = array();
}
// Normalize the input sale_type for comparison (e.g., handle www.gsmalo.org)
$normalized_sale_type = strtolower(trim($sale_type));
if ($normalized_sale_type === 'www.gsmalo.org') {
$normalized_sale_type = 'gsmalo.org';
$sale_type = 'gsmalo.org'; // Correct the sale_type being saved
}
switch ( $normalized_sale_type ) { // Use normalized type for switch
case 'product':
$payment_method = isset($_POST['payment_method_product']) ? sanitize_text_field($_POST['payment_method_product']) : '';
$tnx_id = isset($_POST['tnx_id_product']) ? sanitize_text_field($_POST['tnx_id_product']) : '';
$purchase_price = isset($_POST['purchase_price_product']) ? floatval($_POST['purchase_price_product']) : 0.0;
$selling_price = isset($_POST['selling_price_product']) ? floatval($_POST['selling_price_product']) : 0.0;
$customer_payment = isset($_POST['customer_payment_product']) ? sanitize_text_field($_POST['customer_payment_product']) : '';
if ( empty($payment_method) ) {
$errors[] = "Payment Method is required for Product.";
}
if ( $selling_price <= 0 ) {
$errors[] = "Selling Price must be greater than zero for Product.";
}
break;
case 'service':
$parts_collection = isset($_POST['parts_collection_service']) ? sanitize_text_field($_POST['parts_collection_service']) : '';
$payment_method = isset($_POST['payment_method_service']) ? sanitize_text_field($_POST['payment_method_service']) : '';
$tnx_id = isset($_POST['tnx_id_service']) ? sanitize_text_field($_POST['tnx_id_service']) : '';
$purchase_price = isset($_POST['purchase_price_service']) ? floatval($_POST['purchase_price_service']) : 0.0;
$selling_price = isset($_POST['selling_price_service']) ? floatval($_POST['selling_price_service']) : 0.0;
$customer_payment = isset($_POST['customer_payment_service']) ? sanitize_text_field($_POST['customer_payment_service']) : '';
if ( empty($parts_collection) ) {
$errors[] = "Parts Collection is required for Service.";
}
if ( empty($payment_method) ) {
$errors[] = "Payment Method is required for Service.";
}
if ( $selling_price <= 0 ) {
$errors[] = "Selling Price must be greater than zero for Service.";
}
break;
case 'gsmalo.com':
$order_id = isset($_POST['order_id_gsmalo_com']) ? sanitize_text_field($_POST['order_id_gsmalo_com']) : '';
$customer_payment = isset($_POST['customer_payment_gsmalo_com']) ? sanitize_text_field($_POST['customer_payment_gsmalo_com']) : '';
$payment_method = isset($_POST['payment_method_gsmalo_com']) ? sanitize_text_field($_POST['payment_method_gsmalo_com']) : '';
$tnx_id = isset($_POST['tnx_id_gsmalo_com']) ? sanitize_text_field($_POST['tnx_id_gsmalo_com']) : '';
$purchase_price = isset($_POST['purchase_price_gsmalo_com']) ? floatval($_POST['purchase_price_gsmalo_com']) : 0.0;
$selling_price = isset($_POST['selling_price_gsmalo_com']) ? floatval($_POST['selling_price_gsmalo_com']) : 0.0;
if ( empty($order_id) ) {
$errors[] = "Order ID is required for gsmalo.com.";
}
if ( empty($customer_payment) ) {
$errors[] = "Customer Payment Number is required for gsmalo.com.";
}
if ( empty($payment_method) ) {
$errors[] = "Payment Method is required for gsmalo.com.";
}
if ( $selling_price <= 0 ) {
$errors[] = "Selling Price must be greater than zero for gsmalo.com.";
}
break;
case 'gsmalo.org':
$order_id = isset($_POST['order_id_gsmalo_org']) ? sanitize_text_field($_POST['order_id_gsmalo_org']) : '';
$customer_payment = isset($_POST['customer_payment_gsmalo_org']) ? sanitize_text_field($_POST['customer_payment_gsmalo_org']) : '';
$payment_method = isset($_POST['payment_method_gsmalo_org']) ? sanitize_text_field($_POST['payment_method_gsmalo_org']) : '';
$tnx_id = isset($_POST['tnx_id_gsmalo_org']) ? sanitize_text_field($_POST['tnx_id_gsmalo_org']) : '';
$purchase_price = isset($_POST['purchase_price_gsmalo_org']) ? floatval($_POST['purchase_price_gsmalo_org']) : 0.0; // USD Purchase Price
$selling_price = isset($_POST['selling_price_gsmalo_org']) ? floatval($_POST['selling_price_gsmalo_org']) : 0.0;
if ( empty($order_id) ) {
$errors[] = "Order ID is required for gsmalo.org.";
}
if ( empty($customer_payment) ) {
$errors[] = "Customer Payment Number is required for gsmalo.org.";
}
if ( empty($payment_method) ) {
$errors[] = "Payment Method is required for gsmalo.org.";
}
if ( $selling_price <= 0 ) {
$errors[] = "Selling Price must be greater than zero for gsmalo.org.";
}
// Check if Purchase Price USD is provided for gsmalo.org sales
if ( $purchase_price <= 0 ) {
$errors[] = "Purchase Price (USD) must be greater than zero for gsmalo.org.";
}
break;
case 'gsmcourse.com':
$order_id = isset($_POST['order_id_gsmcourse_com']) ? sanitize_text_field($_POST['order_id_gsmcourse_com']) : '';
$customer_payment = isset($_POST['customer_payment_gsmcourse_com']) ? sanitize_text_field($_POST['customer_payment_gsmcourse_com']) : '';
$payment_method = isset($_POST['payment_method_gsmcourse_com']) ? sanitize_text_field($_POST['payment_method_gsmcourse_com']) : '';
$tnx_id = isset($_POST['tnx_id_gsmcourse_com']) ? sanitize_text_field($_POST['tnx_id_gsmcourse_com']) : '';
$purchase_price = isset($_POST['purchase_price_gsmcourse_com']) ? floatval($_POST['purchase_price_gsmcourse_com']) : 0.0;
$selling_price = isset($_POST['selling_price_gsmcourse_com']) ? floatval($_POST['selling_price_gsmcourse_com']) : 0.0;
if ( empty($order_id) ) {
$errors[] = "Order ID is required for gsmcourse.com.";
}
if ( empty($customer_payment) ) {
$errors[] = "Customer Payment Number is required for gsmcourse.com.";
}
if ( empty($payment_method) ) {
$errors[] = "Payment Method is required for gsmcourse.com.";
}
if ( $selling_price <= 0 ) {
$errors[] = "Selling Price must be greater than zero for gsmcourse.com.";
}
break;
case 'other':
$other_sale_subtype = isset($_POST['other_sale_subtype']) ? sanitize_text_field($_POST['other_sale_subtype']) : '';
if ( empty($other_sale_subtype) ) {
$errors[] = "Other Sale Subtype is required.";
}
// For Other group, use the subtype as the effective sale type.
$sale_type = $other_sale_subtype; // Override the main sale_type variable
if ( strtolower($other_sale_subtype) === 'video' ) {
// Video Group processing
$video_duration_h = isset($_POST['video_duration_h']) ? intval($_POST['video_duration_h']) : 0;
$video_duration_m = isset($_POST['video_duration_m']) ? intval($_POST['video_duration_m']) : 0;
$video_duration_s = isset($_POST['video_duration_s']) ? intval($_POST['video_duration_s']) : 0;
$totalSec = $video_duration_h * 3600 + $video_duration_m * 60 + $video_duration_s;
if ($totalSec < 10) {
$errors[] = "Video Duration is required and must be >= 10 seconds for Video subtype.";
}
$video_phase_input = isset($_POST['video_phase']) ? sanitize_text_field($_POST['video_phase']) : '';
if ( empty($video_phase_input) ) {
$errors[] = "Video work type is required for Video subtype.";
} else {
// Normalize video_phase based on input.
if (stripos($video_phase_input, 'record') !== false) {
$video_phase = 'record';
} elseif (stripos($video_phase_input, 'edit') !== false) {
$video_phase = 'edit_upload';
} elseif (stripos($video_phase_input, 'a to z') !== false || stripos($video_phase_input, 'atoz') !== false || strtolower($video_phase_input) === 'all_complete') {
$video_phase = 'a_to_z_complete';
} else {
$video_phase = strtolower(str_replace(' ', '_', $video_phase_input));
}
// Additional normalization to ensure consistency
$video_phase = strtolower(trim($video_phase));
$video_phase = str_replace(' ', '_', $video_phase);
}
$video_duration = $totalSec;
$work_proof_link = ''; // Reset for video
$proof_text = ''; // Reset for video
} else {
// For non-video Other group (e.g. Marketing, Office Management)
$proof_text = isset($_POST['proof_text']) ? sanitize_text_field($_POST['proof_text']) : '';
if ( empty($proof_text) ) {
$errors[] = "Proof Text is required for {$other_sale_subtype} subtype.";
}
$proof_link = isset($_POST['proof_link']) ? esc_url_raw($_POST['proof_link']) : '';
if (!empty($proof_link) && preg_match('/^https?:\\/\\//i', $proof_link)) {
$dup_count = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}bsm_sales WHERE sale_type IN ('marketing','office_management','other_option') AND work_proof_link = %s",
$proof_link
));
if ($dup_count > 0) {
$errors[] = "This link is already used in another sale.";
}
$work_proof_link = $proof_link;
} else {
$work_proof_link = '';
}
// Handle File Upload for Proof Screen Short
if ( isset($_FILES['proof_screen_short']) && !empty($_FILES['proof_screen_short']['name']) ) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
$uploadedfile = $_FILES['proof_screen_short'];
$upload_overrides = array('test_form' => false);
$movefile = wp_handle_upload($uploadedfile, $upload_overrides);
if ( $movefile && !isset($movefile['error']) ) {
$proof_screen_short = $movefile['url'];
} else {
$errors[] = "Error uploading Proof Screen Short: " . (isset($movefile['error']) ? $movefile['error'] : 'Unknown error');
}
} else {
$proof_screen_short = '';
}
$video_duration = 0.0;
$video_phase = '';
}
$qty = isset($_POST['number_of_jobs']) ? intval($_POST['number_of_jobs']) : 0;
break;
default:
// Handle custom sale types
if( isset($custom_sale_types[$sale_type]) ) {
$cfg = $custom_sale_types[$sale_type];
if(!isset($cfg['fields']) || !is_array($cfg['fields'])){ // Fix: Check if fields index exists
$cfg['fields'] = array();
}
if(in_array('order_id', $cfg['fields'])){
$order_id = isset($_POST["order_id_{$sale_type}"]) ? sanitize_text_field($_POST["order_id_{$sale_type}"]) : '';
}
if(in_array('payment_method', $cfg['fields'])){
$payment_method = isset($_POST["payment_method_{$sale_type}"]) ? sanitize_text_field($_POST["payment_method_{$sale_type}"]) : '';
}
if(in_array('tnx_id', $cfg['fields'])){
$tnx_id = isset($_POST["tnx_id_{$sale_type}"]) ? sanitize_text_field($_POST["tnx_id_{$sale_type}"]) : '';
}
if(in_array('customer_payment', $cfg['fields'])){
$customer_payment = isset($_POST["customer_payment_{$sale_type}"]) ? sanitize_text_field($_POST["customer_payment_{$sale_type}"]) : '';
}
if(in_array('parts_collection', $cfg['fields'])){
$parts_collection = isset($_POST["parts_collection_{$sale_type}"]) ? sanitize_text_field($_POST["parts_collection_{$sale_type}"]) : '';
}
if(in_array('purchase_price_bdt', $cfg['fields'])){
$purchase_price = isset($_POST["purchase_price_{$sale_type}"]) ? floatval($_POST["purchase_price_{$sale_type}"]) : 0.0;
}
if(in_array('purchase_price_usd', $cfg['fields'])){
$purchase_price = isset($_POST["purchase_price_{$sale_type}"]) ? floatval($_POST["purchase_price_{$sale_type}"]) : 0.0; // USD Purchase Price for custom types
}
if(in_array('selling_price_bdt', $cfg['fields'])){
$selling_price = isset($_POST["selling_price_{$sale_type}"]) ? floatval($_POST["selling_price_{$sale_type}"]) : 0.0;
}
// Validation for custom types (basic example)
if(in_array('order_id', $cfg['fields']) && empty($order_id)) { $errors[] = "Order ID is required for this Sale Type."; }
if(in_array('selling_price_bdt', $cfg['fields']) && $selling_price <= 0) { $errors[] = "Selling Price must be greater than zero."; }
if(in_array('purchase_price_usd', $cfg['fields']) && $purchase_price <= 0) { $errors[] = "Purchase Price (USD) must be greater than zero."; }
} else {
$errors[] = "Invalid Sale Type selected.";
}
break;
}
// Check for duplicate transaction ID (if provided)
if ( ! empty($tnx_id) ) {
$dup_count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}bsm_sales WHERE transaction_id = %s",
$tnx_id
));
if ( $dup_count > 0 ) {
$errors[] = "This transaction has already been sold. Please use another transaction.";
}
}
// Check for duplicate Order ID (if provided)
if ( ! empty($order_id) ) {
$dup_order_count = $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}bsm_sales WHERE order_id = %s",
$order_id
));
if ( $dup_order_count > 0 ) {
$errors[] = "An order with this Order ID already exists. Please verify.";
}
}
// If validation errors exist, redirect back
if ( ! empty($errors) ) {
if ( function_exists('bsm_custom_log_error') ) {
bsm_custom_log_error("bsm_submit_sale: Validation errors => " . implode(" | ", $errors));
}
// Store errors in session/transient to display on form? (Optional enhancement)
wp_redirect( add_query_arg('sale_submitted', 'missing_fields', wp_get_referer()) );
exit;
}
// 8) Calculate profit & loss
$sale_type_key = strtolower(trim($sale_type)); // Use the possibly overridden $sale_type
if ( $sale_type_key === 'gsmalo.org' ) {
$dollarRate = get_option('bsm_usd_value_in_taka', 85);
$purchase_price_bdt = $purchase_price * $dollarRate; // Convert USD purchase to BDT for profit calculation
$profit = max(0, $selling_price - $purchase_price_bdt);
$loss = max(0, $purchase_price_bdt - $selling_price);
} else {
// For Other group types (video, marketing, office_management) and others without cost,
// profit calculation based on BDT purchase price (if available)
if ( in_array($sale_type_key, array('video','marketing','office_management','other_option')) ) {
$profit = 0; // Base points calculated differently
$loss = 0;
} else {
$profit = max(0, $selling_price - $purchase_price); // Assuming purchase_price here is BDT for non-gsmalo.org
$loss = max(0, $purchase_price - $selling_price);
}
}
// 9) Prepare sale record data for insertion
$data = array(
'seller_id' => $user_id,
'product_name' => $product_name,
'sale_type' => $sale_type, // Use the potentially overridden sale_type
'order_id' => $order_id,
'payment_method' => $payment_method,
'transaction_id' => $tnx_id,
'purchase_price' => $purchase_price, // Stores USD for gsmalo.org, BDT otherwise
'selling_price' => $selling_price, // Always BDT
'profit' => $profit, // Always BDT
'loss' => $loss, // Always BDT
'status' => 'pending',
'customer_payment' => $customer_payment,
'work_proof_link' => $work_proof_link,
'video_duration' => $video_duration,
'proof_text' => $proof_text,
'proof_screen_short' => $proof_screen_short,
'video_phase' => $video_phase,
'qty' => $qty,
'note' => '', // Initial note is empty
'created_at' => current_time('mysql')
);
$format = array(
'%d','%s','%s','%s','%s','%s','%f','%f','%f','%f','%s','%s','%s','%f','%s','%s','%s','%d','%s','%s'
);
$inserted = $wpdb->insert(
$wpdb->prefix . 'bsm_sales',
$data,
$format
);
if ( false === $inserted ) {
if ( function_exists('bsm_custom_log_error') ) {
bsm_custom_log_error("bsm_submit_sale: DB insertion failed => " . $wpdb->last_error);
}
wp_redirect( add_query_arg('sale_submitted', 'db_error', wp_get_referer()) );
exit;
}
$new_sale_id = $wpdb->insert_id;
// ===== NEW: Balance Deduction for gsmalo.org Sales =====
if ($sale_type_key === 'gsmalo.org' && $purchase_price > 0) {
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$seller_info = $wpdb->get_row($wpdb->prepare("SELECT id, gsmalo_org_balance FROM $table_sellers WHERE user_id = %d", $user_id));
if ($seller_info) {
$current_balance = floatval($seller_info->gsmalo_org_balance);
$new_balance = $current_balance - $purchase_price; // Deduct USD purchase price
$wpdb->update(
$table_sellers,
array('gsmalo_org_balance' => $new_balance),
array('id' => $seller_info->id),
array('%f'),
array('%d')
);
} else {
// If seller doesn't exist in bsm_sellers, create a new record with the deducted balance
$wpdb->insert(
$table_sellers,
array(
'user_id' => $user_id,
'gsmalo_org_balance' => -$purchase_price, // Initial balance is negative deduction
'points_balance' => 0.00, // Assuming points balance starts at 0
'created_at' => current_time('mysql')
),
array('%d', '%f', '%f', '%s')
);
}
// Note: No bsm_balance_adjustments entry is created here per user requirement
}
// ===== END: Balance Deduction =====
// ===== Points Calculation =====
// শুধুমাত্র base points হিসাব করে তা সংরক্ষণ করা হবে।
$points = 0; // Initialize points
if ( in_array($sale_type_key, array('video','marketing','office_management')) ) {
if ($sale_type_key === 'video') {
// Video Group: Calculate based on video_duration (in minutes) and video_phase.
$minutes = $video_duration / 60;
$video_group_points = get_option('bsm_video_group_points', array());
$group_key = $video_phase; // Already normalized
$thresholds = array(
array('min' => 0, 'max' => 2, 'key' => '1+'),
array('min' => 2, 'max' => 3, 'key' => '2+'),
array('min' => 3, 'max' => 4, 'key' => '3+'),
array('min' => 4, 'max' => 5, 'key' => '4+'),
array('min' => 5, 'max' => 7, 'key' => '5+'),
array('min' => 7, 'max' => 10, 'key' => '7+'),
array('min' => 10, 'max' => 13, 'key' => '10+'),
array('min' => 13, 'max' => 16, 'key' => '13+'),
array('min' => 16, 'max' => 20, 'key' => '16+'),
array('min' => 20, 'max' => 25, 'key' => '20+'),
array('min' => 25, 'max' => 30, 'key' => '25+'),
array('min' => 30, 'max' => 40, 'key' => '30+'),
array('min' => 40, 'max' => 50, 'key' => '40+'),
array('min' => 50, 'max' => 60, 'key' => '50+'),
array('min' => 60, 'max' => 80, 'key' => '1_hour+'),
array('min' => 80, 'max' => 100, 'key' => '1_20_hour+'),
array('min' => 100, 'max' => 110, 'key' => '1_40_hour+'),
array('min' => 110, 'max' => 120, 'key' => '2_hour+'),
array('min' => 120, 'max' => 150, 'key' => '2_30_hour+'),
);
foreach ($thresholds as $th) {
if ($minutes >= $th['min'] && $minutes < $th['max']) {
if (isset($video_group_points[$group_key]) && isset($video_group_points[$group_key][$th['key']])) {
$points = floatval($video_group_points[$group_key][$th['key']]);
}
break;
}
}
// Fallback for exceeding highest threshold
if ($points == 0 && $minutes >= end($thresholds)['min']) {
$last_key = end($thresholds)['key'];
if (isset($video_group_points[$group_key]) && isset($video_group_points[$group_key][$last_key])) {
$points = floatval($video_group_points[$group_key][$last_key]);
}
}
} elseif ($sale_type_key === 'marketing' || $sale_type_key === 'office_management') {
$other_group_points = get_option('bsm_other_group_points', array());
$point_rate = isset($other_group_points[$sale_type_key]) ? floatval($other_group_points[$sale_type_key]) : 30; // Default 30 if not set
$points = $point_rate * ($qty > 0 ? $qty : 1);
} else {
$points = 0; // Default for 'other_option' or unhandled subtypes
}
} else {
// For non-Other sale types (product, service, websites, custom), calculate based on profit and daily rate
$daily_point_rates = get_option('bsm_daily_point_rates', array());
$point_rate = 10; // Default rate
if (isset($daily_point_rates[$sale_type_key]) && isset($daily_point_rates[$sale_type_key]['rate'])) {
$point_rate = floatval($daily_point_rates[$sale_type_key]['rate']);
}
// Profit was calculated earlier in BDT
if ($profit > 0) {
$points = round($profit * ($point_rate / 100.0));
} else {
$points = 0;
}
}
// Insert single points row (base points only) if points > 0
if ($points > 0) {
$points_data = array(
'user_id' => $user_id,
'sale_id' => $new_sale_id,
'points' => $points,
'type' => 'base', // Always 'base' for initial entry
'description' => 'Base points for sale entry ID ' . $new_sale_id,
'created_at' => current_time('mysql')
);
$points_format = array('%d', '%d', '%f', '%s', '%s', '%s');
$wpdb->insert($wpdb->prefix . 'bsm_points', $points_data, $points_format);
// Update seller's points_balance (only if points were awarded)
$seller_points_info = $wpdb->get_row($wpdb->prepare("SELECT id, points_balance FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d", $user_id));
if ($seller_points_info) {
$new_points_balance = floatval($seller_points_info->points_balance) + $points;
$wpdb->update(
$wpdb->prefix . 'bsm_sellers',
array('points_balance' => $new_points_balance),
array('id' => $seller_points_info->id), // Use primary key 'id' from bsm_sellers
array('%f'),
array('%d')
);
} else {
// If seller was created just now for balance deduction, update points balance
// Or if seller existed but had no points before
$wpdb->update(
$wpdb->prefix . 'bsm_sellers',
array('points_balance' => $points),
array('user_id' => $user_id), // Find by user_id
array('%f'),
array('%d')
);
// If update affected 0 rows (meaning seller still doesn't exist - unlikely now)
if($wpdb->rows_affected == 0) {
$wpdb->insert(
$wpdb->prefix . 'bsm_sellers',
array(
'user_id' => $user_id,
'gsmalo_org_balance' => ($sale_type_key === 'gsmalo.org' && $purchase_price > 0) ? -$purchase_price : 0.00, // Apply deduction if it's gsmalo.org
'points_balance' => $points,
'created_at' => current_time('mysql')
),
array('%d','%f','%f','%s')
);
}
}
} // end if points > 0
if ( function_exists('bsm_custom_log_error') ) {
bsm_custom_log_error("bsm_submit_sale: Sale inserted successfully => Sale ID: " . $new_sale_id);
}
// Redirect back to the previous page with a success message
$redirect_url = wp_get_referer();
if ( empty($redirect_url) ) {
// Fallback URL if referer is not available
$redirect_url = site_url('/seller-dashboard/?sale_submitted=true'); // Adjust as needed
} else {
$redirect_url = add_query_arg('sale_submitted', 'true', $redirect_url);
}
wp_redirect( $redirect_url );
exit;
}
add_action('admin_post_bsm_submit_sale', 'bsm_submit_sale');
add_action('admin_post_nopriv_bsm_submit_sale', 'bsm_submit_sale'); // Should ideally check if user role is 'seller'
?>
<?php
/**
* File Path: plugins/business-seller-management/includes/seller/status-update.php
*
* AJAX handler for updating sale status with step logic.
* Contains the central function `bsm_handle_gsmalo_balance_after_status_update`.
*
* **Updated:** The central balance function `bsm_handle_gsmalo_balance_after_status_update`
* now retrieves the list of refund trigger statuses from the WordPress option
* 'bsm_refund_trigger_statuses' instead of using a hardcoded array.
*
* - Refunds purchase_price (USD) if status changes *into* the saved refund group.
* - Re-deducts purchase_price (USD) if status changes *out of* the saved refund group.
* - Updates seller's gsmalo_org_balance in bsm_sellers table.
* - **Does NOT** add entries to bsm_balance_adjustments table for these automatic adjustments.
* - Other logic (nonce, permissions, status logging in bsm_sale_notes, step validation) remains unchanged.
*/
if ( ! defined('ABSPATH') ) {
exit;
}
/**
* Central function to handle balance adjustments after status update for gsmalo.org sales.
* Reads refund trigger statuses from WP Options.
*
* @param int $sale_id The ID of the sale being updated.
* @param string $old_status The status before the update.
* @param string $new_status The status after the update.
*/
if ( ! function_exists('bsm_handle_gsmalo_balance_after_status_update') ) {
function bsm_handle_gsmalo_balance_after_status_update($sale_id, $old_status, $new_status) {
global $wpdb;
// --- Get refund trigger statuses from options ---
$default_refund_statuses = ['Reject Delivery Done', 'Refund Done', 'Rejected']; // Default if option not set
$refund_trigger_statuses = get_option('bsm_refund_trigger_statuses', $default_refund_statuses);
// Ensure it's always an array
if (!is_array($refund_trigger_statuses)) {
$refund_trigger_statuses = $default_refund_statuses;
}
// --- End Get refund trigger statuses ---
// Fetch sale details needed for logic
$sale = $wpdb->get_row($wpdb->prepare(
"SELECT seller_id, sale_type, purchase_price FROM {$wpdb->prefix}bsm_sales WHERE id = %d",
$sale_id
));
// Proceed only if it's a gsmalo.org sale with a positive purchase price
if ( $sale && strtolower($sale->sale_type) === 'gsmalo.org' && floatval($sale->purchase_price) > 0 ) {
$purchase_price_usd = floatval($sale->purchase_price);
$seller_id = $sale->seller_id;
$was_in_refund_group = in_array($old_status, $refund_trigger_statuses);
$is_in_refund_group = in_array($new_status, $refund_trigger_statuses);
$balance_change = 0;
// Condition 1: Refund - Entering the refund group from a non-refund status
if (!$was_in_refund_group && $is_in_refund_group) {
$balance_change = $purchase_price_usd; // Add back
}
// Condition 2: Re-Deduct - Leaving the refund group to a non-refund status
elseif ($was_in_refund_group && !$is_in_refund_group) {
$balance_change = -$purchase_price_usd; // Deduct again
}
// Apply the balance change if it's non-zero
if ($balance_change != 0) {
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$seller_info = $wpdb->get_row($wpdb->prepare("SELECT id, gsmalo_org_balance FROM $table_sellers WHERE user_id = %d", $seller_id));
if ($seller_info) {
$new_seller_balance = floatval($seller_info->gsmalo_org_balance) + $balance_change;
// Update seller's balance
$balance_updated = $wpdb->update(
$table_sellers,
array('gsmalo_org_balance' => $new_seller_balance),
array('id' => $seller_info->id),
array('%f'),
array('%d')
);
if ($balance_updated === false) {
error_log("BSM Balance Adjustment Error: Failed to update balance for seller ID {$seller_id} on sale ID {$sale_id} during status change from {$old_status} to {$new_status}. DB Error: " . $wpdb->last_error);
}
// **NO LOGGING TO bsm_balance_adjustments here**
} else {
error_log("BSM Balance Adjustment Error: Could not find seller record for seller ID {$seller_id} to adjust balance for sale ID {$sale_id}.");
}
}
} // End if gsmalo.org sale
} // End function bsm_handle_gsmalo_balance_after_status_update
}
if ( ! function_exists('bsm_ajax_status_update') ) {
function bsm_ajax_status_update() {
global $wpdb; // Removed $positive_statuses_for_balance
// 1) Nonce check (Unchanged)
if ( ! isset($_POST['nonce']) || ! wp_verify_nonce($_POST['nonce'], 'bsm_status_update') ) {
wp_send_json_error( array('message' => 'Invalid nonce') );
}
// 2) Must be logged in (Unchanged)
if ( ! is_user_logged_in() ) {
wp_send_json_error( array('message' => 'User not logged in') );
}
// 3) Identify user roles & capabilities (Unchanged)
$current_user = wp_get_current_user();
$updating_user_id = $current_user->ID;
$roles = (array) $current_user->roles;
$is_admin = in_array('administrator', $roles);
$is_editor = in_array('editor', $roles);
$has_bsm_manage_all = user_can($current_user, 'bsm_manage_all_statuses');
// 4) Get sale_id and new_status (Unchanged)
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
$new_status = isset($_POST['new_status']) ? sanitize_text_field($_POST['new_status']) : '';
if ( $sale_id <= 0 || empty($new_status) ) {
wp_send_json_error( array('message' => 'Invalid sale ID or status') );
}
$table_sales = $wpdb->prefix . 'bsm_sales';
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$table_sellers = $wpdb->prefix . 'bsm_sellers';
// 5) Fetch existing sale details (Unchanged)
$sale = $wpdb->get_row($wpdb->prepare(
"SELECT id, seller_id, status, sale_type, purchase_price FROM $table_sales WHERE id=%d",
$sale_id
));
if ( ! $sale ) {
wp_send_json_error( array('message' => 'Sale record not found') );
}
$current_status = $sale->status;
$seller_id = $sale->seller_id;
// Prevent updating if status is the same (Unchanged)
if ($current_status === $new_status) {
wp_send_json_error( array('message' => 'Status is already ' . $new_status) );
}
// Status color map (Unchanged)
$status_colors = [ /* ... colors remain the same ... */
"Rejected" => "#8B0000", "Refund Done" => "#800080", "Successful All" => "#006400", "Check Admin" => "#808080",
"Refund Required" => "#FF00FF", "Reject Delivery Done" => "#A52A2A", "Cost" => "#333333", "Cancel" => "#8B0000",
"Block" => "#000000", "Success But Not Delivery" => "#008000", "Parts Brought" => "#008080", "On Hold" => "#FFA500",
"In Process" => "#1E90FF", "Failed" => "#FF0000", "Need Parts" => "#FF69B4", "pending" => "#cccccc", "Review Apply" => "#666666",
'Addition' => '#00008B',
'Revert' => '#FF6347'
];
// Helper functions (Unchanged)
if ( ! function_exists('bsm_make_status_log_text') ) {
function bsm_make_status_log_text($old_status, $new_status, $status_colors) {
// Fetch display names for statuses
$all_statuses_display = get_option('bsm_all_statuses', []);
$old_status_display = $all_statuses_display[$old_status] ?? $old_status;
$new_status_display = $all_statuses_display[$new_status] ?? $new_status;
$old_col = isset($status_colors[$old_status]) ? $status_colors[$old_status] : "#999";
$new_col = isset($status_colors[$new_status]) ? $status_colors[$new_status] : "#999";
// Use display names in the log text
return "Status changed from <span style='color:{$old_col}; font-weight:bold;'>{$old_status_display}</span> to <span style='color:{$new_col}; font-weight:bold;'>{$new_status_display}</span>";
}
}
if ( ! function_exists('bsm_perform_status_update_db') ) {
function bsm_perform_status_update_db($sale_id, $old_status, $new_status, $user_id, $status_colors) {
global $wpdb; $table_sales = $wpdb->prefix . 'bsm_sales'; $table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$updated = $wpdb->update( $table_sales, ['status' => $new_status], ['id' => $sale_id], ['%s'], ['%d'] );
if ( $updated === false ) { return "Database update failed: " . $wpdb->last_error; }
$note_text = bsm_make_status_log_text($old_status, $new_status, $status_colors); // This now uses display names if helper is defined correctly
$wpdb->insert($table_sale_notes, [ 'sale_id' => $sale_id, 'user_id' => $user_id, 'note_type' => 'status_change', 'note_text' => $note_text, 'created_at' => current_time('mysql') ], ['%d','%d','%s','%s','%s']);
return true;
}
}
// Step-based transition logic (Unchanged)
$can_update = false;
if ( $is_admin || ($is_editor && $has_bsm_manage_all) ) {
$can_update = true;
} else {
// Fetch current list of all statuses from options for validation
$all_statuses = array_keys(get_option('bsm_all_statuses', $status_colors)); // Use keys from option
$step2_map = [ "Success But Not Delivery" => ["Successful All"], "Parts Brought" => ["Successful All"], "Refund Required" => ["Cancel","Refund Done"] ];
$step3_locked = [ "Refund Done","Successful All","Check Admin","Reject Delivery Done","Cost","Cancel", "Block","Rejected","Failed","Review Apply" ];
$allowed_from_need_parts = [ "Cancel","Parts Brought","Success But Not Delivery", "Successful All","Rejected","In Process","Failed" ];
$transition_map = [
"pending" => array_diff($all_statuses, ["pending"]), "On Hold" => array_diff($all_statuses, ["On Hold"]),
"In Process"=> array_diff($all_statuses, ["In Process"]), "Need Parts"=> $allowed_from_need_parts,
"Success But Not Delivery" => $step2_map["Success But Not Delivery"] ?? [], // Add null coalesce
"Parts Brought" => $step2_map["Parts Brought"] ?? [],
"Refund Required" => $step2_map["Refund Required"] ?? [],
];
foreach ($step3_locked as $st3) { if (in_array($st3, $all_statuses)) $transition_map[$st3] = []; } // Only lock existing statuses
$possible_other_types = ['video','marketing','office_management','other_option','other'];
$is_other_sale_type = in_array(strtolower($sale->sale_type), $possible_other_types);
$allowed_next = isset($transition_map[$current_status]) ? $transition_map[$current_status] : [];
if (empty($allowed_next)) { wp_send_json_error( array('message' => "Current status '{$current_status}' is locked.") ); }
if ($is_other_sale_type) {
$allowed_for_other_seller = ["pending","On Hold","In Process","Review Apply"];
$allowed_next = array_intersect($allowed_next, $allowed_for_other_seller);
}
// Ensure $new_status is a valid status from options before checking transitions
if (!in_array($new_status, $all_statuses)) { wp_send_json_error( array('message' => "Invalid target status '{$new_status}'.") ); }
$admin_only_statuses = []; // Define admin-only statuses if needed, e.g., from another option
if (in_array($new_status, $admin_only_statuses) && !$is_admin && !$is_editor) { wp_send_json_error( array('message' => "You cannot set admin-only status '{$new_status}'.") ); }
if (!in_array($new_status, $allowed_next)) { wp_send_json_error( array('message' => "Cannot go from '{$current_status}' to '{$new_status}'.") ); }
$can_update = true;
}
// --- Perform DB Update and Balance Adjustment ---
if ($can_update) {
// 1. Update Status in bsm_sales and log it in bsm_sale_notes
$update_result = bsm_perform_status_update_db($sale_id, $current_status, $new_status, $updating_user_id, $status_colors);
if ($update_result !== true) {
wp_send_json_error( array('message' => $update_result) );
}
// 2. Call the centralized balance adjustment function (which now uses the saved option)
bsm_handle_gsmalo_balance_after_status_update($sale_id, $current_status, $new_status);
// Send success response (Unchanged)
wp_send_json_success([
'message' => "Status updated successfully from '{$current_status}' to '{$new_status}'.",
'new_status' => $new_status,
'sale_id' => $sale_id
]);
} else {
wp_send_json_error( array('message' => 'Update permission denied or validation failed.') );
}
} // end bsm_ajax_status_update()
add_action('wp_ajax_bsm_status_update','bsm_ajax_status_update');
add_action('wp_ajax_nopriv_bsm_status_update','bsm_ajax_status_update');
} // end if function_exists check
?>
<?php
/**
* File: business-seller-management/includes/seller/statas-log-notes.php
*
* This file handles the storage and retrieval of note logs into/from the
* bsm_statas_log_notes table. It also provides AJAX handlers for adding a note
* and for fetching all logs (both status logs and note logs).
*
* Update:
* - Added helper function `bsm_calculate_balance_change_for_status_transition` to calculate potential balance changes for gsmalo.org sales based on status transitions.
* - Modified `bsm_ajax_fetch_all_logs` to parse status change logs, calculate the associated balance impact using the helper function, and append this info to the log text displayed in the modal popup.
* - **Updated:** The helper function `bsm_calculate_balance_change_for_status_transition` now retrieves the list of refund trigger statuses from the WordPress option 'bsm_refund_trigger_statuses'.
*
* Ensure that the bsm_statas_log_notes table is created (via fix-database.php) with the following columns:
* id, sale_id, user_id, note_type, note_text, created_at
*
* Reminder: Other plugin files (e.g. sale-page.php) include this file via require_once.
*/
if ( ! defined('ABSPATH') ) {
exit; // Prevent direct access.
}
/**
* Insert a new note/log entry into the bsm_statas_log_notes table.
* (Unchanged from previous version)
*/
if ( ! function_exists('bsm_insert_statas_log') ) {
function bsm_insert_statas_log( $sale_id, $note_text, $user_id = 0, $note_type = 'private' ) {
if ( empty($sale_id) || empty($note_text) ) {
return new WP_Error('missing_data', 'Missing sale_id or note_text.');
}
global $wpdb;
$table = $wpdb->prefix . 'bsm_statas_log_notes';
// Get current user ID if not provided
if ($user_id === 0) {
$user_id = get_current_user_id();
}
$data = [
'sale_id' => intval($sale_id),
'user_id' => intval($user_id),
'note_type' => sanitize_text_field($note_type),
'note_text' => sanitize_textarea_field($note_text),
'created_at' => current_time('mysql'),
];
$format = ['%d','%d','%s','%s','%s'];
$inserted = $wpdb->insert($table, $data, $format);
if ( $inserted === false ) {
return new WP_Error('db_error', 'Database insertion failed: ' . $wpdb->last_error);
}
return true;
}
}
/**
* Retrieve note logs from the bsm_statas_log_notes table with correct user name.
* (Unchanged from previous version)
*/
if ( ! function_exists('bsm_get_statas_logs') ) {
function bsm_get_statas_logs( $sale_id ) {
global $wpdb;
$table = $wpdb->prefix . 'bsm_statas_log_notes';
$results = $wpdb->get_results(
$wpdb->prepare("
SELECT l.*, COALESCE(u.display_name, '') AS user_name
FROM $table AS l
LEFT JOIN {$wpdb->prefix}users AS u ON l.user_id = u.ID
WHERE l.sale_id = %d
ORDER BY l.created_at DESC
", $sale_id)
);
return $results;
}
}
/**
* Helper Function: Calculate balance change amount for a specific status transition
* Uses the LATEST definition of refund trigger statuses fetched from WP Options.
*
* @param int $sale_id The Sale ID.
* @param string $old_status The previous status.
* @param string $new_status The new status.
* @return float The calculated balance change (+ for refund, - for re-deduct, 0 for no change).
*/
if ( ! function_exists('bsm_calculate_balance_change_for_status_transition') ) {
function bsm_calculate_balance_change_for_status_transition($sale_id, $old_status, $new_status) {
global $wpdb;
$balance_change = 0.0;
// --- Get refund trigger statuses from options ---
$default_refund_statuses = ['Reject Delivery Done', 'Refund Done', 'Rejected']; // Default if option not set
$refund_trigger_statuses = get_option('bsm_refund_trigger_statuses', $default_refund_statuses);
// Ensure it's always an array
if (!is_array($refund_trigger_statuses)) {
$refund_trigger_statuses = $default_refund_statuses;
}
// --- End Get refund trigger statuses ---
// Fetch sale details
$sale = $wpdb->get_row($wpdb->prepare(
"SELECT sale_type, purchase_price FROM {$wpdb->prefix}bsm_sales WHERE id = %d",
$sale_id
));
// Proceed only if it's a gsmalo.org sale with a positive purchase price
if ( $sale && strtolower($sale->sale_type) === 'gsmalo.org' && floatval($sale->purchase_price) > 0 ) {
$purchase_price_usd = floatval($sale->purchase_price);
$was_in_refund_group = in_array($old_status, $refund_trigger_statuses);
$is_in_refund_group = in_array($new_status, $refund_trigger_statuses);
// Condition 1: Refund - Entering the refund group
if (!$was_in_refund_group && $is_in_refund_group) {
$balance_change = $purchase_price_usd; // Add back
}
// Condition 2: Re-Deduct - Leaving the refund group
elseif ($was_in_refund_group && !$is_in_refund_group) {
$balance_change = -$purchase_price_usd; // Deduct again
}
}
return $balance_change;
}
}
/**
* Helper function to parse old and new status from log text.
* (Unchanged from previous version)
*/
if ( ! function_exists('bsm_parse_status_from_log') ) {
function bsm_parse_status_from_log($log_text) {
$old_status = null;
$new_status = null;
if (preg_match('/from.*?>(.+?)<\/span>.*?to.*?>(.+?)<\/span>/i', $log_text, $matches)) {
if (isset($matches[1])) {
$old_status = trim(strip_tags($matches[1]));
}
if (isset($matches[2])) {
$new_status = trim(strip_tags($matches[2]));
}
}
return array($old_status, $new_status);
}
}
/**
* AJAX handler: Add a new note log entry.
* (Unchanged from previous version)
*/
if ( ! function_exists('bsm_ajax_add_statas_log') ) {
function bsm_ajax_add_statas_log() {
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) { wp_send_json_error("Invalid request method."); }
if ( ! isset($_POST['nonce']) || ! wp_verify_nonce($_POST['nonce'], 'bsm_statas_log') ) { wp_send_json_error("Security check failed."); }
if ( ! is_user_logged_in() ) { wp_send_json_error("User not logged in."); }
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
$note_text = isset($_POST['note_text']) ? sanitize_textarea_field($_POST['note_text']) : '';
$note_type = isset($_POST['note_type']) ? sanitize_text_field($_POST['note_type']) : 'private';
$user_id = get_current_user_id();
$res = bsm_insert_statas_log( $sale_id, $note_text, $user_id, $note_type );
if ( is_wp_error($res) ) { wp_send_json_error( $res->get_error_message() ); }
// --- Fetch updated logs and prepare HTML for display area (e.g., sale-page.php log column) ---
global $wpdb;
$table_status_notes = $wpdb->prefix . 'bsm_sale_notes';
$status_logs = $wpdb->get_results($wpdb->prepare("
SELECT sn.*, COALESCE(u.display_name, '') AS user_name FROM $table_status_notes sn
LEFT JOIN {$wpdb->prefix}users u ON sn.user_id = u.ID
WHERE sn.sale_id = %d AND sn.note_type = 'status_change' ORDER BY sn.created_at DESC
", $sale_id));
$note_logs = bsm_get_statas_logs($sale_id); // Uses the helper function defined above
$max_display = 4;
ob_start();
?>
<div class="log-half">
<p class="log-half-title">STATUS LOG</p>
<div class="log-box-container">
<?php /* Display logic unchanged */
$count_st = 0;
if (!empty($status_logs)) {
foreach($status_logs as $lg) {
if ($count_st >= $max_display) break;
$lg_name = !empty($lg->user_name) ? $lg->user_name : 'Unknown';
$lg_time = date('H:i | d-m-Y', strtotime($lg->created_at));
echo '<div class="status-log-box">';
echo '<p class="compact-info">' . esc_html($lg_name) . ' | ' . esc_html($lg_time) . '</p>';
echo '<p style="margin:0; padding:0;">' . wp_kses_post($lg->note_text) . '</p>';
echo '</div>';
$count_st++;
}
} else { echo '<p style="font-size:11px; text-align:center;">No status logs.</p>'; }
?>
</div>
<?php if (count($status_logs) > $max_display): ?>
<a href="#" class="more-log-link" data-sale-id="<?php echo esc_attr($sale_id); ?>" data-type="status">
+<?php echo (count($status_logs) - $max_display); ?>
</a>
<?php endif; ?>
</div>
<div class="log-half">
<p class="log-half-title">NOTES</p>
<div class="log-box-container">
<?php /* Display logic unchanged */
$count_nt = 0;
if (!empty($note_logs)) {
foreach($note_logs as $nt) {
if ($count_nt >= $max_display) break;
$nt_name = !empty($nt->user_name) ? $nt->user_name : 'Unknown';
$nt_time = date('H:i | d-m-Y', strtotime($nt->created_at));
$bg_color = ($count_nt == 0) ? '#daf8d4' : '#ffffff';
$box_shadow = ($count_nt == 0) ? '0 0 6px rgba(0,128,0,0.4)' : '0 1px 2px rgba(0,0,0,0.1)';
echo '<div class="note-log-box" style="background:'.$bg_color.'; box-shadow:'.$box_shadow.';">';
echo '<p class="compact-info">' . esc_html($nt_name) . ' | ' . esc_html($nt_time) . '</p>';
echo '<p style="margin:0; padding:0;">' . esc_html($nt->note_text) . '</p>';
echo '</div>';
$count_nt++;
}
} else { echo '<p style="font-size:11px; text-align:center;">No notes found.</p>'; }
?>
</div>
<?php if (count($note_logs) > $max_display): ?>
<a href="#" class="more-log-link" data-sale-id="<?php echo esc_attr($sale_id); ?>" data-type="note">
+<?php echo (count($note_logs) - $max_display); ?>
</a>
<?php endif; ?>
</div>
<?php
$logs_html = ob_get_clean();
wp_send_json_success([ 'message' => "Note added successfully.", 'logs_html' => $logs_html ]);
}
add_action('wp_ajax_bsm_statas_log_add','bsm_ajax_add_statas_log');
}
/**
* AJAX handler: Fetch all logs (for modal popup) based on log type.
* Uses the UPDATED refund trigger statuses via the helper function.
* (Unchanged from previous version - relies on updated helper function)
*/
if ( ! function_exists('bsm_ajax_fetch_all_logs') ) {
function bsm_ajax_fetch_all_logs(){
if(!is_user_logged_in()){ wp_send_json_error("Unauthorized user"); }
check_ajax_referer('bsm_note_nonce','nonce'); // Reusing note nonce
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
if($sale_id <= 0){ wp_send_json_error("Invalid sale ID"); }
$log_type = isset($_POST['log_type']) ? sanitize_text_field($_POST['log_type']) : 'status';
ob_start();
if($log_type === 'note'){
// Fetch note logs from bsm_statas_log_notes.
$logs = bsm_get_statas_logs($sale_id);
echo '<div style="font-size:16px; line-height:1.4;">';
if(!empty($logs)){
foreach($logs as $nt){
$nt_name = !empty($nt->user_name) ? $nt->user_name : 'Unknown';
$nt_time = date('h:i A | d-m-Y', strtotime($nt->created_at));
echo '<div class="log-entry">'; // Use generic class
echo '<strong>' . esc_html($nt_name) . '</strong> | <em>' . esc_html($nt_time) . '</em><br>';
echo '<div style="margin-top:4px;">' . esc_html($nt->note_text) . '</div>';
echo '</div>';
}
} else {
echo '<p>No note logs found.</p>';
}
echo '</div>';
} else { // Default case: log_type is 'status'
// Fetch status logs from bsm_sale_notes
global $wpdb;
$table_notes = $wpdb->prefix . 'bsm_sale_notes';
$logs = $wpdb->get_results( $wpdb->prepare(
"SELECT sn.*, COALESCE(u.display_name,'') AS user_name FROM $table_notes sn
LEFT JOIN {$wpdb->prefix}users u ON sn.user_id = u.ID
WHERE sn.sale_id = %d AND sn.note_type = 'status_change'
ORDER BY sn.created_at DESC", $sale_id )
);
echo '<div style="font-size:14px; line-height:1.3;">';
if(!empty($logs)){
foreach($logs as $lg){
$lg_name = !empty($lg->user_name) ? $lg->user_name : 'Unknown';
$lg_time = date('h:i A | d-m-Y', strtotime($lg->created_at));
$log_text_display = wp_kses_post($lg->note_text);
// --- Calculate Balance Change ---
$balance_change_text = '';
list($old_status, $new_status) = bsm_parse_status_from_log($lg->note_text);
if ($old_status !== null && $new_status !== null) {
// Use the helper function (which now uses updated refund statuses from options)
$balance_change_amount = bsm_calculate_balance_change_for_status_transition($lg->sale_id, $old_status, $new_status);
if ($balance_change_amount > 0) {
$balance_change_text = ' <span style="color:blue; font-weight:bold;">(Refund +' . number_format($balance_change_amount, 2) . '$)</span>';
} elseif ($balance_change_amount < 0) {
$balance_change_text = ' <span style="color:red; font-weight:bold;">(Cut ' . number_format($balance_change_amount, 2) . '$)</span>';
}
}
// --- End Calculate Balance Change ---
echo '<div class="log-entry">'; // Use generic class
echo '<strong>' . esc_html($lg_name) . '</strong> | <em>' . esc_html($lg_time) . '</em><br>';
echo '<div style="margin-top:4px;">' . $log_text_display . $balance_change_text . '</div>';
echo '</div>';
}
} else {
echo '<p>No status logs found.</p>';
}
echo '</div>';
}
$html = ob_get_clean();
wp_send_json_success(["html" => $html]);
}
add_action('wp_ajax_bsm_fetch_all_logs','bsm_ajax_fetch_all_logs');
}
?>
business-seller-management/includes/seller/settings.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Shortcode for Seller Settings page
*/
function bsm_seller_dashboard_settings_shortcode() {
ob_start();
// যদি সেলার লগইন না করে থাকে
if (!is_user_logged_in()) {
return '<p style="color:red;font-weight:bold;">Please log in to access the Seller Settings.</p>';
}
// এখানে সেলারদের Settings UI/ডিজাইন লিখবেন
// যেমনঃ সেলার প্রোফাইল এডিট, ফটো আপলোড ইত্যাদি
echo '<h2>Seller Settings Page</h2>';
echo '<p>Placeholder: your profile details, you can update certain fields (email and username cannot be changed).</p>';
return ob_get_clean();
}
add_shortcode('bsm_seller_dashboard_settings', 'bsm_seller_dashboard_settings_shortcode');
business-seller-management/includes/seller/sale.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Shortcode for Seller Dashboard Sale Page
* Usage: [bsm_seller_dashboard_sale]
*/
function bsm_seller_dashboard_sale_shortcode() {
ob_start();
// Check login
if (!is_user_logged_in()) {
return '<p style="color:red;font-weight:bold;">Please log in to access the Seller Sale page.</p>';
}
// Success message if sale_submitted
if (isset($_GET['sale_submitted']) && $_GET['sale_submitted'] === 'true') {
echo '<div style="border:1px solid #4caf50; background:#c8e6c9; padding:10px; margin-bottom:10px;">Sale submitted successfully!</div>';
}
// Include the Sale Page template
include BSM_PLUGIN_PATH . 'templates/seller/sale-page.php';
return ob_get_clean();
}
add_shortcode('bsm_seller_dashboard_sale', 'bsm_seller_dashboard_sale_shortcode');
business-seller-management/includes/seller/report.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Shortcode for Seller Report page
*/
function bsm_seller_dashboard_report_shortcode() {
ob_start();
// যদি সেলার লগইন না করে থাকে
if (!is_user_logged_in()) {
return '<p style="color:red;font-weight:bold;">Please log in to access the Seller Report page.</p>';
}
// এখানে আপনার সেলারদের Report UI/ডিজাইন লিখবেন
echo '<h2>Seller Report Page</h2>';
echo '<p>Placeholder: daily, monthly, yearly, or custom date range reports, with profit/loss analysis.</p>';
return ob_get_clean();
}
add_shortcode('bsm_seller_dashboard_report', 'bsm_seller_dashboard_report_shortcode');
business-seller-management/includes/seller/notes.php
<?php
if (!defined('ABSPATH')) {
exit;
}
// Handle adding a note to a sale
function bsm_add_sale_note() {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bsm_add_note']) ) {
// Check nonce
if (!isset($_POST['bsm_note_nonce']) || !wp_verify_nonce($_POST['bsm_note_nonce'], 'bsm_add_note')) {
wp_die("Security check failed!");
}
if (!is_user_logged_in()) {
wp_die("Unauthorized request.");
}
global $wpdb;
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
$sale_id = intval($_POST['sale_id']);
$user_id = get_current_user_id();
$note_text = sanitize_textarea_field($_POST['note_text']);
// আপনি চাইলে note_type এর অপশন রাখতে পারেন, এখন ডিফল্ট private ধরছি:
$note_type = 'private';
if ($sale_id > 0 && !empty($note_text)) {
// Insert new note
$wpdb->insert($table_sale_notes, [
'sale_id' => $sale_id,
'user_id' => $user_id,
'note_type' => $note_type,
'note_text' => $note_text,
'created_at'=> current_time('mysql')
], ['%d','%d','%s','%s','%s']);
// Redirect back with success param
wp_redirect(add_query_arg('note_added','true', wp_get_referer()));
exit;
} else {
// Missing data
wp_redirect(add_query_arg('note_added','false', wp_get_referer()));
exit;
}
}
}
// Hook for adding note
add_action('admin_post_bsm_add_sale_note', 'bsm_add_sale_note');
add_action('admin_post_nopriv_bsm_add_sale_note', 'bsm_add_sale_note');
business-seller-management/includes/seller/dashboard.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/**
* Shortcode for Seller Dashboard (Main) – Dashboard Page
* Usage: [bsm_seller_dashboard_main]
*/
function bsm_seller_dashboard_main_shortcode() {
ob_start();
// যদি ইউজার লগইন না করে থাকে
if (!is_user_logged_in()) {
return '<p style="color:red;font-weight:bold;">Please log in to access the Seller Dashboard.</p>';
}
// বর্তমান সেলার এর তথ্য
$user_id = get_current_user_id();
global $wpdb;
// org balance সংগ্রহ (placeholder)
$table_sellers = $wpdb->prefix . 'bsm_sellers';
$org_balance = $wpdb->get_var( $wpdb->prepare("SELECT gsmalo_org_balance FROM $table_sellers WHERE user_id = %d", $user_id) );
if (!$org_balance) {
$org_balance = 0;
}
// আজকের সারাংশ (placeholder)
$today_total_sales = 0;
$today_total_profit = 0;
$today_total_works = 0;
// গতকালের সারাংশ (placeholder)
$yesterday_total_sales = 0;
$yesterday_total_profit = 0;
$yesterday_total_works = 0;
// এ মাসের সারাংশ (placeholder)
$month_total_sales = 0;
$month_total_profit = 0;
$month_total_works = 0;
$month_total_points = 0; // লাভের ২০% ধরলাম
// গত মাসের সারাংশ (placeholder)
$last_month_total_sales = 0;
$last_month_total_profit = 0;
$last_month_total_works = 0;
// এখানে চাইলে আপনি আজকের, গতকালের, এই মাসের, গত মাসের সঠিক বিক্রয়-লাভ-ওয়ার্ক ইত্যাদি গণনা করে আনতে পারেন।
// Placeholder হিসেবে আমরা 0 রাখছি।
// ========================================================
// টেমপ্লেট ফাইল ইনক্লুড
include BSM_PLUGIN_PATH . 'templates/seller/dashboard-page.php';
return ob_get_clean();
}
add_shortcode('bsm_seller_dashboard_main', 'bsm_seller_dashboard_main_shortcode');
<?php
/**
* File: business-seller-management/includes/seller/ajax-functions.php
*
* এই ফাইলটি প্লাগিনের AJAX হ্যান্ডলারগুলো ধারণ করে, যাতে ভিডিও, Other (Marketing, Office Management, Other)
* ও Service গ্রুপের এডিট ও আপডেট ফর্মের ডেটা ডাটাবেজে সঠিকভাবে সংযুক্ত ও পয়েন্ট (base) recalculation স্বয়ংক্রিয়ভাবে সম্পন্ন হয়।
*/
if ( ! defined('ABSPATH') ) {
exit;
}
/**
* Helper Function: Update Base Points for a Sale Entry
*
* - এই ফাংশনটি sale_id, sale_type_key, video_duration (যদি থাকে), qty (যদি থাকে) ও video_phase (যদি থাকে) গ্রহণ করে,
* Admin Settings‑এর "Video Group" (for video) অথবা "Other Group" (for marketing, office_management) থেকে পয়েন্ট রেট বের করে,
* bsm_points টেবিলের 'base' টাইপের রেকর্ড আপডেট করে এবং সংশ্লিষ্ট seller-এর points_balance এ প্রভাব ফেলে।
*/
function bsm_update_base_points($sale_id, $sale_type_key, $video_duration = 0, $qty = 0, $video_phase = '') {
global $wpdb;
$new_points = 0;
if ($sale_type_key === 'video') {
// Convert video duration (in seconds) to minutes (as a float).
$minutes = $video_duration / 60;
// Get video group points from Admin Settings.
$video_group_points = get_option('bsm_video_group_points', array());
// Define threshold mapping array (similar to the one in submit-sale.php).
$thresholds = array(
array('min' => 0, 'max' => 2, 'key' => '1+'),
array('min' => 2, 'max' => 3, 'key' => '2+'),
array('min' => 3, 'max' => 4, 'key' => '3+'),
array('min' => 4, 'max' => 5, 'key' => '4+'),
array('min' => 5, 'max' => 7, 'key' => '5+'),
array('min' => 7, 'max' => 10, 'key' => '7+'),
array('min' => 10, 'max' => 13, 'key' => '10+'),
array('min' => 13, 'max' => 16, 'key' => '13+'),
array('min' => 16, 'max' => 20, 'key' => '16+'),
array('min' => 20, 'max' => 25, 'key' => '20+'),
array('min' => 25, 'max' => 30, 'key' => '25+'),
array('min' => 30, 'max' => 40, 'key' => '30+'),
array('min' => 40, 'max' => 50, 'key' => '40+'),
array('min' => 50, 'max' => 60, 'key' => '50+'),
array('min' => 60, 'max' => 80, 'key' => '1_hour+'),
array('min' => 80, 'max' => 100, 'key' => '1_20_hour+'),
array('min' => 100, 'max' => 110, 'key' => '1_40_hour+'),
array('min' => 110, 'max' => 120, 'key' => '2_hour+'),
array('min' => 120, 'max' => 150, 'key' => '2_30_hour+'),
);
// Normalize video_phase key. Expect values: "record", "edit_upload", "a_to_z_complete"
$group_key = strtolower(str_replace(' ', '_', $video_phase));
foreach ($thresholds as $th) {
if ($minutes >= $th['min'] && $minutes < $th['max']) {
if (isset($video_group_points[$group_key]) && isset($video_group_points[$group_key][$th['key']])) {
$new_points = floatval($video_group_points[$group_key][$th['key']]);
}
break;
}
}
} elseif ($sale_type_key === 'marketing') {
$other_group_points = get_option('bsm_other_group_points', array());
$new_points = isset($other_group_points['marketing']) ? floatval($other_group_points['marketing']) : 30;
$new_points = $new_points * ($qty > 0 ? $qty : 1);
} elseif ($sale_type_key === 'office_management') {
$other_group_points = get_option('bsm_other_group_points', array());
$new_points = isset($other_group_points['office_management']) ? floatval($other_group_points['office_management']) : 30;
$new_points = $new_points * ($qty > 0 ? $qty : 1);
} else {
// For other types, no base points update needed.
return;
}
// Get old base points for this sale (assumed one record with type 'base')
$old_points = floatval($wpdb->get_var($wpdb->prepare(
"SELECT points FROM {$wpdb->prefix}bsm_points WHERE sale_id = %d AND type = 'base'",
$sale_id
)));
// Update the base points row with new_points
$wpdb->update(
$wpdb->prefix . 'bsm_points',
array('points' => $new_points),
array('sale_id' => $sale_id, 'type' => 'base'),
array('%f'),
array('%d','%s')
);
// Calculate difference and update seller's points_balance accordingly
$diff = $new_points - $old_points;
$seller = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d",
get_current_user_id()
));
if ($seller) {
$new_balance = floatval($seller->points_balance) + $diff;
$wpdb->update(
$wpdb->prefix . 'bsm_sellers',
array('points_balance' => $new_balance),
array('user_id' => $seller->user_id),
array('%f'),
array('%d')
);
}
}
/**
* AJAX Handler: Video Sale Update
*
* - Edit করার সময়, যদি video_phase "record", "edit_upload" বা "a_to_z_complete" হয়,
* তাহলে POST ডেটা অনুযায়ী initiating sale-এর record আপডেটের পর base points recalculation হবে।
*/
function bsm_update_video_sale() {
if ( ! is_user_logged_in() ) {
wp_send_json_error("User not logged in");
die();
}
$nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
if ( ! wp_verify_nonce($nonce, 'bsm_update_video_sale') ) {
wp_send_json_error("Invalid nonce");
die();
}
// Initiating sale id (যে Sale ID ইনপুট করা হয়েছে)
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
if ( $sale_id <= 0 ) {
wp_send_json_error("Invalid sale id");
die();
}
global $wpdb;
$table_sales = $wpdb->prefix . 'bsm_sales';
$initiating_sale = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_sales WHERE id = %d", $sale_id));
if ( ! $initiating_sale ) {
wp_send_json_error("Initiating sale not found");
die();
}
// Check if editing is allowed on initiating sale
if ( $initiating_sale->status === "Review Apply" || $initiating_sale->status === "Successful All" ) {
wp_send_json_error("Editing not allowed for current status");
die();
}
// Get video_phase from POST. Expected values: "record", "edit_upload", "a_to_z_complete"
$video_phase = isset($_POST['video_phase']) ? trim($_POST['video_phase']) : '';
$video_phase_lower = strtolower($video_phase);
error_log("bsm_update_video_sale: video_phase = " . $video_phase . ", initiating sale_id = " . $sale_id . ", POST data: " . print_r($_POST, true));
if ($video_phase_lower === 'record') {
// Record mode: require Uploaded Sale ID.
$uploaded_sale_id = isset($_POST['uploaded_sale_id']) ? intval($_POST['uploaded_sale_id']) : 0;
if ( $uploaded_sale_id <= 0 ) {
wp_send_json_error("Uploaded Sale ID is required for Record work type.");
die();
}
// Retrieve target record (that holds the data to be copied)
$target_sale = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_sales WHERE id = %d", $uploaded_sale_id));
if ( !$target_sale ) {
wp_send_json_error("Uploaded Sale ID not found.");
die();
}
// Check that target record's video_phase is "Edit & Upload" (case-insensitive)
$target_phase = strtolower(trim($target_sale->video_phase));
$normalized_target_phase = str_replace(' ', '', $target_phase);
if ($normalized_target_phase !== 'edit&upload' && $normalized_target_phase !== 'edit_upload') {
wp_send_json_error("Error: Uploaded Sale ID $uploaded_sale_id is not of type 'Edit & Upload'.");
die();
}
// Prepare update data: copy target record's values.
$target_link = trim($target_sale->work_proof_link);
if (empty($target_link) || !preg_match('/^https?:\\/\\//i', $target_link)) {
$target_link = '';
}
$update_data = array(
'product_name' => !empty($target_sale->product_name) ? $target_sale->product_name : $initiating_sale->product_name,
'sale_type' => !empty($target_sale->sale_type) ? $target_sale->sale_type : $initiating_sale->sale_type,
'work_proof_link' => $target_link,
'video_duration' => ($target_sale->video_duration > 0) ? $target_sale->video_duration : $initiating_sale->video_duration,
'status' => !empty($target_sale->status) ? $target_sale->status : $initiating_sale->status
);
$update_format = array('%s', '%s', '%s', '%f', '%s');
error_log("bsm_update_video_sale: Updating initiating sale (ID: $sale_id) with target record data: " . print_r($update_data, true));
$updated = $wpdb->update(
$table_sales,
$update_data,
array('id' => $sale_id),
$update_format,
array('%d')
);
if ( $updated === false ) {
$error = "Database update failed: " . $wpdb->last_error;
error_log("bsm_update_video_sale error: " . $error);
wp_send_json_error($error);
die();
}
// Additionally, update the edit_upload_id field
$wpdb->update(
$table_sales,
array('edit_upload_id' => $uploaded_sale_id),
array('id' => $sale_id),
array('%d'),
array('%d')
);
// Recalculate and update base points for record mode as well.
bsm_update_base_points($sale_id, 'video', $update_data['video_duration'], 0, 'record');
wp_send_json_success(["message" => "Video sale (Record mode) updated successfully for initiating Sale ID: $sale_id using data from Uploaded Sale ID: $uploaded_sale_id"]);
die();
} elseif ($video_phase_lower === 'edit_upload' || $video_phase_lower === 'a_to_z_complete') {
// For "Edit & Upload" and "A to Z Complete" modes: update initiating sale using POST data.
if ($video_phase_lower === 'edit_upload') {
$final_video_phase = 'edit_upload';
} else {
$final_video_phase = 'a_to_z_complete';
}
$product_name = isset($_POST['product_name']) ? sanitize_text_field($_POST['product_name']) : '';
$work_proof_link = isset($_POST['work_proof_link']) ? esc_url_raw($_POST['work_proof_link']) : '';
$video_duration = isset($_POST['video_duration']) ? floatval($_POST['video_duration']) : 0;
// Duplicate check update for work_proof_link:
if (!empty($work_proof_link) && preg_match('/^https?:\\/\\//i', $work_proof_link)) {
$results = $wpdb->get_results($wpdb->prepare(
"SELECT id, video_phase FROM $table_sales WHERE work_proof_link = %s AND id != %d",
$work_proof_link,
$sale_id
));
$nonRecordDuplicates = 0;
$recordDuplicates = 0;
foreach ($results as $res) {
if (strtolower(trim($res->video_phase)) === 'record') {
$recordDuplicates++;
} else {
$nonRecordDuplicates++;
}
}
if ($nonRecordDuplicates > 0 || $recordDuplicates > 1) {
wp_send_json_error("This link is already used in another sale.");
die();
}
}
error_log("bsm_update_video_sale: Standard update for initiating sale_id = $sale_id, product_name = $product_name, work_proof_link = $work_proof_link, video_duration = $video_duration, video_phase set to $final_video_phase");
$update_data = array(
'product_name' => $product_name,
'work_proof_link' => $work_proof_link,
'video_duration' => $video_duration,
'video_phase' => $final_video_phase
);
$updated = $wpdb->update(
$table_sales,
$update_data,
['id' => $sale_id],
['%s', '%s', '%f', '%s'],
['%d']
);
if ( $updated === false ) {
$error = "Database update failed: " . $wpdb->last_error;
error_log("bsm_update_video_sale standard update error: " . $error);
wp_send_json_error($error);
die();
}
// Recalculate and update base points for video sale after editing.
bsm_update_base_points($sale_id, 'video', $video_duration, 0, $final_video_phase);
wp_send_json_success(["message" => "Video sale updated successfully and base points recalculated"]);
die();
} else {
// Default: Standard update if video_phase not matching expected values.
$product_name = isset($_POST['product_name']) ? sanitize_text_field($_POST['product_name']) : '';
$work_proof_link = isset($_POST['work_proof_link']) ? esc_url_raw($_POST['work_proof_link']) : '';
$video_duration = isset($_POST['video_duration']) ? floatval($_POST['video_duration']) : 0;
error_log("bsm_update_video_sale: Default standard update for initiating sale_id = $sale_id, product_name = $product_name, work_proof_link = $work_proof_link, video_duration = $video_duration");
$updated = $wpdb->update(
$table_sales,
[
'product_name' => $product_name,
'work_proof_link' => $work_proof_link,
'video_duration' => $video_duration
],
['id' => $sale_id],
['%s', '%s', '%f'],
['%d']
);
if ( $updated === false ) {
$error = "Database update failed: " . $wpdb->last_error;
error_log("bsm_update_video_sale default update error: " . $error);
wp_send_json_error($error);
die();
}
wp_send_json_success(["message" => "Video sale updated successfully"]);
die();
}
}
add_action('wp_ajax_bsm_update_video_sale', 'bsm_update_video_sale');
/**
* AJAX Handler: Other Group Sale Update (Marketing, Office Management, Other)
*
* - এখানে, আপডেটের পর যদি "Number of jobs" (qty) পরিবর্তিত হয়,
* তাহলে পুনরায় base points recalculation হবে Admin Settings এর "Other Group Points" অনুযায়ী।
*/
function bsm_update_other_sale() {
if ( ! is_user_logged_in() ) {
wp_send_json_error("User not logged in");
die();
}
$nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
if ( ! wp_verify_nonce($nonce, 'bsm_update_other_sale') ) {
wp_send_json_error("Invalid nonce");
die();
}
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
if ( $sale_id <= 0 ) {
wp_send_json_error("Invalid sale id");
die();
}
global $wpdb;
$table_sales = $wpdb->prefix . 'bsm_sales';
$sale = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_sales WHERE id = %d", $sale_id));
if ( ! $sale ) {
wp_send_json_error("Sale not found");
die();
}
if ( $sale->status === "Review Apply" || $sale->status === "Successful All" ) {
wp_send_json_error("Editing not allowed for current status");
die();
}
$product_name = isset($_POST['product_name']) ? sanitize_text_field($_POST['product_name']) : '';
$proof_text = isset($_POST['proof_text']) ? sanitize_text_field($_POST['proof_text']) : '';
$proof_link = isset($_POST['proof_link']) ? esc_url_raw($_POST['proof_link']) : '';
$number_of_jobs = isset($_POST['number_of_jobs']) ? intval($_POST['number_of_jobs']) : 0;
$proof_screen_short = $sale->proof_screen_short;
if ( isset($_FILES['proof_screen_short']) && !empty($_FILES['proof_screen_short']['name']) ) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
$uploadedfile = $_FILES['proof_screen_short'];
$upload_overrides = array('test_form' => false);
$movefile = wp_handle_upload($uploadedfile, $upload_overrides);
if ( $movefile && !isset($movefile['error']) ) {
$proof_screen_short = $movefile['url'];
} else {
wp_send_json_error("Error uploading Proof Screen Short: " . (isset($movefile['error']) ? $movefile['error'] : 'Unknown error'));
die();
}
}
if ( ! empty($proof_link) ) {
$dup_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $table_sales WHERE sale_type IN ('marketing','office_management','other_option') AND work_proof_link = %s AND id != %d",
$proof_link, $sale_id
)
);
if ( $dup_count > 0 ) {
wp_send_json_error("This Proof Link is already used in another sale.");
die();
}
}
$updated = $wpdb->update(
$table_sales,
[
'product_name' => $product_name,
'proof_text' => $proof_text,
'work_proof_link' => $proof_link,
'proof_screen_short' => $proof_screen_short,
'qty' => $number_of_jobs
],
['id' => $sale_id],
['%s', '%s', '%s', '%s', '%d'],
['%d']
);
if ( $updated === false ) {
wp_send_json_error("Database update failed: " . $wpdb->last_error);
die();
}
// Recalculate and update base points for Other group sale if sale_type is marketing or office_management.
$sale_type_key = strtolower(trim($sale->sale_type));
if (in_array($sale_type_key, array('marketing','office_management'))) {
bsm_update_base_points($sale_id, $sale_type_key, 0, $number_of_jobs, '');
}
wp_send_json_success(["message" => "Sale updated successfully and base points recalculated"]);
die();
}
add_action('wp_ajax_bsm_update_other_sale', 'bsm_update_other_sale');
/**
* AJAX Handler: Service Sale Update (for Service group edit)
* (আগের মত অপরিবর্তিত)
*/
function bsm_update_service_sale() {
if ( ! is_user_logged_in() ) {
wp_send_json_error("User not logged in");
die();
}
$nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
if ( ! wp_verify_nonce($nonce, 'bsm_update_service_sale') ) {
wp_send_json_error("Invalid nonce");
die();
}
$sale_id = isset($_POST['sale_id']) ? intval($_POST['sale_id']) : 0;
if ( $sale_id <= 0 ) {
wp_send_json_error("Invalid sale id");
die();
}
$note = isset($_POST['note']) ? sanitize_textarea_field($_POST['note']) : '';
$purchase_price = isset($_POST['purchase_price']) ? floatval($_POST['purchase_price']) : 0.0;
$selling_price = isset($_POST['selling_price']) ? floatval($_POST['selling_price']) : 0.0;
$profit = max(0, $selling_price - $purchase_price);
$loss = max(0, $purchase_price - $selling_price);
global $wpdb;
$table_sales = $wpdb->prefix . 'bsm_sales';
$sale = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_sales WHERE id=%d", $sale_id));
if ( ! $sale ) {
wp_send_json_error("Sale record not found");
die();
}
if ( strtolower($sale->sale_type) !== 'service' ) {
wp_send_json_error("This sale is not of type Service.");
die();
}
$data = array(
'purchase_price' => $purchase_price,
'selling_price' => $selling_price,
'profit' => $profit,
'loss' => $loss,
'note' => $note,
);
$format = array('%f','%f','%f','%f','%s');
$updated = $wpdb->update($table_sales, $data, array('id' => $sale_id), $format, array('%d'));
if ( false === $updated ) {
wp_send_json_error("Database update failed: " . $wpdb->last_error);
die();
}
wp_send_json_success(array(
'message' => "Service sale updated successfully.",
'sale_id' => $sale_id,
));
die();
}
add_action('wp_ajax_bsm_update_service_sale', 'bsm_update_service_sale');
/**
* (Optional) AJAX Handler to log JS errors from the frontend.
*/
function bsm_log_js_error() {
if ( ! is_user_logged_in() ) {
wp_send_json_error("User not logged in");
die();
}
$nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
if ( ! wp_verify_nonce($nonce, 'bsm_log_js_error') ) {
wp_send_json_error("Invalid nonce");
die();
}
$error_message = isset($_POST['error_message']) ? sanitize_text_field($_POST['error_message']) : '';
wp_send_json_success(["message" => "JS error logged: " . $error_message]);
die();
}
add_action('wp_ajax_bsm_log_js_error', 'bsm_log_js_error');
?>
business-seller-management/templates/admin/status-page.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
?>
<div class="wrap">
<h1>Status</h1>
<p>This is a placeholder page for <strong>Status</strong>.</p>
<!--
ভবিষ্যতে এখানে "Status" সম্পর্কিত ফিচার (On Hold, In Process, Block ইত্যাদির তালিকা,
স্ট্যাটাস আপডেট, রিপোর্ট/লগ, ইত্যাদি) যোগ করা হবে।
-->
</div>
business-seller-management/templates/admin/settings-page.php
<?php
/**
* File: business-seller-management/templates/admin/settings-page.php
*
* Description:
* - এই ফাইলটি এডমিন প্যানেলের সেটিংস পেজের জন্য, যেখানে উপরের দিকে একটি Top Menu (Tabs) থাকবে।
* - প্রতিটি ট্যাব আলাদা আলাদা সেটিংস সাবজেক্টের জন্য তৈরি করা হয়েছে, যেমন:
* General, পয়েন্ট, কয়েন, gsmalo.org ব্যালেন্স, পেমেন্ট মেথড, Permissions, Status Control, Sales Entry Form
* - প্রতিটি ট্যাবের কন্টেন্ট আলাদা ফাইলে রাখা হবে, যাতে ছোট ছোট কোড লিখে সহজে ম্যানেজ করা যায়।
* - পেজ রিলোড হলেও, URL‑এর হ্যাশ (hash) অনুযায়ী পূর্বের ট্যাব active থাকবে।
*/
if ( ! defined('ABSPATH') ) {
exit;
}
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title>Admin Settings</title>
<style>
/* Top Menu Styles */
.settings-top-menu {
margin: 0;
padding: 0;
list-style: none;
background: #0073aa;
display: flex;
flex-wrap: wrap;
}
.settings-top-menu li {
margin: 0;
}
.settings-top-menu li a {
display: block;
padding: 10px 15px;
color: #fff;
text-decoration: none;
font-size: 14px;
}
.settings-top-menu li a.active {
background: #005177;
font-weight: bold;
}
/* Container for tab contents */
.settings-tab-content {
display: none;
padding: 20px;
background: #fff;
border: 1px solid #ddd;
border-top: none;
}
.settings-tab-content.active {
display: block;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function(){
var tabs = document.querySelectorAll(".settings-top-menu li a");
var contents = document.querySelectorAll(".settings-tab-content");
// Function to activate tab by target ID
function activateTab(targetId) {
tabs.forEach(function(t){ t.classList.remove("active"); });
contents.forEach(function(c){ c.classList.remove("active"); });
var activeTab = document.querySelector('.settings-top-menu li a[href="#' + targetId + '"]');
if(activeTab){
activeTab.classList.add("active");
var activeContent = document.getElementById(targetId);
if(activeContent){
activeContent.classList.add("active");
}
}
}
// Check if URL has hash; if yes, activate corresponding tab
var hash = window.location.hash.substring(1); // remove '#' character
if(hash) {
activateTab(hash);
} else if(tabs.length > 0) {
// Default: activate first tab
var defaultTab = tabs[0].getAttribute("href").substring(1);
activateTab(defaultTab);
}
// Add click event listener to each tab
tabs.forEach(function(tab){
tab.addEventListener("click", function(e){
e.preventDefault();
var targetId = this.getAttribute("href").substring(1);
// Update URL hash (without reloading page)
history.pushState(null, "", "#" + targetId);
activateTab(targetId);
});
});
});
</script>
</head>
<body <?php body_class(); ?>>
<div class="wrap">
<h1>Admin Settings</h1>
<!-- Top Menu Tabs -->
<ul class="settings-top-menu">
<li><a href="#tab-general">General</a></li>
<li><a href="#tab-point">পয়েন্ট</a></li>
<li><a href="#tab-coin">কয়েন</a></li>
<li><a href="#tab-gsmalo-balance">gsmalo.org ব্যালেন্স</a></li>
<li><a href="#tab-payment-methods">পেমেন্ট মেথড</a></li>
<li><a href="#tab-permissions">Permissions</a></li>
<li><a href="#tab-status-control">Status Control</a></li>
<li><a href="#tab-sales-entry-form">Sales Entry Form</a></li>
</ul>
<!-- Tab Content Sections -->
<div id="tab-general" class="settings-tab-content">
<?php
// Include the General Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/general.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/general.php';
} else {
echo '<p>General Settings content goes here.</p>';
}
?>
</div>
<div id="tab-point" class="settings-tab-content">
<?php
// Include the Point Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/point.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/point.php';
} else {
echo '<p>পয়েন্ট Settings content goes here.</p>';
}
?>
</div>
<div id="tab-coin" class="settings-tab-content">
<?php
// Include the Coin Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/coin.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/coin.php';
} else {
echo '<p>কয়েন Settings content goes here.</p>';
}
?>
</div>
<div id="tab-gsmalo-balance" class="settings-tab-content">
<?php
// Include the gsmalo.org Balance Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/gsmalo-balance.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/gsmalo-balance.php';
} else {
echo '<p>gsmalo.org ব্যালেন্স Settings content goes here.</p>';
}
?>
</div>
<div id="tab-payment-methods" class="settings-tab-content">
<?php
// Include the Payment Methods Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/payment-methods.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/payment-methods.php';
} else {
echo '<p>পেমেন্ট মেথড Settings content goes here.</p>';
}
?>
</div>
<div id="tab-permissions" class="settings-tab-content">
<?php
// Include the Permissions Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/permissions.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/permissions.php';
} else {
echo '<p>Permissions Settings content goes here.</p>';
}
?>
</div>
<div id="tab-status-control" class="settings-tab-content">
<?php
// Include the Status Control Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/status-control.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/status-control.php';
} else {
echo '<p>Status Control Settings content goes here.</p>';
}
?>
</div>
<div id="tab-sales-entry-form" class="settings-tab-content">
<?php
// Include the Sales Entry Form Settings file
if ( file_exists( BSM_PLUGIN_PATH . 'templates/admin/settings/sales-entry-form.php' ) ) {
include BSM_PLUGIN_PATH . 'templates/admin/settings/sales-entry-form.php';
} else {
echo '<p>Sales Entry Form Settings content goes here.</p>';
}
?>
</div>
</div>
</body>
</html>
business-seller-management/templates/admin/role-permissions-page.php
<div class="wrap">
<h1>Role Permissions</h1>
<!-- Display success messages -->
<?php if (isset($_GET['role_permissions_updated']) && $_GET['role_permissions_updated'] === 'true'): ?>
<div class="bsm-notice bsm-notice-success">
<p>Role permissions updated successfully!</p>
</div>
<?php endif; ?>
<!-- Role Permissions Form -->
<h2>Update Role Permissions</h2>
<form method="post" action="">
<?php wp_nonce_field('bsm_update_role_permissions', 'bsm_update_role_permissions_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="role_name">Select Role</label></th>
<td>
<select name="role_name" id="role_name" required>
<?php
global $wp_roles;
foreach ($wp_roles->roles as $role_name => $role_info): ?>
<option value="<?php echo esc_attr($role_name); ?>"><?php echo esc_html($role_info['name']); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope="row"><label>Select Permissions</label></th>
<td>
<fieldset>
<legend class="screen-reader-text"><span>Permissions</span></legend>
<?php
$capabilities = array(
'manage_sales' => 'Manage Sales',
'manage_sellers' => 'Manage Sellers',
'manage_payment_methods' => 'Manage Payment Methods',
'edit_sales' => 'Edit Sales',
'delete_sales' => 'Delete Sales',
'view_reports' => 'View Reports'
);
foreach ($capabilities as $cap => $label): ?>
<label for="cap_<?php echo esc_attr($cap); ?>">
<input name="permissions[]" type="checkbox" id="cap_<?php echo esc_attr($cap); ?>" value="<?php echo esc_attr($cap); ?>">
<?php echo esc_html($label); ?>
</label><br>
<?php endforeach; ?>
</fieldset>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="update_role_permissions" id="update_role_permissions" class="button button-primary" value="Update Permissions">
</p>
</form>
</div>
business-seller-management/templates/admin/reports-page.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
?>
<div class="wrap">
<h1>Reports</h1>
<p>This is a placeholder page for <strong>Reports</strong>.</p>
<!--
ভবিষ্যতে আপনি এখানে চার্ট, গ্রাফ, সার্চ ও ফিল্টার,
দৈনিক/মাসিক/বাৎসরিক/সেলারভিত্তিক রিপোর্ট ইত্যাদি যুক্ত করতে পারবেন।
-->
</div>
business-seller-management/templates/admin/payment-methods-page.php
<div class="wrap">
<h1>Payment Methods</h1>
<!-- Display success messages -->
<?php if (isset($_GET['payment_method_added']) && $_GET['payment_method_added'] === 'true'): ?>
<div class="bsm-notice bsm-notice-success">
<p>Payment method added successfully!</p>
</div>
<?php endif; ?>
<?php if (isset($_GET['payment_method_updated']) && $_GET['payment_method_updated'] === 'true'): ?>
<div class="bsm-notice bsm-notice-success">
<p>Payment method updated successfully!</p>
</div>
<?php endif; ?>
<?php if (isset($_GET['payment_method_deleted']) && $_GET['payment_method_deleted'] === 'true'): ?>
<div class="bsm-notice bsm-notice-success">
<p>Payment method deleted successfully!</p>
</div>
<?php endif; ?>
<!-- Add Payment Method Form -->
<h2>Add New Payment Method</h2>
<form method="post" action="">
<?php wp_nonce_field('bsm_add_payment_method', 'bsm_add_payment_method_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="method_name">Method Name</label></th>
<td>
<input name="method_name" type="text" id="method_name" class="regular-text" required>
</td>
</tr>
<tr>
<th scope="row"><label for="method_number">Method Number</label></th>
<td>
<input name="method_number" type="text" id="method_number" class="regular-text" required>
</td>
</tr>
<tr>
<th scope="row"><label for="method_type">Method Type</label></th>
<td>
<select name="method_type" id="method_type" required>
<option value="bkash">Bkash</option>
<option value="nagad">Nagad</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="binance">Binance</option>
<option value="other">Other</option>
</select>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="add_payment_method" id="add_payment_method" class="button button-primary" value="Add Payment Method">
</p>
</form>
<!-- Payment Methods Table -->
<h2>Existing Payment Methods</h2>
<?php
global $wpdb;
$table_name_payment_methods = $wpdb->prefix . 'bsm_payment_methods';
$payment_methods = $wpdb->get_results("SELECT * FROM $table_name_payment_methods ORDER BY created_at DESC");
?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>ID</th>
<th>Method Name</th>
<th>Method Number</th>
<th>Method Type</th>
<th>Created At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($payment_methods as $method): ?>
<tr>
<td><?php echo esc_html($method->id); ?></td>
<td><?php echo esc_html($method->method_name); ?></td>
<td><?php echo esc_html($method->method_number); ?></td>
<td><?php echo esc_html($method->method_type); ?></td>
<td><?php echo esc_html($method->created_at); ?></td>
<td>
<a href="<?php echo admin_url('admin.php?page=bsm-edit-payment-method&method_id=' . $method->id); ?>">Edit</a> |
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=bsm-payment-methods&delete_payment_method=' . $method->id), 'bsm_delete_payment_method'); ?>" onclick="return confirm('Are you sure you want to delete this payment method?');">Delete</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
business-seller-management/templates/admin/gsmalo-org-transaction.php
<?php
/**
* File: business-seller-management/templates/admin/gsmalo-org-transaction.php
*
* Description:
* - (১) Seller List View: Shows list of sellers with sorting and "New" indicator. "Warning +X" text is dark red.
* - (২) Seller Detail View: Shows gsmalo.org transaction history for a selected seller.
* - Includes Seller Info Boxes, Search, Warning Summary, Sortable Table, Pagination.
* - Transaction table includes: Time, Date & UserName | ID | Order ID | Reason or Sale Name | Warning/FixedBy | Fix | Status Log | Status | Amount $.
* - Amount column shows +/- sign. Status column shows icon/color/log info. Warning/Fix column shows details for adjustments. Status Log column shows latest log summary + balance impact + clickable count icon.
* - **Update:** "New" indicator logic changed:
* - Based on transaction `created_at` or adjustment `fixed_at` (whichever is later and applicable).
* - Color changes with age: 0-24h (Red), 24-48h (Dark Yellow), 48-72h (Gray).
* - Hidden after 72 hours.
* - Table headers are sortable (JS for History, PHP for List).
* - Layout slightly adjusted for responsiveness.
* - Other features (Fix button, Modals) remain unchanged.
*
* REMINDERS:
* (a) ফাইলের সম্পূর্ণ কোড প্রদান করা হয়েছে।
* (b) শুধুমাত্র নির্দেশিত সমস্যা সমাধান করা হয়েছে, বাকি সব অপরিবর্তিত।
* (c) এই কোডটিই চূড়ান্ত।
* (d) পরবর্তী কাজ এর ভিত্তিতে হবে।
*/
if ( ! defined('ABSPATH') ) {
exit;
}
// Ensure helper function is available (defined in includes/seller/statas-log-notes.php)
if (!function_exists('bsm_calculate_balance_change_for_status_transition')) {
function bsm_calculate_balance_change_for_status_transition($sale_id, $old_status, $new_status) { return 0; }
error_log("BSM Error: Function bsm_calculate_balance_change_for_status_transition not found in gsmalo-org-transaction.php. Check include order.");
}
if (!function_exists('bsm_parse_status_from_log')) {
function bsm_parse_status_from_log($log_text) {
if (preg_match('/from.*?>(.+?)<\/span>.*?to.*?>(.+?)<\/span>/i', $log_text, $matches)) {
$old = isset($matches[1]) ? trim(strip_tags($matches[1])) : null;
$new = isset($matches[2]) ? trim(strip_tags($matches[2])) : null;
return array($old, $new);
}
return array(null, null);
}
error_log("BSM Error: Function bsm_parse_status_from_log not found in gsmalo-org-transaction.php. Check include order.");
}
// Global variables
global $wpdb;
$admin_url = admin_url( 'admin.php?page=gsmalo-org-transaction' );
// ----------------------------------------------------------------------------
// (২) Seller Detail View: If GET parameter "seller_id" is set
// ----------------------------------------------------------------------------
if ( isset($_GET['seller_id']) && !empty($_GET['seller_id']) ) {
$seller_id = intval( $_GET['seller_id'] );
// Search, Date, and Pagination variables
$search_query = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
$search_date = isset($_GET['search_date']) ? sanitize_text_field($_GET['search_date']) : '';
// Records per Page settings
$allowed_per_page = array(50, 100, 200, 400, 500, 1000);
$default_per_page = 100;
$per_page = ( isset($_GET['per_page']) && in_array( intval($_GET['per_page']), $allowed_per_page ) )
? intval($_GET['per_page'])
: $default_per_page;
// Fetch Seller Details
$seller = $wpdb->get_row( $wpdb->prepare(
"SELECT s.*, u.display_name, u.user_email
FROM {$wpdb->prefix}bsm_sellers s
LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
WHERE s.user_id = %d",
$seller_id
) );
if ( ! $seller ) {
echo '<div class="wrap"><h1>Error</h1><p>Seller not found.</p></div>';
return;
}
// Fetch gsmalo.org Sales entries
$sales_table = $wpdb->prefix . 'bsm_sales';
$sales = $wpdb->get_results( $wpdb->prepare(
"SELECT id, order_id, product_name, status, purchase_price, selling_price, created_at, seller_id
FROM $sales_table
WHERE sale_type = %s AND seller_id = %d",
'gsmalo.org', $seller_id
) );
// Fetch Balance Adjustments entries (including fixed_by and fixed_at)
$adjustments_table = $wpdb->prefix . 'bsm_balance_adjustments';
$adjustments = $wpdb->get_results( $wpdb->prepare(
"SELECT id, adjusted_amount, reason, created_at, adjusted_by, fixed_by, fixed_at
FROM $adjustments_table
WHERE seller_id = %d",
$seller_id
) );
// Combine Sales and Adjustments into transactions array
$transactions = array();
// Process Sales
if ( $sales ) {
foreach ( $sales as $sale ) {
$adjuster_data = get_userdata($sale->seller_id);
$adjuster_name = $adjuster_data ? $adjuster_data->display_name : 'Seller (ID: '.$sale->seller_id.')';
$transactions[] = array(
'datetime' => $sale->created_at, // Primary timestamp for sale
'fixed_at' => null, // Sales don't have fixed_at
'id' => $sale->id,
'order_id' => substr($sale->order_id, 0, 8),
'reason' => $sale->product_name,
'warning_text' => '',
'fix_note' => '',
'status' => $sale->status,
'amount' => $sale->purchase_price,
'original_amount_type' => 'purchase_usd',
'type' => 'sale',
'db_reason' => $sale->product_name,
'adjuster_name' => $adjuster_name,
'is_active_warning' => false
);
}
}
// Process Adjustments
if ( $adjustments ) {
foreach ( $adjustments as $adj ) {
$db_reason = trim($adj->reason);
$adjuster_name = 'Unknown';
if ( $adj->adjusted_by > 0 ) {
$adjuster_data = get_userdata($adj->adjusted_by);
if ($adjuster_data && !empty($adjuster_data->display_name)) {
$adjuster_name = $adjuster_data->display_name;
} elseif($adjuster_data) {
$adjuster_name = 'User (ID: ' . $adj->adjusted_by . ')';
} else {
$adjuster_name = 'User ID ' . $adj->adjusted_by;
}
} else {
$adjuster_name = 'System';
}
$has_warning_tag = (stripos($db_reason, '[warning]') !== false);
$is_fixed = !empty($adj->fixed_by);
$is_active_warning = $has_warning_tag && !$is_fixed;
$warningText = "";
if ($is_active_warning) {
$warningCount = substr_count( strtolower($db_reason), "[warning]" );
if ($warningCount == 1) { $warningText = "Warning +1"; }
elseif ($warningCount == 2) { $warningText = "Warning +1, Warning +2"; }
else { $warningText = "Warning +1, ... (+" . $warningCount . ")"; }
}
$fix_note_html = '';
if ($is_fixed) {
$fixer_user = get_userdata($adj->fixed_by);
$fixer_name = $fixer_user ? $fixer_user->display_name : ('User ID: '.$adj->fixed_by);
$fix_time_display = !empty($adj->fixed_at) ? date('h:i A | d-m-Y', strtotime($adj->fixed_at)) : 'N/A';
$fix_note_html = esc_html($fixer_name) . '<br><small>' . esc_html($fix_time_display) . '</small>';
}
$display_reason = trim(preg_replace('/\s*\[warning\]/i', '', $db_reason));
$transactions[] = array(
'datetime' => $adj->created_at, // Primary timestamp for adjustment
'fixed_at' => $adj->fixed_at, // Fixed timestamp
'id' => $adj->id,
'order_id' => '-',
'reason' => $display_reason,
'warning_text' => $warningText,
'fix_note' => $fix_note_html,
'status' => ( floatval($adj->adjusted_amount) >= 0 ) ? "Addition" : "Revert",
'amount' => $adj->adjusted_amount,
'original_amount_type' => 'adjustment',
'type' => 'adjustment',
'db_reason' => $db_reason,
'adjuster_name' => $adjuster_name,
'is_active_warning' => $is_active_warning
);
}
}
// Sort Transactions by primary datetime descending (Default Sort by PHP)
usort($transactions, function($a, $b) {
return strcmp($b['datetime'], $a['datetime']);
});
// Apply Text Search Filter
if ( ! empty($search_query) ) {
$transactions = array_filter($transactions, function($txn) use ($search_query) {
$id_match = isset($txn['id']) && stripos((string)$txn['id'], $search_query) !== false;
$order_match = isset($txn['order_id']) && stripos($txn['order_id'], $search_query) !== false;
$reason_match = isset($txn['db_reason']) && stripos($txn['db_reason'], $search_query) !== false;
return ($id_match || $order_match || $reason_match);
});
$transactions = array_values($transactions);
}
// Apply Date Search Filter
if ( ! empty($search_date) ) {
$transactions = array_filter($transactions, function($txn) use ($search_date) {
// Compare against the primary datetime
return ( date('Y-m-d', strtotime($txn['datetime'])) === $search_date );
});
$transactions = array_values($transactions);
}
// Calculate Pagination details
$page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$total_transactions = count($transactions);
$total_pages = ($total_transactions > 0 && $per_page > 0) ? ceil($total_transactions / $per_page) : 1;
$page = min($page, $total_pages);
$offset = ($page - 1) * $per_page;
$transactions_display = array_slice($transactions, $offset, $per_page);
// Calculate Warning Summary (based on *active* warnings)
$total_warning = 0; $total_negative = 0; $total_positive = 0;
if ( $adjustments ) {
foreach ( $adjustments as $adj ) {
if ( stripos($adj->reason, '[warning]') !== false && empty($adj->fixed_by) ) {
$total_warning++; $amt = floatval($adj->adjusted_amount);
if ($amt < 0) { $total_negative += $amt; } elseif ($amt > 0) { $total_positive += $amt; }
}
}
}
// Seller's current balance
$seller_balance = $wpdb->get_var( $wpdb->prepare("SELECT gsmalo_org_balance FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d", $seller_id) );
// Get current timestamp for comparison
$current_timestamp = current_time('timestamp');
$twenty_four_hours_ago = $current_timestamp - (24 * 3600);
$forty_eight_hours_ago = $current_timestamp - (48 * 3600);
$seventy_two_hours_ago = $current_timestamp - (72 * 3600); // 72 hours
// Define Status Styles (Color and Icon)
$status_styles = [ /* Styles unchanged */
'Addition' => ['color' => '#00008B', 'icon' => '➕'], 'Successful All' => ['color' => '#00008B', 'icon' => '✔'], 'Refund Done' => ['color' => '#00008B', 'icon' => '💵'],
'Revert' => ['color' => '#FF6347', 'icon' => '↩️'], 'Refund Required' => ['color' => '#DAA520', 'icon' => '💸'], 'Success But Not Delivery'=> ['color' => '#FF6347', 'icon' => '❎'],
'pending' => ['color' => '#DAA520', 'icon' => '⏳'], 'On Hold' => ['color' => '#DAA520', 'icon' => '⏸️'], 'In Process' => ['color' => '#DAA520', 'icon' => '🔄'],
'Need Parts' => ['color' => '#DAA520', 'icon' => '⚙️'], 'Parts Brought' => ['color' => '#DAA520', 'icon' => '📦'], 'Check Admin' => ['color' => '#DAA520', 'icon' => '👨💼'],
'Review Apply' => ['color' => '#DAA520', 'icon' => '🔍'], 'Block' => ['color' => '#800080', 'icon' => '🔒'],
'Reject Delivery Done' => ['color' => '#000000', 'icon' => '🔴'], 'Cost' => ['color' => '#000000', 'icon' => '💰'], 'Cancel' => ['color' => '#000000', 'icon' => '❌'],
'Rejected' => ['color' => '#000000', 'icon' => '❌'], 'Failed' => ['color' => '#000000', 'icon' => '❌'],
];
// Define statuses that ADD to the balance for amount sign logic
$positive_statuses = ['Addition', 'Reject Delivery Done', 'Refund Done', 'Rejected'];
// Generate nonce for fetching status logs
$log_fetch_nonce = wp_create_nonce('bsm_note_nonce');
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title>gsmalo.org Transaction History for Seller <?php echo esc_html($seller->display_name); ?></title>
<style>
/* Styles mostly unchanged, added/updated styles for status log column/icon/modal and responsiveness */
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
.container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { color: #0073aa; }
/* Seller Info Boxes Styling */
.seller-info-boxes { display: flex; justify-content: space-around; gap: 15px; margin-bottom: 25px; flex-wrap: wrap; }
.seller-info-box { flex: 1; min-width: 200px; padding: 20px; border-radius: 8px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); box-shadow: 0 4px 8px rgba(0,0,0,0.1); text-align: center; border: 1px solid #ddd; }
.seller-info-box .label { display: block; font-size: 14px; color: #555; margin-bottom: 8px; font-weight: bold; text-transform: uppercase; }
.seller-info-box .value { display: block; font-size: 24px; font-weight: bold; }
.seller-info-box .value.id-name { color: #0073aa; font-size: 20px; }
.seller-info-box .value.email { color: #6a1b9a; font-size: 18px; }
.seller-info-box .value.balance { color: #d32f2f; }
/* Updated Search Bar Styles */
.search-bar-container { background-color: #eaf2f8; padding: 12px 15px; margin-bottom: 20px; border: 1px solid #c5d9e8; border-radius: 6px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.04); }
.search-bar-inner { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; }
.search-bar-inner form { display: contents; }
.search-bar-inner .search-input { padding: 6px 10px; border: 1px solid #a9c4d0; border-radius: 4px; font-size: 14px; flex: 1 1 250px; max-width: 300px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.search-bar-inner .date-input { padding: 6px 8px; border: 1px solid #a9c4d0; border-radius: 4px; font-size: 14px; flex: 0 1 140px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.search-bar-inner .search-button { padding: 7px 14px; border: none; background: #2980b9; color: #fff; border-radius: 4px; cursor: pointer; font-size: 14px; flex-shrink: 0; transition: background-color 0.2s ease; }
.search-bar-inner .search-button:hover { background: #1f648b; }
/* Summary Boxes */
.warning-box-container { margin-bottom: 15px; }
.warning-box { display: inline-block; margin-right: 10px; padding: 10px 15px; border: 2px solid #d00; background: #ffcccc; border-radius: 5px; font-size: 14px; font-weight: bold; }
.warning-box span { color: red; }
.warning-box.positive { border-color: #0073aa; background: #e0f7fa; }
.warning-box.negative { border-color: #0073aa; background: #e0f7fa; }
/* Transaction Table Styling */
table#adminTransactionHistoryTable { width: 100%; border-collapse: collapse; table-layout: auto; }
table#adminTransactionHistoryTable th, table#adminTransactionHistoryTable td {
border: 1px solid #ccc;
padding: 4px 5px; /* Reduced padding */
text-align: center;
vertical-align: middle;
position: relative;
font-size: 12px; /* Smaller font */
word-wrap: break-word;
overflow-wrap: break-word;
}
table#adminTransactionHistoryTable th { background: #f5f5f5; font-weight: bold; cursor: pointer; }
.col-reason { width: 35%; text-align: left; }
.col-order-id { width: 70px; }
.col-datetime span { display: block; font-size: 11px; color: #555;}
.col-datetime small { display: block; font-size: 9px; color: #0073aa; font-weight: bold; }
/* Table Row Hover Effect */
table#adminTransactionHistoryTable tbody tr:hover { background: #fffacd !important; }
.warning-cell { background: #ffcccc !important; color: #a00; font-weight: bold; }
.warning-cell:hover { background: #ffbbbb !important; }
.fix-note { font-size: 11px; color: #555; line-height:1.3; text-align: center;}
.fix-note small { display: block; font-size:10px; color:#777; }
td[data-label="Fix"] div.fix-note { text-align: center; }
.pagination { text-align: center; margin-top: 20px; }
.pagination a, .pagination span { display: inline-block; padding: 6px 12px; margin: 0 2px; border: 1px solid #ccc; border-radius: 4px; text-decoration: none; color: #0073aa; }
.pagination span.current-page { background: #0073aa; color: #fff; }
/* Balance Adjustment Modal Popup */
#balanceModal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; }
#balanceModal .modal-content { background: #fff; width: 500px; max-width: 90%; margin: 100px auto; padding: 20px; border-radius: 5px; position: relative; }
#balanceModal .modal-content span.close { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 18px; }
#modalCurrentBalance { font-size: 16px; margin-bottom: 15px; font-weight: bold; color: #333; }
#modalCurrentBalance span { color: #d32f2f; }
/* Fix Button Styling */
.fix-warning-btn { padding: 5px 10px; background: #d32f2f; color: #fff; border: none; border-radius: 3px; cursor: pointer; }
.fix-warning-btn:hover { background: #a00; }
/* Per Page form styling */
.per-page-form { margin-top: 10px; text-align: center; }
.per-page-form label { margin-right: 5px; }
.per-page-form select { padding: 5px; }
/* --- UPDATED New Indicator Styles --- */
.new-indicator {
position: absolute;
top: 1px; left: 1px;
color: white;
font-size: 9px; font-weight: bold;
padding: 1px 3px; border-radius: 3px;
line-height: 1; z-index: 1;
}
.new-indicator-recent { background-color: red; } /* 0-24h */
.new-indicator-medium { background-color: #DAA520; } /* 24-48h */
.new-indicator-old { background-color: #808080; } /* 48-72h */
/* --- End New Indicator Styles --- */
/* Adjust padding for cells */
td.col-datetime, td.seller-id-cell { padding-top: 15px !important; }
td.seller-id-cell { padding-left: 20px !important; }
/* Status Styling */
.status-text { font-weight: bold; }
.status-icon { margin-left: 5px; margin-right: 5px; }
th .sort-indicator { font-size: 10px; margin-left: 5px; }
/* Amount sign colors */
.amount-plus { color: blue; font-weight: bold; margin-right: 2px;}
.amount-minus { color: red; font-weight: bold; margin-right: 2px;}
/* --- Updated Status Log Column Styles --- */
td.status-log-cell {
font-size: 10px;
line-height: 1.2;
text-align: left;
position: relative;
padding-right: 25px;
vertical-align: top;
}
/* Style for the SPAN containing the latest log text - Normal background */
.latest-log-info-text {
display: inline-block;
padding: 0;
margin-bottom: 2px;
white-space: normal;
}
.latest-log-info-text small { display: block; font-size: 9px; color: #555; }
/* Style for the balance change text next to the log */
.balance-change-indicator {
font-size: 10px;
font-weight: bold;
margin-left: 4px;
white-space: nowrap;
}
/* Style for the clickable icon - base */
.more-status-log-icon {
position: absolute;
top: 2px;
right: 2px;
display: inline-block;
color: white; /* Text color always white */
font-size: 9px;
font-weight: bold;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
border-radius: 50%;
cursor: pointer;
text-decoration: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
z-index: 2;
}
/* Icon Background Colors based on time */
.more-status-log-icon.log-count-recent { background-color: red; } /* 0-24 hours */
.more-status-log-icon.log-count-medium { background-color: #DAA520; } /* 24-48 hours (Dark Yellow) */
.more-status-log-icon.log-count-old { background-color: #808080; } /* > 48 hours (Gray) */
.more-status-log-icon:hover { filter: brightness(85%); }
/* --- End Status Log Styles --- */
/* Status Log Modal Styles */
#status-log-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 10001; }
#status-log-modal .modal-content { background: #fff; width: 700px; max-width: 90%; margin: 80px auto; padding: 20px; border-radius: 5px; position: relative; max-height: 80vh; overflow-y: auto; }
#status-log-modal .modal-content h2 { margin-top: 0; color: #0073aa; font-size: 1.3em; }
#status-log-modal .close { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 24px; color: #888; font-weight: bold; }
#status-log-modal-content .status-log-entry { border-bottom: 1px dashed #eee; padding: 8px 0; font-size: 13px; }
#status-log-modal-content .status-log-entry:last-child { border-bottom: none; }
#status-log-modal-content .status-log-entry strong { color: #333; }
#status-log-modal-content .status-log-entry em { color: #777; font-size: 0.9em; }
</style>
</head>
<body>
<div class="container">
<h1>gsmalo.org Transaction History for <?php echo esc_html($seller->display_name); ?></h1>
<div class="seller-info-boxes">
<div class="seller-info-box">
<span class="label">Seller ID & User Name</span>
<span class="value id-name"><?php echo esc_html($seller_id . ' | ' . $seller->display_name); ?></span>
</div>
<div class="seller-info-box">
<span class="label">Email</span>
<span class="value email"><?php echo esc_html($seller->user_email); ?></span>
</div>
<div class="seller-info-box">
<span class="label">Current Balance</span>
<span class="value balance"><?php echo esc_html(number_format((float)($seller_balance ?? 0), 2)); ?> USD</span>
</div>
</div>
<div class="balance-info" style="margin-bottom:20px;">
<a href="#" class="button button-primary" onclick="openBalanceAdjustmentModal(<?php echo esc_js($seller->user_id); ?>, <?php echo esc_js($seller_balance ?? 0); ?>); return false;">Adjust Balance</a>
</div>
<p><a href="<?php echo esc_url( $admin_url ); ?>">← Back to Seller List</a></p>
<div class="search-bar-container">
<div class="search-bar-inner">
<form method="get" action="">
<input type="hidden" name="page" value="gsmalo-org-transaction">
<input type="hidden" name="seller_id" value="<?php echo intval($seller->user_id); ?>">
<input type="hidden" name="per_page" value="<?php echo esc_attr($per_page); ?>">
<input type="text" name="search" placeholder="Search by ID, Order ID, or Reason" value="<?php echo esc_attr($search_query); ?>" class="search-input">
<input type="date" name="search_date" value="<?php echo esc_attr($search_date); ?>" class="date-input">
<input type="submit" value="Search" class="search-button">
</form>
</div>
</div>
<div class="warning-box-container">
<?php if($total_warning > 0): ?>
<div class="warning-box">
Active Warnings: <span style="color:red;"><?php echo esc_html($total_warning); ?></span>
</div>
<?php endif; ?>
<?php if($total_negative != 0): ?>
<div class="warning-box negative">
Negative Warnings Sum: <span style="color: red;"><?php echo esc_html(number_format($total_negative, 2)); ?> USD</span>
</div>
<?php endif; ?>
<?php if($total_positive != 0): ?>
<div class="warning-box positive">
Positive Warnings Sum: <span style="color: red;"><?php echo esc_html(number_format($total_positive, 2)); ?> USD</span>
</div>
<?php endif; ?>
</div>
<table id="adminTransactionHistoryTable">
<thead>
<tr>
<th onclick="sortTableByColumn(0, 'datetime')">Time, Date & Adjusted By<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumn(1, 'number')">ID<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumn(2, 'string')" class="col-order-id">Order ID<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumn(3, 'string')" class="col-reason">Reason or Sale Name<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumn(4, 'string')">Warning / Fixed By<span class="sort-indicator"></span></th>
<th>Fix</th>
<th>Status Log</th> <th onclick="sortTableByColumn(7, 'string')">Status<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumn(8, 'number')">Amount $<span class="sort-indicator"></span></th>
</tr>
</thead>
<tbody>
<?php
if ( $transactions_display ) {
foreach ( $transactions_display as $tran ) {
// Determine the effective timestamp for the "New" indicator
$primary_timestamp = strtotime($tran['datetime']);
$fixed_timestamp = isset($tran['fixed_at']) && $tran['fixed_at'] ? strtotime($tran['fixed_at']) : 0;
$effective_timestamp = max($primary_timestamp, $fixed_timestamp); // Use the latest relevant time
$age = $current_timestamp - $effective_timestamp;
$indicator_class = '';
if ($age <= 72 * 3600) { // Show indicator up to 72 hours
if ($age <= 24 * 3600) { $indicator_class = 'new-indicator-recent'; } // Red
elseif ($age <= 48 * 3600) { $indicator_class = 'new-indicator-medium'; } // Yellow
else { $indicator_class = 'new-indicator-old'; } // Gray
}
$rowClass = isset($tran['is_active_warning']) && $tran['is_active_warning'] ? 'warning-cell' : '';
// --- Status Styling Logic (Unchanged) ---
$status_text = esc_html($tran['status']);
$status_color = '#000000'; $status_icon = '';
if (isset($status_styles[$tran['status']])) {
$style_info = $status_styles[$tran['status']];
$status_color = $style_info['color']; $status_icon = $style_info['icon'];
}
$status_display_html = '<span class="status-text" style="color:' . esc_attr($status_color) . ';">';
$status_display_html .= $status_text . '<span class="status-icon">' . esc_html($status_icon) . '</span>';
$status_display_html .= '</span>';
$status_change_info = '';
if ($tran['type'] === 'sale') {
$last_status_log_entry_for_status_col = $wpdb->get_row($wpdb->prepare( "SELECT n.created_at, u.display_name FROM {$wpdb->prefix}bsm_sale_notes n LEFT JOIN {$wpdb->users} u ON n.user_id = u.ID WHERE n.sale_id = %d AND n.note_type = 'status_change' ORDER BY n.id DESC LIMIT 1", $tran['id'] ));
if ($last_status_log_entry_for_status_col) {
$status_change_user = $last_status_log_entry_for_status_col->display_name ?: 'System';
$status_change_time = date('h:i A | d-m-Y', strtotime($last_status_log_entry_for_status_col->created_at));
$status_change_info = '<small style="display:block; color:#777; font-size:10px;">' . esc_html($status_change_user) . ' at ' . esc_html($status_change_time) . '</small>';
}
}
// --- End Status Styling Logic ---
// --- Amount Formatting Logic (Unchanged) ---
$amount_val = floatval($tran['amount']);
$amount_display = ''; $amount_abs = number_format(abs($amount_val), 2);
if ($amount_val == 0) { $amount_display = '$' . $amount_abs; }
elseif (in_array($tran['status'], $positive_statuses)) { $amount_display = '<span class="amount-plus">+</span> $' . $amount_abs; }
else { $amount_display = '<span class="amount-minus">-</span> $' . $amount_abs; }
// --- End Amount Formatting Logic ---
?>
<tr class="<?php echo $rowClass; ?>">
<td class="col-datetime" data-sort-value="<?php echo $primary_timestamp; ?>"> <?php if (!empty($indicator_class)): ?>
<span class="new-indicator <?php echo $indicator_class; ?>">New</span>
<?php endif; ?>
<span><?php echo esc_html( date('h:i A | d-m-Y', $primary_timestamp ) ); ?></span>
<small>By: <?php echo esc_html( $tran['adjuster_name'] ?? 'N/A' ); ?></small>
</td>
<td data-sort-value="<?php echo esc_attr($tran['id']); ?>"><?php echo esc_html( $tran['id'] ); ?></td>
<td class="col-order-id"><?php echo esc_html( $tran['order_id'] ); ?></td>
<td class="col-reason"><?php echo esc_html( $tran['reason'] ); ?></td>
<td data-label="Warning / Fixed By">
<?php
if ($tran['type'] == 'adjustment') {
if (isset($tran['is_active_warning']) && $tran['is_active_warning']) {
echo esc_html($tran['warning_text']);
} elseif (isset($tran['fix_note']) && !empty($tran['fix_note'])) {
echo '<div class="fix-note">'.$tran['fix_note'].'</div>';
} else { echo '-'; }
} else { echo '-'; }
?>
</td>
<td data-label="Fix">
<?php if ( $tran['type'] == 'adjustment' && isset($tran['is_active_warning']) && $tran['is_active_warning'] ): ?>
<button class="fix-warning-btn" data-transaction-id="<?php echo esc_attr($tran['id']); ?>">Fix</button>
<?php else: ?>
-
<?php endif; ?>
</td>
<td class="status-log-cell">
<?php
if ($tran['type'] === 'sale') {
$status_logs = $wpdb->get_results($wpdb->prepare(
"SELECT n.created_at, n.note_text, u.display_name
FROM {$wpdb->prefix}bsm_sale_notes n
LEFT JOIN {$wpdb->users} u ON n.user_id = u.ID
WHERE n.sale_id = %d AND n.note_type = 'status_change'
ORDER BY n.created_at DESC", $tran['id']
));
$log_count = count($status_logs);
if ($log_count > 0) {
$latest_log = $status_logs[0];
$latest_log_timestamp = strtotime($latest_log->created_at);
$latest_log_user = $latest_log->display_name ?: 'System';
$latest_log_time = date('h:i A | d-m-Y', $latest_log_timestamp);
// Calculate balance change for the *latest* log entry
list($old_status_latest, $new_status_latest) = bsm_parse_status_from_log($latest_log->note_text);
$balance_change_amount_latest = 0;
$balance_change_text_latest = '';
if ($old_status_latest !== null && $new_status_latest !== null) {
$balance_change_amount_latest = bsm_calculate_balance_change_for_status_transition($tran['id'], $old_status_latest, $new_status_latest);
if ($balance_change_amount_latest > 0) {
$balance_change_text_latest = ' <span class="balance-change-indicator" style="color:blue;">(Refund +' . number_format($balance_change_amount_latest, 2) . '$)</span>';
} elseif ($balance_change_amount_latest < 0) {
$balance_change_text_latest = ' <span class="balance-change-indicator" style="color:red;">(Cut ' . number_format($balance_change_amount_latest, 2) . '$)</span>';
}
}
// Determine icon color class based on age of latest log
$icon_color_class = 'log-count-old'; // Default Gray (>48h)
if ($latest_log_timestamp >= $forty_eight_hours_ago) { // Within 48h
if ($latest_log_timestamp >= $twenty_four_hours_ago) { // Within 24h
$icon_color_class = 'log-count-recent'; // Red
} else { // Between 24h and 48h
$icon_color_class = 'log-count-medium'; // Dark Yellow
}
}
// Display latest log info (user/time) without background highlight
echo '<div class="latest-log-info-text">'; // No background class here
echo esc_html($latest_log_user);
echo '<small>' . esc_html($latest_log_time) . '</small>';
echo '</div>';
// Display balance change next to it
echo $balance_change_text_latest;
// Display clickable icon if more than 1 log exists, with dynamic background color
if ($log_count > 1) {
echo '<a href="#" class="more-status-log-icon ' . $icon_color_class . '" data-sale-id="' . esc_attr($tran['id']) . '" data-log-type="status" title="View all ' . $log_count . ' status logs">+' . ($log_count - 1) . '</a>';
}
} else {
echo '-';
}
} else {
echo '-'; // Adjustments don't have status logs this way
}
?>
</td>
<td> <?php echo $status_display_html; ?>
<?php echo $status_change_info; ?>
</td>
<td data-sort-value="<?php echo esc_attr($tran['amount']); // Sorting uses original value ?>"> <?php echo $amount_display; // Display formatted amount with sign ?>
</td>
</tr>
<?php
}
} else {
echo '<tr><td colspan="9">No gsmalo.org transactions found matching your criteria.</td></tr>'; // Updated colspan to 9
}
?>
</tbody>
</table>
<?php
if ( $total_pages > 1 ) {
echo '<div class="pagination">';
$query_params = $_GET; unset($query_params['paged']); $base_url = add_query_arg($query_params, admin_url('admin.php'));
for ( $i = 1; $i <= $total_pages; $i++ ) {
$url = add_query_arg('paged', $i, $base_url);
if ( $i == $page ) { echo '<span class="current-page">' . $i . '</span>'; }
else { echo '<a href="' . esc_url($url) . '">' . $i . '</a>'; }
}
echo '</div>';
}
?>
<div class="per-page-form">
<form method="get" action="">
<?php
foreach ($_GET as $key => $value) {
if ($key !== 'per_page' && $key !== 'paged') {
echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr(stripslashes($value)) . '">';
}
}
?>
<label for="per_page">Records per page: </label>
<select name="per_page" id="per_page" onchange="this.form.submit();">
<option value="50" <?php selected($per_page, 50); ?>>50</option>
<option value="100" <?php selected($per_page, 100); ?>>100</option>
<option value="200" <?php selected($per_page, 200); ?>>200</option>
<option value="400" <?php selected($per_page, 400); ?>>400</option>
<option value="500" <?php selected($per_page, 500); ?>>500</option>
<option value="1000" <?php selected($per_page, 1000); ?>>1000</option>
</select>
</form>
</div>
</div> <div id="balanceModal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('balanceModal').style.display='none';">×</span>
<h2>Adjust gsmalo.org Balance</h2>
<p id="modalCurrentBalance">Current Balance: <span>...</span> USD</p>
<form id="balanceAdjustForm">
<?php wp_nonce_field('bsm_adjust_balance_action', 'bsm_adjust_balance_nonce'); ?>
<input type="hidden" name="seller_id" id="adjustSellerId" value="<?php echo esc_attr($seller->user_id); ?>">
<label for="adjustAmount">Adjustment Amount (use negative for revert):</label>
<input type="number" step="0.01" name="adjust_amount" id="adjustAmount" required style="width:100%; padding:8px; margin-bottom:10px;">
<label for="adjustReason">Reason for Adjustment:</label>
<input type="text" name="adjust_reason" id="adjustReason" required style="width:100%; padding:8px; margin-bottom:10px;">
<label style="display:inline-block; margin-bottom:10px;"> <input type="checkbox" name="warning_on" id="warning_on" value="1"> Enable Warning </label>
<button type="submit" class="button button-primary">Submit Adjustment</button>
</form>
</div>
</div>
<div id="status-log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('status-log-modal').style.display='none';">×</span>
<h2>Status Change History</h2>
<div id="status-log-modal-content"><p>Loading logs...</p></div>
</div>
</div>
<script>
// Open Balance Modal (Unchanged)
function openBalanceAdjustmentModal(sellerId, currentBalance) {
var sellerInput = document.getElementById('adjustSellerId');
if (!sellerInput || sellerInput.value != sellerId) { if(sellerInput) sellerInput.value = sellerId; }
var balanceSpan = document.querySelector('#modalCurrentBalance span');
if(balanceSpan) { balanceSpan.textContent = parseFloat(currentBalance).toFixed(2); }
var amountInput = document.getElementById('adjustAmount');
var reasonInput = document.getElementById('adjustReason');
var warningCheckbox = document.getElementById('warning_on');
if(amountInput) amountInput.value = '';
if(reasonInput) reasonInput.value = '';
if(warningCheckbox) warningCheckbox.checked = false;
document.getElementById('balanceModal').style.display = 'block';
}
// Handle Balance Adjustment Form (Unchanged)
document.getElementById('balanceAdjustForm').addEventListener('submit', function(e){
e.preventDefault(); var formData = new FormData(this); formData.append('action', 'bsm_adjust_gsmalo_balance');
fetch(ajaxurl, { method: 'POST', body: formData, credentials: 'same-origin' })
.then(function(response) { if (!response.ok) { return response.text().then(text => { throw new Error('Network response was not ok: ' + text); }); } return response.json(); })
.then(function(data) { if(data.success){ alert(data.data.message); location.reload(); } else { alert("Error: " + (data.data && data.data.message ? data.data.message : "Unknown AJAX error occurred.")); } })
.catch(function(error) { console.error("Balance Adjust Fetch Error:", error); alert("AJAX request failed: " + error.message); });
});
// Handle Fix Warning Button (Unchanged)
document.addEventListener('DOMContentLoaded', function() {
var fixButtons = document.querySelectorAll('.fix-warning-btn');
fixButtons.forEach(function(btn){
btn.addEventListener('click', function(e) {
e.preventDefault(); var transactionId = this.getAttribute('data-transaction-id');
if (!confirm('Are you sure you want to fix this warning?')) { return; } if (!confirm('Please confirm again to fix this warning.')) { return; }
var data = new FormData(); data.append('action', 'bsm_fix_warning'); data.append('transaction_id', transactionId); data.append('_ajax_nonce', '<?php echo wp_create_nonce("bsm_fix_warning_nonce"); ?>');
fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: data })
.then(function(response) { if (!response.ok) { return response.text().then(text => { throw new Error('Network response was not ok: ' + text); }); } return response.json(); })
.then(function(json) { console.log("Fix Warning Response:", json); if (json.success) { var row = btn.closest('tr'); if(row) { var warningCell = row.cells[4]; var fixCell = row.cells[5]; if (warningCell) { if (json.data && json.data.fix_note_html) { warningCell.innerHTML = '<div class="fix-note">' + json.data.fix_note_html + '</div>'; } else { warningCell.innerHTML = '<div class="fix-note">Fixed</div>'; } } if(fixCell) { fixCell.innerHTML = '-'; } row.classList.remove('warning-cell'); } else { alert('Warning fixed successfully!'); location.reload(); } } else { alert('Error: ' + (json.data && json.data.message ? json.data.message : 'Failed to fix warning.')); } })
.catch(function(error) { console.error("Fix Warning Error:", error); alert('Ajax error processing fix request: ' + error.message); });
});
});
// --- Bind status log link clicks ---
rebindStatusLogLinks();
}); // End DOMContentLoaded
// Function to bind click events to status log links (Unchanged)
function rebindStatusLogLinks() {
document.querySelectorAll('.more-status-log-icon').forEach(function(link) { // Changed selector to icon
link.removeEventListener('click', handleStatusLogClick);
link.addEventListener('click', handleStatusLogClick);
});
}
// Handler function for status log link clicks (Unchanged)
function handleStatusLogClick(e) {
e.preventDefault(); var saleId = this.getAttribute('data-sale-id'); var logType = this.getAttribute('data-log-type');
var modalContentEl = document.getElementById('status-log-modal-content'); var modalEl = document.getElementById('status-log-modal');
if (!modalContentEl || !modalEl) { console.error("Modal elements not found"); return; }
modalContentEl.innerHTML = '<p>Loading logs...</p>'; modalEl.style.display = 'block';
var fd = new FormData(); fd.append("action", "bsm_fetch_all_logs"); fd.append("sale_id", saleId); fd.append("log_type", logType); fd.append("nonce", "<?php echo esc_js($log_fetch_nonce); ?>");
fetch(ajaxurl, { method: "POST", body: fd, credentials: 'same-origin' })
.then(response => { if (!response.ok) { return response.text().then(text => { throw new Error(text || response.status); }); } return response.json(); })
.then(data => { if (data.success && data.data.html) { modalContentEl.innerHTML = data.data.html; } else { modalContentEl.innerHTML = '<p>Error loading logs: ' + (data.data ? data.data.message || data.data : 'Unknown error') + '</p>'; } })
.catch(error => { console.error('Error fetching logs:', error); modalContentEl.innerHTML = '<p>AJAX Error loading logs: ' + error.message + '</p>'; });
}
// Close Status Log Modal (Unchanged)
document.querySelector('#status-log-modal .close').addEventListener('click', function() { document.getElementById('status-log-modal').style.display = 'none'; });
window.addEventListener('click', function(event) { if (event.target == document.getElementById('status-log-modal')) { document.getElementById('status-log-modal').style.display = 'none'; } });
// Table Sorting Function (Indices adjusted for new column - Unchanged from last step)
const getCellValueAdmin = (tr, idx, type) => {
const td = tr.children[idx]; if (!td) return null;
if (type === 'datetime') { return td.getAttribute('data-sort-value') || 0; }
if (type === 'number') { const val = td.getAttribute('data-sort-value') || td.innerText || td.textContent; let numStr = String(val).replace(/[^0-9.-]+/g,""); if (String(val).trim().startsWith('-')) { numStr = '-' + numStr.replace('-', ''); } return parseFloat(numStr) || 0; }
return (td.innerText || td.textContent).trim();
};
const comparerAdmin = (idx, asc, type) => (a, b) => ((v1, v2) => v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2))(getCellValueAdmin(asc ? a : b, idx, type), getCellValueAdmin(asc ? b : a, idx, type));
function sortTableByColumn(columnIndex, type = 'string') {
// Column Indices: 0=Time, 1=ID, 2=Order, 3=Reason, 4=Warning/FixBy, 5=Fix, 6=StatusLog, 7=Status, 8=Amount
if (columnIndex === 5 || columnIndex === 6) return; // Don't sort Fix or Status Log columns
const table = document.getElementById('adminTransactionHistoryTable'); if (!table) return; const tbody = table.querySelector('tbody'); if (!tbody) return; const th = table.querySelectorAll('th')[columnIndex]; if (!th) return;
const currentIsAscending = th.classList.contains('sort-asc'); const direction = currentIsAscending ? false : true;
table.querySelectorAll('th').forEach(h => { h.classList.remove('sort-asc', 'sort-desc'); const indicator = h.querySelector('.sort-indicator'); if(indicator) indicator.remove(); });
th.classList.toggle('sort-asc', direction); th.classList.toggle('sort-desc', !direction);
const indicatorSpan = document.createElement('span'); indicatorSpan.className = 'sort-indicator'; indicatorSpan.innerHTML = direction ? ' ▲' : ' ▼'; th.appendChild(indicatorSpan);
Array.from(tbody.querySelectorAll('tr')).sort(comparerAdmin(columnIndex, direction, type)).forEach(tr => tbody.appendChild(tr) );
}
</script>
</body>
</html>
<?php
} else {
// -----------------------------------------------------------------------------
// (১) Seller List View Code
// ** UPDATED: Active Warning text color **
// -----------------------------------------------------------------------------
// Sorting parameters
$allowed_orderby = array('user_id', 'display_name', 'user_email', 'gsmalo_org_balance', 'warning', 'last_transaction');
$orderby = isset($_GET['orderby']) && in_array($_GET['orderby'], $allowed_orderby) ? $_GET['orderby'] : 'last_transaction';
$order = (isset($_GET['order']) && in_array(strtoupper($_GET['order']), array('ASC', 'DESC'))) ? strtoupper($_GET['order']) : 'DESC';
// Base query
$query_select = "SELECT s.user_id, s.gsmalo_org_balance, u.display_name, u.user_email";
$query_from = " FROM {$wpdb->prefix}bsm_sellers s LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID";
$query_where = "";
// Search Logic
$search_term = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
if ( ! empty($search_term) ) {
$search_term_like = '%' . $wpdb->esc_like($search_term) . '%';
$query_where = $wpdb->prepare( " WHERE ( CAST(s.user_id AS CHAR) LIKE %s OR u.display_name LIKE %s OR u.user_email LIKE %s )", $search_term_like, $search_term_like, $search_term_like );
}
// Get sellers first
$query = $query_select . $query_from . $query_where;
$sellers = $wpdb->get_results($query);
// Calculate Warning Count, Recent Transaction Flag, and Last Transaction Time
$current_timestamp_list = current_time('timestamp');
$twenty_four_hours_ago_list = $current_timestamp_list - (24 * 60 * 60);
if ($sellers) {
foreach ($sellers as &$seller) {
if ($seller->user_id > 0) {
$warning_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}bsm_balance_adjustments WHERE seller_id = %d AND LOWER(reason) LIKE %s AND fixed_by IS NULL", $seller->user_id, '%[warning]%'));
$seller->warning_count = $warning_count ? intval($warning_count) : 0;
$last_sale_time = $wpdb->get_var($wpdb->prepare( "SELECT MAX(created_at) FROM {$wpdb->prefix}bsm_sales WHERE seller_id = %d AND sale_type = %s", $seller->user_id, 'gsmalo.org'));
$last_adj_time = $wpdb->get_var($wpdb->prepare( "SELECT MAX(created_at) FROM {$wpdb->prefix}bsm_balance_adjustments WHERE seller_id = %d", $seller->user_id ));
if ($last_sale_time && $last_adj_time) { $seller->last_txn_time = max($last_sale_time, $last_adj_time); }
elseif ($last_sale_time) { $seller->last_txn_time = $last_sale_time; }
elseif ($last_adj_time) { $seller->last_txn_time = $last_adj_time; }
else { $seller->last_txn_time = '0000-00-00 00:00:00'; }
$seller->has_recent_transaction = (strtotime($seller->last_txn_time) >= $twenty_four_hours_ago_list);
} else { $seller->warning_count = 0; $seller->has_recent_transaction = false; $seller->last_txn_time = '0000-00-00 00:00:00'; }
} unset($seller);
} else { $sellers = array(); }
// Sort the results in PHP
usort($sellers, function($a, $b) use ($orderby, $order) {
$a_val = ''; $b_val = '';
switch ($orderby) {
case 'last_transaction': $a_val = $a->last_txn_time; $b_val = $b->last_txn_time; if ($a_val == $b_val) { return $a->user_id - $b->user_id; } break;
case 'warning': $a_val = isset($a->warning_count) ? $a->warning_count : 0; $b_val = isset($b->warning_count) ? $b->warning_count : 0; break;
case 'gsmalo_org_balance': $a_val = floatval($a->gsmalo_org_balance ?? 0); $b_val = floatval($b->gsmalo_org_balance ?? 0); break;
case 'user_id': $a_val = intval($a->user_id); $b_val = intval($b->user_id); if ($a_val == $b_val) { return strcmp($b->last_txn_time, $a->last_txn_time); } break;
case 'display_name': $a_val = strtolower($a->display_name ?? ''); $b_val = strtolower($b->display_name ?? ''); break;
case 'user_email': $a_val = strtolower($a->user_email ?? ''); $b_val = strtolower($b->user_email ?? ''); break;
}
if ($a_val == $b_val) { return 0; } $comparison = ($a_val < $b_val) ? -1 : 1; return ($order === 'ASC') ? $comparison : (-$comparison);
});
// Helper function: Header Link Generation
function get_sort_link($column, $display_name) {
$current_orderby = isset($_GET['orderby']) ? $_GET['orderby'] : 'last_transaction';
$current_order = (isset($_GET['order']) && in_array(strtoupper($_GET['order']), array('ASC','DESC'))) ? strtoupper($_GET['order']) : 'DESC';
$new_order = 'ASC';
if ($current_orderby == $column) {
$new_order = ($current_order === 'ASC') ? 'DESC' : 'ASC';
} else {
$new_order = ($column === 'last_transaction') ? 'DESC' : 'ASC';
}
$url = add_query_arg(array(
'page' => 'gsmalo-org-transaction',
'orderby' => $column,
'order' => $new_order,
'search' => isset($_GET['search']) ? $_GET['search'] : ''
), admin_url('admin.php'));
return '<a href="' . esc_url($url) . '">' . esc_html($display_name) . '</a>';
}
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title>gsmalo.org Transactions - Seller List</title>
<style>
/* Styles unchanged from previous seller list view */
body { font-family: Arial, sans-serif; margin: 0; padding: 0; } .container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 20px; } h1 { color: #0073aa; } .search-bar { margin-bottom: 20px; } .search-bar input[type="text"] { padding: 8px; width: 300px; border: 1px solid #ccc; border-radius: 3px;} .search-bar input[type="submit"] { padding: 8px 12px; border: none; background: #0073aa; color: #fff; border-radius: 3px; cursor: pointer;} .search-bar input[type="submit"]:hover { background: #005177; } table { width: 100%; border-collapse: collapse; } table th, table td { border: 1px solid #ccc; padding: 8px; text-align: center; vertical-align: middle; position: relative; } table th { background: #f5f5f5; font-weight: bold; cursor: pointer; } table th a { text-decoration: none; color: inherit; display: block; } table th a:hover { text-decoration: underline; } .warning-cell { background: #ffcccc !important; } .warning-box-container { margin-bottom: 15px; } .warning-box { display: inline-block; margin-right: 10px; margin-bottom: 10px; padding: 10px 15px; border: 2px solid #d00; background: #ffcccc; border-radius: 5px; font-size: 16px; font-weight: bold; } .warning-box span { color: red; } td a { text-decoration: none; color: #0073aa; } td a:hover { text-decoration: underline; color: #005177; } tbody tr:hover { background-color: #fffacd !important; } .warning-cell:hover { background-color: #ffbbbb !important; } .new-indicator { position: absolute; top: 1px; left: 1px; background-color: red; color: white; font-size: 9px; font-weight: bold; padding: 1px 3px; border-radius: 3px; line-height: 1; z-index: 1; } td.seller-id-cell { padding-left: 20px !important; padding-top: 15px !important; } th .sort-indicator { font-size: 10px; margin-left: 5px; }
/* Style for dark red warning text */
.warning-text-alert { color: darkred; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>gsmalo.org Transactions - Seller List</h1>
<div class="search-bar"> <form method="get" action=""> <input type="hidden" name="page" value="gsmalo-org-transaction"> <input type="text" name="search" placeholder="Search by Seller ID, Name, or Email" style="padding:8px; width:300px;" value="<?php echo isset($_GET['search']) ? esc_attr($_GET['search']) : ''; ?>"> <input type="submit" value="Search"> </form> </div>
<div class="warning-box-container"> <?php if($sellers){ $sellers_with_warning = array_filter($sellers, function($s){ return isset($s->warning_count) && $s->warning_count > 0; }); if (!empty($sellers_with_warning)) { foreach ($sellers_with_warning as $seller) { echo '<div class="warning-box">Seller ID ' . esc_html($seller->user_id) . ': <span>' . esc_html($seller->warning_count) . '</span></div>'; } } else { echo '<p>No active warnings found for any seller.</p>'; } } else { echo '<p>No sellers found.</p>'; } ?> </div>
<table>
<thead>
<tr>
<th> <?php echo get_sort_link('user_id', 'Seller ID'); ?> <?php if ($orderby === 'user_id') echo '<span class="sort-indicator">' . ($order === 'ASC' ? '▲' : '▼') . '</span>'; ?> </th>
<th> <?php echo get_sort_link('display_name', 'Name'); ?> <?php if ($orderby === 'display_name') echo '<span class="sort-indicator">' . ($order === 'ASC' ? '▲' : '▼') . '</span>'; ?> </th>
<th> <?php echo get_sort_link('user_email', 'Email'); ?> <?php if ($orderby === 'user_email') echo '<span class="sort-indicator">' . ($order === 'ASC' ? '▲' : '▼') . '</span>'; ?> </th>
<th> <?php echo get_sort_link('warning', 'Active Warning'); ?> <?php if ($orderby === 'warning') echo '<span class="sort-indicator">' . ($order === 'ASC' ? '▲' : '▼') . '</span>'; ?> </th>
<th> <?php echo get_sort_link('last_transaction', 'Last Txn Time'); ?> <?php if ($orderby === 'last_transaction') echo '<span class="sort-indicator">' . ($order === 'ASC' ? '▲' : '▼') . '</span>'; ?> </th>
<th> <?php echo get_sort_link('gsmalo_org_balance', 'gsmalo.org Balance (USD)'); ?> <?php if ($orderby === 'gsmalo_org_balance') echo '<span class="sort-indicator">' . ($order === 'ASC' ? '▲' : '▼') . '</span>'; ?> </th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if ($sellers) { foreach ($sellers as $seller) {
$warning_count_val = isset($seller->warning_count) ? $seller->warning_count : 0;
$warningDisplay = ($warning_count_val > 0) ? "Warning +" . $warning_count_val : "0";
$rowClass = ($warning_count_val > 0) ? 'class="warning-cell"' : '';
$has_recent = isset($seller->has_recent_transaction) && $seller->has_recent_transaction;
$last_txn_display = ($seller->last_txn_time === '0000-00-00 00:00:00') ? 'N/A' : date('h:i A | d-m-Y', strtotime($seller->last_txn_time));
?>
<tr <?php echo $rowClass; ?>>
<td class="seller-id-cell"> <?php if ($has_recent): ?> <span class="new-indicator">New</span> <?php endif; ?> <?php echo esc_html($seller->user_id); ?> </td>
<td><?php echo esc_html($seller->display_name); ?></td>
<td><?php echo esc_html($seller->user_email); ?></td>
<td>
<?php // Apply dark red color only to the warning text
if ($warning_count_val > 0) {
echo '<span class="warning-text-alert">' . esc_html($warningDisplay) . '</span>';
} else {
echo esc_html($warningDisplay); // Display "0" normally
}
?>
</td>
<td><?php echo esc_html($last_txn_display); ?></td>
<td><?php echo esc_html(number_format((float)($seller->gsmalo_org_balance ?? 0), 2)); ?></td>
<td> <a href="<?php echo esc_url( add_query_arg(array('page' => 'gsmalo-org-transaction', 'seller_id' => $seller->user_id), admin_url('admin.php') ) ); ?>"> View Transactions </a> </td>
</tr>
<?php } } else { echo '<tr><td colspan="7">No sellers found' . ($search_term ? ' matching your search.' : '.') . '</td></tr>'; } ?>
</tbody>
</table>
</div>
</body>
</html>
<?php
} // End else for Seller List View
business-seller-management/templates/admin/general.php
<?php
/**
* File: business-seller-management/templates/admin/settings/general.php
*
* Description:
* - এই ফাইলটি এডমিন প্যানেলের General Settings ট্যাবের জন্য।
* - এখানে "USD value in ৳" ইনপুট ফিল্ড থাকবে, যা সেভ করলে wp‑option এ সংরক্ষিত হবে (option name: bsm_usd_value_in_taka)।
* - এই ভেলুটি Sale Page‑এ ডলার কনভার্শন রেট হিসাবে ব্যবহার হবে।
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// ফর্ম সাবমিশনের ক্ষেত্রে: POST হলে, সেটিংস সেভ করে পুনরায় রিডাইরেক্ট করা হবে
if ( isset($_POST['bsm_save_general_settings']) && check_admin_referer('bsm_save_general_settings_action', 'bsm_general_nonce') ) {
$usd_value_in_taka = floatval($_POST['usd_value_in_taka']);
update_option('bsm_usd_value_in_taka', $usd_value_in_taka);
// সফলভাবে সেভ হলে, পেজ রিলোড হয়ে একই General ট্যাব এ যাবে
wp_redirect( admin_url('admin.php?page=bsm-settings#tab-general&settings_updated=true') );
exit;
}
// বর্তমানে সংরক্ষিত মান (ডিফল্ট 85)
$current_usd_value = get_option('bsm_usd_value_in_taka', 85);
?>
<div class="wrap">
<h2>General Settings</h2>
<?php
if ( isset($_GET['settings_updated']) && $_GET['settings_updated'] === 'true' ) {
echo '<div class="updated"><p>General settings saved successfully.</p></div>';
}
?>
<form method="post" action="">
<?php wp_nonce_field('bsm_save_general_settings_action', 'bsm_general_nonce'); ?>
<table class="form-table">
<tr>
<th scope="row"><label for="usd_value_in_taka">USD value in ৳</label></th>
<td><input type="number" step="0.01" name="usd_value_in_taka" id="usd_value_in_taka" value="<?php echo esc_attr($current_usd_value); ?>" required /></td>
</tr>
</table>
<p class="submit">
<input type="submit" name="bsm_save_general_settings" class="button button-primary" value="Save General Settings" />
</p>
</form>
<p>This value will determine the conversion rate (USD to ৳) used on the Sale Page.</p>
</div>
business-seller-management/templates/admin/edit-sale-form.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
global $wpdb;
// Get sale ID from URL
$sale_id = isset($_GET['sale_id']) ? intval($_GET['sale_id']) : 0;
// Fetch sale data
$table_name_sales = $wpdb->prefix . 'bsm_sales';
$sale = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM $table_name_sales WHERE id = %d",
$sale_id
)
);
if (!$sale) {
echo '<p>Sale not found.</p>';
return;
}
?>
<div class="wrap">
<h1>Edit Sale</h1>
<form method="post" action="">
<?php wp_nonce_field('bsm_edit_sale', 'bsm_edit_sale_nonce'); ?>
<input type="hidden" name="sale_id" value="<?php echo esc_attr($sale->id); ?>">
<table class="form-table">
<tr>
<th scope="row"><label for="product_name">Product Name</label></th>
<td>
<input name="product_name" type="text" id="product_name" class="regular-text" value="<?php echo esc_attr($sale->product_name); ?>" required>
</td>
</tr>
<tr>
<th scope="row"><label for="sale_type">Sale Type</label></th>
<td>
<select name="sale_type" id="sale_type" required>
<option value="product" <?php selected($sale->sale_type, 'product'); ?>>Product</option>
<option value="service" <?php selected($sale->sale_type, 'service'); ?>>Service</option>
<option value="gsmalo.com" <?php selected($sale->sale_type, 'gsmalo.com'); ?>>gsmalo.com</option>
<option value="gsmalo.org" <?php selected($sale->sale_type, 'gsmalo.org'); ?>>gsmalo.org</option>
<option value="gsmcourse.com" <?php selected($sale->sale_type, 'gsmcourse.com'); ?>>gsmcourse.com</option>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="payment_method">Payment Method</label></th>
<td>
<select name="payment_method" id="payment_method" required>
<option value="bkash_707" <?php selected($sale->payment_method, 'bkash_707'); ?>>Bkash 707</option>
<option value="bkash_685" <?php selected($sale->payment_method, 'bkash_685'); ?>>Bkash 685</option>
<option value="nagad_707" <?php selected($sale->payment_method, 'nagad_707'); ?>>Nagad 707</option>
<option value="nagad_685" <?php selected($sale->payment_method, 'nagad_685'); ?>>Nagad 685</option>
<option value="rocket" <?php selected($sale->payment_method, 'rocket'); ?>>Rocket</option>
<option value="cash" <?php selected($sale->payment_method, 'cash'); ?>>Cash</option>
<option value="deposit" <?php selected($sale->payment_method, 'deposit'); ?>>Deposit</option>
<option value="binance" <?php selected($sale->payment_method, 'binance'); ?>>Binance</option>
<option value="other" <?php selected($sale->payment_method, 'other'); ?>>Other</option>
</select>
</td>
</tr>
<tr>
<th scope="row"><label for="selling_price">Selling Price</label></th>
<td>
<input name="selling_price" type="number" id="selling_price" class="regular-text" value="<?php echo esc_attr($sale->selling_price); ?>" required>
</td>
</tr>
<tr>
<th scope="row"><label for="purchase_price">Purchase Price</label></th>
<td>
<input name="purchase_price" type="number" id="purchase_price" class="regular-text" value="<?php echo esc_attr($sale->purchase_price); ?>" required>
</td>
</tr>
<tr>
<th scope="row"><label for="status">Status</label></th>
<td>
<select name="status" id="status" required>
<option value="pending" <?php selected($sale->status, 'pending'); ?>>Pending</option>
<option value="completed" <?php selected($sale->status, 'completed'); ?>>Completed</option>
<option value="cancelled" <?php selected($sale->status, 'cancelled'); ?>>Cancelled</option>
</select>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="edit_sale" id="edit_sale" class="button button-primary" value="Update Sale">
</p>
</form>
</div>
business-seller-management/templates/admin/edit-payment-method-form.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
global $wpdb;
// Get method ID from URL
$method_id = isset($_GET['method_id']) ? intval($_GET['method_id']) : 0;
// Fetch payment method data
$table_name_payment_methods = $wpdb->prefix . 'bsm_payment_methods';
$method = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM $table_name_payment_methods WHERE id = %d",
$method_id
)
);
if (!$method) {
echo '<p>Payment method not found.</p>';
return;
}
?>
<div class="wrap">
<h1>Edit Payment Method</h1>
<form method="post" action="">
<?php wp_nonce_field('bsm_edit_payment_method', 'bsm_edit_payment_method_nonce'); ?>
<input type="hidden" name="method_id" value="<?php echo esc_attr($method->id); ?>">
<table class="form-table">
<tr>
<th scope="row"><label for="method_name">Method Name</label></th>
<td>
<input name="method_name" type="text" id="method_name" class="regular-text" value="<?php echo esc_attr($method->method_name); ?>" required>
</td>
</tr>
<tr>
<th scope="row"><label for="method_number">Method Number</label></th>
<td>
<input name="method_number" type="text" id="method_number" class="regular-text" value="<?php echo esc_attr($method->method_number); ?>" required>
</td>
</tr>
<tr>
<th scope="row"><label for="method_type">Method Type</label></th>
<td>
<select name="method_type" id="method_type" required>
<option value="bkash" <?php selected($method->method_type, 'bkash'); ?>>Bkash</option>
<option value="nagad" <?php selected($method->method_type, 'nagad'); ?>>Nagad</option>
<option value="rocket" <?php selected($method->method_type, 'rocket'); ?>>Rocket</option>
<option value="cash" <?php selected($method->method_type, 'cash'); ?>>Cash</option>
<option value="deposit" <?php selected($method->method_type, 'deposit'); ?>>Deposit</option>
<option value="binance" <?php selected($method->method_type, 'binance'); ?>>Binance</option>
<option value="other" <?php selected($method->method_type, 'other'); ?>>Other</option>
</select>
</td>
</tr>
</table>
<p class="submit">
<input type="submit" name="edit_payment_method" id="edit_payment_method" class="button button-primary" value="Update Payment Method">
</p>
</form>
</div>
business-seller-management/templates/admin/balance-adjustments.php
<?php
/**
* File: business-seller-management/templates/admin/balance-adjustments.php
*
* Description:
* - এই ফাইলটি এডমিন প্যানেলের "gsmalo.org ব্যাল্যান্স" ট্যাবে সেলারদের ব্যাল্যান্স অ্যাডজাস্টমেন্টের জন্য ব্যবহৃত হয়।
* - পূর্বে প্রদর্শিত "Balance Adjustment History" অংশটি বাতিল করা হয়েছে, কারণ হিস্ট্রি অন্য মেনুতে দেখানো হবে।
* - এখানে ব্যাল্যান্স অ্যাডজাস্টমেন্ট ফর্মে "Adjustment Amount (use negative for revert):" এবং
* "Reason for Adjustment:" ফাংশনালিটি অপরিবর্তিত রাখা হয়েছে।
*
* File Path: business-seller-management/templates/admin/balance-adjustments.php
*/
if ( ! defined('ABSPATH') ) {
exit;
}
?>
<div id="bsmBalanceAdjustmentPopup" style="display: none;">
<h2>Adjust Seller Balance</h2>
<form id="bsmBalanceAdjustmentForm" method="post">
<!-- Hidden action -->
<input type="hidden" name="action" value="bsm_balance_adjustment_submit" />
<!-- Nonce field -->
<input type="hidden" name="bsm_balance_adjustment_nonce" value="<?php echo esc_attr( wp_create_nonce('bsm_balance_adjustment_action') ); ?>" />
<!-- Seller ID input -->
<label for="adjustSellerId">Seller ID:</label>
<input type="number" name="seller_id" id="adjustSellerId" value="" required />
<!-- Adjustment Amount -->
<label for="adjustAmount">Adjustment Amount (use negative for revert):</label>
<input type="text" name="adjust_amount" id="adjustAmount" value="" required />
<!-- Reason for Adjustment -->
<label for="adjustReason">Reason for Adjustment:</label>
<input type="text" name="adjust_reason" id="adjustReason" value="" />
<button type="submit">Submit Adjustment</button>
</form>
</div>
business-seller-management/templates/admin/all-sales-page.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
?>
<div class="wrap">
<h1>All Sales</h1>
<p>This is a placeholder page for <strong>All Sales</strong>.</p>
<!--
ভবিষ্যতে এখানে আপনার "All Sales" সম্পর্কিত ফিচারগুলো যোগ করবেন।
যেমন:
- Sales list টেবিল
- বিক্রয় সারাংশ (তারিখ অনুযায়ী সার্চ/ফিল্টার)
- পেইজিনেশন
- স্ট্যাটাস আপডেট, ডিলিট ইত্যাদি
- Recycle bin বা Refund অপশন
ইত্যাদি।
-->
</div>
business-seller-management/templates/admin/all-logs-page.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
?>
<div class="wrap">
<h1>All Logs</h1>
<p>This is a placeholder page for <strong>All Logs</strong>.</p>
<!--
ভবিষ্যতে এখানে "All Logs" সম্পর্কিত ফিচার (লগ লিস্ট, ফিল্টার, পেজিনেশন ইত্যাদি) যোগ করা যাবে।
-->
</div>
business-seller-management/templates/admin/admin-dashboard.php
<?php
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
/**
* Helper function to group sales by seller_id.
*/
function bsm_group_sales_by_seller($sales_array) {
$grouped = [];
foreach ($sales_array as $sale) {
$seller_id = $sale->seller_id;
if (!isset($grouped[$seller_id])) {
$grouped[$seller_id] = [];
}
$grouped[$seller_id][] = $sale;
}
return $grouped;
}
?>
<style>
/*-----------------------------------------
মূল layout, স্পেস ও ফন্ট স্টাইল
------------------------------------------*/
.bsm-section {
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 30px;
padding: 15px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.bsm-section h2 {
margin-top: 0;
margin-bottom: 20px;
font-weight: 600;
}
.bsm-section h3 {
margin-top: 30px;
margin-bottom: 10px;
font-weight: 600;
}
.bsm-section h4 {
margin-top: 30px;
margin-bottom: 10px;
font-weight: 600;
}
/*-----------------------------------------
সারাংশের রো (এক লাইনে ৩টি বক্স) ও প্রতিটি বক্সের সাইজ
------------------------------------------*/
.bsm-summary-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.bsm-summary-row .bsm-box-item {
flex: 0 0 calc(30% - 10px);
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.bsm-summary-row .bsm-box-item strong {
display: block;
margin-bottom: 5px;
font-size: 14px;
}
.bsm-summary-row .bsm-box-item span {
font-size: 16px;
font-weight: 700;
}
/* রো অনুযায়ী ব্যাকগ্রাউন্ড রঙ */
.row1 .bsm-box-item { background: #FFCDD2; } /* হালকা লাল */
.row2 .bsm-box-item { background: #C8E6C9; } /* হালকা সবুজ */
.row3 .bsm-box-item { background: #BBDEFB; } /* হালকা নীল */
.row4 .bsm-box-item { background: #FFE0B2; } /* হালকা কমলা */
.row5 .bsm-box-item { background: #E1BEE7; } /* হালকা বেগুনি */
/*-----------------------------------------
বিভিন্ন সেকশনের জন্য ব্যাকগ্রাউন্ড রঙ
------------------------------------------*/
.bsm-balance-section { background: #FFFDE7; } /* Pale Yellow-ish */
.bsm-today-section { background: #E0F7FA; } /* Pale Cyan-ish */
.bsm-yesterday-section { background: #E8EAF6; } /* Pale Indigo-ish */
.bsm-month-section { background: #F1F8E9; } /* Pale Green-ish */
.bsm-last-month-section { background: #E0F2F1; } /* Pale Teal-ish */
.bsm-short-summary { background: #F9FBE7; } /* Pale Lime-ish */
.bsm-last-50-sales { background: #FCE4EC; } /* Pale Pink-ish */
.bsm-other-section { background: #F3E5F5; } /* Pale Purple-ish */
/*-----------------------------------------
টেবিল স্টাইলিং (WordPress-এর ডিফল্ট স্টাইল অনুযায়ী)
------------------------------------------*/
.wp-list-table th {
background: #f0f0f0;
font-weight: 600;
}
</style>
<div class="wrap">
<h1>এডমিন ড্যাশবোর্ড</h1>
<!-- gsmalo.org ব্যালেন্স সারাংশ (সর্বোপরি) -->
<div class="bsm-section bsm-balance-section">
<h2>gsmalo.org ব্যালেন্স সারাংশ</h2>
<?php if (!empty($all_seller_balances)): ?>
<div style="display: flex; flex-wrap: wrap; gap: 10px;">
<?php foreach ($all_seller_balances as $balance_item):
$seller_name = $balance_item->display_name ? $balance_item->display_name : 'Seller #'.$balance_item->user_id;
?>
<div style="flex: 0 0 calc(20% - 10px); background: #ffffff; border: 1px solid #ddd; border-radius: 5px; padding: 15px; text-align: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<h4><?php echo esc_html($seller_name); ?></h4>
<p><?php echo esc_html($balance_item->gsmalo_org_balance); ?> USD</p>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p>কোনো সেলার ব্যালেন্স পাওয়া যায়নি।</p>
<?php endif; ?>
</div>
<!-- আজকের রিপোর্ট -->
<div class="bsm-section bsm-today-section">
<h2>আজকের রিপোর্ট (<?php echo date('Y-m-d'); ?>)</h2>
<!-- আজকের সারাংশ -->
<h3>আজকের সারাংশ</h3>
<!-- Row 1: বিক্রয় (BDT), মোট লাভ (BDT), মোট লস (BDT) -->
<div class="bsm-summary-row row1">
<div class="bsm-box-item">
<strong>বিক্রয় (BDT)</strong>
<span><?php echo esc_html($today_total_sales); ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লাভ (BDT)</strong>
<span><?php echo esc_html($today_total_profit); ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লস (BDT)</strong>
<span><?php echo esc_html($today_total_loss); ?></span>
</div>
</div>
<!-- Row 2: বিক্রয় (সংখ্যা), TNX (সংখ্যা), অন্যান্য কাজ (সংখ্যা) -->
<div class="bsm-summary-row row2">
<div class="bsm-box-item">
<strong>বিক্রয় (সংখ্যা)</strong>
<span><?php echo esc_html($today_product_sale_count); ?></span>
</div>
<div class="bsm-box-item">
<strong>TNX (সংখ্যা)</strong>
<span><?php echo esc_html($today_tnx_count); ?></span>
</div>
<div class="bsm-box-item">
<strong>অন্যান্য কাজ (সংখ্যা)</strong>
<span><?php echo esc_html($today_other_count); ?></span>
</div>
</div>
<!-- Row 3: ওয়েব বিক্রয় (BDT), ওয়েব লাভ (BDT), ওয়েব লস (BDT) -->
<div class="bsm-summary-row row3">
<div class="bsm-box-item">
<strong>ওয়েব বিক্রয় (BDT)</strong>
<span><?php echo isset($today_web_sales) ? esc_html($today_web_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লাভ (BDT)</strong>
<span><?php echo isset($today_web_profit) ? esc_html($today_web_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লস (BDT)</strong>
<span><?php echo isset($today_web_loss) ? esc_html($today_web_loss) : 0; ?></span>
</div>
</div>
<!-- Row 4: সার্ভিসিং বিক্রয় (BDT), সার্ভিসিং লাভ (BDT), সার্ভিসিং লস (BDT) -->
<div class="bsm-summary-row row4">
<div class="bsm-box-item">
<strong>সার্ভিসিং বিক্রয় (BDT)</strong>
<span><?php echo isset($today_service_sales) ? esc_html($today_service_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লাভ (BDT)</strong>
<span><?php echo isset($today_service_profit) ? esc_html($today_service_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লস (BDT)</strong>
<span><?php echo isset($today_service_loss) ? esc_html($today_service_loss) : 0; ?></span>
</div>
</div>
<!-- Row 5: প্রোডাক্ট বিক্রয় (BDT), প্রোডাক্ট লাভ (BDT), প্রোডাক্ট লস (BDT) -->
<div class="bsm-summary-row row5">
<div class="bsm-box-item">
<strong>প্রোডাক্ট বিক্রয় (BDT)</strong>
<span><?php echo isset($today_product_sales) ? esc_html($today_product_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লাভ (BDT)</strong>
<span><?php echo isset($today_product_profit) ? esc_html($today_product_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লস (BDT)</strong>
<span><?php echo isset($today_product_loss) ? esc_html($today_product_loss) : 0; ?></span>
</div>
</div>
<!-- আজকের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ -->
<h3>আজকের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ</h3>
<ul>
<li>প্রোডাক্ট বিক্রয় (BDT): <?php echo esc_html($today_product_sales); ?>, লাভ: <?php echo esc_html($today_product_profit); ?></li>
<li>সার্ভিসিং বিক্রয় (BDT): <?php echo esc_html($today_service_sales); ?>, লাভ: <?php echo esc_html($today_service_profit); ?></li>
<li>gsmalo.org বিক্রয় (BDT): <?php echo esc_html($today_gsmalo_org_sales); ?>, লাভ: <?php echo esc_html($today_gsmalo_org_profit); ?></li>
<li>gsmalo.com বিক্রয় (BDT): <?php echo esc_html($today_gsmalo_com_sales); ?>, লাভ: <?php echo esc_html($today_gsmalo_com_profit); ?></li>
<li>gsmcourse.com বিক্রয় (BDT): <?php echo esc_html($today_gsmcourse_com_sales); ?>, লাভ: <?php echo esc_html($today_gsmcourse_com_profit); ?></li>
</ul>
<!-- আজকের প্রত্যেক সেলার এর বিক্রয় তালিকা -->
<?php
$today_grouped_sales = bsm_group_sales_by_seller($today_sales);
?>
<h3>প্রত্যেক সেলার এর আজকের বিক্রয় তালিকা</h3>
<?php if (!empty($today_grouped_sales)): ?>
<?php foreach ($today_grouped_sales as $seller_id => $seller_sales_list):
$user_info = get_user_by('id', $seller_id);
$seller_name = $user_info ? $user_info->display_name : 'Seller #'.$seller_id;
?>
<h4>সেলার: <?php echo esc_html($seller_name); ?> (ID: <?php echo esc_html($seller_id); ?>)</h4>
<?php if (!empty($seller_sales_list)): ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>ID</th>
<th>পণ্যের নাম</th>
<th>বিক্রয় ধরন</th>
<th>পেমেন্ট মেথড</th>
<th>ক্রয় মূল্য</th>
<th>বিক্রয় মূল্য</th>
<th>লাভ</th>
<th>লস</th>
<th>স্ট্যাটাস</th>
<th>তারিখ</th>
</tr>
</thead>
<tbody>
<?php foreach ($seller_sales_list as $sale): ?>
<tr>
<td><?php echo esc_html($sale->id); ?></td>
<td><?php echo esc_html($sale->product_name); ?></td>
<td><?php echo esc_html($sale->sale_type); ?></td>
<td><?php echo esc_html($sale->payment_method); ?></td>
<td><?php echo esc_html($sale->purchase_price); ?></td>
<td><?php echo esc_html($sale->selling_price); ?> BDT</td>
<td><?php echo esc_html($sale->profit); ?> BDT</td>
<td><?php echo esc_html($sale->loss); ?> BDT</td>
<td><?php echo esc_html($sale->status); ?></td>
<td><?php echo esc_html($sale->created_at); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>এই সেলারের আজকের কোনো বিক্রয় নেই।</p>
<?php endif; ?>
<hr>
<?php endforeach; ?>
<?php else: ?>
<p>আজকের কোনো বিক্রয় নেই (seller-wise breakdown)।</p>
<?php endif; ?>
<h3>আজকের রিপোর্ট: অন্যান্য কাজ</h3>
<p>Placeholder: আজকের অন্যান্য কাজের বিস্তারিত রিপোর্ট।</p>
</div>
<!-- গতকালের রিপোর্ট -->
<div class="bsm-section bsm-yesterday-section" style="background: #E8EAF6;">
<h2>গতকালের রিপোর্ট (<?php echo date('Y-m-d', strtotime('-1 day')); ?>)</h2>
<!-- গতকালের সারাংশ -->
<h3>গতকালের সারাংশ</h3>
<!-- Row 1: বিক্রয় (BDT), মোট লাভ (BDT), মোট লস (BDT) -->
<div class="bsm-summary-row row1">
<div class="bsm-box-item">
<strong>বিক্রয় (BDT)</strong>
<span><?php echo esc_html($yesterday_total_sales); ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লাভ (BDT)</strong>
<span><?php echo esc_html($yesterday_total_profit); ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লস (BDT)</strong>
<span><?php echo esc_html($yesterday_total_loss); ?></span>
</div>
</div>
<!-- Row 2: বিক্রয় (সংখ্যা), TNX (সংখ্যা), অন্যান্য কাজ (সংখ্যা) -->
<div class="bsm-summary-row row2">
<div class="bsm-box-item">
<strong>বিক্রয় (সংখ্যা)</strong>
<span><?php echo esc_html($yesterday_product_sale_count); ?></span>
</div>
<div class="bsm-box-item">
<strong>TNX (সংখ্যা)</strong>
<span><?php echo esc_html($yesterday_tnx_count); ?></span>
</div>
<div class="bsm-box-item">
<strong>অন্যান্য কাজ (সংখ্যা)</strong>
<span><?php echo esc_html($yesterday_other_count); ?></span>
</div>
</div>
<!-- Row 3: ওয়েব বিক্রয় (BDT), ওয়েব লাভ (BDT), ওয়েব লস (BDT) -->
<div class="bsm-summary-row row3">
<div class="bsm-box-item">
<strong>ওয়েব বিক্রয় (BDT)</strong>
<span><?php echo isset($yesterday_web_sales) ? esc_html($yesterday_web_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লাভ (BDT)</strong>
<span><?php echo isset($yesterday_web_profit) ? esc_html($yesterday_web_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লস (BDT)</strong>
<span><?php echo isset($yesterday_web_loss) ? esc_html($yesterday_web_loss) : 0; ?></span>
</div>
</div>
<!-- Row 4: সার্ভিসিং বিক্রয় (BDT), সার্ভিসিং লাভ (BDT), সার্ভিসিং লস (BDT) -->
<div class="bsm-summary-row row4">
<div class="bsm-box-item">
<strong>সার্ভিসিং বিক্রয় (BDT)</strong>
<span><?php echo isset($yesterday_service_sales) ? esc_html($yesterday_service_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লাভ (BDT)</strong>
<span><?php echo isset($yesterday_service_profit) ? esc_html($yesterday_service_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লস (BDT)</strong>
<span><?php echo isset($yesterday_service_loss) ? esc_html($yesterday_service_loss) : 0; ?></span>
</div>
</div>
<!-- Row 5: প্রোডাক্ট বিক্রয় (BDT), প্রোডাক্ট লাভ (BDT), প্রোডাক্ট লস (BDT) -->
<div class="bsm-summary-row row5">
<div class="bsm-box-item">
<strong>প্রোডাক্ট বিক্রয় (BDT)</strong>
<span><?php echo isset($yesterday_product_sales) ? esc_html($yesterday_product_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লাভ (BDT)</strong>
<span><?php echo isset($yesterday_product_profit) ? esc_html($yesterday_product_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লস (BDT)</strong>
<span><?php echo isset($yesterday_product_loss) ? esc_html($yesterday_product_loss) : 0; ?></span>
</div>
</div>
<p style="margin-top: 15px;">
<strong>মোট বিক্রয় (BDT):</strong> <?php echo esc_html($yesterday_total_sales); ?>,
<strong>মোট লাভ:</strong> <?php echo esc_html($yesterday_total_profit); ?>
</p>
<!-- গতকালের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ -->
<h3>গতকালের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ</h3>
<ul>
<li>প্রোডাক্ট বিক্রয় (BDT): <?php echo esc_html($yesterday_product_sales); ?>, লাভ: <?php echo esc_html($yesterday_product_profit); ?></li>
<li>সার্ভিসিং বিক্রয় (BDT): <?php echo esc_html($yesterday_service_sales); ?>, লাভ: <?php echo esc_html($yesterday_service_profit); ?></li>
<li>gsmalo.org বিক্রয় (BDT): <?php echo esc_html($yesterday_gsmalo_org_sales); ?>, লাভ: <?php echo esc_html($yesterday_gsmalo_org_profit); ?></li>
<li>gsmalo.com বিক্রয় (BDT): <?php echo esc_html($yesterday_gsmalo_com_sales); ?>, লাভ: <?php echo esc_html($yesterday_gsmalo_com_profit); ?></li>
<li>gsmcourse.com বিক্রয় (BDT): <?php echo esc_html($yesterday_gsmcourse_com_sales); ?>, লাভ: <?php echo esc_html($yesterday_gsmcourse_com_profit); ?></li>
</ul>
<!-- গতকালের প্রত্যেক সেলার এর বিক্রয় তালিকা -->
<?php
$yesterday_grouped_sales = bsm_group_sales_by_seller($yesterday_sales);
?>
<h3>প্রত্যেক সেলার এর গতকাল এর বিক্রয় তালিকা</h3>
<?php if (!empty($yesterday_grouped_sales)): ?>
<?php foreach ($yesterday_grouped_sales as $seller_id => $seller_sales_list):
$user_info = get_user_by('id', $seller_id);
$seller_name = $user_info ? $user_info->display_name : 'Seller #'.$seller_id;
?>
<h4>সেলার: <?php echo esc_html($seller_name); ?> (ID: <?php echo esc_html($seller_id); ?>)</h4>
<?php if (!empty($seller_sales_list)): ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>ID</th>
<th>পণ্যের নাম</th>
<th>বিক্রয় ধরন</th>
<th>পেমেন্ট মেথড</th>
<th>ক্রয় মূল্য</th>
<th>বিক্রয় মূল্য</th>
<th>লাভ</th>
<th>লস</th>
<th>স্ট্যাটাস</th>
<th>তারিখ</th>
</tr>
</thead>
<tbody>
<?php foreach ($seller_sales_list as $sale): ?>
<tr>
<td><?php echo esc_html($sale->id); ?></td>
<td><?php echo esc_html($sale->product_name); ?></td>
<td><?php echo esc_html($sale->sale_type); ?></td>
<td><?php echo esc_html($sale->payment_method); ?></td>
<td><?php echo esc_html($sale->purchase_price); ?></td>
<td><?php echo esc_html($sale->selling_price); ?> BDT</td>
<td><?php echo esc_html($sale->profit); ?> BDT</td>
<td><?php echo esc_html($sale->loss); ?> BDT</td>
<td><?php echo esc_html($sale->status); ?></td>
<td><?php echo esc_html($sale->created_at); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>এই সেলারের গতকালের কোনো বিক্রয় নেই।</p>
<?php endif; ?>
<hr>
<?php endforeach; ?>
<?php else: ?>
<p>গতকালের কোনো বিক্রয় পাওয়া যায়নি (seller-wise breakdown)।</p>
<?php endif; ?>
<h3>গতকালের রিপোর্ট: অন্যান্য কাজ</h3>
<p>Placeholder: গতকালের অন্যান্য কাজের বিস্তারিত রিপোর্ট।</p>
<h3>গতকালের প্রত্যেক সেলারের সারাংশ</h3>
<?php if (!empty($yesterday_seller_summary)): ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>Seller ID</th>
<th>মোট বিক্রয় (BDT)</th>
<th>মোট লাভ (BDT)</th>
<th>বিক্রয় সংখ্যা</th>
<th>প্রোডাক্ট বিক্রয় সংখ্যা</th>
</tr>
</thead>
<tbody>
<?php foreach ($yesterday_seller_summary as $seller_id => $info): ?>
<tr>
<td><?php echo esc_html($seller_id); ?></td>
<td><?php echo esc_html($info['total_sales']); ?></td>
<td><?php echo esc_html($info['total_profit']); ?></td>
<td><?php echo esc_html($info['count_sales']); ?></td>
<td><?php echo esc_html($info['product_count']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>গতকালের কোনো সেলার সারাংশ পাওয়া যায়নি।</p>
<?php endif; ?>
</div>
<!-- ৪) এ মাসের রিপোর্ট -->
<div class="bsm-section bsm-month-section" style="background: #F1F8E9;">
<h2>এ মাসের রিপোর্ট (<?php echo date('Y-m'); ?>)</h2>
<!-- Row 1 -->
<div class="bsm-summary-row row1">
<div class="bsm-box-item">
<strong>বিক্রয় (BDT)</strong>
<span><?php echo esc_html($month_total_sales); ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লাভ (BDT)</strong>
<span><?php echo esc_html($month_total_profit); ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লস (BDT)</strong>
<span><?php echo esc_html($month_total_loss); ?></span>
</div>
</div>
<!-- Row 2 -->
<div class="bsm-summary-row row2">
<div class="bsm-box-item">
<strong>বিক্রয় (সংখ্যা)</strong>
<span><?php echo esc_html($month_product_sale_count); ?></span>
</div>
<div class="bsm-box-item">
<strong>TNX (সংখ্যা)</strong>
<span><?php echo esc_html($month_tnx_count); ?></span>
</div>
<div class="bsm-box-item">
<strong>অন্যান্য কাজ (সংখ্যা)</strong>
<span><?php echo esc_html($month_other_count); ?></span>
</div>
</div>
<!-- Row 3 -->
<div class="bsm-summary-row row3">
<div class="bsm-box-item">
<strong>ওয়েব বিক্রয় (BDT)</strong>
<span><?php echo isset($month_web_sales) ? esc_html($month_web_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লাভ (BDT)</strong>
<span><?php echo isset($month_web_profit) ? esc_html($month_web_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লস (BDT)</strong>
<span><?php echo isset($month_web_loss) ? esc_html($month_web_loss) : 0; ?></span>
</div>
</div>
<!-- Row 4 -->
<div class="bsm-summary-row row4">
<div class="bsm-box-item">
<strong>সার্ভিসিং বিক্রয় (BDT)</strong>
<span><?php echo isset($month_service_sales) ? esc_html($month_service_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লাভ (BDT)</strong>
<span><?php echo isset($month_service_profit) ? esc_html($month_service_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লস (BDT)</strong>
<span><?php echo isset($month_service_loss) ? esc_html($month_service_loss) : 0; ?></span>
</div>
</div>
<!-- Row 5 -->
<div class="bsm-summary-row row5">
<div class="bsm-box-item">
<strong>প্রোডাক্ট বিক্রয় (BDT)</strong>
<span><?php echo isset($month_product_sales) ? esc_html($month_product_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লাভ (BDT)</strong>
<span><?php echo isset($month_product_profit) ? esc_html($month_product_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লস (BDT)</strong>
<span><?php echo isset($month_product_loss) ? esc_html($month_product_loss) : 0; ?></span>
</div>
</div>
<p style="margin-top: 15px;">
<strong>মোট বিক্রয় (BDT):</strong> <?php echo esc_html($month_total_sales); ?>,
<strong>মোট লাভ:</strong> <?php echo esc_html($month_total_profit); ?>
</p>
<!-- এ মাসের নোট (নতুন যোগ করা) -->
<h3>এ মাসের নোট</h3>
<p>Placeholder: এ মাসে কে, কি নোট করেছেন, সময় ও তারিখ (টেবিলে দেখাবে)।</p>
<!-- এ মাসের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ -->
<h3>এ মাসের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ</h3>
<ul>
<li>প্রোডাক্ট বিক্রয় (BDT): <?php echo esc_html($month_product_sales); ?>, লাভ: <?php echo esc_html($month_product_profit); ?></li>
<li>সার্ভিসিং বিক্রয় (BDT): <?php echo esc_html($month_service_sales); ?>, লাভ: <?php echo esc_html($month_service_profit); ?></li>
<li>gsmalo.org বিক্রয় (BDT): <?php echo esc_html($month_gsmalo_org_sales); ?>, লাভ: <?php echo esc_html($month_gsmalo_org_profit); ?></li>
<li>gsmalo.com বিক্রয় (BDT): <?php echo esc_html($month_gsmalo_com_sales); ?>, লাভ: <?php echo esc_html($month_gsmalo_com_profit); ?></li>
<li>gsmcourse.com বিক্রয় (BDT): <?php echo esc_html($month_gsmcourse_com_sales); ?>, লাভ: <?php echo esc_html($month_gsmcourse_com_profit); ?></li>
</ul>
<!-- এ মাসের প্রত্যেক সেলার এর বিক্রয় তালিকা -->
<?php
$month_grouped_sales = bsm_group_sales_by_seller($monthly_sales);
?>
<h3>প্রত্যেক সেলার এর এই মাসের বিক্রয় তালিকা</h3>
<?php if (!empty($month_grouped_sales)): ?>
<?php foreach ($month_grouped_sales as $seller_id => $seller_sales_list):
$user_info = get_user_by('id', $seller_id);
$seller_name = $user_info ? $user_info->display_name : 'Seller #'.$seller_id;
?>
<h4>সেলার: <?php echo esc_html($seller_name); ?> (ID: <?php echo esc_html($seller_id); ?>)</h4>
<?php if (!empty($seller_sales_list)): ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>ID</th>
<th>পণ্যের নাম</th>
<th>বিক্রয় ধরন</th>
<th>পেমেন্ট মেথড</th>
<th>ক্রয় মূল্য</th>
<th>বিক্রয় মূল্য</th>
<th>লাভ</th>
<th>লস</th>
<th>স্ট্যাটাস</th>
<th>তারিখ</th>
</tr>
</thead>
<tbody>
<?php foreach ($seller_sales_list as $sale): ?>
<tr>
<td><?php echo esc_html($sale->id); ?></td>
<td><?php echo esc_html($sale->product_name); ?></td>
<td><?php echo esc_html($sale->sale_type); ?></td>
<td><?php echo esc_html($sale->payment_method); ?></td>
<td><?php echo esc_html($sale->purchase_price); ?></td>
<td><?php echo esc_html($sale->selling_price); ?> BDT</td>
<td><?php echo esc_html($sale->profit); ?> BDT</td>
<td><?php echo esc_html($sale->loss); ?> BDT</td>
<td><?php echo esc_html($sale->status); ?></td>
<td><?php echo esc_html($sale->created_at); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>এই সেলারের এই মাসের কোনো বিক্রয় নেই।</p>
<?php endif; ?>
<hr>
<?php endforeach; ?>
<?php else: ?>
<p>এই মাসে কোনো বিক্রয় পাওয়া যায়নি (seller-wise breakdown)।</p>
<?php endif; ?>
<h3>এ মাসের স্ট্যাটাস পরিবর্তন</h3>
<div style="background: #FFF3E0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: left;">
<p>Placeholder: স্ট্যাটাস পরিবর্তনের ডেটা নেই।</p>
</div>
<h3>এ মাসের লগ সারাংশ</h3>
<div style="background: #FFF3E0; padding: 10px; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: left;">
<p>Placeholder: লগের ডেটা নেই।</p>
</div>
<h3>এ মাসের রিপোর্ট: অন্যান্য কাজ</h3>
<p>Placeholder: এ মাসের অন্যান্য কাজের বিস্তারিত রিপোর্ট।</p>
</div>
<!-- ৫) গত মাসের রিপোর্ট -->
<div class="bsm-section bsm-last-month-section" style="background: #E0F2F1;">
<h2>গত মাসের রিপোর্ট (<?php echo date('Y-m', strtotime('-1 month')); ?>)</h2>
<!-- Row 1 -->
<div class="bsm-summary-row row1">
<div class="bsm-box-item">
<strong>বিক্রয় (BDT)</strong>
<span><?php echo isset($last_month_total_sales) ? esc_html($last_month_total_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লাভ (BDT)</strong>
<span><?php echo isset($last_month_total_profit) ? esc_html($last_month_total_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>মোট লস (BDT)</strong>
<span><?php echo isset($last_month_total_loss) ? esc_html($last_month_total_loss) : 0; ?></span>
</div>
</div>
<!-- Row 2 -->
<div class="bsm-summary-row row2">
<div class="bsm-box-item">
<strong>বিক্রয় (সংখ্যা)</strong>
<span><?php echo isset($last_month_sale_count) ? esc_html($last_month_sale_count) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>TNX (সংখ্যা)</strong>
<span><?php echo isset($last_month_tnx_count) ? esc_html($last_month_tnx_count) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>অন্যান্য কাজ (সংখ্যা)</strong>
<span><?php echo isset($last_month_other_count) ? esc_html($last_month_other_count) : 0; ?></span>
</div>
</div>
<!-- Row 3 -->
<div class="bsm-summary-row row3">
<div class="bsm-box-item">
<strong>ওয়েব বিক্রয় (BDT)</strong>
<span><?php echo isset($last_month_web_sales) ? esc_html($last_month_web_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লাভ (BDT)</strong>
<span><?php echo isset($last_month_web_profit) ? esc_html($last_month_web_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>ওয়েব লস (BDT)</strong>
<span><?php echo isset($last_month_web_loss) ? esc_html($last_month_web_loss) : 0; ?></span>
</div>
</div>
<!-- Row 4 -->
<div class="bsm-summary-row row4">
<div class="bsm-box-item">
<strong>সার্ভিসিং বিক্রয় (BDT)</strong>
<span><?php echo isset($last_month_service_sales) ? esc_html($last_month_service_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লাভ (BDT)</strong>
<span><?php echo isset($last_month_service_profit) ? esc_html($last_month_service_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>সার্ভিসিং লস (BDT)</strong>
<span><?php echo isset($last_month_service_loss) ? esc_html($last_month_service_loss) : 0; ?></span>
</div>
</div>
<!-- Row 5 -->
<div class="bsm-summary-row row5">
<div class="bsm-box-item">
<strong>প্রোডাক্ট বিক্রয় (BDT)</strong>
<span><?php echo isset($last_month_product_sales) ? esc_html($last_month_product_sales) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লাভ (BDT)</strong>
<span><?php echo isset($last_month_product_profit) ? esc_html($last_month_product_profit) : 0; ?></span>
</div>
<div class="bsm-box-item">
<strong>প্রোডাক্ট লস (BDT)</strong>
<span><?php echo isset($last_month_product_loss) ? esc_html($last_month_product_loss) : 0; ?></span>
</div>
</div>
<p style="margin-top: 15px;">
<strong>মোট বিক্রয় (BDT):</strong> <?php echo isset($last_month_total_sales) ? esc_html($last_month_total_sales) : 0; ?>,
<strong>মোট লাভ:</strong> <?php echo isset($last_month_total_profit) ? esc_html($last_month_total_profit) : 0; ?>
</p>
<!-- গত মাসের নোট -->
<h3>গত মাসের নোট</h3>
<p>Placeholder: গত মাসে কে, কি নোট করেছেন, সময় ও তারিখ (টেবিলে দেখাবে)।</p>
<!-- গত মাসের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ -->
<h3>গত মাসের বিক্রয়ের ধরনভিত্তিক বিশ্লেষণ</h3>
<ul>
<li>প্রোডাক্ট বিক্রয় (BDT): <?php echo isset($last_month_product_sales) ? esc_html($last_month_product_sales) : 0; ?>, লাভ: <?php echo isset($last_month_product_profit) ? esc_html($last_month_product_profit) : 0; ?></li>
<li>সার্ভিসিং বিক্রয় (BDT): <?php echo isset($last_month_service_sales) ? esc_html($last_month_service_sales) : 0; ?>, লাভ: <?php echo isset($last_month_service_profit) ? esc_html($last_month_service_profit) : 0; ?></li>
<li>gsmalo.org বিক্রয় (BDT): <?php echo isset($last_month_gsmalo_org_sales) ? esc_html($last_month_gsmalo_org_sales) : 0; ?>, লাভ: <?php echo isset($last_month_gsmalo_org_profit) ? esc_html($last_month_gsmalo_org_profit) : 0; ?></li>
<li>gsmalo.com বিক্রয় (BDT): <?php echo isset($last_month_gsmalo_com_sales) ? esc_html($last_month_gsmalo_com_sales) : 0; ?>, লাভ: <?php echo isset($last_month_gsmalo_com_profit) ? esc_html($last_month_gsmalo_com_profit) : 0; ?></li>
<li>gsmcourse.com বিক্রয় (BDT): <?php echo isset($last_month_gsmcourse_com_sales) ? esc_html($last_month_gsmcourse_com_sales) : 0; ?>, লাভ: <?php echo isset($last_month_gsmcourse_com_profit) ? esc_html($last_month_gsmcourse_com_profit) : 0; ?></li>
</ul>
<!-- গত মাসের রিপোর্ট: অন্যান্য কাজ -->
<h3>গত মাসের রিপোর্ট: অন্যান্য কাজ</h3>
<p>Placeholder: গত মাসের অন্যান্য কাজের বিস্তারিত রিপোর্ট।</p>
</div>
<!-- ৬) সর্বশেষ ৫০টি বিক্রয় তালিকা -->
<div class="bsm-section bsm-last-50-sales">
<h2>সর্বশেষ ৫০টি বিক্রয় তালিকা</h2>
<?php if (!empty($last_50_sales)): ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th>ID</th>
<th>Seller ID</th>
<th>পণ্যের নাম</th>
<th>বিক্রয় ধরন</th>
<th>পেমেন্ট মেথড</th>
<th>বিক্রয় মূল্য</th>
<th>লাভ</th>
<th>লস</th>
<th>স্ট্যাটাস</th>
<th>তারিখ</th>
</tr>
</thead>
<tbody>
<?php foreach ($last_50_sales as $sale): ?>
<tr>
<td><?php echo esc_html($sale->id); ?></td>
<td><?php echo esc_html($sale->seller_id); ?></td>
<td><?php echo esc_html($sale->product_name); ?></td>
<td><?php echo esc_html($sale->sale_type); ?></td>
<td><?php echo esc_html($sale->payment_method); ?></td>
<td><?php echo esc_html($sale->selling_price); ?> BDT</td>
<td><?php echo esc_html($sale->profit); ?> BDT</td>
<td><?php echo esc_html($sale->loss); ?> BDT</td>
<td><?php echo esc_html($sale->status); ?></td>
<td><?php echo esc_html($sale->created_at); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>কোনো বিক্রয় পাওয়া যায়নি।</p>
<?php endif; ?>
</div>
</div>
<?php
/**
* File: business-seller-management/templates/seller/all-transaction-history-page.php
*
* Description:
* - Displays the COMPLETE transaction history (ALL sale types + adjustments) for the currently logged-in seller.
* - Triggered by the [bsm_seller_all_transaction_history] shortcode.
* - Shows seller name and current gsmalo.org balance.
* - Includes Search, Warning Summary (for adjustments), Sortable Table, Pagination.
* - Transaction table includes (13 Columns):
* 1. Time, Date & User
* 2. ID
* 3. Order ID
* 4. Sale Type
* 5. Reason or Sale Name
* 6. Warning / Fixed By
* 7. Purchase Price (USD/৳)
* 8. Selling Price (৳)
* 9. Point (Base)
* 10. Profit / Loss (৳)
* 11. Last Note
* 12. Status Log
* 13. Status
* - Status Log / Last Note columns show latest log summary + clickable count icon (color-coded by age).
* - Status column shows status text/icon/color + change info.
* - "New" indicator (Red/Yellow/Gray) shown for recent transactions/fixes (up to 72h).
* - Table headers are sortable (JS). Includes pagination.
* - Added horizontal scrolling for responsiveness. Text/Padding reduced slightly for better fit.
* - Fixed PHP Notice: Undefined property.
* - Added date-based row coloring.
*
* Variables available from the shortcode function:
* - $seller_id : The User ID of the currently logged-in seller.
*/
if ( ! defined('ABSPATH') ) {
exit; // Exit if accessed directly
}
// Ensure helper functions are available (should be included by main plugin file)
if (!function_exists('bsm_calculate_balance_change_for_status_transition') || !function_exists('bsm_parse_status_from_log')) {
echo '<p style="color:red;">Error: Required helper functions for status log processing are missing. Please ensure the main plugin file includes `includes/seller/statas-log-notes.php`.</p>';
error_log("BSM Error: Functions bsm_calculate_balance_change_for_status_transition or bsm_parse_status_from_log not found in all-transaction-history-page.php.");
// return; // Consider stopping execution if critical
}
// Ensure $seller_id is set
if ( ! isset( $seller_id ) || ! is_numeric( $seller_id ) || $seller_id <= 0 ) {
$seller_id = get_current_user_id();
if (!$seller_id) {
echo '<p style="color:red;">Error: Seller ID not found or user not logged in.</p>';
return;
}
}
global $wpdb;
// --- Fetch Seller Data ---
$seller_user = get_userdata($seller_id);
if ( ! $seller_user ) {
echo '<p style="color:red;">Error: Seller data could not be retrieved.</p>';
return;
}
$seller_name = $seller_user->display_name;
$seller_balance = $wpdb->get_var( $wpdb->prepare("SELECT gsmalo_org_balance FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d", $seller_id) );
if ( $seller_balance === null ) {
$seller_balance = 0;
}
$dollarRate = get_option('bsm_usd_value_in_taka', 85);
// --- Search, Date, and Pagination variables (using unique keys) ---
$search_query = isset($_GET['search_all_txn']) ? sanitize_text_field($_GET['search_all_txn']) : '';
$search_date = isset($_GET['search_all_date']) ? sanitize_text_field($_GET['search_all_date']) : '';
// Records per Page settings
$allowed_per_page = array(50, 100, 200, 400, 500, 1000);
$default_per_page = 100;
$per_page = ( isset($_GET['all_txn_per_page']) && in_array( intval($_GET['all_txn_per_page']), $allowed_per_page ) )
? intval($_GET['all_txn_per_page'])
: $default_per_page;
// --- Fetch Transactions ---
$sales_table = $wpdb->prefix . 'bsm_sales';
$sales_query = $wpdb->prepare(
"SELECT id, order_id, product_name, status, purchase_price, selling_price, profit, loss, created_at, seller_id, sale_type
FROM $sales_table
WHERE seller_id = %d",
$seller_id
);
$sales = $wpdb->get_results($sales_query);
$adjustments_table = $wpdb->prefix . 'bsm_balance_adjustments';
$adjustments_query = $wpdb->prepare(
"SELECT id, seller_id, adjusted_amount, reason, created_at, adjusted_by, fixed_by, fixed_at
FROM $adjustments_table
WHERE seller_id = %d",
$seller_id
);
$adjustments = $wpdb->get_results($adjustments_query);
// Combine Sales and Adjustments into transactions array
$transactions = array();
if ( $sales ) {
foreach ( $sales as $sale ) {
$transactions[] = array(
'datetime' => $sale->created_at, 'fixed_at' => null, 'id' => $sale->id,
'order_id' => $sale->order_id, 'reason' => $sale->product_name, 'warning_text' => '',
'fix_note' => '', 'status' => $sale->status, 'amount' => null,
'purchase_price'=> $sale->purchase_price, 'selling_price' => $sale->selling_price,
'profit_bdt' => $sale->profit, 'loss_bdt' => $sale->loss, 'sale_type' => $sale->sale_type,
'type' => 'sale', 'db_reason' => $sale->product_name, 'adjuster_name' => $seller_name,
'is_active_warning' => false
);
}
}
if ( $adjustments ) {
foreach ( $adjustments as $adj ) {
$db_reason = trim($adj->reason); $adjuster_name = 'Admin/System';
if ( $adj->adjusted_by > 0 ) { $adjuster_data = get_userdata($adj->adjusted_by); if ($adjuster_data) { $adjuster_name = $adjuster_data->display_name; } }
$has_warning_tag = (stripos($db_reason, '[warning]') !== false); $is_fixed = !empty($adj->fixed_by); $is_active_warning = $has_warning_tag && !$is_fixed;
$warningText = ""; $fix_note_html = '';
if ($is_active_warning) { $warningText = "[Warning Active]"; }
elseif ($is_fixed) { $fixer_user = get_userdata($adj->fixed_by); $fixer_name = $fixer_user ? $fixer_user->display_name : ('User ID: '.$adj->fixed_by); $fix_time_display = !empty($adj->fixed_at) ? date('h:i A | d-m-Y', strtotime($adj->fixed_at)) : 'N/A'; $fix_note_html = 'Fixed By: '. esc_html($fixer_name) . '<br><small>' . esc_html($fix_time_display) . '</small>'; $warningText = $fix_note_html; }
$display_reason = trim(preg_replace('/\s*\[warning\]/i', '', $db_reason));
$transactions[] = array(
'datetime' => $adj->created_at, 'fixed_at' => $adj->fixed_at, 'id' => $adj->id, 'order_id' => '-',
'reason' => $display_reason, 'warning_text' => $warningText, 'fix_note' => '',
'status' => ( floatval($adj->adjusted_amount) >= 0 ) ? "Addition" : "Revert", 'amount' => null,
'purchase_price'=> null, 'selling_price' => null, 'profit_bdt' => null, 'loss_bdt' => null,
'sale_type' => 'Balance Adjustment', 'type' => 'adjustment', 'db_reason' => $db_reason,
'adjuster_name' => $adjuster_name, 'is_active_warning' => $is_active_warning
);
}
}
// Sort Transactions by primary datetime descending
usort($transactions, function($a, $b) { return strcmp($b['datetime'], $a['datetime']); });
// Apply Text Search Filter
if ( ! empty($search_query) ) {
$transactions = array_filter($transactions, function($txn) use ($search_query) {
$id_match = isset($txn['id']) && stripos((string)$txn['id'], $search_query) !== false;
$order_match = isset($txn['order_id']) && $txn['order_id'] !== '-' && stripos($txn['order_id'], $search_query) !== false;
$reason_match = isset($txn['db_reason']) && stripos($txn['db_reason'], $search_query) !== false;
return ($id_match || $order_match || $reason_match);
});
$transactions = array_values($transactions);
}
// Apply Date Search Filter
if ( ! empty($search_date) ) {
$transactions = array_filter($transactions, function($txn) use ($search_date) { return ( date('Y-m-d', strtotime($txn['datetime'])) === $search_date ); });
$transactions = array_values($transactions);
}
// Calculate Pagination details
$page = isset($_GET['all_txn_paged']) ? max(1, intval($_GET['all_txn_paged'])) : 1;
$total_transactions = count($transactions);
$total_pages = ($total_transactions > 0 && $per_page > 0) ? ceil($total_transactions / $per_page) : 1;
$page = min($page, $total_pages);
$offset = ($page - 1) * $per_page;
$transactions_display = array_slice($transactions, $offset, $per_page);
// Calculate Warning Summary for the seller (based on *active* warnings from adjustments)
$total_warning = 0; $total_negative = 0; $total_positive = 0;
if ( $adjustments ) { foreach ( $adjustments as $adj ) { if (isset($adj->seller_id) && $adj->seller_id == $seller_id && stripos($adj->reason, '[warning]') !== false && empty($adj->fixed_by) ) { $total_warning++; $amt = floatval($adj->adjusted_amount); if ($amt < 0) { $total_negative += $amt; } elseif ($amt > 0) { $total_positive += $amt; } } } }
// Timestamps for indicators
$current_timestamp = current_time('timestamp');
$twenty_four_hours_ago = $current_timestamp - (24 * 3600);
$forty_eight_hours_ago = $current_timestamp - (48 * 3600);
$seventy_two_hours_ago = $current_timestamp - (72 * 3600);
// Status Styles Map
$status_styles = [ /* Same map */
'Addition' => ['color' => '#00008B', 'icon' => '➕'], 'Successful All' => ['color' => '#00008B', 'icon' => '✔'], 'Delivery Done' => ['color' => '#00008B', 'icon' => '✔'], 'Refund Done' => ['color' => '#00008B', 'icon' => '💵'],
'Revert' => ['color' => '#FF6347', 'icon' => '↩️'], 'Refund Required' => ['color' => '#DAA520', 'icon' => '💸'], 'Success But Not Delivery'=> ['color' => '#FF6347', 'icon' => '❎'],
'pending' => ['color' => '#DAA520', 'icon' => '⏳'], 'On Hold' => ['color' => '#DAA520', 'icon' => '⏸️'], 'In Process' => ['color' => '#DAA520', 'icon' => '🔄'],
'Need Parts' => ['color' => '#DAA520', 'icon' => '⚙️'], 'Parts Brought' => ['color' => '#DAA520', 'icon' => '📦'], 'Check Admin' => ['color' => '#DAA520', 'icon' => '👨💼'],
'Review Apply' => ['color' => '#DAA520', 'icon' => '🔍'], 'Block' => ['color' => '#800080', 'icon' => '🔒'],
'Reject Delivery Done' => ['color' => '#000000', 'icon' => '🔴'], 'Cost' => ['color' => '#000000', 'icon' => '💰'], 'Cancel' => ['color' => '#000000', 'icon' => '❌'],
'Rejected' => ['color' => '#000000', 'icon' => '❌'], 'Failed' => ['color' => '#000000', 'icon' => '❌'],
];
// Date-based Row Coloring setup
$day_colors = [ '#E8F8F5', '#FEF9E7', '#F4ECF7', '#FDEDEC', '#EBF5FB', '#FDF2E9' ]; // Mon-Sat
$sunday_color = '#FFF8DC'; // Cornsilk for Sunday
$last_processed_date = '';
$color_index = 0;
// Nonce for fetching logs (used in JS)
$log_fetch_nonce = wp_create_nonce('bsm_note_nonce');
?>
<div class="bsm-seller-all-txn-history-wrap">
<h2>Your Complete Transaction History</h2>
<div class="seller-balance-info">
<strong>Welcome, <?php echo esc_html($seller_name); ?>!</strong> Your current gsmalo.org Balance is:
<strong style="color: #d32f2f; font-size: 1.2em;"><?php echo esc_html(number_format((float)$seller_balance, 2)); ?> USD</strong>
</div>
<div class="search-bar-container">
<div class="search-bar-inner">
<form method="get" action="">
<?php
foreach ($_GET as $key => $value) { if (!in_array($key, ['search_all_txn', 'search_all_date', 'all_txn_per_page', 'all_txn_paged'])) { echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr(stripslashes($value)) . '">'; } }
?>
<input type="hidden" name="all_txn_per_page" value="<?php echo esc_attr($per_page); ?>">
<input type="text" name="search_all_txn" placeholder="Search by ID, Order ID, or Reason" value="<?php echo esc_attr($search_query); ?>" class="search-input">
<input type="date" name="search_all_date" value="<?php echo esc_attr($search_date); ?>" class="date-input">
<input type="submit" value="Search" class="search-button">
</form>
</div>
</div>
<div class="warning-box-container">
<?php if($total_warning > 0): ?><div class="warning-box"> Active Warnings: <span style="color:red;"><?php echo esc_html($total_warning); ?></span> </div><?php endif; ?>
<?php if($total_negative != 0): ?><div class="warning-box negative"> Negative Warnings Sum: <span style="color: red;"><?php echo esc_html(number_format($total_negative, 2)); ?> USD</span> </div><?php endif; ?>
<?php if($total_positive != 0): ?><div class="warning-box positive"> Positive Warnings Sum: <span style="color: red;"><?php echo esc_html(number_format($total_positive, 2)); ?> USD</span> </div><?php endif; ?>
</div>
<div class="table-responsive-wrapper"> <table id="sellerAllTransactionHistoryTable">
<thead>
<tr>
<th onclick="sortTableByColumnAll(0, 'datetime')">Time, Date & User<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(1, 'number')">ID<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(2, 'string')" class="col-order-id">Order ID<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(3, 'string')">Sale Type<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(4, 'string')" class="col-reason">Reason or Sale Name<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(5, 'string')">Warning / Fixed By<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(6, 'number')">P.Price<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(7, 'number')">S.Price<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(8, 'number')">Point<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(9, 'number')">Profit / Loss<span class="sort-indicator"></span></th>
<th>Last Note</th> <th>Status Log</th>
<th onclick="sortTableByColumnAll(12, 'string')">Status<span class="sort-indicator"></span></th>
</tr>
</thead>
<tbody>
<?php
if ( $transactions_display ) {
foreach ( $transactions_display as $tran ) {
// Effective timestamp for "New" indicator
$primary_timestamp = strtotime($tran['datetime']);
$fixed_timestamp = isset($tran['fixed_at']) && $tran['fixed_at'] ? strtotime($tran['fixed_at']) : 0;
$effective_timestamp = max($primary_timestamp, $fixed_timestamp);
$age = $current_timestamp - $effective_timestamp;
$indicator_class = '';
if ($age <= 72 * 3600) {
if ($age <= 24 * 3600) { $indicator_class = 'new-indicator-recent'; }
elseif ($age <= 48 * 3600) { $indicator_class = 'new-indicator-medium'; }
else { $indicator_class = 'new-indicator-old'; }
}
$rowClass = isset($tran['is_active_warning']) && $tran['is_active_warning'] ? 'warning-cell' : '';
// Date Based Row Coloring
$current_date = date('Y-m-d', $primary_timestamp);
$current_day_of_week = date('N', $primary_timestamp);
$row_bg_color = '';
if ($current_date !== $last_processed_date) {
if ($current_day_of_week == 7) { $row_bg_color = $sunday_color; }
else { $row_bg_color = $day_colors[$color_index % count($day_colors)]; $color_index++; }
$last_processed_date = $current_date;
} else {
if ($current_day_of_week == 7) { $row_bg_color = $sunday_color; }
else { $previous_color_index = ($color_index > 0) ? ($color_index - 1) : (count($day_colors) - 1); $row_bg_color = $day_colors[$previous_color_index % count($day_colors)]; }
}
$rowStyle = !empty($row_bg_color) ? 'style="background-color:' . esc_attr($row_bg_color) . ';"' : '';
// Status Styling Logic
$status_text = esc_html($tran['status']);
$status_color = '#000000'; $status_icon = '';
if (isset($status_styles[$tran['status']])) { $style_info = $status_styles[$tran['status']]; $status_color = $style_info['color']; $status_icon = $style_info['icon']; }
$status_display_html = '<span class="status-text" style="color:' . esc_attr($status_color) . ';">' . $status_text . '<span class="status-icon">' . esc_html($status_icon) . '</span></span>';
$status_change_info = '';
if ($tran['type'] === 'sale') {
$last_status_log_entry = $wpdb->get_row($wpdb->prepare( "SELECT n.created_at, u.display_name FROM {$wpdb->prefix}bsm_sale_notes n LEFT JOIN {$wpdb->prefix}users u ON n.user_id = u.ID WHERE n.sale_id = %d AND n.note_type = 'status_change' ORDER BY n.id DESC LIMIT 1", $tran['id'] ));
if ($last_status_log_entry) {
$status_change_user = $last_status_log_entry->display_name ?: 'System';
if (user_can( get_user_by('display_name', $status_change_user), 'manage_options')) { $status_change_user = 'Admin'; } elseif ($status_change_user === $seller_name) { $status_change_user = 'You'; }
$status_change_time = date('h:i A | d-m-Y', strtotime($last_status_log_entry->created_at));
$status_change_info = '<small style="display:block; color:#777; font-size:9px;">' . esc_html($status_change_user) . ' at ' . esc_html($status_change_time) . '</small>';
}
}
// Fetch base points for the sale
$base_points_display = 'N/A';
$base_points_value = 0;
if ($tran['type'] === 'sale') {
$points_val = $wpdb->get_var($wpdb->prepare( "SELECT points FROM {$wpdb->prefix}bsm_points WHERE sale_id = %d AND type = 'base' LIMIT 1", $tran['id'] ));
if ($points_val !== null) { $base_points_value = (float)$points_val; $base_points_display = number_format($base_points_value, 2); }
}
// Prepare Profit/Loss Display
$profit_loss_display = 'N/A';
$profit_loss_value = 0;
if ($tran['type'] === 'sale') {
$profit_val = isset($tran['profit_bdt']) ? floatval($tran['profit_bdt']) : 0; // Fixed Notice
$loss_val = isset($tran['loss_bdt']) ? floatval($tran['loss_bdt']) : 0; // Fixed Notice
if ($profit_val > 0) { $profit_loss_display = '<span style="color:green; font-weight:bold;">' . number_format($profit_val, 2) . ' ৳</span>'; $profit_loss_value = $profit_val; }
elseif ($loss_val > 0) { $profit_loss_display = '<span style="color:red; font-weight:bold;">' . number_format($loss_val, 2) . ' ৳</span>'; $profit_loss_value = -$loss_val; }
else { $profit_loss_display = '0.00 ৳'; $profit_loss_value = 0; }
}
// Prepare Purchase/Selling Price Display
$purchase_price_display = 'N/A';
$selling_price_display = 'N/A';
if ($tran['type'] === 'sale') {
$sale_type_lc = strtolower($tran['sale_type'] ?? '');
$pp = isset($tran['purchase_price']) ? $tran['purchase_price'] : null;
$sp = isset($tran['selling_price']) ? $tran['selling_price'] : null;
if($pp !== null){ $purchase_price_display = ($sale_type_lc === 'gsmalo.org') ? '$ ' . number_format((float)$pp, 2) : number_format((float)$pp, 2) . ' ৳'; }
if($sp !== null){ $selling_price_display = number_format((float)$sp, 2) . ' ৳'; }
}
?>
<tr class="<?php echo $rowClass; ?>" <?php echo $rowStyle; ?>>
<td class="col-datetime" data-sort-value="<?php echo $primary_timestamp; ?>">
<?php if (!empty($indicator_class)): ?><span class="new-indicator <?php echo $indicator_class; ?>">New</span><?php endif; ?>
<span><?php echo esc_html( date('h:i A | d-m-Y', $primary_timestamp ) ); ?></span>
<small>By: <?php echo esc_html( ($tran['adjuster_name'] === $seller_name) ? 'You' : $tran['adjuster_name'] ); ?></small>
</td>
<td data-sort-value="<?php echo esc_attr($tran['id']); ?>"><?php echo esc_html( $tran['id'] ); ?></td>
<td class="col-order-id"><?php echo esc_html( ($tran['order_id'] !== '-' && !empty($tran['order_id'])) ? substr($tran['order_id'], 0, 8) : '-' ); ?></td>
<td><?php echo esc_html( $tran['sale_type'] ?? 'N/A' ); ?></td>
<td class="col-reason"><?php echo esc_html( $tran['reason'] ); ?></td>
<td data-label="Warning / Fixed By">
<?php
if ($tran['type'] == 'adjustment') {
if (isset($tran['is_active_warning']) && $tran['is_active_warning']) { echo '<span style="color:red; font-weight:bold;">[Warning Active]</span>'; }
elseif (!empty($tran['warning_text'])) { echo '<div class="fix-note">'.$tran['warning_text'].'</div>'; }
else { echo '-'; }
} else { echo '-'; }
?>
</td>
<td data-sort-value="<?php echo esc_attr(isset($tran['purchase_price']) ? $tran['purchase_price'] : 0); ?>"><?php echo $purchase_price_display; ?></td>
<td data-sort-value="<?php echo esc_attr(isset($tran['selling_price']) ? $tran['selling_price'] : 0); ?>"><?php echo $selling_price_display; ?></td>
<td data-sort-value="<?php echo esc_attr($base_points_value); ?>"><?php echo $base_points_display; ?></td>
<td data-sort-value="<?php echo esc_attr($profit_loss_value); ?>"> <?php echo $profit_loss_display; ?> </td>
<td class="note-log-cell">
<?php
$note_logs_q = $wpdb->get_results($wpdb->prepare("SELECT n.created_at, n.note_text, u.display_name FROM {$wpdb->prefix}bsm_statas_log_notes n LEFT JOIN {$wpdb->prefix}users u ON n.user_id = u.ID WHERE n.sale_id = %d ORDER BY n.created_at DESC", $tran['id']));
$note_log_count_val = count($note_logs_q);
if ($note_log_count_val > 0) {
$latest_note = $note_logs_q[0];
$latest_note_timestamp = strtotime($latest_note->created_at);
$latest_note_user = $latest_note->display_name ?: 'Unknown';
if (user_can( get_user_by('display_name', $latest_note_user), 'manage_options')) { $latest_note_user = 'Admin'; } elseif ($latest_note_user === $seller_name) { $latest_note_user = 'You'; }
$latest_note_time = date('h:i A | d-m-Y', $latest_note_timestamp);
$note_text_display = esc_html(wp_trim_words($latest_note->note_text, 8, '...'));
$note_icon_color_class = 'log-count-old';
$note_age = $current_timestamp - $latest_note_timestamp;
if ($note_age <= 48 * 3600) { if ($note_age <= 24 * 3600) { $note_icon_color_class = 'log-count-recent'; } else { $note_icon_color_class = 'log-count-medium'; } }
echo '<div class="latest-note-container">';
echo '<span class="latest-note-info-text">';
echo '<strong>' . esc_html($latest_note_user) . ':</strong> ' . $note_text_display;
echo '<small>' . esc_html($latest_note_time) . '</small>';
echo '</span>';
echo '</div>';
if ($note_log_count_val > 0) { // Show icon even if only 1 note, count starts from 0
echo '<a href="#" class="more-note-log-icon ' . $note_icon_color_class . '" data-sale-id="' . esc_attr($tran['id']) . '" data-log-type="note" title="View all ' . $note_log_count_val . ' notes">'. $note_log_count_val .'</a>';
}
} else { echo '-'; }
?>
</td>
<td class="status-log-cell">
<?php
if ($tran['type'] === 'sale') {
$status_logs_q = $wpdb->get_results($wpdb->prepare("SELECT n.created_at, n.note_text, u.display_name FROM {$wpdb->prefix}bsm_sale_notes n LEFT JOIN {$wpdb->prefix}users u ON n.user_id = u.ID WHERE n.sale_id = %d AND n.note_type = 'status_change' ORDER BY n.created_at DESC", $tran['id']));
$log_count_val = count($status_logs_q);
if ($log_count_val > 0) {
$latest_log = $status_logs_q[0];
$latest_log_timestamp = strtotime($latest_log->created_at);
$latest_log_user = $latest_log->display_name ?: 'System';
if (user_can( get_user_by('display_name', $latest_log_user), 'manage_options')) { $latest_log_user = 'Admin'; } elseif ($latest_log_user === $seller_name) { $latest_log_user = 'You'; }
$latest_log_time = date('h:i A | d-m-Y', $latest_log_timestamp);
$balance_change_text_all = '';
if(isset($tran['sale_type']) && strtolower($tran['sale_type']) === 'gsmalo.org') {
list($old_status_latest_all, $new_status_latest_all) = function_exists('bsm_parse_status_from_log') ? bsm_parse_status_from_log($latest_log->note_text) : [null, null];
if ($old_status_latest_all !== null && $new_status_latest_all !== null) {
$balance_change_amount_all = function_exists('bsm_calculate_balance_change_for_status_transition') ? bsm_calculate_balance_change_for_status_transition($tran['id'], $old_status_latest_all, $new_status_latest_all) : 0;
if ($balance_change_amount_all > 0) { $balance_change_text_all = ' <span class="balance-change-indicator" style="color:blue;">(Refund +' . number_format($balance_change_amount_all, 2) . '$)</span>'; }
elseif ($balance_change_amount_all < 0) { $balance_change_text_all = ' <span class="balance-change-indicator" style="color:red;">(Cut ' . number_format(abs($balance_change_amount_all), 2) . '$)</span>'; }
}
}
$icon_color_class_all = 'log-count-old';
$log_age = $current_timestamp - $latest_log_timestamp;
if ($log_age <= 48 * 3600) { if ($log_age <= 24 * 3600) { $icon_color_class_all = 'log-count-recent'; } else { $icon_color_class_all = 'log-count-medium'; } }
echo '<div class="latest-log-container">';
echo '<span class="latest-log-info-text">';
echo esc_html($latest_log_user);
echo '<small>' . esc_html($latest_log_time) . '</small>';
echo '</span>';
echo $balance_change_text_all;
echo '</div>';
if ($log_count_val > 0) { // Show icon even if only 1 log
echo '<a href="#" class="more-status-log-icon ' . $icon_color_class_all . '" data-sale-id="' . esc_attr($tran['id']) . '" data-log-type="status" title="View all ' . $log_count_val . ' status logs">' . $log_count_val .'</a>';
}
} else { echo '-'; }
} else { echo '-'; }
?>
</td>
<td> <?php echo $status_display_html; ?>
<?php echo $status_change_info; ?>
</td>
</tr>
<?php
}
} else {
echo '<tr><td colspan="13">No transactions found matching your criteria.</td></tr>'; // Colspan updated to 13
}
?>
</tbody>
</table>
</div><?php
// Pagination Links
if ( $total_pages > 1 ) {
echo '<div class="pagination">';
$query_params = $_GET; unset($query_params['all_txn_paged']); $base_url = add_query_arg($query_params, get_permalink());
for ( $i = 1; $i <= $total_pages; $i++ ) {
$url = add_query_arg('all_txn_paged', $i, $base_url);
if ( $i == $page ) { echo '<span class="current-page">' . $i . '</span>'; } else { echo '<a href="' . esc_url($url) . '">' . $i . '</a>'; }
}
echo '</div>';
}
?>
<div class="per-page-form">
<form method="get" action="">
<?php foreach ($_GET as $key => $value) { if (!in_array($key, ['all_txn_per_page', 'all_txn_paged'])) { echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr(stripslashes($value)) . '">'; } } ?>
<label for="all_txn_per_page">Records per page: </label>
<select name="all_txn_per_page" id="all_txn_per_page" onchange="this.form.submit();">
<option value="50" <?php selected($per_page, 50); ?>>50</option>
<option value="100" <?php selected($per_page, 100); ?>>100</option>
<option value="200" <?php selected($per_page, 200); ?>>200</option>
<option value="400" <?php selected($per_page, 400); ?>>400</option>
<option value="500" <?php selected($per_page, 500); ?>>500</option>
<option value="1000" <?php selected($per_page, 1000); ?>>1000</option>
</select>
</form>
</div>
</div> <div id="all-txn-status-log-modal" class="log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('all-txn-status-log-modal').style.display='none';">×</span>
<h2>Status Change History</h2>
<div id="all-txn-status-log-modal-content" class="log-modal-content-area"><p>Loading logs...</p></div>
</div>
</div>
<div id="all-txn-note-log-modal" class="log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('all-txn-note-log-modal').style.display='none';">×</span>
<h2>Note History</h2>
<div id="all-txn-note-log-modal-content" class="log-modal-content-area"><p>Loading notes...</p></div>
</div>
</div>
<style>
/* CSS Styles */
.bsm-seller-all-txn-history-wrap { padding: 15px; background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 4px; margin: 10px 0; font-family: sans-serif; }
.bsm-seller-all-txn-history-wrap h2 { margin-top: 0; margin-bottom: 15px; color: #333; font-size: 1.5em; }
.seller-balance-info { margin-bottom: 20px; padding: 10px; background: #eef; border: 1px solid #dde; border-radius: 4px; font-size: 1.1em; }
.search-bar-container { background-color: #eaf2f8; padding: 12px 15px; margin-bottom: 20px; border: 1px solid #c5d9e8; border-radius: 6px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.04); }
.search-bar-inner { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; }
.search-bar-inner form { display: contents; }
.search-bar-inner .search-input { padding: 6px 10px; border: 1px solid #a9c4d0; border-radius: 4px; font-size: 14px; flex: 1 1 250px; max-width: 300px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.search-bar-inner .date-input { padding: 6px 8px; border: 1px solid #a9c4d0; border-radius: 4px; font-size: 14px; flex: 0 1 140px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.search-bar-inner .search-button { padding: 7px 14px; border: none; background: #2980b9; color: #fff; border-radius: 4px; cursor: pointer; font-size: 14px; flex-shrink: 0; transition: background-color 0.2s ease; }
.search-bar-inner .search-button:hover { background: #1f648b; }
.warning-box-container { margin-bottom: 15px; }
.warning-box { display: inline-block; margin-right: 10px; padding: 10px 15px; border: 2px solid #d00; background: #ffcccc; border-radius: 5px; font-size: 14px; font-weight: bold; }
.warning-box span { color: red; }
.warning-box.positive { border-color: #0073aa; background: #e0f7fa; }
.warning-box.negative { border-color: #0073aa; background: #e0f7fa; }
/* Responsive Table Wrapper */
.table-responsive-wrapper { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
table#sellerAllTransactionHistoryTable { width: 100%; min-width: 1100px; /* Reduced min-width */ border-collapse: collapse; table-layout: auto; background: #fff; }
table#sellerAllTransactionHistoryTable td[colspan="13"] { text-align: center; padding: 15px; font-style: italic; color: #666; } /* Colspan updated */
table#sellerAllTransactionHistoryTable th, table#sellerAllTransactionHistoryTable td {
border: 1px solid #ccc; padding: 2px 3px; /* Further reduced padding */ text-align: center; vertical-align: middle;
position: relative; font-size: 10px; /* Further reduced font size */ word-wrap: break-word; overflow-wrap: break-word;
white-space: normal; /* Allow wrapping by default */
}
table#sellerAllTransactionHistoryTable th,
table#sellerAllTransactionHistoryTable td.col-order-id, /* Keep short IDs nowrap */
table#sellerAllTransactionHistoryTable td:nth-child(2) /* Keep ID column nowrap */
{
white-space: nowrap;
}
table#sellerAllTransactionHistoryTable th { background: #f1f1f1; font-weight: bold; cursor: pointer; }
table#sellerAllTransactionHistoryTable td.col-reason { width: 20%; text-align: left; } /* Reduced width */
table#sellerAllTransactionHistoryTable td.status-log-cell,
table#sellerAllTransactionHistoryTable td.note-log-cell { max-width: 100px; min-width: 70px;} /* Adjusted width constraints */
.col-order-id { width: 55px; }
.col-datetime span { display: block; font-size: 9px; color: #555;} /* Smaller */
.col-datetime small { display: block; font-size: 8px; color: #0073aa; font-weight: bold; } /* Smaller */
table#sellerAllTransactionHistoryTable tbody tr:hover { background-color: #f1f1f1 !important; }
.warning-cell { background: #ffcccc !important; color: #a00; font-weight: bold; }
.warning-cell:hover { background: #ffbbbb !important; }
.fix-note { font-size: 10px; color: #555; line-height:1.2; text-align: center;}
.fix-note small { display: block; font-size:9px; color:#777; }
.pagination { text-align: center; margin-top: 20px; }
.pagination a, .pagination span { display: inline-block; padding: 6px 12px; margin: 0 2px; border: 1px solid #ccc; border-radius: 4px; text-decoration: none; color: #0073aa; }
.pagination span.current-page { background: #0073aa; color: #fff; }
.per-page-form { margin-top: 10px; text-align: center; }
.per-page-form label { margin-right: 5px; }
.per-page-form select { padding: 5px; }
/* New Indicator Styles */
.new-indicator { position: absolute; top: 1px; left: 1px; color: white; font-size: 8px; font-weight: bold; padding: 1px 3px; border-radius: 3px; line-height: 1; z-index: 1; }
.new-indicator-recent { background-color: red; }
.new-indicator-medium { background-color: #DAA520; }
.new-indicator-old { background-color: #808080; }
td.col-datetime { padding-top: 15px !important; } /* Keep padding for indicator */
.status-text { font-weight: bold; }
.status-icon { margin-left: 3px; margin-right: 3px; }
th .sort-indicator { font-size: 9px; margin-left: 4px; }
.amount-plus { color: blue; font-weight: bold; margin-right: 2px;}
.amount-minus { color: red; font-weight: bold; margin-right: 2px;}
/* Status Log & Note Log Column Styles */
td.status-log-cell, td.note-log-cell {
font-size: 9px; /* Smaller log font */ line-height: 1.2; text-align: left; position: relative;
padding-right: 22px; vertical-align: top; white-space: normal;
max-width: 100px; /* Adjusted width */ min-width: 70px;
}
.latest-log-container, .latest-note-container { margin-bottom: 2px; }
.latest-log-info-text, .latest-note-info-text { display: inline-block; padding: 0; white-space: normal; }
.latest-log-info-text small, .latest-note-info-text small { display: block; font-size: 8px; color: #555; }
.balance-change-indicator { font-size: 9px; font-weight: bold; margin-left: 4px; white-space: nowrap; }
.more-status-log-icon, .more-note-log-icon {
position: absolute; top: 2px; right: 2px; display: inline-block; color: white;
font-size: 8px; font-weight: bold; width: 14px; height: 14px; line-height: 14px;
text-align: center; border-radius: 50%; cursor: pointer; text-decoration: none;
box-shadow: 0 1px 1px rgba(0,0,0,0.2); z-index: 2;
}
.more-status-log-icon.log-count-recent, .more-note-log-icon.log-count-recent { background-color: red; }
.more-status-log-icon.log-count-medium, .more-note-log-icon.log-count-medium { background-color: #DAA520; }
.more-status-log-icon.log-count-old, .more-note-log-icon.log-count-old { background-color: #808080; }
.more-status-log-icon:hover, .more-note-log-icon:hover { filter: brightness(85%); }
/* Log Modal Styles */
.log-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 10001; }
.log-modal .modal-content { background: #fff; width: 700px; max-width: 90%; margin: 80px auto; padding: 20px; border-radius: 5px; position: relative; max-height: 80vh; overflow-y: auto; }
.log-modal .modal-content h2 { margin-top: 0; color: #0073aa; font-size: 1.3em; }
.log-modal .close { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 24px; color: #888; font-weight: bold; }
.log-modal-content-area .log-entry { border-bottom: 1px dashed #eee; padding: 8px 0; font-size: 13px; }
.log-modal-content-area .log-entry:last-child { border-bottom: none; }
.log-modal-content-area .log-entry strong { color: #333; }
.log-modal-content-area .log-entry em { color: #777; font-size: 0.9em; }
</style>
<script>
// Pass PHP variables needed for AJAX to JavaScript
var bsm_ajax_obj = {
ajaxurl: '<?php echo admin_url( 'admin-ajax.php' ); ?>',
fetch_nonce: '<?php echo esc_js($log_fetch_nonce); ?>' // Use the nonce generated in PHP
};
// Client-side Table Sorting Function for Seller All Transaction Table
const getCellValueAll = (tr, idx, type) => {
const td = tr.children[idx]; if (!td) return null;
const sortValue = td.getAttribute('data-sort-value');
// Prefer data-sort-value if available
if (sortValue !== null) {
if (type === 'number' || type === 'datetime') { return parseFloat(sortValue) || 0; } // Use parseFloat for numeric/date sorting based on timestamp
return sortValue;
}
// Fallback to innerText if data-sort-value is not present
const val = (td.innerText || td.textContent).trim();
if (type === 'number') { let numStr = String(val).replace(/[^0-9.-]+/g,"").replace(/,/g, ''); if (String(val).trim().startsWith('-')) { numStr = '-' + numStr.replace('-', ''); } return parseFloat(numStr) || 0; } // Handle currency/negatives
if (type === 'datetime') { // Extract timestamp from the first span's datetime attribute if possible
const timeSpan = td.querySelector('span[data-timestamp]');
if(timeSpan) return parseFloat(timeSpan.getAttribute('data-timestamp')) || 0;
return new Date(val).getTime() || 0; // Fallback basic date parsing
}
return val;
};
const comparerAll = (idx, asc, type) => (a, b) => {
const v1 = getCellValueAll(asc ? a : b, idx, type);
const v2 = getCellValueAll(asc ? b : a, idx, type);
if (type === 'number' || type === 'datetime') { return v1 - v2; }
else { return v1.toString().localeCompare(v2.toString()); }
};
function sortTableByColumnAll(columnIndex, type = 'string') {
// Column Indices: 0=Time, 1=ID, 2=Order, 3=Type, 4=Reason, 5=Warning, 6=P.Price, 7=S.Price, 8=Point, 9=Profit/Loss, 10=NoteLog, 11=StatusLog, 12=Status
if (columnIndex === 10 || columnIndex === 11) return; // Don't sort log columns
const table = document.getElementById('sellerAllTransactionHistoryTable'); if (!table) return;
const tbody = table.querySelector('tbody'); if (!tbody) return;
const th = table.querySelectorAll('th')[columnIndex]; if (!th) return;
const currentIsAscending = th.classList.contains('sort-asc'); const direction = currentIsAscending ? false : true;
table.querySelectorAll('th .sort-indicator').forEach(ind => ind.remove()); // Remove all indicators first
table.querySelectorAll('th').forEach(h => h.classList.remove('sort-asc', 'sort-desc')); // Clear classes
th.classList.toggle('sort-asc', direction); th.classList.toggle('sort-desc', !direction);
const indicatorSpan = document.createElement('span'); indicatorSpan.className = 'sort-indicator'; indicatorSpan.innerHTML = direction ? ' ▲' : ' ▼'; th.appendChild(indicatorSpan);
Array.from(tbody.querySelectorAll('tr')).sort(comparerAll(columnIndex, direction, type)).forEach(tr => tbody.appendChild(tr) );
}
// Bind log link clicks on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
rebindAllTxnLogLinks('.more-status-log-icon'); // Bind status log icons
rebindAllTxnLogLinks('.more-note-log-icon'); // Bind note log icons
});
// Function to bind click events to log links (status or note)
function rebindAllTxnLogLinks(selector) {
document.querySelectorAll('#sellerAllTransactionHistoryTable ' + selector).forEach(function(link) {
// Ensure event listeners are not duplicated
link.removeEventListener('click', handleAllTxnLogClick);
link.addEventListener('click', handleAllTxnLogClick);
});
}
// Handler function for log link clicks specific to this page
function handleAllTxnLogClick(e) {
e.preventDefault();
var saleId = this.getAttribute('data-sale-id');
var logType = this.getAttribute('data-log-type'); // 'status' or 'note'
var modalId = (logType === 'note') ? 'all-txn-note-log-modal' : 'all-txn-status-log-modal';
var modalContentId = (logType === 'note') ? 'all-txn-note-log-modal-content' : 'all-txn-status-log-modal-content';
var modalContentEl = document.getElementById(modalContentId);
var modalEl = document.getElementById(modalId);
if (!modalContentEl || !modalEl) { console.error("All Txn log modal elements not found for type: " + logType); return; }
modalContentEl.innerHTML = '<p>Loading logs...</p>'; // Show loading message
modalEl.style.display = 'block'; // Display the modal
var fd = new FormData();
fd.append("action", "bsm_fetch_all_logs");
fd.append("sale_id", saleId);
fd.append("log_type", logType);
fd.append("nonce", bsm_ajax_obj.fetch_nonce); // Use localized nonce
// Use localized ajaxurl
fetch(bsm_ajax_obj.ajaxurl, { method: "POST", body: fd, credentials: 'same-origin' })
.then(response => { if (!response.ok) { return response.text().then(text => { throw new Error(text || response.status); }); } return response.json(); })
.then(data => { if (data.success && data.data.html) { modalContentEl.innerHTML = data.data.html; } else { modalContentEl.innerHTML = '<p>Error loading logs: ' + (data.data ? data.data.message || data.data : 'Unknown error') + '</p>'; } })
.catch(error => { console.error('Error fetching all txn logs:', error); modalContentEl.innerHTML = '<p>AJAX Error loading logs: ' + error.message + '</p>'; });
}
// Close Log Modals (Generic)
document.querySelectorAll('.log-modal .close').forEach(function(el) {
el.addEventListener('click', function() {
this.closest('.log-modal').style.display = 'none';
});
});
window.addEventListener('click', function(event) {
if (event.target.classList.contains('log-modal')) {
event.target.style.display = 'none';
}
});
</script>
<?php
/**
* File: wp-content/plugins/business-seller-management/templates/seller/sale-page.php
*
* Description:
* - এই ফাইলটি Seller Dashboard Sale Page এর জন্য, Create New Sale / Work ফর্ম এবং
* Existing Sales/Work Entries টেবিল প্রদর্শনের জন্য ব্যবহৃত হয়।
* - আমরা "Today" ও "Next L" বক্সের গননা (calculateSummary ফাংশন) Point Report পেজের মতো
* সঠিকভাবে একীভূত করেছি, যাতে আজকের মোট (bonus ছাড়াও) পয়েন্ট ও লেভেল ঠিকভাবে দেখায়।
* - "Extra Details" সেকশনে "Point" বক্সে শুধুমাত্র নতুন এন্ট্রি থেকে প্রাপ্ত (base) পয়েন্ট দেখানো হবে।
* - অন্যান্য সকল কোড আগের মতোই অপরিবর্তিত রাখা হয়েছে।
*/
if (!defined('ABSPATH')) {
echo '<p style="color:red; font-weight:bold;">You must be logged in to view this page.</p>';
return;
}
if (!is_user_logged_in()) {
echo '<p style="color:red; font-weight:bold;">Please log in to view this page.</p>';
return;
}
global $wpdb;
$table_sales = $wpdb->prefix . 'bsm_sales';
$table_sale_notes = $wpdb->prefix . 'bsm_sale_notes';
// -------------------------------------------------------------------------------------
// (A) সহায়ক ফাংশন (যদি আগেই না থাকে): bsm_get_points_sum_range()
// আজকের ডেইলি পয়েন্ট নিতে ব্যবহার করা হবে (Point Report পেজের মতো)
// -------------------------------------------------------------------------------------
if (!function_exists('bsm_get_points_sum_range')) {
function bsm_get_points_sum_range($user_id, $startDate = null, $endDate = null, $search_query = '', $pointType = '') {
global $wpdb;
$table_points = $wpdb->prefix . 'bsm_points';
$where = $wpdb->prepare("WHERE user_id = %d", $user_id);
if ($startDate) {
$where .= $wpdb->prepare(" AND DATE(created_at) >= %s", $startDate);
}
if ($endDate) {
$where .= $wpdb->prepare(" AND DATE(created_at) <= %s", $endDate);
}
if (!empty($pointType)) {
$where .= $wpdb->prepare(" AND type = %s", $pointType);
}
if (!empty($search_query)) {
$like = '%' . $wpdb->esc_like($search_query) . '%';
$where .= $wpdb->prepare(" AND (description LIKE %s) ", $like);
}
$sql = "SELECT IFNULL(SUM(points), 0) FROM $table_points $where";
return (float) $wpdb->get_var($sql);
}
}
// -------------------------------------------------------------------------------------
// (B) আজকের ডেইলি পয়েন্ট ও ডেইলি লেভেল (Point Report পেজের মতো)
// -------------------------------------------------------------------------------------
$current_user_id = get_current_user_id();
$todayDate = date('Y-m-d');
// আজকের ডেইলি পয়েন্ট (bonus ছাড়া)
$today_balance = bsm_get_points_sum_range($current_user_id, $todayDate, $todayDate, '', 'daily');
// ডেইলি লেভেলসমূহ (bonus পদ্ধতি) আনছি - "Existing Sales/Work Entries" এ দেখানোর জন্য
$daily_levels_bonus = get_option('bsm_daily_levels_bonus', array(
1 => array('name'=>'Newbie Seller', 'range'=>199, 'bonus'=>0, 'enabled'=>1),
2 => array('name'=>'Active Seller', 'range'=>200, 'bonus'=>30, 'enabled'=>1),
3 => array('name'=>'Active Seller', 'range'=>300, 'bonus'=>50, 'enabled'=>1),
));
// "Today Point Info" বক্সে, শুধুমাত্র সার্ভারের বর্তমান (bonus ছাড়া) ডেইলি পয়েন্ট ব্যবহার করা হবে
$today_level_num = 1;
$today_level_name = 'Default';
$sortedKeys = array_keys($daily_levels_bonus);
sort($sortedKeys);
foreach ($sortedKeys as $k) {
$lvl = $daily_levels_bonus[$k];
if (intval($lvl['enabled']) === 1) {
$rangeVal = floatval($lvl['range']);
if ($today_balance >= $rangeVal) {
$today_level_num = $k;
$today_level_name = $lvl['name'];
} else {
break;
}
}
}
$today_point_box_text = sprintf('Today: L%d: %s %sP.', $today_level_num, $today_level_name, number_format($today_balance, 0));
// -------------------------------------------------------------------------------------
// (C) Filter, Search & Pagination Setup (পুরনো কোড অপরিবর্তিত)
// -------------------------------------------------------------------------------------
$search = isset($_GET['sale_search']) ? sanitize_text_field($_GET['sale_search']) : '';
$time_filter = isset($_GET['time_filter']) ? sanitize_text_field($_GET['time_filter']) : 'alltime';
$where_clauses = ["seller_id = " . intval($current_user_id)];
if ($time_filter === 'day') {
$today = date_i18n('Y-m-d');
$where_clauses[] = "DATE(created_at) = '$today'";
} elseif ($time_filter === 'week') {
$start_week = date('Y-m-d', strtotime('monday this week'));
$end_week = date('Y-m-d', strtotime('sunday this week'));
$where_clauses[] = "DATE(created_at) BETWEEN '$start_week' AND '$end_week'";
} elseif ($time_filter === 'month') {
$current_month = date_i18n('Y-m');
$where_clauses[] = "DATE_FORMAT(created_at, '%Y-%m') = '$current_month'";
} elseif ($time_filter === 'year') {
$current_year = date_i18n('Y');
$where_clauses[] = "YEAR(created_at) = '$current_year'";
}
if (!empty($search)) {
$search_esc = esc_sql($search);
$where_clauses[] = "(transaction_id LIKE '%$search_esc%'
OR product_name LIKE '%$search_esc%'
OR customer_payment LIKE '%$search_esc%'
OR note LIKE '%$search_esc%')";
}
$where_sql = implode(' AND ', $where_clauses);
$paged = max(1, get_query_var('paged', 1));
$per_page = isset($_GET['per_page']) ? intval($_GET['per_page']) : 50;
$offset = ($paged - 1) * $per_page;
$total_sales = $wpdb->get_var("SELECT COUNT(*) FROM $table_sales WHERE $where_sql");
$all_sales = $wpdb->get_results("
SELECT *
FROM $table_sales
WHERE $where_sql
ORDER BY id DESC
LIMIT $offset, $per_page
");
// Example gsmalo.org usage
$current_balance = 100;
$dollarRate = get_option('bsm_usd_value_in_taka', 85);
// Retrieve daily point levels (% method) from Admin Settings
$daily_levels = get_option('bsm_daily_levels_default', [
1 => ['name' => 'Newbie Seller','range_from' => 0, 'range_to' => 99, 'rate' => 10,'type'=>'%','enabled'=>1],
2 => ['name' => 'Starter Seller','range_from' => 100,'range_to'=>199, 'rate' => 15,'type'=>'%','enabled'=>1],
]);
// Retrieve daily point levels (bonus method)
$daily_levels_bonus_config = get_option('bsm_daily_levels_bonus', [
1 => ['name' => 'Newbie Seller','range'=>199,'bonus'=>0,'enabled'=>1],
2 => ['name' => 'Active Seller','range'=>200,'bonus'=>30,'enabled'=>1],
3 => ['name' => 'Active Seller','range'=>300,'bonus'=>50,'enabled'=>1],
]);
$daily_bonus_overall_rate = get_option('bsm_daily_bonus_overall_rate', 10);
$active_daily_method = get_option('bsm_active_daily_method', 'default');
$status_colors = [
"Rejected" => "#8B0000",
"Refund Done" => "#800080",
"Successful All" => "#006400",
"Check Admin" => "#808080",
"Refund Required" => "#FF00FF",
"Reject Delivery Done" => "#A52A2A",
"Cost" => "#333333",
"Cancel" => "#8B0000",
"Block" => "#000000",
"Success But Not Delivery"=> "#008000",
"Parts Brought" => "#008080",
"On Hold" => "#FFA500",
"In Process" => "#1E90FF",
"Failed" => "#FF0000",
"Need Parts" => "#FF69B4",
"pending" => "#cccccc",
"Review Apply" => "#666666"
];
function bsm_get_logs_for_sale($sale_id) {
global $wpdb;
$notes_table = $wpdb->prefix . 'bsm_sale_notes';
return $wpdb->get_results(
$wpdb->prepare("
SELECT n.*, COALESCE(u.display_name, '') AS user_name
FROM $notes_table AS n
LEFT JOIN {$wpdb->prefix}users AS u ON n.user_id = u.ID
WHERE n.sale_id = %d
ORDER BY n.created_at DESC
", $sale_id)
);
}
if (!function_exists('bsm_get_statas_logs')) {
function bsm_get_statas_logs($sale_id) {
$all = bsm_get_logs_for_sale($sale_id);
return array_filter($all, function($item){ return $item->note_type !== 'status_change'; });
}
}
function bsm_get_allowed_statuses_for_seller_front($sale) {
$current_user = wp_get_current_user();
$roles = (array)$current_user->roles;
$is_admin = in_array('administrator', $roles);
$is_editor = in_array('editor', $roles);
$has_bsm_manage_all = user_can($current_user, 'bsm_manage_all_statuses');
if ($is_admin || ($is_editor && $has_bsm_manage_all)) {
return [
"pending","On Hold","In Process","Need Parts",
"Success But Not Delivery","Parts Brought","Refund Required",
"Refund Done","Successful All","Check Admin","Reject Delivery Done","Cost",
"Cancel","Block","Rejected","Failed","Review Apply"
];
}
$current_status = $sale->status;
$sale_type = $sale->sale_type;
$possible_other_types = ['video','marketing','office_management','other_option','other'];
$is_other_sale_type = in_array(strtolower($sale_type), $possible_other_types);
$all_statuses = [
"pending","On Hold","In Process","Need Parts",
"Success But Not Delivery","Parts Brought","Refund Required",
"Refund Done","Successful All","Check Admin","Reject Delivery Done","Cost",
"Cancel","Block","Rejected","Failed","Review Apply"
];
$map_step2 = [
"Success But Not Delivery" => ["Successful All"],
"Parts Brought" => ["Successful All"],
"Refund Required" => ["Cancel","Refund Done"]
];
$step3_locked = [
"Refund Done","Successful All","Check Admin","Reject Delivery Done","Cost","Cancel",
"Block","Rejected","Failed","Review Apply"
];
$allowed_from_need_parts = [
"Cancel","Parts Brought","Success But Not Delivery",
"Successful All","Rejected","In Process","Failed"
];
$transition_map = [
"pending" => array_diff($all_statuses, ["pending"]),
"On Hold" => array_diff($all_statuses, ["On Hold"]),
"In Process"=> array_diff($all_statuses, ["In Process"]),
"Need Parts"=> $allowed_from_need_parts,
"Success But Not Delivery" => $map_step2["Success But Not Delivery"],
"Parts Brought" => $map_step2["Parts Brought"],
"Refund Required" => $map_step2["Refund Required"],
];
foreach ($step3_locked as $st3) {
$transition_map[$st3] = [];
}
$allowed_next = isset($transition_map[$current_status]) ? $transition_map[$current_status] : [];
if (empty($allowed_next)) {
return [];
}
if ($is_other_sale_type) {
$allowed_for_other_seller = ["pending","On Hold","In Process","Review Apply"];
$allowed_next = array_intersect($allowed_next, $allowed_for_other_seller);
}
return array_values(array_unique($allowed_next));
}
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title>Sales / Work Listings</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<script type="text/javascript">
var ajaxurl = "<?php echo admin_url('admin-ajax.php'); ?>";
// আজকের ডেইলি ব্যালেন্স (server side থেকে):
var serverTodayBalance = <?php echo json_encode($today_balance); ?>;
// Pass daily levels from Admin Settings
var dailyLevels = <?php echo json_encode($daily_levels); ?>;
var dailyLevelsBonus = <?php echo json_encode($daily_levels_bonus_config); ?>;
var dailyBonusOverallRate = <?php echo floatval($daily_bonus_overall_rate); ?>;
var activeDailyMethod = "<?php echo esc_js($active_daily_method); ?>";
var dailyPointRates = <?php echo json_encode(get_option('bsm_daily_point_rates', array())); ?>;
// Pass Video Group Points from Admin Settings
var videoGroupPoints = <?php echo json_encode(get_option('bsm_video_group_points', array())); ?>;
// Pass Other Group Points (Marketing, Office Management)
var otherGroupPoints = <?php echo json_encode(get_option('bsm_other_group_points', array())); ?>;
</script>
<style>
/* ---------- General Page Styles ---------- */
.seller-sale-page-wrap {
padding: 20px;
background: #fdfdfd;
width: 100%;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
.sale-section {
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 30px;
padding: 20px;
background: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.15);
}
/* পৃথক পৃথক সেকশনের জন্য আলাদা ব্যাকগ্রাউন্ড কালার */
.sale-section.daily { background: #f9f9ff; }
.sale-section.monthly { background: #f9fff9; }
.sale-section.yearly { background: #fff9f9; }
.sale-section.video-group { background: #fffff0; }
.sale-section.other-group { background: #f0ffff; }
/* নতুন: বিক্রয় এন্ট্রির উপর হোভার করলে আলাদা রং দেখাবে */
.sale-entry:hover {
background: #ffffcc !important;
}
.sale-section h2,
.sale-section h3 {
margin-top: 0;
margin-bottom: 20px;
font-weight: 600;
color: #333;
}
.notice {
margin-bottom: 15px;
padding: 10px;
border-radius: 3px;
}
.notice-error {
background: #ffdddd;
border: 2px solid #ffaaaa;
color: #a00;
}
.notice-success {
background: #ddffdd;
border: 2px solid #aaffaa;
color: #070;
}
/* ---------- Create New Sale / Work Section ---------- */
.create-sale-wrap {
margin-top: 20px;
border: 1px solid #ddd;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
.create-sale-wrap .form-table th {
font-size: 18px;
font-weight: bold;
padding: 16px;
width: 280px;
height: 65px;
background: #e0f7fa;
box-shadow: 3px 3px 5px rgba(0,150,136,0.3);
border: 2px solid #00bcd4;
}
.create-sale-wrap .form-table tr:nth-child(even) th {
background: #f1f8e9;
box-shadow: 3px 3px 5px rgba(76,175,80,0.3);
border: 2px solid #8bc34a;
}
.create-sale-wrap .form-table td input,
.create-sale-wrap .form-table td select {
border: 2px solid #00bcd4;
padding: 8px;
border-radius: 3px;
}
.form-table td {
padding: 8px;
}
.form-action-row {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
}
.action-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
/* ---------- Info Boxes Styles ---------- */
.info-box {
border: 2px solid #ccc;
border-radius: 5px;
background: #fff;
padding: 10px;
text-align: center;
min-width: 100px;
margin-right: 10px;
margin-bottom: 5px;
}
.info-box h4 {
margin: 0 0 10px 0;
font-size: 16px;
}
.info-box p {
margin: 0;
font-size: 14px;
}
/* ---------- Table Styles ---------- */
.wp-list-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
table-layout: auto;
background: #fef4d1;
}
.wp-list-table thead th {
background: #f2dc99;
font-size: 13px;
color: #333;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid #ccc;
padding: 10px;
font-weight: bold;
text-align: center;
}
.wp-list-table th,
.wp-list-table td {
padding: 10px;
border: 1px solid #ddd;
text-align: center;
vertical-align: middle;
word-wrap: break-word;
white-space: normal;
font-weight: normal;
}
.wp-list-table td:first-child {
text-align: left;
}
/* Custom Edit Button Styles */
.video-edit-btn, .other-edit-btn, .service-edit-btn {
background-color: #FF5722;
color: #fff;
padding: 6px 10px;
font-size: 13px;
border: none;
border-radius: 4px;
cursor: pointer;
margin: 2px;
}
.video-edit-btn:hover, .other-edit-btn:hover, .service-edit-btn:hover {
background-color: #E64A19;
}
/* Status column: larger background */
.status-box {
display: inline-block;
padding: 12px 8px;
border-radius: 4px;
color: #fff;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
/* New Duration column styling */
.duration-col {
font-size: 13px;
}
/* ---------- Extra Details Section ---------- */
.extra-details-container {
display: flex;
gap: 10px;
border-top: 1px solid #ddd;
padding-top: 10px;
margin-top: 10px;
font-size: 13px;
}
.col2 {
width: 250px;
border: 1px solid #000;
border-radius: 4px;
background: #f7f7f7;
box-sizing: border-box;
padding: 6px;
}
.col3 {
flex: 1;
border: 1px solid #000;
border-radius: 4px;
background: #f7f7f7;
box-sizing: border-box;
padding: 6px;
}
.col4 {
width: 200px;
display: flex;
flex-direction: column;
gap: 8px;
border: 1px solid #000;
border-radius: 4px;
background: #f7f7f7;
box-sizing: border-box;
padding: 6px;
}
.small-info-box {
border: 1px solid #ddd;
border-radius: 3px;
padding: 2px 4px;
margin-bottom: 2px;
background: #f9f9f9;
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
text-align: center;
display: inline-block;
white-space: normal;
}
.small-info-box .info-label,
.small-info-box .info-value {
display: inline;
font-weight: bold;
margin: 0;
padding: 0;
line-height: 1.1;
}
.small-info-box .info-label {
color: #ff6600;
margin-right: 3px;
}
.small-info-box .info-value {
color: #003366;
}
/* Extra Details: Status Update / Note Creation Styling */
.col4 .status-dropdown {
font-size: 12px;
}
.col4 .status-update-btn {
padding: 4px 8px;
font-size: 13px;
}
.col4 .note-update-btn {
padding: 4px;
width: 100%;
font-size: 13px;
}
/* Logs portion */
.log-half {
width: 100%;
margin-bottom: 3px;
}
.log-half-title {
margin: 1px 0;
font-size: 7px;
font-weight: bold;
background: #fafafa;
padding: 1px 3px;
border: 1px solid #ddd;
border-radius: 3px;
text-align: left;
}
.log-box-container {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.status-log-box,
.note-log-box {
flex: 1 1 calc(50% - 3px);
border: 1px solid #ccc;
padding: 3px 4px;
border-radius: 3px;
box-sizing: border-box;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
font-size: 13px;
}
p.compact-info {
margin: 0;
padding: 0;
font-size: 13px;
font-weight: bold;
color: #333;
}
.more-log-link {
display: inline-block;
margin-top: 2px;
background: #fff;
padding: 1px 4px;
font-size: 9px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 2px;
text-decoration: none;
color: #333;
font-weight: bold;
}
/* ---------- Header Search + Today Info Section ---------- */
.existing-sales-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 15px;
gap: 10px;
}
.existing-sales-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.today-point-box {
font-size: 14px;
color: #0073aa;
font-weight: bold;
padding: 5px 8px;
border: 2px solid #0073aa;
border-radius: 4px;
background: #eef;
white-space: nowrap;
}
.existing-sales-header form {
display: flex;
align-items: center;
}
.existing-sales-header form input[type="text"] {
padding: 6px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 3px;
width: 320px;
margin-right: 8px;
}
.existing-sales-header form button {
padding: 6px 10px;
font-size: 14px;
background: #0274be;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
}
/* ---------- Pagination Styles ---------- */
.pagination {
margin: 20px 0;
text-align: center;
}
.pagination a, .pagination span {
display: inline-block;
padding: 6px 12px;
margin: 0 2px;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #0274be;
}
.pagination span.current-page {
background: #0274be;
color: #fff;
border-color: #0274be;
}
/* ---------- Modal Popup for Full Logs/Notes ---------- */
#log-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
}
#log-modal .modal-content {
background: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 700px;
border-radius: 6px;
font-size: 15px;
color: #333;
position: relative;
}
#log-modal .modal-content h2 {
margin: 0 0 15px 0;
font-size: 18px;
color: #0073aa;
}
#log-modal .close {
color: #aaa;
position: absolute;
right: 14px;
top: 10px;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
/* ---------- Modal Popup for Video Edit ---------- */
#video-edit-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
}
#video-edit-modal .modal-content {
background: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 750px;
border-radius: 6px;
position: relative;
font-size: 14px;
color: #333;
}
#video-edit-modal .modal-content h2 {
margin: 0 0 15px 0;
font-size: 16px;
color: #0073aa;
}
#video-edit-modal .close {
color: #aaa;
position: absolute;
right: 10px;
top: 5px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
}
/* ---------- Modal Popup for Other Group Edit ---------- */
#other-edit-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
}
#other-edit-modal .modal-content {
background: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 650px;
border-radius: 6px;
position: relative;
font-size: 14px;
color: #333;
}
#other-edit-modal .modal-content h2 {
margin: 0 0 15px 0;
font-size: 16px;
color: #0073aa;
}
#other-edit-modal .close {
color: #aaa;
position: absolute;
right: 10px;
top: 5px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
}
/* ---------- Modal Popup for Service Edit ---------- */
#service-edit-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
}
#service-edit-modal .modal-content {
background: #fefefe;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 650px;
border-radius: 6px;
position: relative;
font-size: 14px;
color: #333;
}
#service-edit-modal .modal-content h2 {
margin: 0 0 15px 0;
font-size: 16px;
color: #0073aa;
}
#service-edit-modal .close {
color: #aaa;
position: absolute;
right: 10px;
top: 5px;
font-size: 20px;
font-weight: bold;
cursor: pointer;
}
/* ---------- Footer ---------- */
.sale-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
margin-top: 20px;
border-top: 2px solid #ddd;
background: #fafafa;
}
.footer-left {
font-weight: 600;
color: #555;
}
.footer-right {
display: flex;
align-items: center;
gap: 5px;
}
.footer-right label {
font-weight: 600;
color: #555;
}
.footer-right select {
padding: 6px;
border: 2px solid #ccc;
border-radius: 3px;
}
</style>
</head>
<body <?php body_class(); ?>>
<div class="seller-sale-page-wrap">
<!-- SUBMISSION NOTICES -->
<?php if (isset($_GET['sale_submitted'])):
$subres = sanitize_text_field($_GET['sale_submitted']);
if ($subres === 'true'): ?>
<div class="notice notice-success"><strong>Robot says:</strong> Sale submitted successfully!</div>
<?php elseif ($subres === 'missing_fields'): ?>
<div class="notice notice-error"><strong>Robot says:</strong> Required fields missing!</div>
<?php elseif ($subres === 'db_error'): ?>
<div class="notice notice-error"><strong>Robot says:</strong> Database error occurred!</div>
<?php elseif ($subres === 'duplicate_tnx'): ?>
<div class="notice notice-error"><strong>Robot says:</strong> This transaction is already sold!</div>
<?php elseif ($subres === 'duplicate_order'): ?>
<div class="notice notice-error"><strong>Robot says:</strong> This order ID was used before!</div>
<?php else: ?>
<div class="notice notice-error"><strong>Robot says:</strong> Unknown error: <?php echo esc_html($subres); ?></div>
<?php endif;
endif; ?>
<!-- Create New Sale -->
<div class="sale-section">
<button class="button button-primary" onclick="document.getElementById('create-sale-form').style.display='block';">
Create New Sale
</button>
<div id="create-sale-form" class="create-sale-wrap" style="display:none;">
<h3>Create New Sale / Work</h3>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" enctype="multipart/form-data">
<input type="hidden" name="action" value="bsm_submit_sale">
<?php wp_nonce_field('bsm_submit_sale', 'bsm_sale_nonce'); ?>
<table class="form-table">
<tr>
<th><label for="product_name">Product/Work Description</label></th>
<td><input type="text" name="product_name" id="product_name" required></td>
</tr>
<tr>
<th><label for="sale_type">Sale Type</label></th>
<td>
<select name="sale_type" id="sale_type" required onchange="toggleSaleTypeFields(this.value); calculateSummary();">
<option value="">-- Select --</option>
<option value="product">Product</option>
<option value="service">Service</option>
<option value="gsmalo.com">gsmalo.com</option>
<option value="gsmalo.org">gsmalo.org</option>
<option value="gsmcourse.com">gsmcourse.com</option>
<option value="other">Other</option>
<?php
// Dynamically add custom sale types
$custom_sale_types = get_option('bsm_custom_sale_types', array());
if (!empty($custom_sale_types)) {
foreach ($custom_sale_types as $slug => $cfg) {
echo '<option value="'.esc_attr($slug).'">'.esc_html($cfg['label']).'</option>';
}
}
?>
</select>
</td>
</tr>
<!-- PRODUCT Group -->
<tbody id="fields_product" style="display:none;">
<tr>
<th>Payment Method</th>
<td>
<select name="payment_method_product" onchange="toggleTxnField(this.value, 'txn_row_product'); toggleCustomerPaymentField(this.value, 'product');">
<option value="">-- Select --</option>
<option value="bkash_707">Bkash 707</option>
<option value="bkash_685">Bkash 685</option>
<option value="nagad_707">Nagad 707</option>
<option value="nagad_685">Nagad 685</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="binance">Binance</option>
<option value="other">Other</option>
</select>
</td>
</tr>
<tr id="custpay_row_product" style="display:none;">
<th id="custpay_label_product">Customer Payment Number</th>
<td><input type="text" name="customer_payment_product"></td>
</tr>
<tr id="txn_row_product" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_product" placeholder="Enter Transaction ID"></td>
</tr>
<tr>
<th>Purchase Price</th>
<td><input type="number" name="purchase_price_product" oninput="calculateSummary();"></td>
</tr>
<tr>
<th>Selling Price</th>
<td><input type="number" name="selling_price_product" oninput="calculateSummary();"></td>
</tr>
</tbody>
<!-- SERVICE Group -->
<tbody id="fields_service" style="display:none;">
<tr>
<th>Parts Collection</th>
<td>
<select name="parts_collection_service">
<option value="">-- Select --</option>
<option value="our_store">Our Store</option>
<option value="others_shops">Others' Shops</option>
<option value="not_spent">Not Spent</option>
</select>
</td>
</tr>
<tr>
<th>Payment Method</th>
<td>
<select name="payment_method_service" onchange="toggleTxnField(this.value, 'txn_row_service'); toggleCustomerPaymentField(this.value, 'service');">
<option value="">-- Select --</option>
<option value="bkash_707">Bkash 707</option>
<option value="bkash_685">Bkash 685</option>
<option value="nagad_707">Nagad 707</option>
<option value="nagad_685">Nagad 685</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="binance">Binance</option>
<option value="other">Other</option>
</select>
</td>
</tr>
<tr id="custpay_row_service" style="display:none;">
<th id="custpay_label_service">Customer Payment Number</th>
<td><input type="text" name="customer_payment_service"></td>
</tr>
<tr id="txn_row_service" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_service" placeholder="Enter Transaction ID"></td>
</tr>
<tr>
<th>Purchase Price</th>
<td><input type="number" name="purchase_price_service" oninput="calculateSummary();"></td>
</tr>
<tr>
<th>Selling Price</th>
<td><input type="number" name="selling_price_service" oninput="calculateSummary();"></td>
</tr>
</tbody>
<!-- gsmalo.com Group -->
<tbody id="fields_gsmalo_com" style="display:none;">
<tr>
<th>Order ID</th>
<td><input type="text" name="order_id_gsmalo_com"></td>
</tr>
<tr>
<th>Payment Method</th>
<td>
<select name="payment_method_gsmalo_com" onchange="toggleTxnField(this.value, 'txn_row_gsmalo_com'); toggleCustomerPaymentField(this.value, 'gsmalo_com');">
<option value="">-- Select --</option>
<option value="bkash_707">Bkash 707</option>
<option value="bkash_685">Bkash 685</option>
<option value="nagad_707">Nagad 707</option>
<option value="nagad_685">Nagad 685</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="binance">Binance</option>
<option value="other">Other</option>
</select>
</td>
</tr>
<tr id="custpay_row_gsmalo_com" style="display:none;">
<th id="custpay_label_gsmalo_com">Customer Payment Number</th>
<td><input type="text" name="customer_payment_gsmalo_com"></td>
</tr>
<tr id="txn_row_gsmalo_com" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_gsmalo_com" placeholder="Enter Transaction ID"></td>
</tr>
<tr>
<th>Purchase Price</th>
<td><input type="number" name="purchase_price_gsmalo_com" oninput="calculateSummary();"></td>
</tr>
<tr>
<th>Selling Price</th>
<td><input type="number" name="selling_price_gsmalo_com" oninput="calculateSummary();"></td>
</tr>
</tbody>
<!-- gsmalo.org Group -->
<tbody id="fields_gsmalo_org" style="display:none;">
<tr>
<th>Order ID</th>
<td><input type="text" name="order_id_gsmalo_org"></td>
</tr>
<tr>
<th>Payment Method</th>
<td>
<select name="payment_method_gsmalo_org" onchange="toggleTxnField(this.value, 'txn_row_gsmalo_org'); toggleCustomerPaymentField(this.value, 'gsmalo_org');">
<option value="">-- Select --</option>
<option value="bkash_707">Bkash 707</option>
<option value="bkash_685">Bkash 685</option>
<option value="nagad_707">Nagad 707</option>
<option value="nagad_685">Nagad 685</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="binance">Binance</option>
<option value="other">Other</option>
</select>
</td>
</tr>
<tr id="custpay_row_gsmalo_org" style="display:none;">
<th id="custpay_label_gsmalo_org">Customer Payment Number</th>
<td><input type="text" name="customer_payment_gsmalo_org"></td>
</tr>
<tr id="txn_row_gsmalo_org" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_gsmalo_org" placeholder="Enter Transaction ID"></td>
</tr>
<tr>
<th>Purchase Price (USD) <br>(Balance $<?php echo $current_balance; ?>)</th>
<td>
<input type="number" step="any" name="purchase_price_gsmalo_org" oninput="calculateSummary();">
</td>
</tr>
<tr>
<th>Selling Price</th>
<td><input type="number" name="selling_price_gsmalo_org" oninput="calculateSummary();"></td>
</tr>
</tbody>
<!-- gsmcourse.com Group -->
<tbody id="fields_gsmcourse_com" style="display:none;">
<tr>
<th>Order ID</th>
<td><input type="text" name="order_id_gsmcourse_com"></td>
</tr>
<tr>
<th>Payment Method</th>
<td>
<select name="payment_method_gsmcourse_com" onchange="toggleTxnField(this.value, 'txn_row_gsmcourse_com'); toggleCustomerPaymentField(this.value, 'gsmcourse_com');">
<option value="">-- Select --</option>
<option value="bkash_707">Bkash 707</option>
<option value="bkash_685">Bkash 685</option>
<option value="nagad_707">Nagad 707</option>
<option value="nagad_685">Nagad 685</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="binance">Binance</option>
<option value="other">Other</option>
</select>
</td>
</tr>
<tr id="custpay_row_gsmcourse_com" style="display:none;">
<th id="custpay_label_gsmcourse_com">Customer Payment Number</th>
<td><input type="text" name="customer_payment_gsmcourse_com"></td>
</tr>
<tr id="txn_row_gsmcourse_com" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_gsmcourse_com" placeholder="Enter Transaction ID"></td>
</tr>
<tr>
<th>Purchase Price</th>
<td><input type="number" name="purchase_price_gsmcourse_com" oninput="calculateSummary();"></td>
</tr>
<tr>
<th>Selling Price</th>
<td><input type="number" name="selling_price_gsmcourse_com" oninput="calculateSummary();"></td>
</tr>
</tbody>
<!-- other Group -->
<tbody id="fields_other" style="display:none;">
<tr>
<th>Other Sale Subtype</th>
<td>
<select name="other_sale_subtype" id="other_sale_subtype" onchange="toggleOtherSubtypeFields(this.value); calculateSummary();">
<option value="">-- Select --</option>
<option value="video">Video</option>
<option value="marketing">Marketing</option>
<option value="office_management">Office Management</option>
<option value="other_option">Other</option>
</select>
</td>
</tr>
<tr id="txn_row_other" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_other" placeholder="Enter Transaction ID"></td>
</tr>
<tr id="custpay_row_other" style="display:none;">
<th id="custpay_label_other">Customer Payment Number</th>
<td><input type="text" name="customer_payment_other"></td>
</tr>
<!-- Video subtype fields -->
<tr id="other_video_duration" style="display:none;">
<th>Video Record Duration (HH:MM:SS)</th>
<td>
<input type="number" name="video_duration_h" min="0" placeholder="HH" style="width:80px;" value="00" oninput="calculateSummary()">
:
<input type="number" name="video_duration_m" min="0" max="59" placeholder="MM" style="width:80px;" value="00" oninput="calculateSummary()">
:
<input type="number" name="video_duration_s" min="0" max="59" placeholder="SS" style="width:80px;" value="00" oninput="calculateSummary()">
</td>
</tr>
<tr id="other_video_phase" style="display:none;">
<th>Video work type</th>
<td>
<select name="video_phase" id="video_phase" onchange="calculateSummary()">
<option value="record">Record</option>
<option value="edit_upload">Edit & Upload</option>
<option value="a_to_z_complete">A to Z Complete</option>
</select>
</td>
</tr>
<!-- For non-video subtypes -->
<tr id="other_proof_text_row" style="display:none;">
<th>Proof Text</th>
<td><input type="text" name="proof_text" placeholder="Enter proof text"></td>
</tr>
<tr id="other_proof_screen" style="display:none;">
<th>Proof Screen Short (Optional)</th>
<td><input type="file" name="proof_screen_short"></td>
</tr>
<tr id="other_qty_row" style="display:none;">
<th>Qty</th>
<td><input type="number" name="number_of_jobs" placeholder="Enter number of jobs" oninput="calculateSummary()"></td>
</tr>
</tbody>
<!-- DYNAMIC CUSTOM SALE TYPES (added) -->
<?php
if (!empty($custom_sale_types)):
foreach ($custom_sale_types as $slug => $cfg):
$fields = (isset($cfg['fields']) && is_array($cfg['fields'])) ? $cfg['fields'] : array();
?>
<tbody id="fields_<?php echo $slug; ?>" style="display:none;">
<?php if (in_array('order_id', $fields)): ?>
<tr>
<th>Order ID</th>
<td><input type="text" name="order_id_<?php echo $slug; ?>"></td>
</tr>
<?php endif; ?>
<?php if (in_array('payment_method', $fields)): ?>
<tr>
<th>Payment Method</th>
<td>
<select name="payment_method_<?php echo $slug; ?>" onchange="toggleTxnField(this.value, 'txn_row_<?php echo $slug; ?>'); toggleCustomerPaymentField(this.value, '<?php echo $slug; ?>');">
<option value="">-- Select --</option>
<option value="bkash">Bkash</option>
<option value="nagad">Nagad</option>
<option value="rocket">Rocket</option>
<option value="cash">Cash</option>
<option value="deposit">Deposit</option>
<option value="other">Other</option>
</select>
</td>
</tr>
<tr id="custpay_row_<?php echo $slug; ?>" style="display:none;">
<th id="custpay_label_<?php echo $slug; ?>">Customer Payment Number</th>
<td><input type="text" name="customer_payment_<?php echo $slug; ?>"></td>
</tr>
<tr id="txn_row_<?php echo $slug; ?>" style="display:none;">
<th>Transaction ID</th>
<td><input type="text" name="tnx_id_<?php echo $slug; ?>" placeholder="Enter Transaction ID"></td>
</tr>
<?php endif; ?>
<?php if (in_array('parts_collection', $fields)): ?>
<tr>
<th>Parts Collection</th>
<td>
<select name="parts_collection_<?php echo $slug; ?>">
<option value="">-- Select --</option>
<option value="our_store">Our Store</option>
<option value="others_shops">Others' Shops</option>
<option value="not_spent">Not Spent</option>
</select>
</td>
</tr>
<?php endif; ?>
<?php if (in_array('purchase_price_bdt', $fields)): ?>
<tr>
<th>Purchase Price</th>
<td><input type="number" name="purchase_price_<?php echo $slug; ?>" oninput="calculateSummary();"></td>
</tr>
<?php endif; ?>
<?php if (in_array('purchase_price_usd', $fields)): ?>
<tr>
<th>Purchase Price (USD)</th>
<td><input type="number" step="any" name="purchase_price_<?php echo $slug; ?>" oninput="calculateSummary();"></td>
</tr>
<?php endif; ?>
<?php if (in_array('selling_price_bdt', $fields)): ?>
<tr>
<th>Selling Price</th>
<td><input type="number" name="selling_price_<?php echo $slug; ?>" oninput="calculateSummary();"></td>
</tr>
<?php endif; ?>
</tbody>
<?php
endforeach;
endif;
?>
</table>
<div class="form-action-row">
<div class="action-buttons">
<input type="submit" class="button button-primary" value="Submit Sale">
<button type="button" class="button" onclick="document.getElementById('create-sale-form').style.display='none';">Close</button>
</div>
<!-- Info Boxes Row: $ Value, Profit, Point, Today, Next L -->
<div class="info-box">
<h4>$ Value</h4>
<p id="usdt_value_text">
<?php
echo "$1=" . number_format($dollarRate,2) . " ৳; Your Purchase: $0 = 0 ৳";
?>
</p>
<input type="hidden" id="usd_value_in_taka_setting" value="<?php echo esc_attr($dollarRate); ?>" />
</div>
<div class="info-box">
<h4>Profit</h4>
<p id="profit_text">0 ৳</p>
</div>
<div class="info-box">
<h4>Point</h4>
<!-- Using a div with id "point_text" to display calculated points -->
<div class="info-value" id="point_text">0</div>
</div>
<div class="info-box">
<h4>Today</h4>
<p id="today_text">L?: 0</p>
</div>
<div class="info-box">
<h4>Next L</h4>
<p id="next_text">L?: 0</p>
</div>
<!-- Note: Extra Details block for gsmalo.org Purchase Price removed here as it's not applicable for new sale entries -->
</div>
</form>
</div>
</div>
<!-- Existing Sales/Work Entries -->
<div class="sale-section">
<div class="existing-sales-header">
<h3>Existing Sales/Work Entries</h3>
<!-- (নতুন) Today Point Info box -->
<div class="today-point-box">
<?php echo esc_html($today_point_box_text); ?>
</div>
<form method="get" action="">
<input type="text" name="sale_search" placeholder="Search Sales..." value="<?php echo isset($_GET['sale_search']) ? esc_attr($_GET['sale_search']) : ''; ?>" />
<button type="submit">Search</button>
</form>
</div>
<?php if (!empty($all_sales)): ?>
<?php
$bg_colors = ['#f0f8ff', '#f5f5dc', '#e6e6fa', '#f0fff0', '#fff0f5'];
$color_index = 0;
$last_date = '';
function bsm_build_row_columns($sale, $status_colors) {
global $wpdb;
$columns = [];
$sale_type_lower = strtolower($sale->sale_type);
$icon = '';
if ($sale_type_lower === 'product') {
$icon = '🛍️ ';
} elseif ($sale_type_lower === 'service') {
$icon = '🔧 ';
} elseif ($sale_type_lower === 'gsmalo.com' || $sale_type_lower === 'gsmalo.org') {
$icon = '🌐 ';
} elseif ($sale_type_lower === 'video') {
$icon = '🎥 ';
} elseif (in_array($sale_type_lower, ['marketing','office_management','other_option'])) {
if ($sale_type_lower === 'marketing') {
$icon = '📢 ';
} elseif ($sale_type_lower === 'office_management') {
$icon = '🏢 ';
} elseif ($sale_type_lower === 'other_option') {
$icon = '❓ ';
}
}
// Prod/Work column
$columns['Prod/Work'] = $icon . (!empty($sale->product_name) ? esc_html($sale->product_name) : '');
// Attachment indicator for Video group
if ($sale_type_lower === 'video' && isset($sale->video_phase) && $sale->video_phase === 'Edit & Upload' && !empty($sale->work_proof_link)) {
$table = $wpdb->prefix . 'bsm_sales';
$record_sale_id = $wpdb->get_var($wpdb->prepare("SELECT id FROM $table WHERE sale_type = %s AND video_phase = %s AND work_proof_link = %s LIMIT 1", 'video', 'Record', $sale->work_proof_link));
if ($record_sale_id) {
$columns['Prod/Work'] .= ' <span style="color:#FF0000; font-weight:bold;">[📎 ' . esc_html($record_sale_id) . ']</span>';
}
}
// Type column
$columns['Type'] = !empty($sale->sale_type) ? esc_html($sale->sale_type) : '';
// Video group details
if ($sale_type_lower === 'video') {
$display_video_phase = $sale->video_phase;
if ($display_video_phase === 'Edit & Upload') {
$display_video_phase = 'Edit & Upload';
} elseif ($display_video_phase === 'A to Z Complete') {
$display_video_phase = 'A to Z Complete';
}
$columns['Work type'] = !empty($sale->video_phase) ? esc_html($display_video_phase) : 'Not Selected';
if (strtolower($sale->video_phase) === 'record' && !empty($sale->edit_upload_id)) {
$columns['Edit Upload ID'] = esc_html($sale->edit_upload_id);
}
if (!empty($sale->work_proof_link) && preg_match('/^https?:\\/\\//i', $sale->work_proof_link)) {
$columns['Video Link'] = '<a href="' . esc_url($sale->work_proof_link) . '" target="_blank">Link</a>';
}
}
// Parts Collection for non-Other groups
if (!in_array($sale_type_lower, ['marketing','office_management','other_option','video'])) {
if (!empty($sale->parts_collection)) {
$columns['P.Collection'] = esc_html($sale->parts_collection);
}
}
// Other group details
if (in_array($sale_type_lower, ['marketing','office_management','other_option'])) {
if (!empty($sale->proof_text)) {
$columns['Proof Text'] = esc_html($sale->proof_text);
}
if (!empty($sale->work_proof_link)) {
$columns['Proof Link'] = '<a href="' . esc_url($sale->work_proof_link) . '" target="_blank">Link</a>';
}
if (!empty($sale->proof_screen_short)) {
$columns['Screen Short'] = '<img src="' . esc_url($sale->proof_screen_short) . '" alt="Proof Image" style="max-height:50px; max-width:50px;">';
}
if (isset($sale->qty) && intval($sale->qty) > 0) {
$columns['Qty'] = intval($sale->qty);
}
}
if (!empty($sale->transaction_id)) {
$columns['Tnx ID'] = esc_html($sale->transaction_id);
}
if (!empty($sale->customer_payment)) {
$columns['C.P. Number'] = esc_html($sale->customer_payment);
}
// gsmalo.org conversion
if (isset($sale->purchase_price) && $sale->purchase_price !== '') {
if ($sale_type_lower === 'gsmalo.org') {
$usdPrice = floatval($sale->purchase_price);
$conversionRate = floatval(get_option('bsm_usd_value_in_taka', 85));
$bd_value = $usdPrice * $conversionRate;
$columns['P.Price'] = number_format($bd_value, 2) . ' ৳';
} else {
$columns['P.Price'] = esc_html($sale->purchase_price) . ' ৳';
}
}
if (isset($sale->selling_price) && floatval($sale->selling_price) > 0) {
$columns['S.Price'] = esc_html($sale->selling_price) . ' ৳';
}
if (isset($sale->profit) && floatval($sale->profit) > 0) {
$columns['Profit'] = esc_html($sale->profit) . ' ৳';
}
if (isset($sale->loss) && floatval($sale->loss) > 0) {
$columns['Loss'] = esc_html($sale->loss) . ' ৳';
}
if ($sale_type_lower === "video") {
if (isset($sale->video_duration) && floatval($sale->video_duration) > 0) {
$totSec = (int)$sale->video_duration;
$hh = floor($totSec / 3600);
$rem = $totSec % 3600;
$mm = floor($rem / 60);
$ss = $rem % 60;
$parts = [];
if ($hh > 0) $parts[] = $hh . 'h';
if ($mm > 0) $parts[] = $mm . 'm';
if ($ss > 0) $parts[] = $ss . 's';
$time_str = implode(' ', $parts);
$columns['Duration'] = '<span class="duration-col">' . esc_html($time_str) . '</span>';
} else {
$columns['Duration'] = '';
}
}
if ($sale_type_lower === "service") {
$allowed_edit_status = ["pending","On Hold","In Process","Need Parts"];
if ( in_array($sale->status, $allowed_edit_status) ) {
$columns['Action'] = '<button class="service-edit-btn" data-sale-id="' . $sale->id . '" data-product_name="' . esc_attr($sale->product_name) . '" data-purchase_price="' . esc_attr($sale->purchase_price) . '" data-selling_price="' . esc_attr($sale->selling_price) . '" data-note="' . esc_attr($sale->note) . '">Edit</button>';
} else {
$columns['Action'] = '';
}
$st_bg = isset($status_colors[$sale->status]) ? $status_colors[$sale->status] : '#ccc';
$columns['Status'] = '<div class="status-box" style="background-color:' . esc_attr($st_bg) . ';">' . esc_html($sale->status) . '</div>';
} elseif ($sale_type_lower === "video" || in_array($sale_type_lower, ['marketing','office_management','other_option'])) {
if ($sale->status !== "Review Apply" && $sale->status !== "Successful All") {
if ($sale_type_lower === "video") {
$columns['Action'] = '<button class="video-edit-btn" data-sale-id="' . $sale->id . '" data-video_phase="' . esc_attr($sale->video_phase) . '" data-product_name="' . esc_attr($sale->product_name) . '" data-work_proof_link="' . esc_attr($sale->work_proof_link) . '" data-video_duration="' . esc_attr($sale->video_duration) . '">Edit</button>';
} else {
$groupName = ucfirst($sale_type_lower);
$columns['Action'] = '<button class="other-edit-btn" data-sale-id="' . $sale->id . '" data-group="' . esc_attr($groupName) . '" data-product_name="' . esc_attr($sale->product_name) . '" data-proof_text="' . esc_attr($sale->proof_text) . '" data-work_proof_link="' . esc_attr($sale->work_proof_link) . '" data-proof_screen_short="' . esc_attr($sale->proof_screen_short) . '" data-number_of_jobs="' . esc_attr($sale->qty) . '">Edit</button>';
}
} else {
$columns['Action'] = '';
}
$st_bg = isset($status_colors[$sale->status]) ? $status_colors[$sale->status] : '#ccc';
$columns['Status'] = '<div class="status-box" style="background-color:' . esc_attr($st_bg) . ';">' . esc_html($sale->status) . '</div>';
} else {
$st_bg = isset($status_colors[$sale->status]) ? $status_colors[$sale->status] : '#ccc';
$columns['Status'] = '<div class="status-box" style="background-color:' . esc_attr($st_bg) . ';">' . esc_html($sale->status) . '</div>';
}
return $columns;
}
foreach ($all_sales as $sale):
$sale_date = date_i18n('Y-m-d', strtotime($sale->created_at));
if ($sale_date !== $last_date) {
$bgc = $bg_colors[$color_index % count($bg_colors)];
$color_index++;
$last_date = $sale_date;
}
$profitVal = isset($sale->profit) ? floatval($sale->profit) : 0;
// Get base points for the sale entry (Extra Details Point box)
$pointVal = floatval($wpdb->get_var($wpdb->prepare("SELECT IFNULL(points, 0) FROM {$wpdb->prefix}bsm_points WHERE sale_id = %d AND type = 'base' LIMIT 1", $sale->id)));
$row_cols = bsm_build_row_columns($sale, $status_colors);
?>
<div class="sale-entry" style="background:<?php echo esc_attr($bgc); ?>;">
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<?php foreach (array_keys($row_cols) as $col_name): ?>
<th><?php echo esc_html($col_name); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<tr>
<?php foreach ($row_cols as $col_val): ?>
<td><?php echo (strpos($col_val, '<') !== false) ? $col_val : esc_html($col_val); ?></td>
<?php endforeach; ?>
</tr>
</tbody>
</table>
<!-- Extra Details Section -->
<?php
$seller_user = get_userdata($sale->seller_id);
$seller_name = $seller_user ? $seller_user->display_name : 'Unknown';
$all_logs = bsm_get_logs_for_sale($sale->id);
$status_logs = array_filter($all_logs, function($l) { return $l->note_type === 'status_change'; });
$note_logs = bsm_get_statas_logs($sale->id);
$max_display = 4;
?>
<div class="extra-details-container">
<div class="col2">
<?php
$paymentLogo = get_option('bsm_payment_method_logo');
if (!empty($sale->payment_method)) :
?>
<div class="small-info-box" style="background-color:#eef;">
<div class="info-label">P.Method</div>
<div class="info-value">
<?php
if (!empty($paymentLogo)) {
echo '<img src="'.esc_url($paymentLogo).'" alt="Logo" style="max-height:20px; max-width:20px; vertical-align:middle;"> ';
}
echo esc_html($sale->payment_method);
?>
</div>
</div>
<?php endif; ?>
<div class="small-info-box" style="background-color:#ffe;">
<div class="info-label">Point</div>
<?php
// Set icon based on sale status.
$icon = "";
if (in_array($sale->status, array("Failed", "Rejected", "Cancel"))) {
$icon = ' <span style="color: red; font-size: 16px;">❌</span>';
} elseif ($sale->status === "Successful All") {
$icon = ' <span style="color: green; font-size: 16px;">✔</span>';
}
?>
<div class="info-value"><?php echo number_format($pointVal, 2) . $icon; ?></div>
</div>
<?php if (strtolower($sale->sale_type) === 'gsmalo.org'): ?>
<div class="small-info-box" style="background-color:#eef;">
<div class="info-label">Purchase ($)</div>
<div class="info-value">
<?php
$usdPrice = floatval($sale->purchase_price);
$bd_value = $usdPrice * $dollarRate;
echo esc_html('$' . number_format($usdPrice, 2) . ' (BD ' . number_format($bd_value, 0) . ')');
?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($sale->order_id)): ?>
<div class="small-info-box" style="background-color:#f2f0ff;">
<div class="info-label">Order ID</div>
<div class="info-value"><?php echo esc_html($sale->order_id); ?></div>
</div>
<?php endif; ?>
<?php if (!empty($sale->id)): ?>
<div class="small-info-box" style="background-color:#e6f7ff;">
<div class="info-label">Sale ID</div>
<div class="info-value"><?php echo esc_html($sale->id); ?></div>
</div>
<?php endif; ?>
<?php if (!empty($seller_name) && $seller_name != 'Unknown'): ?>
<div class="small-info-box" style="background-color:#fce4ec;">
<div class="info-label">Seller</div>
<div class="info-value"><?php echo esc_html($seller_name); ?></div>
</div>
<?php endif; ?>
<div class="small-info-box" style="background-color:#e8eaf6;">
<div class="info-label">Created</div>
<div class="info-value">
<?php
$db_created = $sale->created_at;
if (empty($db_created) || $db_created === '0000-00-00 00:00:00') {
$db_created = current_time('mysql');
}
echo esc_html(date_i18n(get_option('date_format').' ' . get_option('time_format'), strtotime($db_created)));
?>
</div>
</div>
</div>
<div class="col3" id="log-column-<?php echo esc_attr($sale->id); ?>">
<?php if (!empty($status_logs) || !empty($note_logs)): ?>
<div class="log-half">
<p class="log-half-title">STATUS LOG</p>
<div class="log-box-container">
<?php
$count_st = 0;
foreach ($status_logs as $lg) {
if ($count_st < $max_display) {
$lg_name = !empty($lg->user_name) ? $lg->user_name : 'Unknown';
$lg_time = date('H:i | d-m-Y', strtotime($lg->created_at));
echo '<div class="status-log-box">';
echo '<p class="compact-info">' . esc_html($lg_name) . ' | ' . esc_html($lg_time) . '</p>';
echo '<p style="margin:0; padding:0;">' . wp_kses_post($lg->note_text) . '</p>';
echo '</div>';
$count_st++;
}
}
?>
</div>
<?php if (count($status_logs) > $max_display): ?>
<a href="#" class="more-log-link" data-sale-id="<?php echo esc_attr($sale->id); ?>" data-type="status">
+<?php echo (count($status_logs) - $max_display); ?>
</a>
<?php endif; ?>
</div>
<div class="log-half">
<p class="log-half-title">NOTES</p>
<div class="log-box-container">
<?php
$count_nt = 0;
foreach ($note_logs as $nt) {
if ($count_nt < $max_display) {
$nt_name = !empty($nt->user_name) ? $nt->user_name : 'Unknown';
$nt_time = date('H:i | d-m-Y', strtotime($nt->created_at));
$bg_color = ($count_nt == 0) ? '#daf8d4' : '#ffffff';
$box_shadow = ($count_nt == 0) ? '0 0 6px rgba(0,128,0,0.4)' : '0 1px 2px rgba(0,0,0,0.1)';
echo '<div class="note-log-box" style="background:'.$bg_color.'; box-shadow:'.$box_shadow.';">';
echo '<p class="compact-info">' . esc_html($nt_name) . ' | ' . esc_html($nt_time) . '</p>';
echo '<p style="margin:0; padding:0;">' . esc_html($nt->note_text) . '</p>';
echo '</div>';
$count_nt++;
}
}
?>
</div>
<?php if (count($note_logs) > $max_display): ?>
<a href="#" class="more-log-link" data-sale-id="<?php echo esc_attr($sale->id); ?>" data-type="note">
+<?php echo (count($note_logs) - $max_display); ?>
</a>
<?php endif; ?>
</div>
<?php else: ?>
<p>No logs found.</p>
<?php endif; ?>
</div>
<!-- 8) col4: Status Update / Note Creation -->
<?php
$hide_status_block = (strtolower($sale->sale_type) === 'video' && strtolower($sale->video_phase) === 'record');
?>
<div class="col4">
<?php if (!$hide_status_block): ?>
<?php
$allowed_statuses = bsm_get_allowed_statuses_for_seller_front($sale);
$step1_list = ["pending","On Hold","In Process","Need Parts"];
$step2_list = ["Success But Not Delivery","Parts Brought","Refund Required"];
$step3_list = [
"Refund Done","Successful All","Check Admin","Reject Delivery Done","Cost",
"Cancel","Block","Rejected","Failed","Review Apply"
];
$allowed_step1 = array_values(array_intersect($allowed_statuses, $step1_list));
$allowed_step2 = array_values(array_intersect($allowed_statuses, $step2_list));
$allowed_step3 = array_values(array_intersect($allowed_statuses, $step3_list));
$total_allowed = count($allowed_step1) + count($allowed_step2) + count($allowed_step3);
?>
<?php if ($total_allowed > 0): ?>
<select class="status-dropdown" data-sale-id="<?php echo esc_attr($sale->id); ?>">
<option value="">-- স্ট্যাটাস নির্বাচন করুন --</option>
<?php if (!empty($allowed_step1)): ?>
<optgroup label="ধাপ 1 (Changeable)">
<?php foreach ($allowed_step1 as $st):
$clr = isset($status_colors[$st]) ? $status_colors[$st] : '#999'; ?>
<option value="<?php echo esc_attr($st); ?>" style="background-color:<?php echo esc_attr($clr); ?>; color:#fff;">
<?php echo esc_html($st); ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php if (!empty($allowed_step2)): ?>
<optgroup label="ধাপ 2 (Limited lock)">
<?php foreach ($allowed_step2 as $st):
$clr = isset($status_colors[$st]) ? $status_colors[$st] : '#999'; ?>
<option value="<?php echo esc_attr($st); ?>" style="background-color:<?php echo esc_attr($clr); ?>; color:#fff;">
<?php echo esc_html($st); ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php if (!empty($allowed_step3)): ?>
<optgroup label="ধাপ 3 (Lock)">
<?php foreach ($allowed_step3 as $st):
$clr = isset($status_colors[$st]) ? $status_colors[$st] : '#999'; ?>
<option value="<?php echo esc_attr($st); ?>" style="background-color:<?php echo esc_attr($clr); ?>; color:#fff;">
<?php echo esc_html($st); ?>
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
</select>
<button class="status-update-btn" data-sale-id="<?php echo esc_attr($sale->id); ?>"
<?php echo in_array($sale->status, $step3_list) ? 'disabled' : ''; ?>>
স্ট্যাটাস আপডেট
</button>
<?php else: ?>
<p style="font-size:12px; font-weight:bold;">No further status updates possible.</p>
<?php endif; ?>
<?php else: ?>
<p style="font-size:12px; font-weight:bold;">[Record mode: No manual status update]</p>
<?php endif; ?>
<div class="note-creation">
<textarea class="note-input" data-sale-id="<?php echo esc_attr($sale->id); ?>" placeholder="Write a note here..."></textarea>
<button class="note-update-btn" data-sale-id="<?php echo esc_attr($sale->id); ?>">আপডেট নোট</button>
</div>
</div>
</div> <!-- end of extra-details-container -->
</div>
<?php endforeach; ?>
<?php else: ?>
<p>No sales or work entries found.</p>
<?php endif; ?>
</div>
</div>
<!-- Pagination & Footer -->
<?php
$total_pages = ceil($total_sales / $per_page);
if ($total_pages > 1):
$query_args = $_GET;
unset($query_args['paged']);
?>
<div class="pagination">
<?php for ($i = 1; $i <= $total_pages; $i++):
$query_args['paged'] = $i;
$page_url = add_query_arg($query_args);
if ($i == $paged): ?>
<span class="current-page"><?php echo $i; ?></span>
<?php else: ?>
<a class="page-number" href="<?php echo esc_url($page_url); ?>"><?php echo $i; ?></a>
<?php endif;
endfor; ?>
</div>
<?php endif; ?>
<div class="sale-footer">
<div class="footer-left">
Developer: GSM Al-Masud 01751886004
</div>
<div class="footer-right">
<label for="entries_per_page">Entries per page:</label>
<select id="entries_per_page" name="per_page" onchange="window.location.href = addQueryArg({'per_page': this.value, 'paged': 1});">
<option value="10" <?php selected($per_page,10); ?>>10</option>
<option value="25" <?php selected($per_page,25); ?>>25</option>
<option value="50" <?php selected($per_page,50); ?>>50</option>
<option value="100" <?php selected($per_page,100); ?>>100</option>
</select>
</div>
</div>
<!-- Modal Popup for Full Logs/Notes -->
<div id="log-modal">
<div class="modal-content">
<span class="close">×</span>
<h2>All Logs/Notes</h2>
<div id="log-modal-content"></div>
</div>
</div>
<!-- Modal Popup for Video Edit -->
<div id="video-edit-modal">
<div class="modal-content">
<span class="close" id="video-edit-close">×</span>
<h2 id="video-edit-modal-title">Edit Video Sale</h2>
<div class="video-edit-form">
<input type="hidden" id="video_edit_sale_id" value="">
<input type="hidden" id="video_edit_product_name_hidden" name="product_name" value="">
<div id="video-edit-standard">
<label for="video_edit_product_name">Prod/Work</label>
<input type="text" id="video_edit_product_name" placeholder="Enter product/work description">
<label for="video_edit_proof_link">Work Proof Link</label>
<input type="text" id="video_edit_proof_link" placeholder="Enter full link">
<label for="video_edit_duration">Video Duration (HH:MM:SS)</label>
<input type="number" id="video_edit_duration_h" min="0" placeholder="HH" style="width:80px;" value="00">
<input type="number" id="video_edit_duration_m" min="0" max="59" placeholder="MM" style="width:80px;" value="00">
<input type="number" id="video_edit_duration_s" min="0" max="59" placeholder="SS" style="width:80px;" value="00">
</div>
<div id="video-edit-record" style="display:none;">
<p><strong>Prod/Work:</strong> <span id="video_edit_product_name_ro"></span></p>
<label for="uploaded_sale_id">Uploaded Sale ID</label>
<input type="text" id="uploaded_sale_id" placeholder="Enter Uploaded Sale ID">
</div>
<button type="button" id="video_edit_update_btn">Update</button>
</div>
</div>
</div>
<!-- Modal Popup for Other Group Edit -->
<div id="other-edit-modal">
<div class="modal-content">
<span class="close" id="other-edit-close">×</span>
<h2 id="other-edit-title">Edit Other Sale</h2>
<div class="other-edit-form">
<input type="hidden" id="other_edit_sale_id" value="">
<label for="other_edit_product_name">Prod/Work</label>
<input type="text" id="other_edit_product_name" placeholder="Enter product/work description">
<label for="other_edit_proof_text">Proof Text</label>
<input type="text" id="other_edit_proof_text" placeholder="Enter proof text">
<label for="other_edit_proof_link">Proof Link</label>
<input type="text" id="other_edit_proof_link" placeholder="Enter proof link (optional)">
<label for="other_edit_proof_screen">Proof Screen Short</label>
<input type="file" id="other_edit_proof_screen">
<label for="other_edit_number_of_jobs">Number of jobs</label>
<input type="number" id="other_edit_number_of_jobs" placeholder="Enter number of jobs (optional)">
<button type="button" id="other_edit_update_btn">Update</button>
</div>
</div>
</div>
<!-- Modal Popup for Service Edit -->
<div id="service-edit-modal">
<div class="modal-content">
<span class="close" id="service-edit-close">×</span>
<h2>Edit Service Sale</h2>
<div class="service-edit-form">
<input type="hidden" id="service_edit_sale_id" value="">
<p><strong>Prod/Work:</strong> <span id="service_edit_product_name"></span></p>
<label for="service_edit_note">Note (to update Prod/Work Note):</label>
<textarea id="service_edit_note" placeholder="Enter note..."></textarea>
<label for="service_edit_purchase_price">Purchase Price:</label>
<input type="number" id="service_edit_purchase_price" oninput="calculateSummary();">
<label for="service_edit_selling_price">Selling Price:</label>
<input type="number" id="service_edit_selling_price" oninput="calculateSummary();">
<button type="button" id="service_edit_update_btn">Update</button>
</div>
</div>
</div>
<script>
// Reminders: Only the specified parts are modified; all other code remains unchanged.
function logJsError(errorMessage) {
var fd = new FormData();
fd.append("action", "bsm_log_js_error");
fd.append("nonce", "<?php echo wp_create_nonce('bsm_log_js_error'); ?>");
fd.append("error_message", errorMessage);
fetch(ajaxurl, { method: "POST", body: fd });
}
var existingProofLinks = <?php
$links = [];
foreach ($all_sales as $sl) {
if (!empty($sl->work_proof_link)) {
$links[] = $sl->work_proof_link;
}
}
echo json_encode($links);
?>;
function checkProofLinkDup(inputField, currentSaleId) {
var wlink = inputField.value.trim();
if (wlink) {
if (existingProofLinks.includes(wlink)) {
alert("This link is already used in another sale.");
inputField.value = '';
}
}
}
function addQueryArg(params) {
var url = new URL(window.location.href);
for (var key in params) {
url.searchParams.set(key, params[key]);
}
return url.href;
}
function toggleTxnField(value, row_id) {
if (value.trim().toLowerCase() === 'cash' || value.trim() === '') {
document.getElementById(row_id).style.display = 'none';
} else {
document.getElementById(row_id).style.display = 'table-row';
}
}
function toggleCustomerPaymentField(value, suffix) {
var row = document.getElementById('custpay_row_' + suffix);
var label = document.getElementById('custpay_label_' + suffix);
if (!row || !label) return;
var lv = value.trim().toLowerCase();
if (lv === 'cash' || lv === 'other' || lv === 'binance' || lv === 'deposit') {
row.style.display = 'table-row';
label.innerText = "C.P Note";
} else if (value.trim() !== '') {
row.style.display = 'table-row';
label.innerText = "Customer Payment Number";
} else {
row.style.display = 'none';
}
}
function toggleSaleTypeFields(value) {
document.getElementById('fields_product').style.display = 'none';
document.getElementById('fields_service').style.display = 'none';
document.getElementById('fields_gsmalo_com').style.display = 'none';
document.getElementById('fields_gsmalo_org').style.display = 'none';
document.getElementById('fields_gsmcourse_com').style.display = 'none';
document.getElementById('fields_other').style.display = 'none';
<?php
$custom_sale_types_for_js = get_option('bsm_custom_sale_types', array());
if (!empty($custom_sale_types_for_js)) {
foreach ($custom_sale_types_for_js as $cslug => $cfgc) {
echo "document.getElementById('fields_{$cslug}').style.display = 'none';\n";
}
}
?>
if (value === 'product') {
document.getElementById('fields_product').style.display = 'table-row-group';
} else if (value === 'service') {
document.getElementById('fields_service').style.display = 'table-row-group';
} else if (value === 'gsmalo.com') {
document.getElementById('fields_gsmalo_com').style.display = 'table-row-group';
} else if (value === 'gsmalo.org') {
document.getElementById('fields_gsmalo_org').style.display = 'table-row-group';
} else if (value === 'gsmcourse.com') {
document.getElementById('fields_gsmcourse_com').style.display = 'table-row-group';
} else if (value === 'other') {
document.getElementById('fields_other').style.display = 'table-row-group';
<?php
if (!empty($custom_sale_types_for_js)) {
foreach ($custom_sale_types_for_js as $cslug => $cfgc) {
echo "} else if(value === '{$cslug}') {\n";
echo " document.getElementById('fields_{$cslug}').style.display = 'table-row-group';\n";
}
}
?>
} else {
// do nothing
}
}
function toggleOtherSubtypeFields(value) {
var otherVideoRow = document.getElementById('other_video_duration');
var otherVideoPhase = document.getElementById('other_video_phase');
var otherProofTextRow = document.getElementById('other_proof_text_row');
var otherProofScreen = document.getElementById('other_proof_screen');
var otherQtyRow = document.getElementById('other_qty_row');
if (otherVideoRow) otherVideoRow.style.display = 'none';
if (otherVideoPhase) otherVideoPhase.style.display = 'none';
if (otherProofTextRow) otherProofTextRow.style.display = 'none';
if (otherProofScreen) otherProofScreen.style.display = 'none';
if (otherQtyRow) otherQtyRow.style.display = 'none';
if (value === 'video') {
if (otherVideoRow) otherVideoRow.style.display = 'table-row';
if (otherVideoPhase) otherVideoPhase.style.display = 'table-row';
} else if (value !== '') {
if (otherProofTextRow) otherProofTextRow.style.display = 'table-row';
if (otherProofScreen) otherProofScreen.style.display = 'table-row';
if (otherQtyRow) otherQtyRow.style.display = 'table-row';
}
}
/**
* calculateSummary():
* - এই ফাংশনটি Create New Sale/Work ফর্মে ইনপুট দেওয়ার সাথে সাথে
* "Point" বক্সে পয়েন্ট সংখ্যা আপডেট করে।
* - যদি Sale Type "other" হয় এবং subtype হিসেবে "video" নির্বাচন করা থাকে, তাহলে
* ভিডিওর সময় (HH:MM:SS) এবং video_phase অনুযায়ী Video Group Points থেকে পয়েন্ট হিসাব করে দেখাবে।
* - এছাড়াও, যদি subtype "marketing" অথবা "office_management" হয়, তাহলে "Number of jobs" (Qty) অনুযায়ী,
* Admin Settings‑এর "bsm_other_group_points" থেকে পয়েন্ট বের করে দেখাবে।
* - অন্যান্য ক্ষেত্রে পূর্বের মতই হিসাব করা হবে।
*/
function calculateSummary() {
var saleType = document.getElementById('sale_type').value;
var pointElem = document.getElementById('point_text');
// Handle 'other' sale type specially.
if (saleType === 'other') {
var otherSubtypeElem = document.getElementById('other_sale_subtype');
if (otherSubtypeElem) {
var otherSubtype = otherSubtypeElem.value.toLowerCase().trim();
if (otherSubtype === 'video') {
// Video subtype calculation (existing logic)
var vh = parseInt(document.querySelector('input[name="video_duration_h"]').value) || 0;
var vm = parseInt(document.querySelector('input[name="video_duration_m"]').value) || 0;
var vs = parseInt(document.querySelector('input[name="video_duration_s"]').value) || 0;
var totalSec = vh * 3600 + vm * 60 + vs;
var minutes = totalSec / 60;
var videoPhaseElem = document.getElementById('video_phase');
var videoPhase = "";
if (videoPhaseElem) {
videoPhase = videoPhaseElem.value.toLowerCase().trim();
videoPhase = videoPhase.replace(/\s+/g, '_');
}
var thresholds = [
{min: 0, max: 2, key: "1+"},
{min: 2, max: 3, key: "2+"},
{min: 3, max: 4, key: "3+"},
{min: 4, max: 5, key: "4+"},
{min: 5, max: 7, key: "5+"},
{min: 7, max: 10, key: "7+"},
{min: 10, max: 13, key: "10+"},
{min: 13, max: 16, key: "13+"},
{min: 16, max: 20, key: "16+"},
{min: 20, max: 25, key: "20+"},
{min: 25, max: 30, key: "25+"},
{min: 30, max: 40, key: "30+"},
{min: 40, max: 50, key: "40+"},
{min: 50, max: 60, key: "50+"},
{min: 60, max: 80, key: "1_hour+"},
{min: 80, max: 100, key: "1_20_hour+"},
{min: 100, max: 110, key: "1_40_hour+"},
{min: 110, max: 120, key: "2_hour+"},
{min: 120, max: 150, key: "2_30_hour+"}
];
var points = 0;
if (videoGroupPoints && videoGroupPoints[videoPhase]) {
for (var i = 0; i < thresholds.length; i++) {
var th = thresholds[i];
if (minutes >= th.min && minutes < th.max) {
points = parseFloat(videoGroupPoints[videoPhase][th.key] || 0);
break;
}
}
if (points === 0 && minutes >= thresholds[thresholds.length - 1].min) {
var lastKey = thresholds[thresholds.length - 1].key;
points = parseFloat(videoGroupPoints[videoPhase][lastKey] || 0);
}
}
if (pointElem) {
pointElem.innerText = points.toFixed(0);
}
return;
} else if (otherSubtype === 'marketing' || otherSubtype === 'office_management') {
// For Marketing and Office Management: calculate based on quantity.
var qtyInput = document.querySelector('input[name="number_of_jobs"]');
var qty = (qtyInput && qtyInput.value) ? parseInt(qtyInput.value) : 1;
var perPoint = otherGroupPoints[otherSubtype] ? parseFloat(otherGroupPoints[otherSubtype]) : 30;
var calculatedPoints = perPoint * (qty > 0 ? qty : 1);
if (pointElem) {
pointElem.innerText = calculatedPoints.toFixed(0);
}
return;
}
}
}
// For non-'other' sale types, process as before.
var purchasePrice = 0, sellingPrice = 0;
if (saleType === 'gsmalo.org') {
purchasePrice = parseFloat(document.querySelector('input[name="purchase_price_gsmalo_org"]')?.value || "0");
sellingPrice = parseFloat(document.querySelector('input[name="selling_price_gsmalo_org"]')?.value || "0");
} else if (saleType === 'product') {
purchasePrice = parseFloat(document.querySelector('input[name="purchase_price_product"]')?.value || "0");
sellingPrice = parseFloat(document.querySelector('input[name="selling_price_product"]')?.value || "0");
} else if (saleType === 'service') {
purchasePrice = parseFloat(document.querySelector('input[name="purchase_price_service"]')?.value || "0");
sellingPrice = parseFloat(document.querySelector('input[name="selling_price_service"]')?.value || "0");
} else if (saleType === 'gsmalo.com') {
purchasePrice = parseFloat(document.querySelector('input[name="purchase_price_gsmalo_com"]')?.value || "0");
sellingPrice = parseFloat(document.querySelector('input[name="selling_price_gsmalo_com"]')?.value || "0");
} else if (saleType === 'gsmcourse.com') {
purchasePrice = parseFloat(document.querySelector('input[name="purchase_price_gsmcourse_com"]')?.value || "0");
sellingPrice = parseFloat(document.querySelector('input[name="selling_price_gsmcourse_com"]')?.value || "0");
} else {
purchasePrice = 0;
sellingPrice = 0;
}
var dollarRate = parseFloat(document.getElementById('usd_value_in_taka_setting').value) || 0;
let profit = 0;
if (saleType === 'gsmalo.org') {
let purchaseBdt = purchasePrice * dollarRate;
profit = Math.max(0, sellingPrice - purchaseBdt);
} else {
profit = Math.max(0, sellingPrice - purchasePrice);
}
let profitElem = document.getElementById('profit_text');
if (profitElem) {
profitElem.innerText = profit.toFixed(2) + " ৳";
}
let usdtValueElem = document.getElementById('usdt_value_text');
if (usdtValueElem) {
if (saleType === 'gsmalo.org') {
let purchaseBdt = (purchasePrice * dollarRate).toFixed(2);
usdtValueElem.innerText = "$1=" + dollarRate.toFixed(2) + " ৳; Your Purchase:$" + purchasePrice.toFixed(2) + " = " + purchaseBdt + " ৳";
} else {
usdtValueElem.innerText = "$1=" + dollarRate.toFixed(2) + " ৳; [gsmalo.org only]";
}
}
let rateForSaleType = 10;
if (typeof dailyPointRates[saleType] !== 'undefined') {
rateForSaleType = parseFloat(dailyPointRates[saleType].rate);
}
let basePoints = profit * (rateForSaleType / 100.0);
basePoints = parseFloat(basePoints.toFixed(2));
if (pointElem) {
pointElem.innerText = basePoints.toFixed(0);
}
// "Today" ও "Next L" box calculation using serverTodayBalance.
let currentDailyPoints = parseFloat(serverTodayBalance) || 0;
let todayElem = document.getElementById('today_text');
let nextElem = document.getElementById('next_text');
if (activeDailyMethod === 'bonus') {
let currentLevel = 1;
let sortedKeys = Object.keys(dailyLevelsBonus).map(x => parseInt(x)).sort((a, b) => a - b);
for (let i = 0; i < sortedKeys.length; i++) {
let k = sortedKeys[i];
let dd = dailyLevelsBonus[k];
if (parseInt(dd.enabled) === 1) {
let rangeVal = parseFloat(dd.range) || 0;
if (currentDailyPoints >= rangeVal) {
currentLevel = k;
} else {
break;
}
}
}
todayElem.innerText = "L" + currentLevel + ": " + currentDailyPoints.toFixed(0);
let nextLev = currentLevel + 1;
if (dailyLevelsBonus[nextLev] && parseInt(dailyLevelsBonus[nextLev].enabled) === 1) {
let nextRange = parseFloat(dailyLevelsBonus[nextLev].range) || 0;
let needed = Math.max(0, nextRange - currentDailyPoints);
let nBonus = parseFloat(dailyLevelsBonus[nextLev].bonus) || 0;
nextElem.innerText = "L" + nextLev + ": " + needed.toFixed(0) + " (+" + nBonus.toFixed(0) + ")";
} else {
nextElem.innerText = "Max";
}
} else {
let sortedKeys = Object.keys(dailyLevels).map(x => parseInt(x)).sort((a, b) => a - b);
let foundLevelNum = 0;
for (let i = 0; i < sortedKeys.length; i++) {
let k = sortedKeys[i];
let dd = dailyLevels[k];
let fromVal = parseFloat(dd.range_from) || 0;
let toVal = (dd.range_to === "Unlimited") ? Infinity : parseFloat(dd.range_to);
if (currentDailyPoints >= fromVal && currentDailyPoints <= toVal && parseInt(dd.enabled) === 1) {
foundLevelNum = k;
break;
}
}
if (!foundLevelNum && sortedKeys.length > 0) {
foundLevelNum = sortedKeys[sortedKeys.length - 1];
}
todayElem.innerText = "L" + foundLevelNum + ": " + currentDailyPoints.toFixed(0);
let nextLev = foundLevelNum + 1;
if (dailyLevels[nextLev] && parseInt(dailyLevels[nextLev].enabled) === 1) {
let nFrom = parseFloat(dailyLevels[nextLev].range_from) || 0;
let needed = Math.max(0, nFrom - currentDailyPoints);
nextElem.innerText = "L" + nextLev + ": " + needed.toFixed(0);
} else {
nextElem.innerText = "Max Level";
}
}
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.video-edit-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var saleId = this.getAttribute('data-sale-id');
var productName = this.getAttribute('data-product_name') || "";
var workProofLink = this.getAttribute('data-work_proof_link') || "";
var videoDuration = this.getAttribute('data-video_duration') || "";
var videoPhase = this.getAttribute('data-video_phase') || "";
document.getElementById('video_edit_sale_id').value = saleId;
if (videoPhase.toLowerCase() === "record") {
document.getElementById('video-edit-standard').style.display = 'none';
document.getElementById('video-edit-record').style.display = 'block';
document.getElementById('video_edit_product_name_ro').innerText = productName;
document.getElementById('video_edit_product_name_hidden').value = productName;
} else {
document.getElementById('video-edit-standard').style.display = 'block';
document.getElementById('video-edit-record').style.display = 'none';
document.getElementById('video_edit_product_name').value = productName;
document.getElementById('video_edit_proof_link').value = workProofLink;
var totSec = parseInt(videoDuration) || 0;
var hh = Math.floor(totSec / 3600);
var rem = totSec % 3600;
var mm = Math.floor(rem / 60);
var ss = rem % 60;
document.getElementById('video_edit_duration_h').value = (hh < 10 ? "0" + hh : hh);
document.getElementById('video_edit_duration_m').value = (mm < 10 ? "0" + mm : mm);
document.getElementById('video_edit_duration_s').value = (ss < 10 ? "0" + ss : ss);
}
if (videoPhase.toLowerCase() === "record") {
document.getElementById('video-edit-modal-title').innerText = "Edit Video 'Record' Sale";
} else if (videoPhase.toLowerCase() === "edit_upload") {
document.getElementById('video-edit-modal-title').innerText = "Edit Video 'Edit & Upload' Sale";
} else if (videoPhase.toLowerCase() === "a_to_z_complete") {
document.getElementById('video-edit-modal-title').innerText = "Edit Video 'A to Z Complete' Sale";
} else {
document.getElementById('video-edit-modal-title').innerText = "Edit Video Sale";
}
document.getElementById('video-edit-modal').style.display = 'block';
});
});
document.getElementById('video_edit_update_btn').addEventListener('click', function () {
var saleId = document.getElementById('video_edit_sale_id').value;
var videoStandard = document.getElementById('video-edit-standard');
var videoRecord = document.getElementById('video-edit-record');
var fd = new FormData();
fd.append('sale_id', saleId);
if (videoRecord.style.display !== 'none') {
var uploadedSaleId = document.getElementById('uploaded_sale_id').value.trim();
if (uploadedSaleId === "") {
alert("Uploaded Sale ID is required for Record work type.");
return;
}
fd.append('uploaded_sale_id', uploadedSaleId);
var hiddenProductName = document.getElementById('video_edit_product_name_hidden').value.trim();
fd.append('product_name', hiddenProductName);
fd.append('video_duration', 0);
fd.append('video_phase', 'Record');
} else {
var productName = document.getElementById('video_edit_product_name').value.trim();
var workProofLink = document.getElementById('video_edit_proof_link').value.trim();
var hh = parseInt(document.getElementById('video_edit_duration_h').value) || 0;
var mm = parseInt(document.getElementById('video_edit_duration_m').value) || 0;
var ss = parseInt(document.getElementById('video_edit_duration_s').value) || 0;
var totalSec = hh * 3600 + mm * 60 + ss;
if (totalSec < 10) {
alert("Minimum 10 seconds is required for video duration.");
return;
}
fd.append('product_name', productName);
fd.append('work_proof_link', workProofLink);
fd.append('video_duration', totalSec);
fd.append('video_phase', 'edit_upload');
}
fd.append('action', 'bsm_update_video_sale');
fd.append('nonce', '<?php echo wp_create_nonce("bsm_update_video_sale"); ?>');
fetch(ajaxurl, {
method: 'POST',
body: fd
})
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(text || response.status); });
}
return response.json();
})
.then(data => {
if (data.success) {
alert(data.data.message);
document.getElementById('video-edit-modal').style.display = 'none';
location.reload();
} else {
alert("Error: " + (data.data ? data.data : "Unknown error"));
}
})
.catch(error => {
logJsError(error.message);
alert("AJAX Error: " + error.message);
});
});
document.querySelectorAll('.other-edit-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var saleId = this.getAttribute('data-sale-id');
var group = this.getAttribute('data-group') || "Other";
var productName = this.getAttribute('data-product_name') || "";
var proofText = this.getAttribute('data-proof_text') || "";
var proofLink = this.getAttribute('data-work_proof_link') || "";
var proofScreenShort = this.getAttribute('data-proof_screen_short') || "";
var numberOfJobs = this.getAttribute('data-number_of_jobs') || "";
document.getElementById('other_edit_sale_id').value = saleId;
document.getElementById('other_edit_product_name').value = productName;
document.getElementById('other_edit_proof_text').value = proofText;
document.getElementById('other_edit_proof_link').value = proofLink;
document.getElementById('other_edit_number_of_jobs').value = numberOfJobs;
document.getElementById('other-edit-modal').querySelector('h2').innerText = "Edit " + group + " Sale";
document.getElementById('other-edit-modal').style.display = 'block';
});
});
document.getElementById('other_edit_update_btn').addEventListener('click', function () {
var saleId = document.getElementById('other_edit_sale_id').value;
var productName = document.getElementById('other_edit_product_name').value.trim();
var proofText = document.getElementById('other_edit_proof_text').value.trim();
var proofLink = document.getElementById('other_edit_proof_link').value.trim();
var numberOfJobs = parseInt(document.getElementById('other_edit_number_of_jobs').value.trim()) || 0;
if (productName.length < 10) {
alert("Product/Work description must be at least 10 characters.");
return;
}
var fd = new FormData();
fd.append('action', 'bsm_update_other_sale');
fd.append('nonce', '<?php echo wp_create_nonce("bsm_update_other_sale"); ?>');
fd.append('sale_id', saleId);
fd.append('product_name', productName);
fd.append('proof_text', proofText);
fd.append('proof_link', proofLink);
var proofScreenInput = document.getElementById('other_edit_proof_screen');
if (proofScreenInput && proofScreenInput.files.length > 0) {
fd.append('proof_screen_short', proofScreenInput.files[0]);
}
fd.append('number_of_jobs', numberOfJobs);
fetch(ajaxurl, {
method: 'POST',
body: fd
})
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(text || response.status); });
}
return response.json();
})
.then(data => {
if (data.success) {
alert(data.data.message);
document.getElementById('other-edit-modal').style.display = 'none';
location.reload();
} else {
alert("Error: " + (data.data ? data.data : "Unknown error"));
}
})
.catch(error => {
logJsError(error.message);
alert("AJAX Error: " + error.message);
});
});
document.querySelectorAll('.service-edit-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var saleId = this.getAttribute('data-sale-id');
var productName = this.getAttribute('data-product_name') || "";
var purchasePrice = this.getAttribute('data-purchase_price') || "";
var sellingPrice = this.getAttribute('data-selling_price') || "";
var note = this.getAttribute('data-note') || "";
document.getElementById('service_edit_sale_id').value = saleId;
document.getElementById('service_edit_product_name').innerText = productName;
document.getElementById('service_edit_purchase_price').value = purchasePrice;
document.getElementById('service_edit_selling_price').value = sellingPrice;
document.getElementById('service_edit_note').value = note;
document.getElementById('service-edit-modal').style.display = 'block';
});
});
document.getElementById('service_edit_update_btn').addEventListener('click', function () {
var saleId = document.getElementById('service_edit_sale_id').value;
var note = document.getElementById('service_edit_note').value.trim();
var purchasePrice = parseFloat(document.getElementById('service_edit_purchase_price').value) || 0;
var sellingPrice = parseFloat(document.getElementById('service_edit_selling_price').value) || 0;
var fd = new FormData();
fd.append('action', 'bsm_update_service_sale');
fd.append('nonce', '<?php echo wp_create_nonce("bsm_update_service_sale"); ?>');
fd.append('sale_id', saleId);
fd.append('purchase_price', purchasePrice);
fd.append('selling_price', sellingPrice);
fd.append('note', note);
fetch(ajaxurl, {
method: 'POST',
body: fd
})
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(text || response.status); });
}
return response.json();
})
.then(data => {
if (data.success) {
alert(data.data.message);
document.getElementById('service-edit-modal').style.display = 'none';
location.reload();
} else {
alert("Error: " + (data.data ? data.data : "Unknown error"));
}
})
.catch(error => {
logJsError(error.message);
alert("AJAX Error: " + error.message);
});
});
document.getElementById('video-edit-close').addEventListener('click', function () {
document.getElementById('video-edit-modal').style.display = 'none';
});
document.getElementById('other-edit-close').addEventListener('click', function () {
document.getElementById('other-edit-modal').style.display = 'none';
});
document.getElementById('service-edit-close').addEventListener('click', function () {
document.getElementById('service-edit-modal').style.display = 'none';
});
window.addEventListener('click', function (e) {
if (e.target === document.getElementById('video-edit-modal')) {
document.getElementById('video-edit-modal').style.display = 'none';
}
if (e.target === document.getElementById('other-edit-modal')) {
document.getElementById('other-edit-modal').style.display = 'none';
}
if (e.target === document.getElementById('service-edit-modal')) {
document.getElementById('service-edit-modal').style.display = 'none';
}
if (e.target === document.getElementById('log-modal')) {
document.getElementById('log-modal').style.display = 'none';
}
});
document.querySelectorAll('.status-update-btn').forEach(function(b) {
b.addEventListener('click', function() {
var saleId = this.getAttribute('data-sale-id');
var parent = this.parentNode;
var sel = parent.querySelector('.status-dropdown');
if (!sel) { alert("Dropdown not found."); return; }
var newSt = sel.value;
if (newSt === "") { alert("দয়া করে নতুন স্ট্যাটাস নির্বাচন করুন।"); return; }
if (!confirm("নিশ্চিত করুন, স্ট্যাটাস '" + newSt + "' এ পরিবর্তন করবেন?")) return;
var data = new URLSearchParams();
data.append("action", "bsm_status_update");
data.append("sale_id", saleId);
data.append("new_status", newSt);
data.append("nonce", "<?php echo wp_create_nonce('bsm_status_update'); ?>");
fetch(ajaxurl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: data.toString()
})
.then(r => { if (!r.ok) return r.text().then(tx => { throw new Error(tx || r.status); }); return r.json(); })
.then(res => {
if (res.success) {
alert(res.data.message);
location.reload();
} else {
alert("Error: " + (res.data ? res.data : "Unknown error"));
}
})
.catch(er => { logJsError(er.message); alert("AJAX error:" + er); });
});
});
document.querySelectorAll('.note-update-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var sid = this.getAttribute('data-sale-id');
var pr = this.parentNode;
var noteArea = pr.querySelector('.note-input');
if (!noteArea) { alert("Note input not found!"); return; }
var txt = noteArea.value.trim();
if (txt === "") { alert("Please enter a note first."); return; }
var fd = new FormData();
fd.append("action", "bsm_statas_log_add");
fd.append("nonce", "<?php echo wp_create_nonce('bsm_statas_log'); ?>");
fd.append("sale_id", sid);
fd.append("note_text", txt);
fd.append("note_type", "private");
fetch(ajaxurl, {
method: "POST",
body: fd
})
.then(r => { if (!r.ok) return r.text().then(tx => { throw new Error(tx || r.status); }); return r.json(); })
.then(data => {
if (data.success) {
alert(data.data.message);
noteArea.value = "";
var logc = document.getElementById("log-column-" + sid);
if (logc) {
logc.innerHTML = data.data.logs_html;
rebindMoreLogLinks();
}
} else {
alert("Error: " + (data.data ? data.data : "Unknown error"));
}
})
.catch(e => { logJsError(e.message); alert("AJAX Error:" + e); });
});
});
function rebindMoreLogLinks() {
document.querySelectorAll(".more-log-link").forEach(function(ml) {
ml.addEventListener('click', function(e) {
e.preventDefault();
var sid = this.getAttribute('data-sale-id');
var type = this.getAttribute('data-type') || 'status';
var fm = new FormData();
fm.append("action", "bsm_fetch_all_logs");
fm.append("sale_id", sid);
fm.append("log_type", type);
fm.append("nonce", "<?php echo wp_create_nonce('bsm_note_nonce'); ?>");
fetch(ajaxurl, {
method: "POST",
body: fm
})
.then(rr => { if (!rr.ok) return rr.text().then(tx => { throw new Error(tx || rr.status); }); return rr.json(); })
.then(rrr => {
if (rrr.success) {
showModal(rrr.data.html);
} else {
alert("Error: " + (rrr.data ? rrr.data : "Unknown error"));
}
})
.catch(er => { logJsError(er.message); alert("AJAX error:" + er); });
});
});
}
rebindMoreLogLinks();
var modalEl = document.getElementById("log-modal");
var modalContent = document.getElementById("log-modal-content");
var closeX = document.querySelector("#log-modal .close");
closeX.addEventListener('click', function() {
modalEl.style.display = 'none';
});
window.addEventListener('click', function(e) {
if (e.target === modalEl) {
modalEl.style.display = 'none';
}
});
function showModal(html) {
modalContent.innerHTML = html;
modalEl.style.display = 'block';
}
});
</script>
</body>
</html>
<?php
/**
* File: business-seller-management/templates/seller/point-report.php
*
* Description:
* - সেলার তার পয়েন্ট ব্যালেন্স (আজকের, গতকালের, মাসিক, বাৎসরিক ও আল টাইম) দেখতে পারবেন।
* - ইনকাম হিস্ট্রি বিভাগে টেবিলে কলামগুলো থাকবে:
* SN | Date & Time | Sale ID | Sale Type | Sale Name | Paused P.B | Point B | Action
* - Date & Time কলামে সময় (h:i A) ও তারিখ (d/m/Y) দেখানো হবে।
* - টেবিল ডিফল্টভাবে Date & Time (DESC) অনুযায়ী সাজানো থাকবে।
* - ব্যবহারকারী নির্দিষ্ট মাস ও সাল নির্বাচন করে আল টাইম ইনকাম হিস্ট্রি রিপোর্ট দেখতে পারবেন, pagination সহ।
*
* - **নতুন: পয়েন্ট লেভেল প্রদর্শনী সেকশন**:
* - পেজের উপরে ৩টি সেকশনে বিভক্ত থাকবে: বাৎসরিক, মাসিক, ও দৈনিক লেভেল।
* - প্রতিটি লেভেল বক্সে (L1, L2, ...) তাদের নাম ও নম্বর দেখানো হবে; বর্তমান লেভেল বক্সটি একটু বড় দেখানো হবে।
* - লেভেল বক্সে ক্লিক করলে নিচের "levelMessage" সেকশনে সংশ্লিষ্ট লেভেলের বিস্তারিত বার্তা দেখাবে।
*
* - **নতুন: ইনকাম হিস্ট্রি হেডার** – প্রতিটি ইনকাম হিস্ট্রি সেকশনের header–এ, বাম পাশে টেবিলের শিরোনাম ও তারিখ, এবং header–এর ডান পাশে স্ট্যাটাস সারাংশ (স্ট্যাটাস আইকন ও সংখ্যা) দেখানো হবে।
*
* - "Paused P.B" কলামে, যদি স্ট্যাটাস 'Successful All' বা 'Delivery Done' না হয়, তাহলে মোট পয়েন্ট (total_points) দেখানো হবে, সাথে স্ট্যাটাস আইকন।
* - "Point B" কলামে, যদি স্ট্যাটাস 'Successful All' (এবং bonus entry) হয়, তাহলে মোট পয়েন্ট দেখানো হবে, যার পাশে ✔ আইকন দেখানো হবে।
* - "Action" কলামে, প্রতিটি বিক্রয়ের বর্তমান স্ট্যাটাসের আইকন ও রঙ দেখানো হবে।
*
* - **নতুন: ব্যালেন্স সেকশন এখন "পয়েন্ট" নামে দেখানো হবে** – যেমন: "আজকের Point", "গতকালের Point", ইত্যাদি।
*
* - **নতুন: Bonus Entry Insertion**:
* - যখন Daily, Monthly বা Yearly level cross হবে, তখন স্বয়ংক্রিয়ভাবে bonus entry insert হবে।
* - Bonus entry-এর ক্ষেত্রে:
* - Sale Type: (খালি থাকবে)
* - Sale Name: "Daily/Monthly/Yearly Level X: {Level Name} Bonus" – যা bsm_points টেবিলের description এর মতো হবে।
* - Status: "Successful All"
* - Point B: bonus পয়েন্ট (✔ আইকন সহ)
* - Action: "Successful All" (✔ আইকন ও নির্দিষ্ট সবুজ রঙে)
* - যদি নির্দিষ্ট সময়ের (ঐ দিন/ঐ মাস/ঐ বছর) জন্য bonus entry ইতিমধ্যে exists থাকে (description এর ভিত্তিতে), তাহলে পুনরায় bonus প্রদান করা হবে না; তবে যদি bonus entryটি ডিলিট হয়ে যায়, তাহলে পরবর্তী level cross এর সময় bonus পুনরায় insert হবে।
*
* - **নতুন: Level Crossing-এর ক্ষেত্রে শুধুমাত্র "Point B" কলামের (অর্থাৎ, bonus) পয়েন্টের উপর ভিত্তি করে লেভেল উত্তীর্ণ নির্ধারণ করা হবে।**
*
* - বাকি সকল বৈশিষ্ট্য অপরিবর্তিত।
*/
// Security check
if ( ! defined('ABSPATH') ) {
exit;
}
$search_query = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
$current_user_id = get_current_user_id();
if (!$current_user_id) {
echo "<p style='color:red; font-weight:bold;'>You must be logged in as a seller to view this page.</p>";
return;
}
global $wpdb;
/*-------------- Helper Functions --------------*/
/**
* Get total points sum for a given date range.
*/
function bsm_get_points_sum_range($user_id, $startDate = null, $endDate = null, $search_query = '', $pointType = '') {
global $wpdb;
$table_points = $wpdb->prefix . 'bsm_points';
$where = $wpdb->prepare("WHERE user_id = %d", $user_id);
if ($startDate) {
$startDateTime = $startDate . " 00:00:00";
$where .= $wpdb->prepare(" AND created_at >= %s", $startDateTime);
}
if ($endDate) {
$endDateTime = $endDate . " 23:59:59";
$where .= $wpdb->prepare(" AND created_at <= %s", $endDateTime);
}
if (!empty($pointType)) {
if ($pointType === 'daily') {
$where .= " AND type IN ('daily','daily_bonus')";
} else {
$where .= $wpdb->prepare(" AND type = %s", $pointType);
}
}
if (!empty($search_query)) {
$like = '%' . $wpdb->esc_like($search_query) . '%';
$where .= $wpdb->prepare(" AND (description LIKE %s) ", $like);
}
$sql = "SELECT IFNULL(SUM(points), 0) FROM $table_points $where";
return (float) $wpdb->get_var($sql);
}
/**
* Get sum of Point B (only for entries with status 'Successful All' or 'Delivery Done')
* এবং শুধুমাত্র সেই রো গুলো যা type IN ('base','daily','daily_bonus','bonus') – অর্থাৎ, যেই রো গুলোই "Point B" কলামে দেখানো হয়।
*/
function bsm_get_pointB_sum_range($user_id, $startDate = null, $endDate = null, $search_query = '') {
global $wpdb;
$table_points = $wpdb->prefix . 'bsm_points';
$table_sales = $wpdb->prefix . 'bsm_sales';
$where = $wpdb->prepare("WHERE p.user_id = %d", $user_id);
if ($startDate) {
$startDateTime = $startDate . " 00:00:00";
$where .= $wpdb->prepare(" AND p.created_at >= %s", $startDateTime);
}
if ($endDate) {
$endDateTime = $endDate . " 23:59:59";
$where .= $wpdb->prepare(" AND p.created_at <= %s", $endDateTime);
}
if (!empty($search_query)) {
$like = '%' . $wpdb->esc_like($search_query) . '%';
$where .= $wpdb->prepare(" AND (p.description LIKE %s) ", $like);
}
// শুধুমাত্র সেই রো গুলো বিবেচনায় নেওয়া হবে যেগুলোতে type আছে ('base','daily','daily_bonus','bonus')
$where .= " AND p.type IN ('base','daily','daily_bonus','bonus')";
// Only include points from sales with status 'Successful All' or 'Delivery Done'
$where .= " AND p.sale_id IN (SELECT id FROM $table_sales WHERE status IN ('Successful All','Delivery Done'))";
$sql = "SELECT IFNULL(SUM(p.points), 0) FROM $table_points p $where";
return (float) $wpdb->get_var($sql);
}
/**
* Get individual daily entries for a given date range.
*/
function bsm_get_daily_entries($user_id, $startDate = null, $endDate = null, $search_query = '') {
global $wpdb;
$table_points = $wpdb->prefix . 'bsm_points';
$startDateTime = $startDate ? $startDate . " 00:00:00" : null;
$endDateTime = $endDate ? $endDate . " 23:59:59" : null;
$sql = $wpdb->prepare("
SELECT
sale_id,
MIN(created_at) AS created_at,
SUM(points) AS total_points,
MAX(CASE WHEN type = 'base' THEN points ELSE 0 END) AS base_points,
MAX(type) AS entry_type,
MAX(description) AS description
FROM $table_points
WHERE user_id = %d
AND created_at >= %s
AND created_at <= %s
AND type IN ('base','daily','daily_bonus','bonus')
GROUP BY sale_id
ORDER BY created_at DESC
", $user_id, $startDateTime, $endDateTime);
$results = $wpdb->get_results($sql);
return is_array($results) ? $results : array();
}
/**
* Render the status summary bar as HTML boxes.
*/
function render_status_summary_bar($entries, $dummy = "", $dummy2 = "") {
global $wpdb;
$statusCounts = array();
foreach ($entries as $entry) {
$sale = $wpdb->get_row($wpdb->prepare("SELECT status FROM {$wpdb->prefix}bsm_sales WHERE id = %d", $entry->sale_id));
if ($sale && !empty($sale->status)) {
$status = $sale->status;
if (!isset($statusCounts[$status])) {
$statusCounts[$status] = 0;
}
$statusCounts[$status]++;
}
}
if (empty($statusCounts)) {
return "";
}
$groupA = array("Successful All", "pending", "In Process", "Need Parts", "Refund Required", "Refund Done");
$groupB = array("On Hold", "Success But Not Delivery", "Parts Brought", "Review Apply", "Block");
$ordered = array();
foreach ($groupA as $stat) {
if (isset($statusCounts[$stat])) {
$ordered[$stat] = $statusCounts[$stat];
unset($statusCounts[$stat]);
}
}
foreach ($groupB as $stat) {
if (isset($statusCounts[$stat])) {
$ordered[$stat] = $statusCounts[$stat];
unset($statusCounts[$stat]);
}
}
if (!empty($statusCounts)) {
ksort($statusCounts);
foreach ($statusCounts as $stat => $cnt) {
$ordered[$stat] = $cnt;
}
}
$maxBoxes = 7;
if (count($ordered) > $maxBoxes) {
$displayBoxes = array();
$i = 0;
$otherSum = 0;
foreach ($ordered as $stat => $cnt) {
if ($i < ($maxBoxes - 1)) {
$displayBoxes[$stat] = $cnt;
} else {
$otherSum += $cnt;
}
$i++;
}
if ($otherSum > 0) {
$displayBoxes["Other"] = $otherSum;
}
$ordered = $displayBoxes;
}
$statusMapping = array(
"Successful All" => array('icon' => '✔', 'color' => '#32CD32'),
"pending" => array('icon' => '⏳', 'color' => '#FFA500'),
"In Process" => array('icon' => '🔄', 'color' => '#0000FF'),
"Need Parts" => array('icon' => '⚙️', 'color' => '#8B0000'),
"Refund Required" => array('icon' => '💸', 'color' => '#FF6347'),
"Refund Done" => array('icon' => '💵', 'color' => '#32CD32'),
"On Hold" => array('icon' => '⏸', 'color' => '#808080'),
"Success But Not Delivery" => array('icon' => '❎', 'color' => '#FF4500'),
"Parts Brought" => array('icon' => '📦', 'color' => '#FF8C00'),
"Review Apply" => array('icon' => '🔍', 'color' => '#FFD700'),
"Block" => array('icon' => '🚫', 'color' => '#A52A2A'),
"Other" => array('icon' => '', 'color' => '#666666')
);
$html = "";
foreach ($ordered as $stat => $cnt) {
if ($cnt > 0) {
$icon = isset($statusMapping[$stat]) ? $statusMapping[$stat]['icon'] : "";
$color = isset($statusMapping[$stat]) ? $statusMapping[$stat]['color'] : "#CCCCCC";
$html .= '<span class="status-box" style="background-color:' . $color . ';">';
$html .= $icon . " " . $stat . " " . $cnt;
$html .= '</span>';
}
}
return $html;
}
/*-------------- Date Variables --------------*/
$now = current_time('timestamp');
$todayDate = date('Y-m-d', $now);
$yesterdayDate = date('Y-m-d', $now - 86400);
$thisMonthStart = date('Y-m-01', $now);
$thisMonthEnd = date('Y-m-t', $now);
$lastMonthStart = date('Y-m-01', strtotime('-1 month', $now));
$lastMonthEnd = date('Y-m-t', strtotime('-1 month', $now));
$current_year = date('Y', $now);
$year_start = $current_year . '-01-01';
$year_end = $current_year . '-12-31';
$last_year = date('Y', strtotime('-1 year', $now));
$last_year_start = $last_year . '-01-01';
$last_year_end = $last_year . '-12-31';
/*-------------- All Time (Report) - Month/Year Filter --------------*/
$report_year = isset($_GET['report_year']) ? intval($_GET['report_year']) : date('Y', $now);
$report_month = isset($_GET['report_month']) ? intval($_GET['report_month']) : date('n', $now);
$report_month_start = sprintf("%04d-%02d-01", $report_year, $report_month);
$report_month_end = date("Y-m-t", strtotime($report_month_start));
$per_page_all = 20;
$page_all = isset($_GET['page_all']) ? max(1, intval($_GET['page_all'])) : 1;
$offset_all = ($page_all - 1) * $per_page_all;
$table_points = $wpdb->prefix . 'bsm_points';
$where_all = $wpdb->prepare("WHERE user_id = %d AND created_at BETWEEN %s AND %s", $current_user_id, $report_month_start . " 00:00:00", $report_month_end . " 23:59:59");
if (!empty($search_query)) {
$like = '%' . $wpdb->esc_like($search_query) . '%';
$where_all .= $wpdb->prepare(" AND (description LIKE %s) ", $like);
}
$where_all .= " AND type IN ('daily', 'daily_bonus','base','bonus')";
$sql_all = "SELECT sale_id, MIN(created_at) AS created_at, SUM(points) AS total_points, MAX(CASE WHEN type = 'base' THEN points ELSE 0 END) AS base_points FROM $table_points $where_all GROUP BY sale_id ORDER BY created_at DESC LIMIT $offset_all, $per_page_all";
$all_time_income_history = $wpdb->get_results($sql_all);
if (!is_array($all_time_income_history)) {
$all_time_income_history = array();
}
$total_all_time = $wpdb->get_var("SELECT COUNT(DISTINCT sale_id) FROM $table_points $where_all");
/*-------------- New Balance Calculations using Point B --------------*/
$today_point = bsm_get_pointB_sum_range($current_user_id, $todayDate, $todayDate, $search_query);
$yesterday_point = bsm_get_pointB_sum_range($current_user_id, $yesterdayDate, $yesterdayDate, $search_query);
$this_month_point = bsm_get_pointB_sum_range($current_user_id, $thisMonthStart, $thisMonthEnd, $search_query);
$last_month_point = bsm_get_pointB_sum_range($current_user_id, $lastMonthStart, $lastMonthEnd, $search_query);
$this_year_point = bsm_get_pointB_sum_range($current_user_id, $year_start, $year_end, $search_query);
$last_year_point = bsm_get_pointB_sum_range($current_user_id, $last_year_start, $last_year_end, $search_query);
$total_point = bsm_get_pointB_sum_range($current_user_id, null, null, $search_query);
/*-------------- Option values for level thresholds --------------*/
// Admin Settings থেকে level related setting নেওয়া হচ্ছে
$daily_levels_bonus = (array)get_option('bsm_daily_levels_bonus', array(
1 => array('name'=>'Newbie Seller','range'=>199,'bonus'=>0,'enabled'=>1),
2 => array('name'=>'Starter Seller','range'=>200,'bonus'=>30,'enabled'=>1),
3 => array('name'=>'Active Seller','range'=>300,'bonus'=>50,'enabled'=>1),
));
ksort($daily_levels_bonus);
$monthly_levels = (array)get_option('bsm_monthly_levels', array(
1 => array('name'=>'Default Level (Monthly)','threshold'=>0,'bonus'=>0,'enabled'=>1),
2 => array('name'=>'Fast Achiever', 'threshold' => 16000, 'bonus'=>1000,'enabled'=>1),
3 => array('name'=>'Sales Master', 'threshold' => 23000, 'bonus'=>1500,'enabled'=>1),
4 => array('name'=>'Power Seller', 'threshold' => 30000, 'bonus'=>2000,'enabled'=>1),
5 => array('name'=>'Top Performer', 'threshold' => 35000, 'bonus'=>3000,'enabled'=>1),
));
ksort($monthly_levels);
$yearly_levels = (array)get_option('bsm_yearly_levels', array(
1 => array('name'=>'Default Level (Yearly)','threshold'=>0,'bonus'=>0,'enabled'=>1),
2 => array('name'=>'Ultimate Seller', 'threshold' => 250000, 'bonus'=>2000,'enabled'=>1),
3 => array('name'=>'Market King/Queen', 'threshold' => 300000, 'bonus'=>3000,'enabled'=>1),
4 => array('name'=>'Sales Tycoon', 'threshold' => 360000, 'bonus'=>4000,'enabled'=>1),
5 => array('name'=>'Empire Builder', 'threshold' => 456000, 'bonus'=>5000,'enabled'=>1),
));
ksort($yearly_levels);
/*-------------- Define color arrays for level boxes --------------*/
$annual_colors = array('#FFCDD2', '#F8BBD0', '#E1BEE7', '#D1C4E9', '#C5CAE9');
$monthly_colors = array('#BBDEFB', '#B3E5FC', '#B2EBF2', '#B2DFDB', '#C8E6C9');
$daily_colors = array('#DCEDC8', '#F0F4C3', '#FFF9C4', '#FFECB3', '#FFE0B2', '#FFCCBC', '#D7CCC8', '#CFD8DC', '#E0E0E0', '#F5F5F5');
/**
* Determine current level based on point (Point B) and levels.
*/
function determine_current_level($point, $levels, $type = 'daily') {
$current_level = 1;
if ($type === 'daily') {
foreach ($levels as $lvl_num => $lvl) {
if (intval($lvl['enabled']) === 1 && $point >= floatval($lvl['range'])) {
$current_level = $lvl_num;
} else {
break;
}
}
} else {
foreach ($levels as $lvl_num => $lvl) {
if (intval($lvl['enabled']) === 1 && $point >= floatval($lvl['threshold'])) {
$current_level = $lvl_num;
} else {
break;
}
}
}
return $current_level;
}
$current_daily_level = determine_current_level($today_point, $daily_levels_bonus, 'daily');
$current_monthly_level = determine_current_level($this_month_point, $monthly_levels, 'monthly');
$current_annual_level = determine_current_level($this_year_point, $yearly_levels, 'annual');
$serverTime = date('h:i A', $now);
$serverDate = date('d/m/Y', $now);
/*-------------- Bonus Entry Insertion (Daily) --------------*/
foreach ($daily_levels_bonus as $lvl_num => $lvl) {
if (intval($lvl['enabled']) === 1 && floatval($lvl['bonus']) > 0) {
// লেভেল ক্রসিং: শুধুমাত্র "Point B" কলামের মোট পয়েন্ট (today_point) ভিত্তিতে তুলনা
if ($today_point >= floatval($lvl['range'])) {
$bonus_name = "Daily Level " . $lvl_num . ": " . $lvl['name'] . " Bonus";
$existing_bonus = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}bsm_points WHERE user_id = %d AND type = 'bonus' AND description = %s AND DATE(created_at) = %s",
$current_user_id, $bonus_name, $todayDate
));
if ($existing_bonus < 1) {
$current_time = current_time('mysql');
$last_sale_id = $wpdb->get_var("SELECT MAX(id) FROM {$wpdb->prefix}bsm_sales");
$new_sale_id = ($last_sale_id) ? $last_sale_id + 1 : 1;
$data = array(
'id' => $new_sale_id,
'seller_id' => $current_user_id,
'product_name' => $bonus_name,
'sale_type' => '',
'status' => 'Successful All',
'created_at' => $current_time,
);
$format = array('%d','%d','%s','%s','%s','%s');
$wpdb->insert($wpdb->prefix . 'bsm_sales', $data, $format);
$bonus_sale_id = $new_sale_id;
$points_data = array(
'user_id' => $current_user_id,
'sale_id' => $bonus_sale_id,
'points' => floatval($lvl['bonus']),
'type' => 'bonus',
'description' => $bonus_name,
'created_at' => $current_time,
);
$points_format = array('%d','%d','%f','%s','%s','%s');
$wpdb->insert($wpdb->prefix . 'bsm_points', $points_data, $points_format);
}
}
}
}
/*-------------- Bonus Entry Insertion (Monthly) --------------*/
foreach ($monthly_levels as $lvl_num => $lvl) {
if (intval($lvl['enabled']) === 1 && floatval($lvl['bonus']) > 0) {
if ($this_month_point >= floatval($lvl['threshold'])) {
$bonus_name = "Monthly Level " . $lvl_num . ": " . $lvl['name'] . " Bonus";
$current_year_val = date('Y', $now);
$current_month_val = date('m', $now);
$existing_bonus = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}bsm_points WHERE user_id = %d AND type = 'bonus' AND description = %s AND YEAR(created_at) = %d AND MONTH(created_at) = %d",
$current_user_id, $bonus_name, $current_year_val, $current_month_val
));
if ($existing_bonus < 1) {
$current_time = current_time('mysql');
$last_sale_id = $wpdb->get_var("SELECT MAX(id) FROM {$wpdb->prefix}bsm_sales");
$new_sale_id = ($last_sale_id) ? $last_sale_id + 1 : 1;
$data = array(
'id' => $new_sale_id,
'seller_id' => $current_user_id,
'product_name' => $bonus_name,
'sale_type' => '',
'status' => 'Successful All',
'created_at' => $current_time,
);
$format = array('%d','%d','%s','%s','%s','%s');
$wpdb->insert($wpdb->prefix . 'bsm_sales', $data, $format);
$bonus_sale_id = $new_sale_id;
$points_data = array(
'user_id' => $current_user_id,
'sale_id' => $bonus_sale_id,
'points' => floatval($lvl['bonus']),
'type' => 'bonus',
'description' => $bonus_name,
'created_at' => $current_time,
);
$points_format = array('%d','%d','%f','%s','%s','%s');
$wpdb->insert($wpdb->prefix . 'bsm_points', $points_data, $points_format);
}
}
}
}
/*-------------- Bonus Entry Insertion (Yearly) --------------*/
foreach ($yearly_levels as $lvl_num => $lvl) {
if (intval($lvl['enabled']) === 1 && floatval($lvl['bonus']) > 0) {
if ($this_year_point >= floatval($lvl['threshold'])) {
$bonus_name = "Yearly Level " . $lvl_num . ": " . $lvl['name'] . " Bonus";
$current_year_val = date('Y', $now);
$existing_bonus = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->prefix}bsm_points WHERE user_id = %d AND type = 'bonus' AND description = %s AND YEAR(created_at) = %d",
$current_user_id, $bonus_name, $current_year_val
));
if ($existing_bonus < 1) {
$current_time = current_time('mysql');
$last_sale_id = $wpdb->get_var("SELECT MAX(id) FROM {$wpdb->prefix}bsm_sales");
$new_sale_id = ($last_sale_id) ? $last_sale_id + 1 : 1;
$data = array(
'id' => $new_sale_id,
'seller_id' => $current_user_id,
'product_name' => $bonus_name,
'sale_type' => '',
'status' => 'Successful All',
'created_at' => $current_time,
);
$format = array('%d','%d','%s','%s','%s','%s');
$wpdb->insert($wpdb->prefix . 'bsm_sales', $data, $format);
$bonus_sale_id = $new_sale_id;
$points_data = array(
'user_id' => $current_user_id,
'sale_id' => $bonus_sale_id,
'points' => floatval($lvl['bonus']),
'type' => 'bonus',
'description' => $bonus_name,
'created_at' => $current_time,
);
$points_format = array('%d','%d','%f','%s','%s','%s');
$wpdb->insert($wpdb->prefix . 'bsm_points', $points_data, $points_format);
}
}
}
}
/**
* Function to display income table.
* 8টি কলাম: SN, Date & Time, Sale ID, Sale Type, Sale Name, Paused P.B, Point B, Action.
* - যদি bonus entry (type bonus) হয়, তাহলে:
* Sale Type: (খালি থাকবে)
* Sale Name: bonus entry এর বর্ণনা (যেমন "Daily Level 3: Active Seller Bonus")
* Sale ID: bonus entry এর সঠিক auto-generated id দেখাবে
* Point B: bonus entry এর পয়েন্ট (✔ আইকন সহ)
* Action: শুধুমাত্র Status কলামে "Successful All" (✔ আইকন সহ নির্দিষ্ট সবুজ রঙে)
* - অন্যান্য ক্ষেত্রে, পূর্বের মতো প্রদর্শন হবে।
*/
function display_income_table($entries, $title) {
$entries = (array)$entries;
if(empty($entries)) {
echo "<tr><td colspan='8'>{$title} পাওয়া যায়নি</td></tr>";
return;
}
$sn = 1;
$sum_paused = 0;
$sum_pointb = 0;
// Status mapping for icons and colors
$statusMapping = array(
'pending' => array('icon' => '⏳', 'color' => '#FFA500'),
'On Hold' => array('icon' => '⏸', 'color' => '#808080'),
'In Process' => array('icon' => '🔄', 'color' => '#0000FF'),
'Need Parts' => array('icon' => '⚙️', 'color' => '#8B0000'),
'Refund Required' => array('icon' => '💸', 'color' => '#FF6347'),
'Refund Done' => array('icon' => '💵', 'color' => '#32CD32'),
'Successful All' => array('icon' => '✔', 'color' => '#32CD32'),
'Delivery Done' => array('icon' => '✔', 'color' => '#32CD32'),
'Check Admin' => array('icon' => '👨💼', 'color' => '#000080'),
'Reject Delivery Done' => array('icon' => '🔴', 'color' => '#FF0000'),
'Cost' => array('icon' => '💰', 'color' => '#8B4513'),
'Cancel' => array('icon' => '❌', 'color' => '#FF0000'),
'Rejected' => array('icon' => '❌', 'color' => '#FF0000'),
'Failed' => array('icon' => '❌', 'color' => '#FF0000'),
'Review Apply' => array('icon' => '🔍', 'color' => '#FFD700')
);
foreach($entries as $entry):
global $wpdb;
// bsm_sales থেকে সেলার ডিটেইলস নেওয়া হচ্ছে
$sale = $wpdb->get_row($wpdb->prepare("SELECT product_name, sale_type, status FROM {$wpdb->prefix}bsm_sales WHERE id = %d", $entry->sale_id));
if (isset($entry->entry_type) && $entry->entry_type == 'bonus') {
// Bonus entry: যদি sale টেবিল থেকে ডাটা না পাওয়া যায়, তাহলে bsm_points টেবিলের description ব্যবহার করা হবে
$sale_name = $sale ? $sale->product_name : $entry->description;
$sale_type = ''; // Bonus এ Sale Type খালি থাকবে
$sale_status = 'Successful All';
$actionDisplay = '<span style="color: ' . $statusMapping['Successful All']['color'] . ';">' . $statusMapping['Successful All']['icon'] . ' Successful All</span>';
$pointBDisplay = ($entry->total_points > 0) ? number_format($entry->total_points, 2) . " ✔" : "";
$pausedDisplay = "";
// bonus entry এর পয়েন্ট যোগ করা হচ্ছে
$sum_pointb += $entry->total_points;
} else {
$sale_name = $sale ? $sale->product_name : 'N/A';
$sale_type = $sale ? $sale->sale_type : '';
$sale_status = $sale ? $sale->status : 'N/A';
if( in_array($sale_status, array('Successful All', 'Delivery Done')) ) {
$pointB = $entry->total_points;
$pausedPB = 0;
} else {
$pausedPB = $entry->total_points;
$pointB = 0;
}
$sum_paused += $pausedPB;
$sum_pointb += $pointB;
$pausedDisplay = ($pausedPB > 0) ? number_format($pausedPB, 2) . " " . (isset($statusMapping[$sale_status]) ? $statusMapping[$sale_status]['icon'] : "") : "";
$pointBDisplay = ($pointB > 0) ? number_format($pointB, 2) . " ✔" : "";
if(isset($statusMapping[$sale_status])) {
$mapping = $statusMapping[$sale_status];
$actionDisplay = '<span style="color: ' . $mapping['color'] . ';">' . $mapping['icon'] . " " . esc_html($sale_status) . '</span>';
} else {
$actionDisplay = esc_html($sale_status);
}
}
?>
<tr>
<td><?php echo $sn++; ?></td>
<td><?php echo esc_html(date('h:i A | d/m/Y', strtotime($entry->created_at))); ?></td>
<td><?php echo esc_html($entry->sale_id); ?></td>
<td><?php echo esc_html($sale_type); ?></td>
<td><?php echo esc_html($sale_name); ?></td>
<td><?php echo $pausedDisplay; ?></td>
<td style="color: #00cc00;"><?php echo $pointBDisplay; ?></td>
<td><?php echo $actionDisplay; ?></td>
</tr>
<?php endforeach; ?>
<tr style="font-weight:bold; background:#f9ffe0;">
<td colspan="5" style="text-align:right;">Total</td>
<td><?php echo ($sum_paused > 0) ? number_format($sum_paused, 2) : ""; ?></td>
<td><?php echo ($sum_pointb > 0) ? number_format($sum_pointb, 2) : ""; ?></td>
<td></td>
</tr>
<?php
}
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title>Seller Point Report</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<style>
/* Container and overall layout */
.point-report-container {
padding: 20px;
background: #f0f8ff;
border: 1px solid #ccc;
border-radius: 5px;
font-family: Arial, sans-serif;
}
h1 { color: #0073aa; }
/* Balance (Point) Summary Boxes */
.balance-summary {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
}
.balance-box {
flex: 1;
min-width: 150px;
padding: 15px;
border: 2px solid #0073aa;
border-radius: 5px;
background: #e0f7fa;
text-align: center;
}
.balance-box h3 {
margin: 0 0 5px;
font-size: 12px;
color: #0073aa;
}
.balance-box p {
font-size: 18px;
font-weight: bold;
margin: 0;
}
/* Search Bar */
.search-bar {
margin-bottom: 20px;
}
.search-bar input[type="text"] {
padding: 8px;
width: 300px;
border: 1px solid #ccc;
border-radius: 3px;
}
.search-bar input[type="submit"] {
padding: 8px 14px;
border: none;
border-radius: 3px;
background: #0073aa;
color: #fff;
cursor: pointer;
}
.search-bar input[type="submit"]:hover {
background: #005177;
}
/* Income History Sections */
.income-history-section {
margin-bottom: 20px;
}
.income-history-header {
background: #0073aa;
color: #fff;
padding: 10px;
cursor: pointer;
border-radius: 3px;
position: relative;
}
.income-history-header:hover {
background-color: #87CEEB;
}
.header-left {
display: inline-block;
}
.header-right {
display: inline-block;
float: right;
}
.income-history-content {
border: 1px solid #0073aa;
border-top: none;
padding: 10px;
display: none;
background: #eaf4ff;
}
.income-history-content table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
.income-history-content th,
.income-history-content td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
.income-history-content th {
background: #f5f5f5;
cursor: pointer;
}
/* Hover effect for table rows */
.income-history-content table tr:hover {
background-color: #87CEEB;
}
/* Table header (8 columns) */
.income-history-content thead tr th:nth-child(1),
.income-history-content thead tr th:nth-child(2),
.income-history-content thead tr th:nth-child(3),
.income-history-content thead tr th:nth-child(4),
.income-history-content thead tr th:nth-child(5),
.income-history-content thead tr th:nth-child(6),
.income-history-content thead tr th:nth-child(7),
.income-history-content thead tr th:nth-child(8) { cursor: pointer; }
/* Pagination */
.pagination {
text-align: center;
margin-top: 10px;
}
.pagination a, .pagination span {
display: inline-block;
padding: 6px 12px;
margin: 0 2px;
border: 1px solid #ccc;
border-radius: 4px;
text-decoration: none;
color: #0073aa;
}
.pagination span.current-page {
background: #0073aa;
color: #fff;
}
/* PDF Download Link */
.download-pdf {
float: right;
background: #28a745;
color: #fff;
padding: 5px 10px;
text-decoration: none;
border-radius: 3px;
margin-bottom: 10px;
}
/* Month/Year Filter Form for All Time Report */
.report-filter {
margin-bottom: 10px;
}
.report-filter select {
padding: 5px;
font-size: 14px;
}
/* New CSS for Point Level Display Section */
.point-level-display {
margin-bottom: 20px;
}
.point-level-section {
margin-bottom: 10px;
}
.point-level-section h2 {
margin: 0 0 5px;
font-size: 14px;
color: #333;
}
.point-level-row {
display: flex;
gap: 2px;
flex-wrap: wrap;
}
.point-level-box {
flex: 1;
min-width: 60px;
padding: 5px;
text-align: center;
font-size: 12px;
cursor: pointer;
border: 1px solid #ccc;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
position: relative;
transition: background-color 0.3s;
}
.point-level-box:hover {
background-color: #f5f5f5;
overflow: visible;
white-space: normal;
}
.point-level-box.unlocked {
background: inherit;
}
.point-level-box.current {
font-size: 14px;
font-weight: bold;
border: 2px solid #000;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transform: scale(1.33);
}
.point-level-box.locked {
opacity: 0.7;
}
.point-level-box.locked::after {
content: "\1F512";
font-size: 16px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Updated Message Box CSS */
.level-message {
margin-top: 10px;
padding: 10px;
border: 2px solid #FFB74D;
background: #FFF3CD;
text-align: center;
font-size: 13px;
font-weight: bold;
border-radius: 5px;
}
/* New CSS for Status Summary Boxes inside header */
.status-box {
border-radius: 5px;
padding: 3px 6px;
margin-left: 10px;
display: inline-block;
font-size: 13px;
color: #fff;
}
</style>
<script>
function toggleHistory(headerElem) {
var contentElem = headerElem.nextElementSibling;
if (contentElem.style.display === "block") {
contentElem.style.display = "none";
} else {
contentElem.style.display = "block";
}
}
/* PDF ডাউনলোড ফাংশন: এখন এটি window.open() ব্যবহার করে নতুন ট্যাবে PDF ডাউনলোড শুরু করবে */
function downloadPdf(type) {
window.open("<?php echo esc_url(plugin_dir_url(__FILE__) . 'pdf-download.php'); ?>?type=" + encodeURIComponent(type), '_blank');
}
function sortTable(tableId, colIndex) {
var table = document.getElementById(tableId);
if(!table) return;
var tbody = table.querySelector("tbody");
if(!tbody) return;
var rows = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
if(rows.length < 2) return;
var sortedAsc = table.getAttribute("data-sort-col") == colIndex && table.getAttribute("data-sort-dir") == "asc";
var sortDir = sortedAsc ? "desc" : "asc";
rows.sort(function(a, b) {
var tdA = a.getElementsByTagName("td")[colIndex];
var tdB = b.getElementsByTagName("td")[colIndex];
if(!tdA || !tdB) return 0;
var valA = tdA.innerText || tdA.textContent;
var valB = tdB.innerText || tdB.textContent;
var numA = parseFloat(valA.replace(/,/g, ""));
var numB = parseFloat(valB.replace(/,/g, ""));
if(!isNaN(numA) && !isNaN(numB)) {
return (sortDir === "asc") ? (numA - numB) : (numB - numA);
} else {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
if(valA < valB) return (sortDir === "asc") ? -1 : 1;
if(valA > valB) return (sortDir === "asc") ? 1 : -1;
return 0;
}
});
while(tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
for(var i = 0; i < rows.length; i++){
tbody.appendChild(rows[i]);
}
table.setAttribute("data-sort-col", colIndex);
table.setAttribute("data-sort-dir", sortDir);
}
/* নতুন: লেভেল বক্সে ক্লিক হলে বিস্তারিত ম্যাসেজ দেখানোর জন্য JavaScript ফাংশন */
var dailyLevels = <?php echo json_encode($daily_levels_bonus); ?>;
var monthlyLevels = <?php echo json_encode($monthly_levels); ?>;
var yearlyLevels = <?php echo json_encode($yearly_levels); ?>;
var currentDailyLevel = <?php echo $current_daily_level; ?>;
var currentMonthlyLevel = <?php echo $current_monthly_level; ?>;
var currentAnnualLevel = <?php echo $current_annual_level; ?>;
var dailyPoint = <?php echo $today_point; ?>;
var monthlyPoint = <?php echo $this_month_point; ?>;
var annualPoint = <?php echo $this_year_point; ?>;
var serverTime = "<?php echo $serverTime; ?>";
var serverDate = "<?php echo $serverDate; ?>";
function showLevelDetails(type, lvlNum, lvlName) {
var currentLevel, currentPoint, levels, typeLabel;
if (type === "daily") {
currentLevel = currentDailyLevel;
currentPoint = dailyPoint;
levels = dailyLevels;
typeLabel = "দৈনিক লেভেল";
} else if (type === "monthly") {
currentLevel = currentMonthlyLevel;
currentPoint = monthlyPoint;
levels = monthlyLevels;
typeLabel = "মাসিক লেভেল";
} else if (type === "annual") {
currentLevel = currentAnnualLevel;
currentPoint = annualPoint;
levels = yearlyLevels;
typeLabel = "বাৎসরিক লেভেল";
}
var message = "";
// Default level (L1)
if (lvlNum == 1) {
if (levels["2"] !== undefined) {
var threshold = (type === "daily") ? parseFloat(levels["2"].range) : parseFloat(levels["2"].threshold);
var required = threshold - currentPoint;
if (required < 0) { required = 0; }
message = typeLabel + ": Default Level L1 এ আছেন। আপনার মান উন্নয়নের জন্য দ্রুত লেভেল অর্জন করুন। পরবর্তী L2: " + levels["2"].name + " টি আনলক করতে প্রয়োজন " + required + " পয়েন্ট।";
} else {
message = typeLabel + ": Default Level L1 এ আছেন।";
}
}
// Already unlocked level
else if (lvlNum < currentLevel) {
var bonus = levels[lvlNum].bonus;
message = typeLabel + ": Level " + lvlNum + ": " + lvlName + " এ উত্তীর্ণ হয়েছেন " + serverTime + " বাজে | বোনাস পেয়েছেন " + bonus + " পয়েন্ট " + "<span style='color:green;'>✔</span>";
}
// Current level
else if (lvlNum == currentLevel) {
var bonus = levels[lvlNum].bonus;
if (levels[parseInt(lvlNum) + 1] !== undefined) {
var nextThreshold = (type === "daily") ? parseFloat(levels[parseInt(lvlNum) + 1].range) : parseFloat(levels[parseInt(lvlNum) + 1].threshold);
var required = nextThreshold - currentPoint;
if (required < 0) { required = 0; }
var nextLevelName = levels[parseInt(lvlNum) + 1].name;
message = typeLabel + ": Level " + lvlNum + ": " + lvlName + " এ আপনাকে স্বাগতম " + "<span style='color:green;'>✔</span>" + " | উত্তীর্ণ হয়েছে " + serverTime + " বাজে | বোনাস পেয়েছেন " + bonus + " পয়েন্ট | পরবর্তী L" + (parseInt(lvlNum) + 1) + ": " + nextLevelName + " টি আনলক করতে প্রয়োজন " + required + " পয়েন্ট।";
} else {
message = typeLabel + ": Level " + lvlNum + ": " + lvlName + " এ আপনাকে স্বাগতম " + "<span style='color:green;'>✔</span>" + " | উত্তীর্ণ হয়েছে " + serverTime + " বাজে | বোনাস পেয়েছেন " + bonus + " পয়েন্ট | সর্বোচ্চ লেভেল অর্জিত।";
}
}
// Locked level
else if (lvlNum > currentLevel) {
var threshold = (type === "daily") ? parseFloat(levels[lvlNum].range) : parseFloat(levels[lvlNum].threshold);
var required = threshold - currentPoint;
if (required < 0) { required = 0; }
message = "<span style='color:red;'>⚠</span> " + typeLabel + ": Level " + lvlNum + ": " + lvlName + " টি এমুহূর্তে লক " + "<span style='color:gray;'>🔒</span>" + " আছে। আনলক করতে প্রয়োজন " + required + " পয়েন্ট। যত বেশি লেভেল অর্জন করবেন, আপনার সুযোগ-সুবিধা ও মূল্যায়ন তত বৃদ্ধি পাবে।";
}
// Append date info for monthly and annual levels
if (type === "monthly" || type === "annual") {
message += " (" + serverDate + ")";
}
document.getElementById("levelMessage").innerHTML = message;
}
</script>
</head>
<body <?php body_class(); ?>>
<div class="point-report-container">
<h1>Seller Point Report</h1>
<!-- নতুন: পয়েন্ট লেভেল প্রদর্শনী সেকশন -->
<div class="point-level-display">
<!-- বাৎসরিক লেভেল -->
<div class="point-level-section">
<div class="point-level-row">
<?php
$i = 1;
foreach($yearly_levels as $lvl_num => $lvl) {
$color = isset($annual_colors[$i-1]) ? $annual_colors[$i-1] : '#CCCCCC';
$class = '';
if($lvl_num < $current_annual_level) {
$class = 'unlocked';
} elseif($lvl_num == $current_annual_level) {
$class = 'current';
} else {
$class = 'locked';
}
$display_name = $lvl['name'];
?>
<div class="point-level-box <?php echo $class; ?>" style="background: <?php echo $color; ?>;"
data-level="<?php echo $lvl_num; ?>" data-type="annual"
title="Level <?php echo $lvl_num; ?>: <?php echo esc_attr($lvl['name']); ?>"
onclick="showLevelDetails('annual', <?php echo $lvl_num; ?>, '<?php echo esc_js($lvl['name']); ?>');">
L<?php echo $lvl_num; ?>: <?php echo esc_html($display_name); ?>
</div>
<?php
$i++;
}
?>
</div>
</div>
<!-- মাসিক লেভেল -->
<div class="point-level-section">
<div class="point-level-row">
<?php
$i = 1;
foreach($monthly_levels as $lvl_num => $lvl) {
$color = isset($monthly_colors[$i-1]) ? $monthly_colors[$i-1] : '#CCCCCC';
$class = '';
if($lvl_num < $current_monthly_level) {
$class = 'unlocked';
} elseif($lvl_num == $current_monthly_level) {
$class = 'current';
} else {
$class = 'locked';
}
$display_name = $lvl['name'];
?>
<div class="point-level-box <?php echo $class; ?>" style="background: <?php echo $color; ?>;"
data-level="<?php echo $lvl_num; ?>" data-type="monthly"
title="Level <?php echo $lvl_num; ?>: <?php echo esc_attr($lvl['name']); ?>"
onclick="showLevelDetails('monthly', <?php echo $lvl_num; ?>, '<?php echo esc_js($lvl['name']); ?>');">
L<?php echo $lvl_num; ?>: <?php echo esc_html($display_name); ?>
</div>
<?php
$i++;
}
?>
</div>
</div>
<!-- দৈনিক লেভেল -->
<div class="point-level-section">
<div class="point-level-row">
<?php
$i = 1;
foreach($daily_levels_bonus as $lvl_num => $lvl) {
$color = isset($daily_colors[$i-1]) ? $daily_colors[$i-1] : '#CCCCCC';
$class = '';
if($lvl_num < $current_daily_level) {
$class = 'unlocked';
} elseif($lvl_num == $current_daily_level) {
$class = 'current';
} else {
$class = 'locked';
}
$display_name = $lvl['name'];
?>
<div class="point-level-box <?php echo $class; ?>" style="background: <?php echo $color; ?>;"
data-level="<?php echo $lvl_num; ?>" data-type="daily"
title="Level <?php echo $lvl_num; ?>: <?php echo esc_attr($lvl['name']); ?>"
onclick="showLevelDetails('daily', <?php echo $lvl_num; ?>, '<?php echo esc_js($lvl['name']); ?>');">
L<?php echo $lvl_num; ?>: <?php echo esc_html($display_name); ?>
</div>
<?php
$i++;
}
?>
</div>
</div>
<!-- বার্তা সেকশন -->
<div id="levelMessage" class="level-message">
বর্তমান দৈনিক লেভেল: L<?php echo $current_daily_level; ?>, বর্তমান পয়েন্ট: <?php echo $today_point; ?>
</div>
</div>
<!-- Balance (Point) Summary Section -->
<div class="balance-summary">
<div class="balance-box">
<h3>আজকের Point</h3>
<p><?php echo esc_html($today_point); ?></p>
</div>
<div class="balance-box">
<h3>গতকালের Point</h3>
<p><?php echo esc_html($yesterday_point); ?></p>
</div>
<div class="balance-box">
<h3>এ মাসের Point</h3>
<p><?php echo esc_html($this_month_point); ?></p>
</div>
<div class="balance-box">
<h3>গত মাসের Point</h3>
<p><?php echo esc_html($last_month_point); ?></p>
</div>
<div class="balance-box">
<h3>এ বছরের Point</h3>
<p><?php echo esc_html($this_year_point); ?></p>
</div>
<div class="balance-box">
<h3>গত বছরের Point</h3>
<p><?php echo esc_html($last_year_point); ?></p>
</div>
<div class="balance-box">
<h3>টোটাল Point</h3>
<p><?php echo esc_html($total_point); ?></p>
</div>
</div>
<!-- Search Bar -->
<div class="search-bar">
<form method="get">
<input type="text" name="search" placeholder="Search income history..." value="<?php echo esc_attr($search_query); ?>">
<input type="submit" value="Search">
</form>
</div>
<!-- আজকের ইনকাম হিস্ট্রি Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "আজকের ইনকাম হিস্ট্রি [" . date('d-m-Y', strtotime($todayDate)) . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar(bsm_get_daily_entries($current_user_id, $todayDate, $todayDate, $search_query)); ?>
</span>
</div>
<div class="income-history-content" style="display: block;">
<a href="#" class="download-pdf" onclick="downloadPdf('today'); return false;">PDF ডাউনলোড</a>
<table id="todayTable">
<thead>
<tr>
<th onclick="sortTable('todayTable', 0)">SN</th>
<th onclick="sortTable('todayTable', 1)">Date & Time</th>
<th onclick="sortTable('todayTable', 2)">Sale ID</th>
<th onclick="sortTable('todayTable', 3)">Sale Type</th>
<th onclick="sortTable('todayTable', 4)">Sale Name</th>
<th onclick="sortTable('todayTable', 5)">Paused P.B</th>
<th onclick="sortTable('todayTable', 6)">Point B</th>
<th onclick="sortTable('todayTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table(bsm_get_daily_entries($current_user_id, $todayDate, $todayDate, $search_query), "আজকের ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
</div>
</div>
<!-- গতকালকের ইনকাম হিস্ট্রি Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "গতকালকের ইনকাম হিস্ট্রি [" . date('d-m-Y', strtotime($yesterdayDate)) . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar(bsm_get_daily_entries($current_user_id, $yesterdayDate, $yesterdayDate, $search_query)); ?>
</span>
</div>
<div class="income-history-content">
<a href="#" class="download-pdf" onclick="downloadPdf('yesterday'); return false;">PDF ডাউনলোড</a>
<table id="yesterdayTable">
<thead>
<tr>
<th onclick="sortTable('yesterdayTable', 0)">SN</th>
<th onclick="sortTable('yesterdayTable', 1)">Date & Time</th>
<th onclick="sortTable('yesterdayTable', 2)">Sale ID</th>
<th onclick="sortTable('yesterdayTable', 3)">Sale Type</th>
<th onclick="sortTable('yesterdayTable', 4)">Sale Name</th>
<th onclick="sortTable('yesterdayTable', 5)">Paused P.B</th>
<th onclick="sortTable('yesterdayTable', 6)">Point B</th>
<th onclick="sortTable('yesterdayTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table(bsm_get_daily_entries($current_user_id, $yesterdayDate, $yesterdayDate, $search_query), "গতকালকের ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
</div>
</div>
<!-- এ মাসের ইনকাম হিস্ট্রি Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "এ মাসের ইনকাম হিস্ট্রি [" . date('F Y', strtotime($thisMonthStart)) . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar(bsm_get_daily_entries($current_user_id, $thisMonthStart, $thisMonthEnd, $search_query)); ?>
</span>
</div>
<div class="income-history-content">
<a href="#" class="download-pdf" onclick="downloadPdf('this_month'); return false;">PDF ডাউনলোড</a>
<table id="thisMonthTable">
<thead>
<tr>
<th onclick="sortTable('thisMonthTable', 0)">SN</th>
<th onclick="sortTable('thisMonthTable', 1)">Date & Time</th>
<th onclick="sortTable('thisMonthTable', 2)">Sale ID</th>
<th onclick="sortTable('thisMonthTable', 3)">Sale Type</th>
<th onclick="sortTable('thisMonthTable', 4)">Sale Name</th>
<th onclick="sortTable('thisMonthTable', 5)">Paused P.B</th>
<th onclick="sortTable('thisMonthTable', 6)">Point B</th>
<th onclick="sortTable('thisMonthTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table(bsm_get_daily_entries($current_user_id, $thisMonthStart, $thisMonthEnd, $search_query), "এ মাসের ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
</div>
</div>
<!-- গত মাসের ইনকাম হিস্ট্রি Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "গত মাসের ইনকাম হিস্ট্রি [" . date('F Y', strtotime($lastMonthStart)) . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar(bsm_get_daily_entries($current_user_id, $lastMonthStart, $lastMonthEnd, $search_query)); ?>
</span>
</div>
<div class="income-history-content">
<a href="#" class="download-pdf" onclick="downloadPdf('last_month'); return false;">PDF ডাউনলোড</a>
<table id="lastMonthTable">
<thead>
<tr>
<th onclick="sortTable('lastMonthTable', 0)">SN</th>
<th onclick="sortTable('lastMonthTable', 1)">Date & Time</th>
<th onclick="sortTable('lastMonthTable', 2)">Sale ID</th>
<th onclick="sortTable('lastMonthTable', 3)">Sale Type</th>
<th onclick="sortTable('lastMonthTable', 4)">Sale Name</th>
<th onclick="sortTable('lastMonthTable', 5)">Paused P.B</th>
<th onclick="sortTable('lastMonthTable', 6)">Point B</th>
<th onclick="sortTable('lastMonthTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table(bsm_get_daily_entries($current_user_id, $lastMonthStart, $lastMonthEnd, $search_query), "গত মাসের ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
</div>
</div>
<!-- এ বছরের ইনকাম হিস্ট্রি Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "এ বছরের ইনকাম হিস্ট্রি [" . $current_year . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar(bsm_get_daily_entries($current_user_id, $year_start, $year_end, $search_query)); ?>
</span>
</div>
<div class="income-history-content">
<a href="#" class="download-pdf" onclick="downloadPdf('this_year'); return false;">PDF ডাউনলোড</a>
<table id="thisYearTable">
<thead>
<tr>
<th onclick="sortTable('thisYearTable', 0)">SN</th>
<th onclick="sortTable('thisYearTable', 1)">Date & Time</th>
<th onclick="sortTable('thisYearTable', 2)">Sale ID</th>
<th onclick="sortTable('thisYearTable', 3)">Sale Type</th>
<th onclick="sortTable('thisYearTable', 4)">Sale Name</th>
<th onclick="sortTable('thisYearTable', 5)">Paused P.B</th>
<th onclick="sortTable('thisYearTable', 6)">Point B</th>
<th onclick="sortTable('thisYearTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table(bsm_get_daily_entries($current_user_id, $year_start, $year_end, $search_query), "এ বছরের ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
</div>
</div>
<!-- গত বছরের ইনকাম হিস্ট্রি Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "গত বছরের ইনকাম হিস্ট্রি [" . $last_year . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar(bsm_get_daily_entries($current_user_id, $last_year_start, $last_year_end, $search_query)); ?>
</span>
</div>
<div class="income-history-content">
<a href="#" class="download-pdf" onclick="downloadPdf('last_year'); return false;">PDF ডাউনলোড</a>
<table id="lastYearTable">
<thead>
<tr>
<th onclick="sortTable('lastYearTable', 0)">SN</th>
<th onclick="sortTable('lastYearTable', 1)">Date & Time</th>
<th onclick="sortTable('lastYearTable', 2)">Sale ID</th>
<th onclick="sortTable('lastYearTable', 3)">Sale Type</th>
<th onclick="sortTable('lastYearTable', 4)">Sale Name</th>
<th onclick="sortTable('lastYearTable', 5)">Paused P.B</th>
<th onclick="sortTable('lastYearTable', 6)">Point B</th>
<th onclick="sortTable('lastYearTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table(bsm_get_daily_entries($current_user_id, $last_year_start, $last_year_end, $search_query), "গত বছরের ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
</div>
</div>
<!-- আল টাইম ইনকাম হিস্ট্রি (One month's report per page) Section -->
<div class="income-history-section">
<div class="income-history-header" onclick="toggleHistory(this)">
<span class="header-left">
<?php echo "আল টাইম ইনকাম হিস্ট্রি [" . date('F Y', strtotime($report_month_start)) . "]"; ?>
</span>
<span class="header-right">
<?php echo render_status_summary_bar($all_time_income_history, "", ""); ?>
</span>
</div>
<div class="income-history-content">
<div class="report-filter">
<form method="get">
<label>মাস: </label>
<select name="report_month">
<?php
for($m = 1; $m <= 12; $m++){
$selected = ($m == $report_month) ? "selected" : "";
echo "<option value='{$m}' {$selected}>" . date('F', mktime(0,0,0,$m,1)) . "</option>";
}
?>
</select>
<label>সাল: </label>
<select name="report_year">
<?php
$currentY = date('Y', $now);
for($y = $currentY - 5; $y <= $currentY; $y++){
$selected = ($y == $report_year) ? "selected" : "";
echo "<option value='{$y}' {$selected}>{$y}</option>";
}
?>
</select>
<input type="submit" value="Apply">
</form>
</div>
<a href="#" class="download-pdf" onclick="downloadPdf('all_time'); return false;">PDF ডাউনলোড</a>
<table id="allTimeTable">
<thead>
<tr>
<th onclick="sortTable('allTimeTable', 0)">SN</th>
<th onclick="sortTable('allTimeTable', 1)">Date & Time</th>
<th onclick="sortTable('allTimeTable', 2)">Sale ID</th>
<th onclick="sortTable('allTimeTable', 3)">Sale Type</th>
<th onclick="sortTable('allTimeTable', 4)">Sale Name</th>
<th onclick="sortTable('allTimeTable', 5)">Paused P.B</th>
<th onclick="sortTable('allTimeTable', 6)">Point B</th>
<th onclick="sortTable('allTimeTable', 7)">Action</th>
</tr>
</thead>
<tbody>
<?php display_income_table($all_time_income_history, "আল টাইম ইনকাম হিস্ট্রি"); ?>
</tbody>
</table>
<?php
$total_pages_all = ceil($total_all_time / $per_page_all);
if ($total_pages_all > 1):
?>
<div class="pagination">
<?php
for($i=1; $i<=$total_pages_all; $i++):
if($i == $page_all):
?>
<span class="current-page"><?php echo $i; ?></span>
<?php else:
$url = add_query_arg(array('page_all' => $i, 'report_year' => $report_year, 'report_month' => $report_month));
?>
<a href="<?php echo esc_url($url); ?>"><?php echo $i; ?></a>
<?php
endif;
endfor;
?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
function toggleHistory(headerElem) {
var contentElem = headerElem.nextElementSibling;
if (contentElem.style.display === "block") {
contentElem.style.display = "none";
} else {
contentElem.style.display = "block";
}
}
function downloadPdf(type) {
window.open("<?php echo esc_url(plugin_dir_url(__FILE__) . 'pdf-download.php'); ?>?type=" + encodeURIComponent(type), '_blank');
}
function sortTable(tableId, colIndex) {
var table = document.getElementById(tableId);
if(!table) return;
var tbody = table.querySelector("tbody");
if(!tbody) return;
var rows = Array.prototype.slice.call(tbody.querySelectorAll("tr"));
if(rows.length < 2) return;
var sortedAsc = table.getAttribute("data-sort-col") == colIndex && table.getAttribute("data-sort-dir") == "asc";
var sortDir = sortedAsc ? "desc" : "asc";
rows.sort(function(a, b) {
var tdA = a.getElementsByTagName("td")[colIndex];
var tdB = b.getElementsByTagName("td")[colIndex];
if(!tdA || !tdB) return 0;
var valA = tdA.innerText || tdA.textContent;
var valB = tdB.innerText || tdB.textContent;
var numA = parseFloat(valA.replace(/,/g, ""));
var numB = parseFloat(valB.replace(/,/g, ""));
if(!isNaN(numA) && !isNaN(numB)) {
return (sortDir === "asc") ? (numA - numB) : (numB - numA);
} else {
valA = valA.toLowerCase();
valB = valB.toLowerCase();
if(valA < valB) return (sortDir === "asc") ? -1 : 1;
if(valA > valB) return (sortDir === "asc") ? 1 : -1;
return 0;
}
});
while(tbody.firstChild) {
tbody.removeChild(tbody.firstChild);
}
for(var i = 0; i < rows.length; i++){
tbody.appendChild(rows[i]);
}
table.setAttribute("data-sort-col", colIndex);
table.setAttribute("data-sort-dir", sortDir);
}
</script>
</body>
</html>
<?php
/**
* File: business-seller-management/templates/seller/pdf-download.php
*
* Description:
* এই ফাইলটি FPDF186 লাইব্রেরি ব্যবহার করে ইনকাম হিস্ট্রি (PDF) ডাউনলোডের জন্য।
* GET প্যারামিটার:
* - pdf_download=1 (এই প্যারামিটার থাকলে Advanced Error Guard ফিল্টার সরিয়ে দেয়া হবে)
* - type = today | yesterday | this_month | last_month | this_year | last_year | all_time
*
* কোনো এরর হলে তা error_log()-এ লগ হবে।
*/
// WordPress এনভায়রনমেন্ট নিশ্চিত করা
if ( ! defined('ABSPATH') ) {
require_once( dirname( dirname( dirname( dirname(__FILE__) ) ) ) . '/wp-load.php' );
}
// ডিবাগ মোড (ডিবাগিংয়ের জন্য – প্রোডাকশনে এ অংশ নিষ্ক্রিয় করুন)
ini_set('display_errors', 1);
error_reporting(E_ALL);
// কাস্টম ডিবাগ লগ ফাংশন
function bsm_log_debug($message) {
error_log('[BSM PDF Download] ' . $message);
}
// যদি pdf_download=1 প্যারামিটার থাকে, তাহলে Advanced Error Guard ফিল্টার সরিয়ে ফেলুন
if ( isset($_GET['pdf_download']) && $_GET['pdf_download'] == 1 ) {
remove_all_filters('wp_die_handler');
bsm_log_debug("Removed all wp_die_handler filters due to pdf_download=1 parameter.");
}
// GET প্যারামিটার যাচাই
$type = isset($_GET['type']) ? sanitize_text_field($_GET['type']) : 'today';
$allowed_types = array('today', 'yesterday', 'this_month', 'last_month', 'this_year', 'last_year', 'all_time');
if ( ! in_array( $type, $allowed_types ) ) {
bsm_log_debug("Invalid type parameter: " . $type);
wp_die("Invalid type parameter.");
}
bsm_log_debug("Type parameter: " . $type);
// FPDF186 লাইব্রেরি অন্তর্ভুক্তি
$fpdfPath = plugin_dir_path(__FILE__) . 'fpdf186/fpdf.php';
if ( ! file_exists( $fpdfPath ) ) {
bsm_log_debug("FPDF library not found at: " . $fpdfPath);
wp_die("FPDF library is missing.");
} else {
require_once($fpdfPath);
bsm_log_debug("FPDF library included from: " . $fpdfPath);
}
// এখানে আপনি যদি ডাটাবেস থেকে ইনকাম হিস্ট্রি/রিপোর্ট ডেটা আনতে চান,
// তাহলে কোড যুক্ত করুন। উদাহরণস্বরূপ, এখানে আমরা একটি টেস্ট PDF তৈরি করছি।
try {
$pdf = new FPDF();
$pdf->AddPage();
$pdf->SetFont('Arial', 'B', 16);
// টেস্ট টেক্সট – এখানে আপনার রিপোর্ট ডেটা বা টেবিল যুক্ত করুন
$pdf->Cell(40, 10, "Income Report for: " . strtoupper($type));
// PDF কে স্ট্রিং হিসেবে আউটপুট নিন
$pdfContent = $pdf->Output('S');
bsm_log_debug("PDF generated successfully for type: " . $type);
} catch (Exception $e) {
bsm_log_debug("Exception generating PDF: " . $e->getMessage());
wp_die("Error generating PDF: " . $e->getMessage());
}
// যদি কোন পূর্ববর্তী আউটপুট থাকে, তা পরিষ্কার করুন
if (ob_get_length()) {
ob_clean();
}
// HTTP হেডার সেট করুন
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="income-report-' . $type . '.pdf"');
header('Cache-Control: private, max-age=0, must-revalidate');
header('Pragma: public');
// PDF আউটপুট করুন
echo $pdfContent;
exit;
<?php
/**
* File: business-seller-management/templates/seller/last-status-activities-page.php
*
* Description:
* - Displays clickable status buttons including "All Status" (default).
* - Buttons are ordered specifically, colored, show counts, hide if count is 0 (except Add/Revert),
* and have 3 time-based indicators (Today-Red, Yesterday-Yellow, Day Before-Gray).
* - Below, a table displays sales & adjustments filtered by the selected status button (or all for "All Status").
* - Loads ALL transaction data initially via PHP; filtering/table updates are instant via JavaScript.
* - Table shows 50 entries per page with JS pagination.
* - Table Columns (Dynamic based on filter):
* - All Status: Status, Time | Date | By (Creator/Changer), ID, Order ID, Sale Type, Reason or Sale Name, Amount ৳ (Purchase Price BDT / Adjusted Amount BDT), Org $ (GSMALO Purchase USD / Adjusted Amount USD), Balance (Running USD)
* - Other Statuses: Status, Time | Date | By (Creator), ID, Order ID, Sale Type, Reason or Sale Name, Amount ৳ (Purchase Price BDT / Adjusted Amount BDT), Org $ (GSMALO Purchase USD / Adjusted Amount USD)
* - Status column shows current status text/icon/color + last status change time/user for Sales. For Adjustments, only status text/icon/color.
* - Time | Date | By column shows event timestamp/date + creator/changer name.
* - Amount ৳ shows Purchase Price in BDT (converted for gsmalo.org) for sales, or Adjusted Amount in BDT for adjustments.
* - Org $ shows Purchase Price in USD for gsmalo.org sales, or Adjusted Amount in USD for adjustments. Empty for other sales.
* - Balance shows the running USD balance after the specific event (sale creation, adjustment, or gsmalo.org status change), only in the All Status view.
* - "New" indicator (Red/Yellow/Gray up to 72h) shown for recent effective updates (latest event timestamp).
* - Table rows have background colors based on the *creation day* of the week.
* - Table rows are sorted by *event timestamp* descending by default. Column headers allow re-sorting.
*
* Changes:
* - Fixed PHP Notices for seller_name and seller_id by fetching seller data.
* - Modified data fetching and processing to create a list of chronological events (sale create, adjustment create, gsmalo.org status change).
* - Implemented chronological calculation of running gsmalo.org balance across all relevant events.
* - Updated JavaScript to handle dynamic columns based on "All Status" filter.
* - Formatted column data strictly as requested ("Amount ৳", "Org $", "Balance").
* - Updated JavaScript to handle client-client filtering, sorting (by event timestamp by default, and other columns), and pagination.
* - Adjusted "New" indicator position and colors.
* - Added row background colors based on creation date (primary event datetime).
* - Updated Status column and Time | Date | By column to reflect creator or changer info based on event type in All Status view.
* - Removed Last Note and Status Log columns from the table view as per the latest instruction for non-"All Status" views.
* - Log modals are kept but triggers are not added in this table as per current scope.
* - Added current seller balance to the page title in JavaScript.
* - Ensured 'Amount ৳' and 'Org $' columns are correctly populated for all relevant event types.
*
*/
if ( ! defined('ABSPATH') ) {
exit; // Exit if accessed directly
}
// Ensure helper functions are available (should be included by main plugin file)
if (!function_exists('bsm_calculate_balance_change_for_status_transition') || !function_exists('bsm_parse_status_from_log')) {
// Define placeholders or log error if functions are missing
if (!function_exists('bsm_calculate_balance_change_for_status_transition')) {
function bsm_calculate_balance_change_for_status_transition($sale_id, $old_status, $new_status) {
// Log an error if the actual function is missing
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log("BSM Error: Required function bsm_calculate_balance_change_for_status_transition is missing.");
}
return 0; // Return 0 to prevent fatal error
}
}
if (!function_exists('bsm_parse_status_from_log')) {
function bsm_parse_status_from_log($log_text) {
// Log an error if the actual function is missing
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log("BSM Error: Required function bsm_parse_status_from_log is missing.");
}
return array(null, null); // Return default value to prevent fatal error
}
}
}
// Check if user is logged in
if ( ! is_user_logged_in() ) {
echo '<p style="color:red; font-weight:bold;">Please log in to view this page.</p>';
return;
}
global $wpdb;
$current_user_id = get_current_user_id();
// --- Fetch Seller Data ---
$seller_user = get_userdata($current_user_id);
$seller_name = $seller_user ? $seller_user->display_name : 'N/A';
$seller_balance_current_db = $wpdb->get_var( $wpdb->prepare("SELECT gsmalo_org_balance FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d", $current_user_id) );
if ( $seller_balance_current_db === null ) {
$seller_balance_current_db = 0;
}
// --- Define Custom Status Order ---
$status_order = [
"Refund Required", "pending", "Need Parts", "Cost", "Parts Brought", "On Hold", "In Process",
"Success But Not Delivery", "Check Admin", "Review Apply", "Block", "Failed", "Cancel",
"Successful All", "Delivery Done", "Reject Delivery Done", "Refund Done", "Rejected",
"Addition", "Revert" // Added adjustment types to order
];
// --- Fetch All Statuses Map (Slug => Label) from Option ---
$core_statuses_ref = [
"pending", "On Hold", "In Process", "Need Parts", "Success But Not Delivery", "Parts Brought",
"Refund Required", "Refund Done", "Successful All", "Delivery Done", "Check Admin",
"Reject Delivery Done", "Cost", "Cancel", "Block", "Rejected", "Failed", "Review Apply"
];
$default_all_statuses = array_combine($core_statuses_ref, array_map('ucwords', str_replace(['_', '-'], ' ', $core_statuses_ref)));
$all_statuses = get_option('bsm_all_statuses', $default_all_statuses);
if (!is_array($all_statuses)) $all_statuses = $default_all_statuses;
// Ensure core statuses and adjustment types are in the map
foreach($core_statuses_ref as $core_slug) { if (!isset($all_statuses[$core_slug])) { $all_statuses[$core_slug] = ucwords(str_replace(['_', '-'], ' ', $core_slug)); } }
if (!isset($all_statuses['Addition'])) $all_statuses['Addition'] = 'Addition'; // Adjustment status
if (!isset($all_statuses['Revert'])) $all_statuses['Revert'] = 'Revert'; // Adjustment status
$all_statuses_for_js = $all_statuses; // Keep this map for JS display names
// --- Fetch Total Count for Each Status (Sales Only for Sales Statuses) ---
$sales_table = $wpdb->prefix . 'bsm_sales';
$adjustments_table = $wpdb->prefix . 'bsm_balance_adjustments';
$notes_table = $wpdb->prefix . 'bsm_sale_notes';
// Fetch counts for sales statuses
$status_counts_raw = $wpdb->get_results( $wpdb->prepare( "SELECT status, COUNT(*) as count FROM {$sales_table} WHERE seller_id = %d GROUP BY status", $current_user_id ), ARRAY_A );
$status_counts = [];
if ($status_counts_raw) { foreach($status_counts_raw as $row) { if (!empty($row['status'])) { $status_counts[$row['status']] = (int)$row['count']; } } }
// Fetch counts for adjustment statuses separately
$addition_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$adjustments_table} WHERE seller_id = %d AND adjusted_amount >= 0", $current_user_id ) );
$revert_count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$adjustments_table} WHERE seller_id = %d AND adjusted_amount < 0", $current_user_id ) );
$status_counts['Addition'] = (int) $addition_count;
$status_counts['Revert'] = (int) $revert_count;
// --- Fetch Count of Status Changes in Last 72 Hours ---
$current_timestamp_php = current_time('timestamp');
$seventy_two_hours_ago_sql = date('Y-m-d H:i:s', $current_timestamp_php - (72 * 3600));
// We need logs for status changes on THIS seller's sales entries
$recent_status_logs_raw = $wpdb->get_results($wpdb->prepare( "SELECT sn.note_text, sn.created_at FROM {$notes_table} sn JOIN {$sales_table} s ON sn.sale_id = s.id WHERE s.seller_id = %d AND sn.note_type = 'status_change' AND sn.created_at >= %s", $current_user_id, $seventy_two_hours_ago_sql ));
$counts_today = []; $counts_yesterday = []; $counts_day_before = [];
if ($recent_status_logs_raw) {
foreach($recent_status_logs_raw as $log) {
// Extract status slug from log text (Assuming format "...status changed from ... to <span>Status Name</span>...")
if (preg_match('/to.*?<span.*?>(.+?)<\/span>/i', $log->note_text, $matches)) {
if (isset($matches[1])) {
$new_status_from_log_label = trim(strip_tags($matches[1]));
// Find the slug corresponding to the label
$found_slug = array_search($new_status_from_log_label, $all_statuses_for_js);
if ($found_slug === false) {
// Fallback check if the label itself is a slug (less likely but safer)
if (isset($all_statuses_for_js[$new_status_from_log_label])) {
$found_slug = $new_status_from_log_label;
} else {
// Could not map label to slug, skip this log for indicators
continue;
}
}
$log_timestamp = strtotime($log->created_at);
$log_age = $current_timestamp_php - $log_timestamp;
if ($found_slug !== false) {
// Add to counts for the slug
if ($log_age <= 24 * 3600) { if (!isset($counts_today[$found_slug])) $counts_today[$found_slug] = 0; $counts_today[$found_slug]++; }
elseif ($log_age <= 48 * 3600) { if (!isset($counts_yesterday[$found_slug])) $counts_yesterday[$found_slug] = 0; $counts_yesterday[$found_slug]++; }
elseif ($log_age <= 72 * 3600) { if (!isset($counts_day_before[$found_slug])) $counts_day_before[$found_slug] = 0; $counts_day_before[$found_slug]++; }
}
}
}
}
}
// Note: Adjustment changes don't trigger these specific "status change" logs in the sales notes table,
// so indicators for Addition/Revert would reflect changes in the *sales* status to those labels
// if that were a workflow, but not the *creation* or *fix* of the adjustment itself.
// Keeping it this way based on the original code's log fetching.
// --- Create the ordered list of statuses to display for Buttons ---
$statuses_to_display = [];
// Add "All Status" first
$statuses_to_display['all'] = 'All Status';
// Add statuses based on custom order
foreach ($status_order as $slug) {
if ($slug === 'all') continue; // Skip 'all' as it's already added
if (isset($all_statuses[$slug])) {
$count = $status_counts[$slug] ?? 0;
// Show if count > 0 OR if it's specific types like Addition/Revert (even if count is 0 initially)
if ($count > 0 || in_array($slug, ['Addition', 'Revert'])) {
$statuses_to_display[$slug] = $all_statuses[$slug];
}
}
}
// Add any remaining custom statuses with count > 0 that weren't in the manual order
foreach ($all_statuses as $slug => $name) {
if (!in_array($slug, $status_order) && $slug !== 'all') {
$total_count = $status_counts[$slug] ?? 0;
if ($total_count > 0) { $statuses_to_display[$slug] = $name; }
}
}
$first_visible_status_slug = 'all'; // Default to 'all'
// --- Define Status Colors ---
$status_colors = [
"pending"=> "#cccccc", "On Hold"=> "#FFA500", "In Process"=> "#1E90FF", "Need Parts"=> "#FF69B4",
"Success But Not Delivery"=> "#FF8C00", "Parts Brought"=> "#008080", "Refund Required"=> "#FF00FF",
"Refund Done"=> "#800080", "Successful All"=> "#006400", "Delivery Done"=> "#006400",
"Check Admin"=> "#808080", "Reject Delivery Done"=> "#A52A2A", "Cost"=> "#333333",
"Cancel"=> "#DC143C", "Block"=> "#000000", "Rejected"=> "#8B0000", "Failed"=> "#FF0000",
"Review Apply"=> "#6A5ACD", 'Addition' => '#00008B', 'Revert' => '#FF6347'
];
$default_custom_color = "#607D8B";
$status_styles_for_js = []; // For JS Status cell formatting
foreach ($all_statuses_for_js as $slug => $name) { $status_styles_for_js[$slug] = [ 'color' => $status_colors[$slug] ?? $default_custom_color, 'icon' => '', ]; }
$temp_icon_map = [ 'Addition' => '➕', 'Successful All' => '✔', 'Delivery Done' => '✔', 'Refund Done' => '💵', 'Revert' => '↩️', 'Refund Required' => '💸', 'Success But Not Delivery'=> '❎', 'pending' => '⏳', 'On Hold' => '⏸️', 'In Process' => '🔄', 'Need Parts' => '⚙️', 'Parts Brought' => '📦', 'Check Admin' => '👨💼', 'Review Apply' => '🔍', 'Block' => '🔒', 'Reject Delivery Done' => '🔴', 'Cost' => '💰', 'Cancel' => '❌', 'Rejected' => '❌', 'Failed' => '❌' ];
foreach ($temp_icon_map as $slug => $icon) { if(isset($status_styles_for_js[$slug])) { $status_styles_for_js[$slug]['icon'] = $icon; } }
// Get USD to BDT conversion rate
$usd_to_bdt_rate = get_option('bsm_usd_value_in_taka', 85);
// Get refund trigger statuses from options
$refund_trigger_statuses = get_option('bsm_refund_trigger_statuses', ['Reject Delivery Done', 'Refund Done', 'Rejected']);
if (!is_array($refund_trigger_statuses)) {
$refund_trigger_statuses = ['Reject Delivery Done', 'Refund Done', 'Rejected'];
}
// --- Fetch ALL Events (Sale Creation, Adjustment, Status Change) ---
$events_list_raw = []; // List of events for chronological balance calculation
// Fetch Sales
$sales_for_events = $wpdb->get_results( $wpdb->prepare( "SELECT id, order_id, sale_type, product_name, purchase_price, selling_price, profit, loss, status, created_at FROM {$sales_table} WHERE seller_id = %d", $current_user_id ));
if ($sales_for_events) {
foreach($sales_for_events as $sale) {
// 1. Event: Sale Creation
// Calculate Amount ৳ and Org $ for the sale creation event
$sale_amount_bdt_display = null;
$sale_org_usd_display = null;
if (!empty($sale->purchase_price) && floatval($sale->purchase_price) > 0) {
if (strtolower($sale->sale_type) === 'gsmalo.org') {
$purchase_usd = floatval($sale->purchase_price);
$sale_amount_bdt_display = $purchase_usd * $usd_to_bdt_rate; // Purchase Price BDT
$sale_org_usd_display = $purchase_usd; // Purchase Price USD
} else {
// Assume BDT purchase price for other types
$sale_amount_bdt_display = floatval($sale->purchase_price); // Purchase Price BDT
$sale_org_usd_display = null; // Not applicable
}
}
$events_list_raw[] = [
'timestamp' => $sale->created_at,
'type' => 'sale_create',
'id' => (int) $sale->id,
'sale_type' => $sale->sale_type,
'status' => $sale->status, // Status at creation
'reason' => $sale->product_name,
'amount_usd_impact' => (strtolower($sale->sale_type) === 'gsmalo.org' ? -floatval($sale->purchase_price) : 0), // Impact on USD balance
'amount_bdt_display' => $sale_amount_bdt_display, // Purchase Price BDT
'org_usd_display' => $sale_org_usd_display, // Purchase Price USD (only for gsmalo.org sales)
'purchase_price' => floatval($sale->purchase_price), // Store original purchase price
'selling_price' => floatval($sale->selling_price),
'profit_bdt' => floatval($sale->profit),
'loss_bdt' => floatval($sale->loss),
'order_id' => $sale->order_id,
'creator_name' => $seller_name, // Creator is the seller
'warning_text' => '', // Sales don't have warnings in this context
];
// Fetch Status Change Logs for this sale
$status_change_logs = $wpdb->get_results($wpdb->prepare(
"SELECT sn.created_at, sn.note_text, u.display_name FROM {$notes_table} sn LEFT JOIN {$wpdb->users} u ON sn.user_id = u.ID WHERE sn.sale_id = %d AND sn.note_type = 'status_change' ORDER BY sn.created_at ASC", $sale->id
));
$previous_status = $sale->status; // Start from the status at creation
if($status_change_logs) {
foreach($status_change_logs as $log) {
list($old_status_from_log, $new_status_from_log) = bsm_parse_status_from_log($log->note_text);
// If parsing failed or sale is not gsmalo.org, skip this log for balance events
if ($old_status_from_log === null || $new_status_from_log === null || strtolower($sale->sale_type) !== 'gsmalo.org') {
$previous_status = $new_status_from_log; // Update status for the next log in sequence
continue;
}
$old_status_in_refund_group = in_array($old_status_from_log, $refund_trigger_statuses);
$new_status_in_refund_group = in_array($new_status_from_log, $refund_trigger_statuses);
$balance_impact_usd = 0;
// Determine if this status change impacts the balance
if (!$old_status_in_refund_group && $new_status_in_refund_group) {
$balance_impact_usd = floatval($sale->purchase_price); // Add back purchase price (USD)
$event_type = 'status_refund';
$event_reason = 'Status changed to ' . $new_status_from_log . ' (Refund)';
}
// 3. Event: Status Change out of Refund Group
elseif ($old_status_in_refund_group && !$new_status_in_refund_group) {
$balance_impact_usd = -floatval($sale->purchase_price); // Deduct purchase price (USD) again
$event_type = 'status_deduct';
$event_reason = 'Status changed to ' . $new_status_from_log . ' (Re-deduction)';
} else {
// Status change did not cross the refund group boundary, no balance impact event needed
$previous_status = $new_status_from_log; // Update status for the next log in sequence
continue;
}
// Add the status change event if it impacted the balance
$events_list_raw[] = [
'timestamp' => $log->created_at,
'type' => $event_type,
'id' => (int) $sale->id,
'sale_type' => $sale->sale_type,
'status' => $new_status_from_log, // Status after this change
'reason' => $event_reason,
'amount_usd_impact' => $balance_impact_usd,
'purchase_price' => null, // Not applicable for this event type
'selling_price' => null,
'profit_bdt' => null,
'loss_bdt' => null,
'order_id' => $sale->order_id,
'creator_name' => $log->display_name ?: 'System', // User who changed status
'warning_text' => '',
'amount_bdt_display' => $balance_impact_usd * $usd_to_bdt_rate, // Convert impact to BDT
'org_usd_display' => $balance_impact_usd, // Impact in USD
];
$previous_status = $new_status_from_log; // Update status for the next log in sequence
}
}
}
}
// Fetch Adjustments
$adjustments_for_events = $wpdb->get_results( $wpdb->prepare( "SELECT id, seller_id, adjusted_amount, reason, created_at, adjusted_by FROM {$adjustments_table} WHERE seller_id = %d", $current_user_id ));
if($adjustments_for_events) {
foreach($adjustments_for_events as $adj) {
// 4. Event: Adjustment Creation
$adjuster_name = 'Admin/System';
if ( $adj->adjusted_by > 0 ) { $adjuster_data = get_userdata($adj->adjusted_by); if ($adjuster_data) { $adjuster_name = $adjuster_data->display_name; } }
$adjusted_amount_usd = floatval($adj->adjusted_amount);
$events_list_raw[] = [
'timestamp' => $adj->created_at,
'type' => 'adjustment',
'id' => (int) $adj->id,
'sale_type' => 'Balance Adjustment', // Use this as sale_type for adjustments
'status' => ($adjusted_amount_usd >= 0) ? "Addition" : "Revert", // Status for adjustment
'reason' => $adj->reason,
'amount_usd_impact' => $adjusted_amount_usd, // Impact on USD balance
'purchase_price' => null,
'selling_price' => null,
'profit_bdt' => null,
'loss_bdt' => null,
'order_id' => '-', // Adjustments don't have order IDs
'creator_name' => $adjuster_name, // Creator is the adjuster
'warning_text' => '', // Warnings handled separately in the adjustment data itself if needed
'amount_bdt_display' => $adjusted_amount_usd * $usd_to_bdt_rate, // Converted Adjustment Amount BDT
'org_usd_display' => $adjusted_amount_usd, // Adjusted Amount USD
];
}
}
// Sort ALL events chronologically by timestamp ascending
usort($events_list_raw, function($a, $b) {
$timeA = strtotime($a['timestamp']);
$timeB = strtotime($b['timestamp']);
// If timestamps are the same, order consistently (e.g., sales create > status changes > adjustments)
if ($timeA === $timeB) {
$order = ['sale_create' => 1, 'status_deduct' => 2, 'status_refund' => 3, 'adjustment' => 4];
$typeA = $order[$a['type']] ?? 5;
$typeB = $order[$b['type']] ?? 5;
if ($typeA === $typeB) return 0;
return $typeA - $typeB;
}
return $timeA - $timeB; // Ascending
});
// Calculate Running Balance
$current_running_balance = 0.0; // Start from 0 for the entire history
$bsm_event_data = []; // This will be the final array of events with calculated balance
foreach ($events_list_raw as $event) {
$current_running_balance += $event['amount_usd_impact']; // Use the impact amount
$event['calculated_balance'] = $current_running_balance;
$event['effective_timestamp'] = strtotime($event['timestamp']); // Add effective timestamp for sorting later
$bsm_event_data[] = $event;
}
// Sort the FINAL list of events by effective_timestamp (event timestamp) descending for display
usort($bsm_event_data, function($a, $b) {
$timeA = $a['effective_timestamp'] ?? 0;
$timeB = $b['effective_timestamp'] ?? 0;
if ($timeA === $timeB) return 0;
return ($timeA < $timeB) ? 1 : -1; // Descending
});
// --- End Data Fetching and Calculation ---
// Generate nonce for fetching logs (used in JS)
$log_fetch_nonce = wp_create_nonce('bsm_note_nonce'); // Re-using the same nonce for both log types
// Row colors definition
$row_day_colors = [ '#E8F8F5', '#FEF9F7', '#F4ECF7', '#FDEDEC', '#EBF5FB', '#FDF2E9' ]; // Mon-Sat
$row_sunday_color = '#FFFACD'; // LemonChiffon for Sunday
// Determine initial column count for placeholder (assuming All Status view first)
$initial_colspan = 9; // Status, Time|Date|By, ID, OrderID, SaleType, Reason, Amount ৳, Org $, Balance
// Pass necessary data to JavaScript
?>
<div class="bsm-last-status-activities-wrap">
<h1>Last Status Activities</h1>
<div class="status-display-section">
<div class="status-boxes-container">
<?php
// Add "All Status" button first
$status_slug = 'all';
$status_name = 'All Status';
$total_count = array_sum($status_counts); // Sum of all counts
$count_today = array_sum($counts_today);
$count_yesterday = array_sum($counts_yesterday);
$count_day_before = array_sum($counts_day_before);
$bg_color = '#4682B4'; // SteelBlue for All Status
$text_color = '#FFFFFF';
?>
<div class="status-item-wrapper">
<div class="status-indicators-row">
<?php if ($count_today > 0): ?><span class="status-change-indicator indicator-today" title="<?php echo $count_today; ?> total changes today (0-24h)"><?php echo $count_today; ?></span><?php endif; ?>
<?php if ($count_yesterday > 0): ?><span class="status-change-indicator indicator-yesterday" title="<?php echo $count_yesterday; ?> total changes yesterday (24-48h)"><?php echo $count_yesterday; ?></span><?php endif; ?>
<?php if ($count_day_before > 0): ?><span class="status-change-indicator indicator-daybefore" title="<?php echo $count_day_before; ?> total changes the day before (48-72h)"><?php echo $count_day_before; ?></span><?php endif; ?>
</div>
<button type="button" class="status-box" data-status-slug="<?php echo esc_attr($status_slug); ?>" style="background-color: <?php echo esc_attr($bg_color); ?>; color: <?php echo esc_attr($text_color); ?>;" title="<?php echo esc_attr($status_slug); ?>">
<?php echo esc_html($status_name); ?> (<?php echo $total_count; ?>)
</button>
</div>
<?php
// Now add other status buttons based on custom order and counts
foreach ($status_order as $status_slug) {
if ($status_slug === 'all') continue; // Skip 'all' as it's already added
if (isset($all_statuses[$status_slug])) {
$status_name = $all_statuses[$status_slug];
$total_count = $status_counts[$status_slug] ?? 0;
// Only show if count > 0, unless it's Addition/Revert
if ($total_count > 0 || in_array($status_slug, ['Addition', 'Revert'])) {
$count_today = $counts_today[$status_slug] ?? 0;
$count_yesterday = $counts_yesterday[$status_slug] ?? 0;
$count_day_before = $counts_day_before[$status_slug] ?? 0;
$bg_color = isset($status_colors[$status_slug]) ? $status_colors[$status_slug] : $default_custom_color;
// Determine text color based on background luminance
$r = hexdec(substr($bg_color, 1, 2)); $g = hexdec(substr($bg_color, 3, 2)); $b = hexdec(substr($bg_color, 5, 2));
$luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
$text_color = ($luminance > 0.55) ? '#000000' : '#FFFFFF';
?>
<div class="status-item-wrapper">
<div class="status-indicators-row">
<?php if ($count_today > 0): ?><span class="status-change-indicator indicator-today" title="<?php echo $count_today; ?> items changed to <?php echo esc_attr($status_name); ?> today (0-24h)"><?php echo $count_today; ?></span><?php endif; ?>
<?php if ($count_yesterday > 0): ?><span class="status-change-indicator indicator-yesterday" title="<?php echo $count_yesterday; ?> items changed to <?php echo esc_attr($status_name); ?> yesterday (24-48h)"><?php echo $count_yesterday; ?></span><?php endif; ?>
<?php if ($count_day_before > 0): ?><span class="status-change-indicator indicator-daybefore" title="<?php echo $count_day_before; ?> items changed to <?php echo esc_attr($status_name); ?> the day before (48-72h)"><?php echo $count_day_before; ?></span><?php endif; ?>
</div>
<button type="button" class="status-box" data-status-slug="<?php echo esc_attr($status_slug); ?>" style="background-color: <?php echo esc_attr($bg_color); ?>; color: <?php echo esc_attr($text_color); ?>;" title="<?php echo esc_attr($status_slug); ?>">
<?php echo esc_html($status_name); ?> (<?php echo $total_count; ?>)
</button>
</div>
<?php
}
}
}
// Add any remaining custom statuses with count > 0 that weren't in the manual order
foreach ($all_statuses as $status_slug => $status_name) {
if (!in_array($status_slug, $status_order) && $status_slug !== 'all') {
$total_count = $status_counts[$status_slug] ?? 0;
if ($total_count > 0) {
$count_today = $counts_today[$status_slug] ?? 0;
$count_yesterday = $counts_yesterday[$status_slug] ?? 0;
$count_day_before = $counts_day_before[$status_slug] ?? 0;
$bg_color = isset($status_colors[$status_slug]) ? $status_colors[$status_slug] : $default_custom_color;
$r = hexdec(substr($bg_color, 1, 2)); $g = hexdec(substr($bg_color, 3, 2)); $b = hexdec(substr($bg_color, 5, 2));
$luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
$text_color = ($luminance > 0.55) ? '#000000' : '#FFFFFF';
?>
<div class="status-item-wrapper">
<div class="status-indicators-row">
<?php if ($count_today > 0): ?><span class="status-change-indicator indicator-today" title="<?php echo $count_today; ?> items changed to <?php echo esc_attr($status_name); ?> today (0-24h)"><?php echo $count_today; ?></span><?php endif; ?>
<?php if ($count_yesterday > 0): ?><span class="status-change-indicator indicator-yesterday" title="<?php echo $count_yesterday; ?> items changed to <?php echo esc_attr($status_name); ?> yesterday (24-48h)"><?php echo $count_yesterday; ?></span><?php endif; ?>
<?php if ($count_day_before > 0): ?><span class="status-change-indicator indicator-daybefore" title="<?php echo $count_day_before; ?> items changed to <?php echo esc_attr($status_name); ?> the day before (48-72h)"><?php echo $count_day_before; ?></span><?php endif; ?>
</div>
<button type="button" class="status-box" data-status-slug="<?php echo esc_attr($status_slug); ?>" style="background-color: <?php echo esc_attr($bg_color); ?>; color: <?php echo esc_attr($text_color); ?>;" title="<?php echo esc_attr($status_slug); ?>">
<?php echo esc_html($status_name); ?> (<?php echo $total_count; ?>)
</button>
</div>
<?php
}
}
}
?>
</div>
<?php if (empty($statuses_to_display) || (count($statuses_to_display) === 1 && isset($statuses_to_display['all']) && ($status_counts['all'] ?? 0) === 0 )): ?>
<p>No relevant status data found.</p>
<?php endif; ?>
</div>
<div id="status-sales-table-section" class="sale-section" style="margin-top: 20px;">
<h3 id="table-status-title">Loading...</h3>
<div class="table-responsive-wrapper">
<table id="status-sales-table">
<thead>
<tr><th colspan="<?php echo $initial_colspan; ?>">Loading Headers...</th></tr>
</thead>
<tbody>
<tr><td colspan="<?php echo $initial_colspan; ?>">Loading Data...</td></tr>
</tbody>
</table>
</div>
<div class="pagination" id="status-sales-pagination">
</div>
</div>
</div>
<div id="all-txn-status-log-modal" class="log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('all-txn-status-log-modal').style.display='none';">×</span>
<h2>Status Change History</h2>
<div id="all-txn-status-log-modal-content" class="log-modal-content-area"><p>Loading logs...</p></div>
</div>
</div>
<div id="all-txn-note-log-modal" class="log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('all-txn-note-log-modal').style.display='none';">×</span>
<h2>Note History</h2>
<div id="all-txn-note-log-modal-content" class="log-modal-content-area"><p>Loading notes...</p></div>
</div>
</div>
<style>
/* CSS Styles (Updated based on requirements) */
.bsm-seller-all-txn-history-wrap { padding: 15px; background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 4px; margin: 10px 0; font-family: sans-serif; }
.bsm-seller-all-txn-history-wrap h1 { margin-top: 0; margin-bottom: 20px; color: #0073aa; font-size: 1.6em; text-align: center; }
.status-display-section { margin-top: 15px; margin-bottom: 25px; }
.status-boxes-container { display: flex; flex-wrap: wrap; gap: 6px 4px; justify-content: center; }
.status-item-wrapper { position: relative; padding-top: 10px; flex: 0 1 auto; margin-bottom: 0; display: inline-block; vertical-align: top; }
.status-indicators-row { position: absolute; top: -1px; left: 50%; transform: translateX(-50%); display: flex; justify-content: center; gap: 1px; z-index: 2; height: 16px; pointer-events: none; width: auto; white-space: nowrap; }
.status-change-indicator { display: inline-block; color: white; border-radius: 50%; width: 16px; height: 16px; font-size: 9px; font-weight: bold; line-height: 16px; text-align: center; border: 1px solid rgba(255,255,255,0.7); box-shadow: 0 1px 2px rgba(0,0,0,0.3); cursor: help; flex-shrink: 0; }
.indicator-today { background-color: red; }
.indicator-yesterday { background-color: #DAA520; } /* Ghara Holud */
.indicator-daybefore { background-color: #808080; } /* Chhai color */
.status-box { /* Using button tag now */
padding: 3px 7px; /* Thin padding */
border-radius: 6px; text-align: center; font-weight: bold; font-size: 10px; /* Slightly larger font */
box-shadow: 0 1px 2px rgba(0,0,0,0.10); cursor: pointer; transition: transform 0.1s ease, box-shadow 0.1s ease, border-color 0.1s ease;
white-space: normal; overflow: visible; display: block; box-sizing: border-box; border: 1px solid rgba(0,0,0,0.1);
line-height: 1.3; min-height: 22px; min-width: 55px; /* Adjusted min-width */
border-width: 2px; border-color: transparent; margin: 0;
}
.status-box.active { border-color: #000; box-shadow: 0 2px 5px rgba(0,0,0,0.3); transform: scale(1.03); }
.status-box:hover:not(.active) { box-shadow: 0 2px 4px rgba(0,0,0,0.2); filter: brightness(95%); }
/* Table Section */
#status-sales-table-section { margin-top: 20px; padding: 15px; background: #fff; border: 1px solid #ddd; border-radius: 4px; }
#status-sales-table-section h3 { margin-top: 0; font-size: 1.2em; color: #444; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; }
.table-responsive-wrapper { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
table#status-sales-table { width: 100%; min-width: 900px; /* Adjusted min-width based on new columns */ border-collapse: collapse; table-layout: auto; background: #fff; }
table#status-sales-table td[colspan] { text-align: center; padding: 15px; font-style: italic; color: #666; } /* Updated selector for colspan */
table#status-sales-table th, table#status-sales-table td {
border: 1px solid #ccc; padding: 4px 5px; text-align: center; vertical-align: middle;
position: relative; font-size: 11px; word-wrap: break-word; overflow-wrap: break-word;
white-space: nowrap;
}
table#status-sales-table th { background: #f1f1f1; font-weight: bold; cursor: pointer; white-space: nowrap; }
table#status-sales-table td.col-reason { white-space: normal; width: 18%; } /* Allow wrapping */
table#status-sales-table td.status-col { white-space: normal; } /* Allow wrapping status + time */
.col-order-id { width: 50px; }
.col-datetime span { display: block; font-size: 10px; color: #555;}
.col-datetime small { display: block; font-size: 9px; color: #0073aa; font-weight: bold; }
table#status-sales-table tbody tr:hover { background-color: #eaf5fb !important; } /* Peaceful blue hover */
.pagination { text-align: center; margin-top: 20px; }
.pagination a, .pagination span { display: inline-block; padding: 5px 10px; margin: 0 2px; border: 1px solid #ccc; border-radius: 4px; text-decoration: none; color: #0073aa; font-size: 12px; }
.pagination span.current-page { background: #0073aa; color: #fff; }
.status-text { font-weight: bold; }
.status-icon { margin-left: 3px; margin-right: 3px; }
th .sort-indicator { font-size: 9px; margin-left: 4px; }
.amount-plus { color: green; font-weight: bold; } /* Changed plus color to green for clarity */
.amount-minus { color: red; font-weight: bold; }
/* New Indicator Styles for Table Rows */
.new-indicator { position: absolute; top: -7px; left: -7px; color: white; font-size: 8px; font-weight: bold; padding: 1px 3px; border-radius: 3px; line-height: 1; z-index: 3; border: 1px solid white; box-shadow: 0 1px 1px rgba(0,0,0,0.4); }
.new-indicator-recent { background-color: red; }
.new-indicator-medium { background-color: #DAA520; } /* Ghara Holud */
.new-indicator-old { background-color: #808080; } /* Chhai color */
table#status-sales-table td:first-child { position: relative; padding-left: 10px; } /* Add padding for indicator */
/* Log Modal Styles (Keep code, AJAX handlers are elsewhere) */
.log-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 10001; }
.log-modal .modal-content { background: #fff; width: 700px; max-width: 90%; margin: 80px auto; padding: 20px; border-radius: 5px; position: relative; max-height: 80vh; overflow-y: auto; }
.log-modal .modal-content h2 { margin-top: 0; color: #0073aa; font-size: 1.3em; }
.log-modal .close { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 24px; color: #888; font-weight: bold; }
.log-modal-content-area .log-entry { border-bottom: 1px dashed #eee; padding: 8px 0; font-size: 13px; }
.log-modal-content-area .log-entry:last-child { border-bottom: none; }
.log-modal-content-area .log-entry strong { color: #333; }
.log-modal-content-area .log-entry em { color: #777; font-size: 0.9em; }
</style>
<script>
// Pass PHP variables needed for AJAX to JavaScript
var bsm_ajax_obj = {
ajaxurl: '<?php echo admin_url( 'admin-ajax.php' ); ?>',
fetch_nonce: '<?php echo esc_js($log_fetch_nonce); ?>' // Use the nonce generated in PHP
};
// IMPORTANT: Encode the combined and balance-calculated event data
var bsm_event_data = <?php echo wp_json_encode($bsm_event_data); ?>;
// var bsm_status_colors = <?php echo wp_json_encode($status_colors); ?>; // Not directly used in this JS, but good to keep if needed
var bsm_all_statuses_map = <?php echo wp_json_encode($all_statuses_for_js); ?>;
var bsm_default_status_slug = <?php echo wp_json_encode($first_visible_status_slug); ?>;
var bsm_status_styles_map = <?php echo wp_json_encode($status_styles_for_js); ?>; // Status icon/color map
// Timestamps for indicator calculation
var bsm_current_timestamp = <?php echo $current_timestamp_php; ?>;
var bsm_24h_ago = <?php echo $current_timestamp_php - (24 * 3600); ?>;
var bsm_48h_ago = <?php echo $current_timestamp_php - (48 * 3600); ?>;
var bsm_72h_ago = <?php echo $current_timestamp_php - (72 * 3600); ?>;
// Day colors for row styling
var bsm_day_colors = <?php echo wp_json_encode($row_day_colors); ?>;
var bsm_sunday_color = <?php echo wp_json_encode($row_sunday_color); ?>;
// USD to BDT rate
var bsm_usd_to_bdt_rate = <?php echo floatval($usd_to_bdt_rate); ?>;
// Current seller balance from DB (for title display)
var bsm_current_seller_balance_db = <?php echo floatval($seller_balance_current_db); ?>;
// Pagination state
var bsm_current_page = 1;
var bsm_items_per_page = 50; // Default items per page for the table
var bsm_current_filter_slug = bsm_default_status_slug; // Default to 'all'
var bsm_filtered_data = []; // To store currently filtered transactions
var bsm_current_sort_column_index = -1; // Index of the column currently sorted
var bsm_current_sort_direction = 'desc'; // Default sort direction
// Helper to format datetime string (UTC assumed from PHP) with creator/changer user
function bsm_format_datetime_with_user(item) {
const sqlDateTime = item.timestamp; // Use event timestamp for this column
if (!sqlDateTime || sqlDateTime === '0000-00-00 00:00:00') return 'N/A';
try {
// Ensure input is treated as UTC
const date = new Date(sqlDateTime.replace(' ', 'T') + 'Z');
if (isNaN(date.getTime())) { return 'Invalid Date'; }
// Format time (h:i A) and date (d-m-Y) in UTC
const timeString = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true, timeZone: 'UTC' });
const dateString = date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC' }); // en-GB gives dd/mm/yyyy
let userName = item.creator_name || 'System'; // User name depends on event type creator/changer
if (userName === '<?php echo esc_js($seller_name); ?>') userName = 'You';
return `${timeString}<br>${dateString.replace(/\//g, '-')}<small>By: ${userName}</small>`;
} catch (e) {
console.error("Error formatting datetime:", sqlDateTime, e);
return 'Date Error';
}
}
// Helper to format status column with last change info
function bsm_format_status_column(item) {
const statusText = bsm_all_statuses_map[item.status] || item.status;
const statusColor = bsm_status_styles_map[item.status] ? bsm_status_styles_map[item.status].color : '#ccc';
const statusIcon = bsm_status_styles_map[item.status] ? bsm_status_styles_map[item.status].icon : '';
let html = `<span class="status-text" style="color:${statusColor};">${statusText}<span class="status-icon">${statusIcon}</span></span>`;
// For Sales creation events, status is the status at creation.
// For Status change events, status is the status *after* the change.
// No separate last change info needed here as each row IS an event.
return html;
}
// Helper function to get the row background color based on date (using primary event timestamp)
function bsm_get_row_bg_color(eventTimestamp) {
if (!eventTimestamp || eventTimestamp <= 0) return ''; // Use numeric timestamp
try {
const date = new Date(eventTimestamp * 1000); // Convert timestamp to Date object
if (isNaN(date.getTime())) return '';
const dayOfWeekUTC = date.getUTCDay(); // 0=Sun, 1=Mon...
if (dayOfWeekUTC === 0) { // Sunday
return bsm_sunday_color;
} else {
const colorIndex = (dayOfWeekUTC - 1) % bsm_day_colors.length; // 1-1=0, 2-1=1 ... 6-1=5
return bsm_day_colors[colorIndex];
}
} catch (e) {
console.error("Error getting row color:", eventTimestamp, e);
return '';
}
}
// Helper function to get the correct value for sorting based on column index and type
// This maps column index to the property in the event data structure
function getCellValueAll(item, colIndex, sortType, isAllStatusView) {
let dataKey = null;
// Define the keys used for sorting in order of appearance in the table columns
// These map directly to properties in the 'item' (event data)
// Note: Order is Status, Time|Date|By, ID, Order ID, Sale Type, Reason, Amount ৳, Org $, [Balance]
const sortKeysBase = ['status', 'effective_timestamp', 'id', 'order_id', 'sale_type', 'reason', 'amount_bdt_display', 'org_usd_display'];
const sortKeysAll = ['status', 'effective_timestamp', 'id', 'order_id', 'sale_type', 'reason', 'amount_bdt_display', 'org_usd_display', 'calculated_balance'];
let keys = isAllStatusView ? sortKeysAll : sortKeysBase;
if (colIndex < keys.length) {
dataKey = keys[colIndex];
} else {
return null; // Column index out of bounds
}
if (dataKey) {
let value = item[dataKey];
// Special handling for sortType 'datetime' and 'number'
if (sortType === 'datetime') {
return parseFloat(item['effective_timestamp']) || 0; // Always sort Time | Date | By by effective timestamp
}
if (sortType === 'number') {
// Ensure we're sorting the numeric value, not the formatted string
if (dataKey === 'amount_bdt_display' || dataKey === 'org_usd_display' || dataKey === 'calculated_balance') {
return parseFloat(value) || 0;
}
// Fallback for other numeric keys like 'id'
return parseFloat(value) || 0;
}
// For string sorting, use the raw string value
return String(value);
}
return null;
}
// Generic Sort Function for any column
function sortTableByColumnAll(columnIndex, sortType) {
const table = document.getElementById('status-sales-table');
const tableHead = table.querySelector('thead');
const tbody = table.querySelector('tbody');
if (!table || !tableHead || !tbody) return;
// Find the corresponding header cell
const headers = tableHead.querySelectorAll('th');
const th = headers[columnIndex];
if (!th) return;
// Determine sort direction
const isAsc = th.classList.contains('asc');
bsm_current_sort_direction = isAsc ? 'desc' : 'asc';
bsm_current_sort_column_index = columnIndex; // Store column index
// Reset sort indicators
tableHead.querySelectorAll('th .sort-indicator').forEach(span => span.textContent = '');
headers.forEach(header => header.classList.remove('asc', 'desc'));
// Add indicator to current column
th.classList.add(bsm_current_sort_direction);
const indicatorSpan = th.querySelector('.sort-indicator');
if (indicatorSpan) {
indicatorSpan.textContent = bsm_current_sort_direction === 'asc' ? ' ▲' : ' ▼';
}
// Sort the currently filtered data (bsm_filtered_data is a list of events)
const isAllStatusView = bsm_current_filter_slug === 'all';
bsm_filtered_data.sort((a, b) => {
let valA = getCellValueAll(bsm_current_sort_direction === 'asc' ? a : b, bsm_current_sort_column_index, sortType, isAllStatusView);
let valB = getCellValueAll(bsm_current_sort_direction === 'asc' ? b : a, bsm_current_sort_column_index, sortType, isAllStatusView);
if (sortType === 'number' || sortType === 'datetime') {
// Handle nulls/NaNs for numeric/datetime comparisons
const numA = parseFloat(valA) || 0;
const numB = parseFloat(valB) || 0;
if (numA === numB) return 0;
return numA - numB;
} else { // string sort
if (valA === null || valB === null) return 0; // Handle nulls during comparison
valA = String(valA).toLowerCase();
valB = String(valB).toLowerCase();
return valA.localeCompare(valB);
}
});
// After sorting filtered data, re-display the current page
bsm_current_page = 1; // Reset to page 1 after sort
displaySalesForStatus(bsm_current_filter_slug, false); // Don't reset sort state in display function
}
// Main function to update table display based on selected status
function displaySalesForStatus(statusSlug, resetSort = true) {
console.log("Displaying events for status:", statusSlug, "Reset Sort:", resetSort);
bsm_current_filter_slug = statusSlug; // Store the current filter
var table = document.getElementById('status-sales-table');
var tableHead = table.querySelector('thead');
var tableBody = table.querySelector('tbody');
var tableTitle = document.getElementById('table-status-title');
var paginationContainer = document.getElementById('status-sales-pagination');
if (!table || !tableHead || !tableBody || !tableTitle || !paginationContainer) { console.error("Essential table elements not found!"); return; }
if (typeof bsm_event_data === 'undefined' || !Array.isArray(bsm_event_data)) { console.error("Event data (bsm_event_data) is not loaded."); tableBody.innerHTML = `<tr><td colspan="${<?php echo $initial_colspan; ?>}">Error: Transaction data not loaded.</td></tr>`; return; }
// Update Title - Add current balance
var statusName = (statusSlug === 'all') ? 'All Status' : (bsm_all_statuses_map[statusSlug] || statusSlug);
tableTitle.innerHTML = `Entries for Status: ${statusName} <span style="font-size: 0.8em; color: #555; margin-left: 20px;">Current balance: ${bsm_current_seller_balance_db.toFixed(2)}$</span>`;
// Filter Data (filter events based on the status they represent)
if (statusSlug === 'all') {
bsm_filtered_data = [...bsm_event_data]; // Use all event data
} else {
bsm_filtered_data = bsm_event_data.filter(function(event) {
return event.status === statusSlug; // Filter by the 'status' property of the event
});
}
console.log(`Filtered events count for ${statusSlug}:`, bsm_filtered_data.length);
// Sort Data (Initial sort is by effective_timestamp DESC - which is the event timestamp)
if (resetSort) {
bsm_current_sort_column_index = 1; // Index of Time | Date | By column (which sorts by effective_timestamp)
bsm_current_sort_direction = 'desc';
bsm_filtered_data.sort(function(a, b) {
let timeA = a.effective_timestamp || 0;
let timeB = b.effective_timestamp || 0;
if (timeA === timeB) return 0;
return (timeA < timeB) ? 1 : -1; // Descending
});
} else {
// Data is already sorted by the last applied sort (either default or column click)
}
// Determine columns and colspan based on statusSlug
const isAllStatus = statusSlug === 'all';
const displayedColumns = [];
let currentColumnIndex = 0;
// Define column structure and map to event properties
// Names: Status, Time | Date | By, ID, Order ID, Sale Type, Reason or Sale Name, Amount ৳, Org $, Balance
const columnDefinitions = [
{ name: 'Status', sortable: false, sortType: 'string', class: 'status-col' }, // Status column is NOT sortable
{ name: 'Time | Date | By', sortable: true, sortType: 'datetime', class: 'col-datetime' },
{ name: 'ID', sortable: true, sortType: 'number' },
{ name: 'Order ID', sortable: true, sortType: 'string', class: 'col-order-id' },
{ name: 'Sale Type', sortable: true, sortType: 'string' },
{ name: 'Reason or Sale Name', sortable: true, sortType: 'string', class: 'col-reason' },
{ name: 'Amount ৳', sortable: true, sortType: 'number' }, // Sort by numeric value
{ name: 'Org $', sortable: true, sortType: 'number' }, // Sort by numeric value
{ name: 'Balance', sortable: true, sortType: 'number' }, // Only for All Status
];
// Build displayedColumns array and map sort indices
columnDefinitions.forEach(colDef => {
if (colDef.name === 'Balance') {
if (isAllStatus) {
// For 'All Status', the Balance column is present
displayedColumns.push({ name: colDef.name, sortable: colDef.sortable, sortType: colDef.sortType, class: colDef.class, sortIndex: currentColumnIndex });
currentColumnIndex++;
}
} else {
// Other columns are always present
displayedColumns.push({ name: colDef.name, sortable: colDef.sortable, sortType: colDef.sortType, class: colDef.class, sortIndex: currentColumnIndex });
currentColumnIndex++;
}
});
const currentColumnCount = displayedColumns.length;
const emptyMessageColspan = currentColumnCount;
// Generate Table Headers
let headerHtml = '<tr>';
displayedColumns.forEach((col, index) => {
headerHtml += `<th ${col.sortable ? `onclick="sortTableByColumnAll(${col.sortIndex}, '${col.sortType}')"` : ''} ${col.class ? `class="${col.class}"` : ''}>
${col.name}
${col.sortable ? '<span class="sort-indicator"></span>' : ''}
</th>`;
});
headerHtml += '</tr>';
tableHead.innerHTML = headerHtml; // Update headers
// Re-apply sort indicator after headers are regenerated
const headers = tableHead.querySelectorAll('th');
if (headers.length > bsm_current_sort_column_index && bsm_current_sort_column_index !== -1) { // Check if a column has been sorted
const currentHeader = headers[bsm_current_sort_column_index];
currentHeader.classList.add(bsm_current_sort_direction);
const indicatorSpan = currentHeader.querySelector('.sort-indicator'); // Use .sort-indicator
if (indicatorSpan) {
indicatorSpan.textContent = bsm_current_sort_direction === 'asc' ? ' ▲' : ' ▼';
}
}
// Pagination Calculation
var totalItems = bsm_filtered_data.length;
var totalPages = Math.ceil(totalItems / bsm_items_per_page);
bsm_current_page = Math.min(bsm_current_page, totalPages) || 1;
var offset = (bsm_current_page - 1) * bsm_items_per_page;
var paginatedItems = bsm_filtered_data.slice(offset, offset + bsm_items_per_page);
console.log("Displaying page:", bsm_current_page, "of", totalPages, "Items on page:", paginatedItems.length);
// Clear existing table rows
tableBody.innerHTML = '';
// Populate Table
if (paginatedItems.length > 0) {
// Row colors calculation (using primary event timestamp for row color consistency)
let lastProcessedDate = '';
let colorIndex = 0;
const processedDates = new Set(); // To track dates already assigned a color index
paginatedItems.forEach(function(item) {
var row = tableBody.insertRow();
// Apply Date-Based Row Coloring (using primary event timestamp)
let rowBgColor = '';
if (item.timestamp && item.timestamp !== '0000-00-00 00:00:00') {
try {
const creationDate = new Date(item.timestamp.replace(' ', 'T') + 'Z'); // Treat as UTC
const dateString = creationDate.toISOString().split('T')[0]; //YYYY-MM-DD
if (!processedDates.has(dateString)) {
const dayOfWeekUTC = creationDate.getUTCDay(); // 0=Sun, 1=Mon...
if (dayOfWeekUTC === 0) {
rowBgColor = bsm_sunday_color;
} else {
const dayIndex = (dayOfWeekUTC - 1); // Mon=0, Tue=1... Sat=5
colorIndex = dayIndex % bsm_day_colors.length; // Cycle through colors based on day of week
rowBgColor = bsm_day_colors[colorIndex];
}
processedDates.add(dateString); // Mark this date as processed for color assignment
lastProcessedDate = dateString; // Keep track of the last date processed
} else {
// If this date has already been processed, use the same color as the last entry of that date
const creationDateForColor = new Date(item.timestamp.replace(' ', 'T') + 'Z');
const dayOfWeekUTCForColor = creationDateForColor.getUTCDay();
if (dayOfWeekUTCForColor === 0) {
rowBgColor = bsm_sunday_color;
} else {
const dayIndexForColor = (dayOfWeekUTCForColor - 1);
rowBgColor = bsm_day_colors[dayIndexForColor % bsm_day_colors.length];
}
}
} catch (e) { /* Handle date parsing error */ }
}
if (rowBgColor) { row.style.backgroundColor = rowBgColor; }
// Calculate "New" Indicator based on effective update time (which is event timestamp for events)
var effectiveTimestamp = item.effective_timestamp || 0;
var age = bsm_current_timestamp - effectiveTimestamp;
var indicator_class = '';
if (effectiveTimestamp > 0 && age <= 72 * 3600) { // Compare against 72 hours ago timestamp directly
if (age <= 24 * 3600) { indicator_class = 'new-indicator-recent'; }
else if (age <= 48 * 3600) { indicator_class = 'new-indicator-medium'; }
else { indicator_class = 'new-indicator-old'; }
}
// --- Populate Cells based on dynamic columns ---
let cellIndex = 0;
// Find the column index for 'Time | Date | By' to determine indicator placement
const timeDateColIndex = displayedColumns.findIndex(col => col.name.includes('Time | Date')); // Find by part of name
displayedColumns.forEach((colDef, index) => {
const cell = row.insertCell(index);
// Add indicator to the Time | Date | By cell (index 1)
if (index === timeDateColIndex && indicator_class) {
cell.innerHTML += '<span class="new-indicator ' + indicator_class + '">New</span>';
cell.classList.add('col-datetime'); // Add class for padding
} else if (colDef.class) {
cell.classList.add(colDef.class); // Add predefined class if any
}
// Populate cell content based on column name
switch(colDef.name) {
case 'Status':
cell.innerHTML = bsm_format_status_column(item);
break;
case 'Time | Date | By':
cell.innerHTML += bsm_format_datetime_with_user(item); // Append to existing indicator span if any
cell.setAttribute('data-sort-value', item.effective_timestamp || 0);
break;
case 'ID':
cell.textContent = item.id || '-';
cell.setAttribute('data-sort-value', item.id || 0);
break;
case 'Order ID':
cell.textContent = (item.order_id && item.order_id !== '-') ? String(item.order_id).substring(0, 8) : '-';
break;
case 'Sale Type':
cell.textContent = item.sale_type || 'N/A';
break;
case 'Reason or Sale Name':
cell.textContent = item.reason || 'N/A';
break;
case 'Amount ৳':
// Show Amount ৳ only if amount_bdt_display is available and not null
cell.textContent = (item.amount_bdt_display !== null && !isNaN(parseFloat(item.amount_bdt_display))) ? `${parseFloat(item.amount_bdt_display).toFixed(2)} ৳` : '-';
if (item.amount_bdt_display !== null && !isNaN(parseFloat(item.amount_bdt_display))) {
if (parseFloat(item.amount_bdt_display) > 0) cell.classList.add('amount-plus');
else if (parseFloat(item.amount_bdt_display) < 0) cell.classList.add('amount-minus');
}
cell.setAttribute('data-sort-value', parseFloat(item.amount_bdt_display) || 0);
break;
case 'Org $':
// Show Org $ only if org_usd_display is available and not null
cell.textContent = (item.org_usd_display !== null && !isNaN(parseFloat(item.org_usd_display))) ? `$ ${parseFloat(item.org_usd_display).toFixed(2)}` : '-';
if (item.org_usd_display !== null && !isNaN(parseFloat(item.org_usd_display))) {
if (parseFloat(item.org_usd_display) > 0) cell.classList.add('amount-plus');
else if (parseFloat(item.org_usd_display) < 0) cell.classList.add('amount-minus');
}
cell.setAttribute('data-sort-value', parseFloat(item.org_usd_display) || 0);
break;
case 'Balance': // Only appears for All Status
if (isAllStatus) {
let balance = parseFloat(item.calculated_balance) || 0;
cell.textContent = `$ ${balance.toFixed(2)}`;
if (balance >= 0) cell.classList.add('amount-plus');
else cell.classList.add('amount-minus');
cell.setAttribute('data-sort-value', balance);
}
break;
default:
cell.textContent = 'N/A'; // Fallback
break;
}
});
});
} else {
// Update colspan based on whether Balance column is shown
const colspan = isAllStatus ? 9 : 8; // 9 columns for All Status, 8 for others
tableBody.innerHTML = `<tr><td colspan="${colspan}">No entries found for this status.</td></tr>`;
}
// Update Pagination
paginationContainer.innerHTML = '';
if (totalPages > 1) {
for (var i = 1; i <= totalPages; i++) {
var pageLink = document.createElement(i === bsm_current_page ? 'span' : 'a');
pageLink.href = '#status-sales-table-section'; // Link to section id
pageLink.textContent = i;
if (i === bsm_current_page) { pageLink.className = 'current-page'; }
else {
pageLink.className = 'page-number'; pageLink.setAttribute('data-page', i);
// Use a delegated event listener or rebind after population if necessary
// For simplicity here, direct binding on generated elements:
pageLink.addEventListener('click', function(e) {
e.preventDefault();
bsm_current_page = parseInt(this.getAttribute('data-page'));
displaySalesForStatus(bsm_current_filter_slug, false); // Don't reset sort on pagination click
var tableSection = document.getElementById('status-sales-table-section');
if (tableSection) { tableSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Scroll to table
});
}
paginationContainer.appendChild(pageLink); paginationContainer.appendChild(document.createTextNode(' '));
}
}
// Rebind modal trigger links (if any) - currently no triggers in this table view, but keeping the structure
// rebindAllTxnLogLinks('#status-sales-table .note-log-cell a.more-note-log-icon', 'note');
// rebindAllTxnLogLinks('#status-sales-table .status-log-cell a.more-status-log-icon', 'status');
}
// Add Click Handlers to Status Buttons
document.addEventListener('DOMContentLoaded', function() {
console.log("DOM Loaded. Initializing status activities page...");
console.log("Event Data Available:", typeof bsm_event_data !== 'undefined' ? bsm_event_data.length + " items" : "No");
console.log("Default Status Slug:", bsm_default_status_slug);
var statusButtons = document.querySelectorAll('.status-box');
console.log("Found status buttons:", statusButtons.length);
statusButtons.forEach(function(button) {
button.addEventListener('click', function() {
var statusSlug = this.getAttribute('data-status-slug');
console.log("Status button clicked:", statusSlug);
statusButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
bsm_current_page = 1; // Reset pagination on status change
displaySalesForStatus(statusSlug, true); // Reset sort on status change
});
});
// Initial Table Load (Default to the specified slug, or "all" if not found)
var initialSlugToLoad = bsm_default_status_slug || 'all';
var firstButton = document.querySelector('.status-box[data-status-slug="' + initialSlugToLoad + '"]');
if(firstButton) {
console.log("Activating default status:", initialSlugToLoad);
firstButton.classList.add('active');
displaySalesForStatus(initialSlugToLoad, true); // Initial load resets sort
} else {
console.error("Could not find the default status button for slug:", initialSlugToLoad, ". Falling back to 'all'.");
var allButton = document.querySelector('.status-box[data-status-slug="all"]');
if (allButton) {
console.log("Activating 'All Status' button as fallback.");
allButton.classList.add('active');
displaySalesForStatus('all', true);
} else {
console.error("Could not find the 'All Status' button either! Table cannot be initialized.");
document.getElementById('table-status-title').textContent = 'Error: Could not initialize transaction list.';
document.querySelector('#status-sales-table tbody').innerHTML = `<tr><td colspan="${<?php echo $initial_colspan; ?>}">Initialization error. No status buttons found.</td></tr>`;
}
}
});
// --- Log Modal Logic ---
// Handler function for log link clicks (Note: Trigger icons are not added in this version of the table)
function handleAllTxnLogClick(e) {
e.preventDefault();
var saleId = this.getAttribute('data-sale-id');
var logType = this.getAttribute('data-log-type'); // 'status' or 'note'
var modalId = (logType === 'note') ? 'all-txn-note-log-modal' : 'all-txn-status-log-modal';
var modalContentId = (logType === 'note') ? 'all-txn-note-log-modal-content' : 'all-txn-status-log-modal-content';
var modalContentEl = document.getElementById(modalContentId);
var modalEl = document.getElementById(modalId);
if (!modalContentEl || !modalEl) { console.error("All Txn log modal elements not found for type: " + logType); return; }
modalContentEl.innerHTML = '<p>Loading logs...</p>'; // Show loading message
modalEl.style.display = 'block'; // Display the modal
var fd = new FormData();
fd.append("action", "bsm_fetch_all_logs"); // This AJAX action is in includes/seller/statas-log-notes.php
fd.append("sale_id", saleId);
fd.append("log_type", logType);
fd.append("nonce", bsm_ajax_obj.fetch_nonce); // Use localized nonce
fetch(bsm_ajax_obj.ajaxurl, { method: "POST", body: fd, credentials: 'same-origin' })
.then(response => {
if (!response.ok) {
return response.text().then(text => { throw new Error(text || response.status); });
}
return response.json();
})
.then(data => {
if (data.success && data.data.html) {
modalContentEl.innerHTML = data.data.html;
} else {
modalContentEl.innerHTML = '<p>Error loading logs: ' + (data.data ? (data.data.message || data.data) : 'Unknown error') + '</p>';
console.error("AJAX Error Response:", data); // Log error details
}
})
.catch(error => {
console.error('Error fetching all txn logs:', error);
modalContentEl.innerHTML = '<p>AJAX Error loading logs: ' + error.message + '</p>';
});
}
// Function to bind click events to log links (status or note) - called after table is populated
function rebindAllTxnLogLinks(selector, logTypeToUse) {
// This function is currently not adding event listeners because the trigger icons are not in this table version.
// If you add trigger icons/links for modals in the table later, this function would be used to bind their click events.
console.log(`Log link re-binding called for selector: ${selector}, type: ${logTypeToUse} (triggers not present in this table view)`);
}
// Generic modal close binding
document.querySelectorAll('.log-modal .close').forEach(function(el) {
el.addEventListener('click', function() {
this.closest('.log-modal').style.display = 'none';
});
});
// Close modal when clicking outside of it
window.addEventListener('click', function(event) {
if (event.target.classList.contains('log-modal')) {
event.target.style.display = 'none';
}
});
</script>
business-seller-management/templates/seller/dashboard-page.php
<?php
if (!defined('ABSPATH')) {
exit;
}
/*
Note:
- এখানে আমরা $org_balance, $today_total_sales, $today_total_profit, $today_total_works,
$yesterday_total_sales, $yesterday_total_profit, $yesterday_total_works,
$month_total_sales, $month_total_profit, $month_total_works, $month_total_points,
$last_month_total_sales, $last_month_total_profit, $last_month_total_works ইত্যাদি
ভেরিয়েবলগুলোকে হিসাবের জন্য ডাটাবেজ থেকে সংগ্রহ করবো।
- **নতুন:** Summary এ আমরা শুধুমাত্র "Successful All" ও "Delivery Done" স্ট্যাটাসের বিক্রয় (Other গ্রুপ বাদে) থেকে
Sales, Profit ও Works Done গণনা করবো; আর Point গণনায় সকল (সফল) বিক্রয়ের পয়েন্ট ব্যবহার করা হবে।
- Level, Delay, Void – এগুলো নিম্নলিখিত নিয়মে গণনা হবে:
* Level (Today's & Yesterday's): ঐ দিনের মোট পয়েন্টের ভিত্তিতে daily_levels_bonus অপশন থেকে বর্তমান লেভেল।
* Bu Level (This Month's): এই মাসের মোট পয়েন্টের ভিত্তিতে monthly_levels অপশন থেকে বর্তমান লেভেল।
* Profit (BDT): শুধুমাত্র ঐ বিক্রয়ের লাভ (profit) – Other গ্রুপ বাদে।
* Works Done: ঐ দিনের/মাসের মোট সফল (status "Successful All" বা "Delivery Done") বিক্রয়ের সংখ্যা।
* Delay: ঐ দিনের/মাসের, status গুলোতে যেগুলো "Refund Required","Refund Done","Reject Delivery Done","Cancel","Block","Rejected","Failed","Successful All","Delivery Done" ছাড়া বিক্রয় সংখ্যা (Other গ্রুপ বাদে)।
* Void: ঐ দিনের/মাসের, status গুলোতে যেগুলো উপরের সেটে আছে, তাদের মোট সংখ্যা (Other গ্রুপ বাদে)।
*/
// ---------------------- নতুন হিসাবের কোড শুরু ----------------------
$current_user_id = get_current_user_id();
if (!$current_user_id) {
echo '<p style="color:red; font-weight:bold;">Please log in to access the Seller Dashboard.</p>';
return;
}
global $wpdb;
// Use WordPress local time (current_time('timestamp'))
$now = current_time('timestamp');
$todayDate = date('Y-m-d', $now);
$yesterdayDate = date('Y-m-d', $now - 86400);
$firstDayOfMonth = date('Y-m-01', $now);
$lastDayOfMonth = date('Y-m-t', $now);
$firstDayLastMonth = date('Y-m-01', strtotime('first day of last month', $now));
$lastDayLastMonth = date('Y-m-t', strtotime('last day of last month', $now));
// Organization balance
$org_balance = $wpdb->get_var(
$wpdb->prepare("SELECT gsmalo_org_balance FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d", $current_user_id)
);
// (নতুন) আজকের ডলারের দাম, ডিফল্ট 85
$today_dollar_price = get_option('bsm_usd_value_in_taka', 85);
// Only include sales with status "Successful All" or "Delivery Done", exclude Other group
$status_condition = "AND status IN ('Successful All','Delivery Done') AND sale_type NOT IN ('marketing','video','office_management','other_option','other')";
// Today's Summary
$today_total_sales = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(selling_price), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
$status_condition",
$current_user_id,
$todayDate
));
$today_total_profit = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(profit), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
$status_condition",
$current_user_id,
$todayDate
));
$today_total_works = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
$status_condition",
$current_user_id,
$todayDate
));
// Yesterday's Summary
$yesterday_total_sales = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(selling_price), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
$status_condition",
$current_user_id,
$yesterdayDate
));
$yesterday_total_profit = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(profit), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
$status_condition",
$current_user_id,
$yesterdayDate
));
$yesterday_total_works = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
$status_condition",
$current_user_id,
$yesterdayDate
));
// This Month's Summary
$month_total_sales = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(selling_price), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
$status_condition",
$current_user_id,
$firstDayOfMonth,
$lastDayOfMonth
));
$month_total_profit = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(profit), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
$status_condition",
$current_user_id,
$firstDayOfMonth,
$lastDayOfMonth
));
$month_total_works = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
$status_condition",
$current_user_id,
$firstDayOfMonth,
$lastDayOfMonth
));
// Last Month's Summary
$last_month_total_sales = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(selling_price), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
$status_condition",
$current_user_id,
$firstDayLastMonth,
$lastDayLastMonth
));
$last_month_total_profit = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(profit), 0)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
$status_condition",
$current_user_id,
$firstDayLastMonth,
$lastDayLastMonth
));
$last_month_total_works = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
$status_condition",
$current_user_id,
$firstDayLastMonth,
$lastDayLastMonth
));
// ---------------------- Point Calculations ----------------------
$today_point = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(p.points), 0)
FROM {$wpdb->prefix}bsm_points p
JOIN {$wpdb->prefix}bsm_sales s ON p.sale_id = s.id
WHERE p.user_id = %d
AND s.status IN ('Successful All','Delivery Done')
AND DATE(p.created_at) = %s",
$current_user_id,
$todayDate
));
$yesterday_point = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(p.points), 0)
FROM {$wpdb->prefix}bsm_points p
JOIN {$wpdb->prefix}bsm_sales s ON p.sale_id = s.id
WHERE p.user_id = %d
AND s.status IN ('Successful All','Delivery Done')
AND DATE(p.created_at) = %s",
$current_user_id,
$yesterdayDate
));
$month_point = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(p.points), 0)
FROM {$wpdb->prefix}bsm_points p
JOIN {$wpdb->prefix}bsm_sales s ON p.sale_id = s.id
WHERE p.user_id = %d
AND s.status IN ('Successful All','Delivery Done')
AND DATE(p.created_at) BETWEEN %s AND %s",
$current_user_id,
$firstDayOfMonth,
$lastDayOfMonth
));
$last_month_point = $wpdb->get_var($wpdb->prepare(
"SELECT IFNULL(SUM(p.points), 0)
FROM {$wpdb->prefix}bsm_points p
JOIN {$wpdb->prefix}bsm_sales s ON p.sale_id = s.id
WHERE p.user_id = %d
AND s.status IN ('Successful All','Delivery Done')
AND DATE(p.created_at) BETWEEN %s AND %s",
$current_user_id,
$firstDayLastMonth,
$lastDayLastMonth
));
// ---------------------- Level Calculations ----------------------
// Daily
$daily_levels_bonus = get_option('bsm_daily_levels_bonus', array(
1 => array('name'=>'Newbie Seller','range'=>199,'bonus'=>0,'enabled'=>1),
2 => array('name'=>'Starter Seller','range'=>200,'bonus'=>30,'enabled'=>1),
3 => array('name'=>'Active Seller','range'=>300,'bonus'=>50,'enabled'=>1),
));
$current_today_level = 1;
if (!empty($daily_levels_bonus) && is_array($daily_levels_bonus)) {
ksort($daily_levels_bonus);
foreach ($daily_levels_bonus as $lvl => $conf) {
if (intval($conf['enabled']) === 1 && $today_point >= floatval($conf['range'])) {
$current_today_level = $lvl;
} else {
break;
}
}
}
$current_yesterday_level = 1;
if (!empty($daily_levels_bonus) && is_array($daily_levels_bonus)) {
ksort($daily_levels_bonus);
foreach ($daily_levels_bonus as $lvl => $conf) {
if (intval($conf['enabled']) === 1 && $yesterday_point >= floatval($conf['range'])) {
$current_yesterday_level = $lvl;
} else {
break;
}
}
}
// Monthly
$monthly_levels = get_option('bsm_monthly_levels', array(
1 => array('name'=>'Default Level (Monthly)','threshold'=>0,'bonus'=>0,'enabled'=>1),
2 => array('name'=>'Fast Achiever', 'threshold' => 16000, 'bonus'=>1000,'enabled'=>1),
3 => array('name'=>'Sales Master', 'threshold' => 23000, 'bonus'=>1500,'enabled'=>1),
4 => array('name'=>'Power Seller', 'threshold' => 30000, 'bonus'=>2000,'enabled'=>1),
5 => array('name'=>'Top Performer', 'threshold' => 35000, 'bonus'=>3000,'enabled'=>1),
));
$current_month_level = 1;
if (!empty($monthly_levels) && is_array($monthly_levels)) {
ksort($monthly_levels);
foreach ($monthly_levels as $lvl => $conf) {
if (intval($conf['enabled']) === 1 && $month_point >= floatval($conf['threshold'])) {
$current_month_level = $lvl;
} else {
break;
}
}
}
$current_last_month_level = 1;
if (!empty($monthly_levels) && is_array($monthly_levels)) {
ksort($monthly_levels);
foreach ($monthly_levels as $lvl => $conf) {
if (intval($conf['enabled']) === 1 && $last_month_point >= floatval($conf['threshold'])) {
$current_last_month_level = $lvl;
} else {
break;
}
}
}
// ---------------------- Delay and Void Calculations ----------------------
$other_exclude = "sale_type NOT IN ('marketing','video','office_management','other_option','other')";
$void_statuses = "('Refund Required','Refund Done','Reject Delivery Done','Cancel','Block','Rejected','Failed','Successful All','Delivery Done')";
// Today
$today_delay = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
AND status NOT IN ('Refund Required','Refund Done','Reject Delivery Done','Cancel','Block','Rejected','Failed','Successful All','Delivery Done')
AND $other_exclude",
$current_user_id,
$todayDate
));
$today_void = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
AND status IN $void_statuses
AND $other_exclude",
$current_user_id,
$todayDate
));
// Yesterday
$yesterday_delay = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
AND status NOT IN ('Refund Required','Refund Done','Reject Delivery Done','Cancel','Block','Rejected','Failed','Successful All','Delivery Done')
AND $other_exclude",
$current_user_id,
$yesterdayDate
));
$yesterday_void = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) = %s
AND status IN $void_statuses
AND $other_exclude",
$current_user_id,
$yesterdayDate
));
// This Month
$month_delay = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
AND status NOT IN ('Refund Required','Refund Done','Reject Delivery Done','Cancel','Block','Rejected','Failed','Successful All','Delivery Done')
AND $other_exclude",
$current_user_id,
$firstDayOfMonth,
$lastDayOfMonth
));
$month_void = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
AND status IN $void_statuses
AND $other_exclude",
$current_user_id,
$firstDayOfMonth,
$lastDayOfMonth
));
// Last Month
$last_month_delay = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
AND status NOT IN ('Refund Required','Refund Done','Reject Delivery Done','Cancel','Block','Rejected','Failed','Successful All','Delivery Done')
AND $other_exclude",
$current_user_id,
$firstDayLastMonth,
$lastDayLastMonth
));
$last_month_void = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*)
FROM {$wpdb->prefix}bsm_sales
WHERE seller_id = %d
AND DATE(created_at) BETWEEN %s AND %s
AND status IN $void_statuses
AND $other_exclude",
$current_user_id,
$firstDayLastMonth,
$lastDayLastMonth
));
// ---------------------- নতুন হিসাবের কোড শেষ ----------------------
?>
<style>
.seller-dashboard-page-wrap {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
font-family: Arial, sans-serif;
background: #f7f7f7;
}
.seller-dash-section {
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 30px;
padding: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.15);
}
.seller-dash-section h2 {
margin-top: 0;
margin-bottom: 20px;
font-weight: 600;
color: #333;
}
.dash-summary-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
}
.dash-box-item {
flex: 1 1 calc(20% - 15px);
border-radius: 8px;
padding: 15px;
text-align: center;
color: #fff;
box-shadow: 0 3px 6px rgba(0,0,0,0.12);
}
.dash-box-item strong {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.dash-box-item span {
font-size: 18px;
font-weight: 700;
}
/* Gradient backgrounds for different boxes */
/* আপনি চাইলে আলাদা রঙ কোড ব্যবহার করতে পারেন */
.box-balance {
background: linear-gradient(45deg, #F44336, #E91E63);
}
.box-month-sales {
background: linear-gradient(45deg, #4CAF50, #8BC34A);
}
.box-month-works {
background: linear-gradient(45deg, #2196F3, #03A9F4);
}
.box-month-profit {
background: linear-gradient(45deg, #FF9800, #FFC107);
}
.box-month-points {
background: linear-gradient(45deg, #9C27B0, #CE93D8);
}
/* Responsive adjustments */
@media (max-width: 992px) {
.dash-box-item {
flex: 1 1 calc(50% - 15px);
}
}
@media (max-width: 576px) {
.dash-box-item {
flex: 1 1 100%;
}
}
</style>
<div class="seller-dashboard-page-wrap">
<!-- নতুন "top summary" সেকশন (সবার উপরে): কোনো Heading দেখাবে না -->
<div class="seller-dash-section" style="background:#f0f0f0;">
<div class="dash-summary-row">
<!-- 1) gsmalo.org Balance -->
<div class="dash-box-item" style="background: linear-gradient(45deg, #F44336, #E91E63);">
<strong>gsmalo.org Balance</strong>
<span><?php echo esc_html($org_balance); ?> USD</span>
</div>
<!-- 2) Today's dollar price -->
<div class="dash-box-item" style="background: linear-gradient(45deg, #00ACC1, #26C6DA);">
<strong>Today's dollar price</strong>
<span><?php echo esc_html($today_dollar_price); ?></span>
</div>
<!-- 3) This Month Points -->
<div class="dash-box-item" style="background: linear-gradient(45deg, #9C27B0, #CE93D8);">
<strong>This Month Points</strong>
<span><?php echo esc_html($month_point); ?></span>
</div>
<!-- 4) This Month Level -->
<div class="dash-box-item" style="background: linear-gradient(45deg, #8E24AA, #BA68C8);">
<strong>This Month Level</strong>
<span>L<?php echo esc_html($current_month_level); ?></span>
</div>
<!-- 5) Delay (এখানে আমরা মাসের Delay দেখাচ্ছি) -->
<div class="dash-box-item" style="background: linear-gradient(45deg, #FF7043, #FF8A65);">
<strong>Delay</strong>
<span><?php echo esc_html($month_delay); ?></span>
</div>
</div>
</div>
<!-- top summary সেকশন শেষ -->
<!-- ১) Today's Summary -->
<div class="seller-dash-section" style="background: #E3F2FD;">
<h2>Today's Summary</h2>
<div class="dash-summary-row">
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #2196F3, #42A5F5);">
<strong>Sales</strong>
<span><?php echo esc_html($today_total_sales); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #00ACC1, #26C6DA);">
<strong>Point</strong>
<span><?php echo esc_html($today_point); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #8E24AA, #BA68C8);">
<strong>Level</strong>
<span>L<?php echo esc_html($current_today_level); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FDD835, #FFF176);">
<strong>Profit (BDT)</strong>
<span><?php echo esc_html($today_total_profit); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #5C6BC0, #9FA8DA);">
<strong>Works Done</strong>
<span><?php echo esc_html($today_total_works); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FF7043, #FF8A65);">
<strong>Delay</strong>
<span><?php echo esc_html($today_delay); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #757575, #9E9E9E);">
<strong>Void</strong>
<span><?php echo esc_html($today_void); ?></span>
</div>
</div>
</div>
<!-- ২) Yesterday's Summary -->
<div class="seller-dash-section" style="background: #FFF9C4;">
<h2>Yesterday's Summary</h2>
<div class="dash-summary-row">
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FFA726, #FFB74D);">
<strong>Sales</strong>
<span><?php echo esc_html($yesterday_total_sales); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #00ACC1, #26C6DA);">
<strong>Point</strong>
<span><?php echo esc_html($yesterday_point); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #8E24AA, #BA68C8);">
<strong>Level</strong>
<span>L<?php echo esc_html($current_yesterday_level); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FDD835, #FFF176);">
<strong>Profit (BDT)</strong>
<span><?php echo esc_html($yesterday_total_profit); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #9E9E9E, #E0E0E0);">
<strong>Works Done</strong>
<span><?php echo esc_html($yesterday_total_works); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FF7043, #FF8A65);">
<strong>Delay</strong>
<span><?php echo esc_html($yesterday_delay); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #757575, #9E9E9E);">
<strong>Void</strong>
<span><?php echo esc_html($yesterday_void); ?></span>
</div>
</div>
</div>
<!-- ৩) This Month's Summary -->
<div class="seller-dash-section" style="background:#F1F8E9;">
<h2>This Month's Summary</h2>
<div class="dash-summary-row">
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #4CAF50, #66BB6A);">
<strong>Sales</strong>
<span><?php echo esc_html($month_total_sales); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #00ACC1, #26C6DA);">
<strong>Point</strong>
<span><?php echo esc_html($month_point); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #8E24AA, #BA68C8);">
<strong>Bu Level</strong>
<span>L<?php echo esc_html($current_month_level); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #9E9E9E, #E0E0E0);">
<strong>Works Done</strong>
<span><?php echo esc_html($month_total_works); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FF7043, #FF8A65);">
<strong>Delay</strong>
<span><?php echo esc_html($month_delay); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #757575, #9E9E9E);">
<strong>Void</strong>
<span><?php echo esc_html($month_void); ?></span>
</div>
</div>
</div>
<!-- ৪) Last Month's Summary -->
<div class="seller-dash-section" style="background:#FFECB3;">
<h2>Last Month's Summary</h2>
<div class="dash-summary-row">
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FF7043, #FFAB91);">
<strong>Sales</strong>
<span><?php echo esc_html($last_month_total_sales); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #00ACC1, #26C6DA);">
<strong>Point</strong>
<span><?php echo esc_html($last_month_point); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #8E24AA, #BA68C8);">
<strong>Level</strong>
<span>L<?php echo esc_html($current_last_month_level); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FDD835, #FFF176);">
<strong>Profit (BDT)</strong>
<span><?php echo esc_html($last_month_total_profit); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #9E9E9E, #E0E0E0);">
<strong>Works Done</strong>
<span><?php echo esc_html($last_month_total_works); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #FF7043, #FF8A65);">
<strong>Delay</strong>
<span><?php echo esc_html($last_month_delay); ?></span>
</div>
<div class="dash-box-item" style="flex:1; background: linear-gradient(45deg, #757575, #9E9E9E);">
<strong>Void</strong>
<span><?php echo esc_html($last_month_void); ?></span>
</div>
</div>
</div>
</div>
<?php
/**
* File: business-seller-management/templates/seller/all-transaction-history-page.php
*
* Description:
* - Displays the COMPLETE transaction history (ALL sale types + adjustments) for the currently logged-in seller.
* - Triggered by the [bsm_seller_all_transaction_history] shortcode.
* - Shows seller name and current gsmalo.org balance.
* - Includes Search, Warning Summary (for adjustments), Sortable Table, Pagination.
* - Transaction table includes (NEW ORDER & CONTENT):
* 1. Time, Date & User
* 2. ID
* 3. Order ID
* 4. Sale Type
* 5. Reason or Sale Name
* 6. Warning / Fixed By
* 7. Last Note (Sortable)
* 8. Status Log (Sortable)
* 9. Status
* 10. Purchase Price (USD/৳)
* 11. Selling Price (৳)
* 12. Point (Base)
* 13. Profit / Loss (৳)
* - Status Log / Last Note columns show latest log summary + clickable count icon (color-coded by age).
* - Status column shows status text/icon/color + change info.
* - "New" indicator (Red/Yellow/Gray) shown for recent transactions/fixes (up to 72h).
* - Table headers are sortable (JS). Includes pagination.
* - Added horizontal scrolling for responsiveness.
* - Fixed PHP Notice: Undefined property.
* - Added date-based row coloring (Mon-Sat cycle, special Sunday color).
*
* Variables available from the shortcode function:
* - $seller_id : The User ID of the currently logged-in seller.
*/
if ( ! defined('ABSPATH') ) {
exit; // Exit if accessed directly
}
// Ensure helper functions are available (should be included by main plugin file)
if (!function_exists('bsm_calculate_balance_change_for_status_transition') || !function_exists('bsm_parse_status_from_log')) {
echo '<p style="color:red;">Error: Required helper functions for status log processing are missing. Please ensure the main plugin file includes `includes/seller/statas-log-notes.php`.</p>';
// error_log("BSM Error: Functions bsm_calculate_balance_change_for_status_transition or bsm_parse_status_from_log not found in all-transaction-history-page.php.");
// return; // Stop execution if critical
}
// Ensure $seller_id is set
if ( ! isset( $seller_id ) || ! is_numeric( $seller_id ) || $seller_id <= 0 ) {
$seller_id = get_current_user_id();
if (!$seller_id) {
echo '<p style="color:red;">Error: Seller ID not found or user not logged in.</p>';
return;
}
}
global $wpdb;
// --- Fetch Seller Data ---
$seller_user = get_userdata($seller_id);
if ( ! $seller_user ) {
echo '<p style="color:red;">Error: Seller data could not be retrieved.</p>';
return;
}
$seller_name = $seller_user->display_name;
$seller_balance = $wpdb->get_var( $wpdb->prepare("SELECT gsmalo_org_balance FROM {$wpdb->prefix}bsm_sellers WHERE user_id = %d", $seller_id) );
if ( $seller_balance === null ) {
$seller_balance = 0;
}
$dollarRate = get_option('bsm_usd_value_in_taka', 85);
// --- Search, Date, and Pagination variables (using unique keys) ---
$search_query = isset($_GET['search_all_txn']) ? sanitize_text_field($_GET['search_all_txn']) : '';
$search_date = isset($_GET['search_all_date']) ? sanitize_text_field($_GET['search_all_date']) : '';
// Records per Page settings
$allowed_per_page = array(50, 100, 200, 400, 500, 1000);
$default_per_page = 100;
$per_page = ( isset($_GET['all_txn_per_page']) && in_array( intval($_GET['all_txn_per_page']), $allowed_per_page ) )
? intval($_GET['all_txn_per_page'])
: $default_per_page;
// --- Fetch Transactions ---
// Fetch ALL Sales details needed for the table
$sales_table = $wpdb->prefix . 'bsm_sales';
$sales_query = $wpdb->prepare(
"SELECT id, order_id, product_name, status, purchase_price, selling_price, profit, loss, created_at, seller_id, sale_type
FROM $sales_table
WHERE seller_id = %d",
$seller_id
);
$sales = $wpdb->get_results($sales_query);
// Fetch Balance Adjustments for the seller
$adjustments_table = $wpdb->prefix . 'bsm_balance_adjustments';
$adjustments_query = $wpdb->prepare(
"SELECT id, seller_id, adjusted_amount, reason, created_at, adjusted_by, fixed_by, fixed_at
FROM $adjustments_table
WHERE seller_id = %d",
$seller_id
);
$adjustments = $wpdb->get_results($adjustments_query);
// Combine Sales and Adjustments into transactions array
$transactions = array();
// Process Sales into the common format
if ( $sales ) {
foreach ( $sales as $sale ) {
$transactions[] = array(
'datetime' => $sale->created_at,
'fixed_at' => null,
'id' => $sale->id,
'order_id' => $sale->order_id,
'reason' => $sale->product_name,
'warning_text' => '',
'fix_note' => '',
'status' => $sale->status,
'amount' => null,
'purchase_price'=> $sale->purchase_price,
'selling_price' => $sale->selling_price,
'profit_bdt' => $sale->profit,
'loss_bdt' => $sale->loss,
'sale_type' => $sale->sale_type,
'type' => 'sale',
'db_reason' => $sale->product_name,
'adjuster_name' => $seller_name, // Use seller name for sales
'is_active_warning' => false
);
}
}
// Process Adjustments into the common format
if ( $adjustments ) {
foreach ( $adjustments as $adj ) {
$db_reason = trim($adj->reason);
$adjuster_name = 'Admin/System';
if ( $adj->adjusted_by > 0 ) {
$adjuster_data = get_userdata($adj->adjusted_by);
if ($adjuster_data) { $adjuster_name = $adjuster_data->display_name; }
}
$has_warning_tag = (stripos($db_reason, '[warning]') !== false);
$is_fixed = !empty($adj->fixed_by);
$is_active_warning = $has_warning_tag && !$is_fixed;
$warningText = "";
$fix_note_html = '';
if ($is_active_warning) {
$warningText = "[Warning Active]";
} elseif ($is_fixed) {
$fixer_user = get_userdata($adj->fixed_by);
$fixer_name = $fixer_user ? $fixer_user->display_name : ('User ID: '.$adj->fixed_by);
$fix_time_display = !empty($adj->fixed_at) ? date('h:i A | d-m-Y', strtotime($adj->fixed_at)) : 'N/A';
$fix_note_html = 'Fixed By: '. esc_html($fixer_name) . '<br><small>' . esc_html($fix_time_display) . '</small>';
$warningText = $fix_note_html;
}
$display_reason = trim(preg_replace('/\s*\[warning\]/i', '', $db_reason));
$transactions[] = array(
'datetime' => $adj->created_at,
'fixed_at' => $adj->fixed_at,
'id' => $adj->id,
'order_id' => '-',
'reason' => $display_reason,
'warning_text' => $warningText,
'fix_note' => '',
'status' => ( floatval($adj->adjusted_amount) >= 0 ) ? "Addition" : "Revert",
'amount' => null,
'purchase_price'=> null,
'selling_price' => null,
'profit_bdt' => null,
'loss_bdt' => null,
'sale_type' => 'Balance Adjustment',
'type' => 'adjustment',
'db_reason' => $db_reason,
'adjuster_name' => $adjuster_name,
'is_active_warning' => $is_active_warning
);
}
}
// Sort Transactions by primary datetime descending
usort($transactions, function($a, $b) {
return strcmp($b['datetime'], $a['datetime']);
});
// Apply Text Search Filter
if ( ! empty($search_query) ) {
$transactions = array_filter($transactions, function($txn) use ($search_query) {
$id_match = isset($txn['id']) && stripos((string)$txn['id'], $search_query) !== false;
$order_match = isset($txn['order_id']) && $txn['order_id'] !== '-' && stripos($txn['order_id'], $search_query) !== false;
$reason_match = isset($txn['db_reason']) && stripos($txn['db_reason'], $search_query) !== false;
return ($id_match || $order_match || $reason_match);
});
$transactions = array_values($transactions); // Re-index array
}
// Apply Date Search Filter
if ( ! empty($search_date) ) {
$transactions = array_filter($transactions, function($txn) use ($search_date) {
return ( date('Y-m-d', strtotime($txn['datetime'])) === $search_date );
});
$transactions = array_values($transactions); // Re-index array
}
// Calculate Pagination details
$page = isset($_GET['all_txn_paged']) ? max(1, intval($_GET['all_txn_paged'])) : 1;
$total_transactions = count($transactions);
$total_pages = ($total_transactions > 0 && $per_page > 0) ? ceil($total_transactions / $per_page) : 1;
$page = min($page, $total_pages);
$offset = ($page - 1) * $per_page;
$transactions_display = array_slice($transactions, $offset, $per_page);
// Calculate Warning Summary for the seller (based on *active* warnings from adjustments)
$total_warning = 0; $total_negative = 0; $total_positive = 0;
if ( $adjustments ) {
foreach ( $adjustments as $adj ) {
if (isset($adj->seller_id) && $adj->seller_id == $seller_id && stripos($adj->reason, '[warning]') !== false && empty($adj->fixed_by) ) {
$total_warning++; $amt = floatval($adj->adjusted_amount);
if ($amt < 0) { $total_negative += $amt; } elseif ($amt > 0) { $total_positive += $amt; }
}
}
}
// Timestamps for indicators
$current_timestamp = current_time('timestamp');
$twenty_four_hours_ago = $current_timestamp - (24 * 3600);
$forty_eight_hours_ago = $current_timestamp - (48 * 3600);
$seventy_two_hours_ago = $current_timestamp - (72 * 3600);
// Status Styles Map
$status_styles = [ /* Same map */
'Addition' => ['color' => '#00008B', 'icon' => '➕'], 'Successful All' => ['color' => '#00008B', 'icon' => '✔'], 'Delivery Done' => ['color' => '#00008B', 'icon' => '✔'], 'Refund Done' => ['color' => '#00008B', 'icon' => '💵'],
'Revert' => ['color' => '#FF6347', 'icon' => '↩️'], 'Refund Required' => ['color' => '#DAA520', 'icon' => '💸'], 'Success But Not Delivery'=> ['color' => '#FF6347', 'icon' => '❎'],
'pending' => ['color' => '#DAA520', 'icon' => '⏳'], 'On Hold' => ['color' => '#DAA520', 'icon' => '⏸️'], 'In Process' => ['color' => '#DAA520', 'icon' => '🔄'],
'Need Parts' => ['color' => '#DAA520', 'icon' => '⚙️'], 'Parts Brought' => ['color' => '#DAA520', 'icon' => '📦'], 'Check Admin' => ['color' => '#DAA520', 'icon' => '👨💼'],
'Review Apply' => ['color' => '#DAA520', 'icon' => '🔍'], 'Block' => ['color' => '#800080', 'icon' => '🔒'],
'Reject Delivery Done' => ['color' => '#000000', 'icon' => '🔴'], 'Cost' => ['color' => '#000000', 'icon' => '💰'], 'Cancel' => ['color' => '#000000', 'icon' => '❌'],
'Rejected' => ['color' => '#000000', 'icon' => '❌'], 'Failed' => ['color' => '#000000', 'icon' => '❌'],
];
// Refund trigger statuses (used by helper function called via AJAX)
$positive_statuses = get_option('bsm_refund_trigger_statuses', ['Reject Delivery Done', 'Refund Done', 'Rejected']);
if (!is_array($positive_statuses)) $positive_statuses = ['Reject Delivery Done', 'Refund Done', 'Rejected'];
$positive_statuses[] = 'Addition';
// Date-based Row Coloring setup
$day_colors = [ '#E8F8F5', '#FEF9E7', '#F4ECF7', '#FDEDEC', '#EBF5FB', '#FDF2E9' ]; // Mon-Sat
$sunday_color = '#FFF8DC'; // Cornsilk for Sunday
$last_processed_date = '';
$color_index = 0;
// Generate nonce for fetching logs (used in JS)
$log_fetch_nonce = wp_create_nonce('bsm_note_nonce'); // Re-using the same nonce for both log types
?>
<div class="bsm-seller-all-txn-history-wrap">
<h2>Your Complete Transaction History</h2>
<div class="seller-balance-info">
<strong>Welcome, <?php echo esc_html($seller_name); ?>!</strong> Your current gsmalo.org Balance is:
<strong style="color: #d32f2f; font-size: 1.2em;"><?php echo esc_html(number_format((float)$seller_balance, 2)); ?> USD</strong>
</div>
<div class="search-bar-container">
<div class="search-bar-inner">
<form method="get" action="">
<?php foreach ($_GET as $key => $value) { if (!in_array($key, ['search_all_txn', 'search_all_date', 'all_txn_per_page', 'all_txn_paged'])) { echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr(stripslashes($value)) . '">'; } } ?>
<input type="hidden" name="all_txn_per_page" value="<?php echo esc_attr($per_page); ?>">
<input type="text" name="search_all_txn" placeholder="Search by ID, Order ID, or Reason" value="<?php echo esc_attr($search_query); ?>" class="search-input">
<input type="date" name="search_all_date" value="<?php echo esc_attr($search_date); ?>" class="date-input">
<input type="submit" value="Search" class="search-button">
</form>
</div>
</div>
<div class="warning-box-container">
<?php if($total_warning > 0): ?><div class="warning-box"> Active Warnings: <span style="color:red;"><?php echo esc_html($total_warning); ?></span> </div><?php endif; ?>
<?php if($total_negative != 0): ?><div class="warning-box negative"> Negative Warnings Sum: <span style="color: red;"><?php echo esc_html(number_format($total_negative, 2)); ?> USD</span> </div> <?php endif; ?>
<?php if($total_positive != 0): ?><div class="warning-box positive"> Positive Warnings Sum: <span style="color: red;"><?php echo esc_html(number_format($total_positive, 2)); ?> USD</span> </div> <?php endif; ?>
</div>
<div class="table-responsive-wrapper">
<table id="sellerAllTransactionHistoryTable">
<thead>
<tr>
<th onclick="sortTableByColumnAll(0, 'datetime')">Time, Date & User<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(1, 'number')">ID<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(2, 'string')" class="col-order-id">Order ID<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(3, 'string')">Sale Type<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(4, 'string')" class="col-reason">Reason or Sale Name<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(5, 'string')">Warning / Fixed By<span class="sort-indicator"></span></th>
<th onclick="sortTableByColumnAll(6, 'datetime')">Last Note<span class="sort-indicator"></span></th> <th onclick="sortTableByColumnAll(7, 'datetime')">Status Log<span class="sort-indicator"></span></th> <th onclick="sortTableByColumnAll(8, 'string')">Status<span class="sort-indicator"></span></th> <th onclick="sortTableByColumnAll(9, 'number')">Purchase Price<span class="sort-indicator"></span></th> <th onclick="sortTableByColumnAll(10, 'number')">Selling Price<span class="sort-indicator"></span></th> <th onclick="sortTableByColumnAll(11, 'number')">Point<span class="sort-indicator"></span></th> <th onclick="sortTableByColumnAll(12, 'number')">Profit / Loss<span class="sort-indicator"></span></th> </tr>
</thead>
<tbody>
<?php
if ( $transactions_display ) {
foreach ( $transactions_display as $tran ) {
// Effective timestamp & "New" Indicator
$primary_timestamp = strtotime($tran['datetime']);
$fixed_timestamp = isset($tran['fixed_at']) && $tran['fixed_at'] ? strtotime($tran['fixed_at']) : 0;
$effective_timestamp = max($primary_timestamp, $fixed_timestamp);
$age = $current_timestamp - $effective_timestamp;
$indicator_class = '';
if ($age <= 72 * 3600) { if ($age <= 24 * 3600) { $indicator_class = 'new-indicator-recent'; } elseif ($age <= 48 * 3600) { $indicator_class = 'new-indicator-medium'; } else { $indicator_class = 'new-indicator-old'; } }
$rowClass = isset($tran['is_active_warning']) && $tran['is_active_warning'] ? 'warning-cell' : '';
// Date Based Row Coloring
$current_date = date('Y-m-d', $primary_timestamp);
$current_day_of_week = date('N', $primary_timestamp);
$row_bg_color = '';
if ($current_date !== $last_processed_date) { if ($current_day_of_week == 7) { $row_bg_color = $sunday_color; } else { $row_bg_color = $day_colors[$color_index % count($day_colors)]; $color_index++; } $last_processed_date = $current_date; }
else { if ($current_day_of_week == 7) { $row_bg_color = $sunday_color; } else { $previous_color_index = ($color_index > 0) ? ($color_index - 1) : (count($day_colors) - 1); $row_bg_color = $day_colors[$previous_color_index % count($day_colors)]; } }
$rowStyle = !empty($row_bg_color) ? 'style="background-color:' . esc_attr($row_bg_color) . ';"' : '';
// Status Styling Logic
$status_text = esc_html($tran['status']);
$status_color = '#000000'; $status_icon = '';
if (isset($status_styles[$tran['status']])) { $style_info = $status_styles[$tran['status']]; $status_color = $style_info['color']; $status_icon = $style_info['icon']; }
$status_display_html = '<span class="status-text" style="color:' . esc_attr($status_color) . ';">' . $status_text . '<span class="status-icon">' . esc_html($status_icon) . '</span></span>';
$status_change_info = '';
if ($tran['type'] === 'sale') {
$last_status_log_entry = $wpdb->get_row($wpdb->prepare( "SELECT n.created_at, u.display_name FROM {$wpdb->prefix}bsm_sale_notes n LEFT JOIN {$wpdb->prefix}users u ON n.user_id = u.ID WHERE n.sale_id = %d AND n.note_type = 'status_change' ORDER BY n.id DESC LIMIT 1", $tran['id'] ));
if ($last_status_log_entry) {
$status_change_user = $last_status_log_entry->display_name ?: 'System';
if (user_can( get_user_by('display_name', $status_change_user), 'manage_options')) { $status_change_user = 'Admin'; } elseif ($status_change_user === $seller_name) { $status_change_user = 'You'; }
$status_change_time = date('h:i A | d-m-Y', strtotime($last_status_log_entry->created_at));
$status_change_info = '<small style="display:block; color:#777; font-size:9px;">' . esc_html($status_change_user) . ' at ' . esc_html($status_change_time) . '</small>';
}
}
// Fetch base points for the sale
$base_points_display = 'N/A';
$base_points_value = 0;
if ($tran['type'] === 'sale') {
$points_val = $wpdb->get_var($wpdb->prepare( "SELECT points FROM {$wpdb->prefix}bsm_points WHERE sale_id = %d AND type = 'base' LIMIT 1", $tran['id'] ));
if ($points_val !== null) { $base_points_value = (float)$points_val; $base_points_display = number_format($base_points_value, 2); }
}
// Prepare Profit/Loss Display
$profit_loss_display = 'N/A';
$profit_loss_value = 0;
if ($tran['type'] === 'sale') {
$profit_val = isset($tran['profit_bdt']) ? floatval($tran['profit_bdt']) : 0;
$loss_val = isset($tran['loss_bdt']) ? floatval($tran['loss_bdt']) : 0;
if ($profit_val > 0) { $profit_loss_display = '<span style="color:green; font-weight:bold;">' . number_format($profit_val, 2) . ' ৳</span>'; $profit_loss_value = $profit_val; }
elseif ($loss_val > 0) { $profit_loss_display = '<span style="color:red; font-weight:bold;">' . number_format($loss_val, 2) . ' ৳</span>'; $profit_loss_value = -$loss_val; }
else { $profit_loss_display = '0.00 ৳'; $profit_loss_value = 0; }
}
// Prepare Purchase/Selling Price Display
$purchase_price_display = 'N/A';
$selling_price_display = 'N/A';
if ($tran['type'] === 'sale') {
$sale_type_lc = strtolower($tran['sale_type'] ?? '');
$pp = isset($tran['purchase_price']) ? $tran['purchase_price'] : null;
$sp = isset($tran['selling_price']) ? $tran['selling_price'] : null;
if($pp !== null){ $purchase_price_display = ($sale_type_lc === 'gsmalo.org') ? '$ ' . number_format((float)$pp, 2) : number_format((float)$pp, 2) . ' ৳'; }
if($sp !== null){ $selling_price_display = number_format((float)$sp, 2) . ' ৳'; }
}
// Fetch Last Note Info
$note_logs_q = $wpdb->get_results($wpdb->prepare("SELECT n.created_at, n.note_text, u.display_name FROM {$wpdb->prefix}bsm_statas_log_notes n LEFT JOIN {$wpdb->prefix}users u ON n.user_id = u.ID WHERE n.sale_id = %d ORDER BY n.created_at DESC", $tran['id']));
$note_log_count_val = count($note_logs_q);
$last_note_display = '-';
$last_note_timestamp = 0;
if ($note_log_count_val > 0) {
$latest_note = $note_logs_q[0];
$last_note_timestamp = strtotime($latest_note->created_at);
$latest_note_user = $latest_note->display_name ?: 'Unknown';
if (user_can( get_user_by('display_name', $latest_note_user), 'manage_options')) { $latest_note_user = 'Admin'; } elseif ($latest_note_user === $seller_name) { $latest_note_user = 'You'; }
$latest_note_time = date('h:i A | d-m-Y', $last_note_timestamp);
$note_text_display = esc_html(wp_trim_words($latest_note->note_text, 8, '...'));
$note_icon_color_class = 'log-count-old';
$note_age = $current_timestamp - $last_note_timestamp;
if ($note_age <= 48 * 3600) { if ($note_age <= 24 * 3600) { $note_icon_color_class = 'log-count-recent'; } else { $note_icon_color_class = 'log-count-medium'; } }
$last_note_display = '<div class="latest-note-container">';
$last_note_display .= '<span class="latest-note-info-text">';
$last_note_display .= '<strong>' . esc_html($latest_note_user) . ':</strong> ' . $note_text_display;
$last_note_display .= '<small>' . esc_html($latest_note_time) . '</small>';
$last_note_display .= '</span>';
$last_note_display .= '</div>';
if ($note_log_count_val > 1) {
$last_note_display .= '<a href="#" class="more-note-log-icon ' . $note_icon_color_class . '" data-sale-id="' . esc_attr($tran['id']) . '" data-log-type="note" title="View all ' . $note_log_count_val . ' notes">+' . ($note_log_count_val - 1) . '</a>';
}
}
// Fetch Last Status Log Info
$status_log_display = '-';
$last_status_log_timestamp = 0;
if ($tran['type'] === 'sale') {
$status_logs_all = $wpdb->get_results($wpdb->prepare("SELECT n.created_at, n.note_text, u.display_name FROM {$wpdb->prefix}bsm_sale_notes n LEFT JOIN {$wpdb->prefix}users u ON n.user_id = u.ID WHERE n.sale_id = %d AND n.note_type = 'status_change' ORDER BY n.created_at DESC", $tran['id']));
$log_count_val = count($status_logs_all);
if ($log_count_val > 0) {
$latest_log_all = $status_logs_all[0];
$last_status_log_timestamp = strtotime($latest_log_all->created_at);
$latest_log_user_all = $latest_log_all->display_name ?: 'System';
if (user_can( get_user_by('display_name', $latest_log_user_all), 'manage_options')) { $latest_log_user_all = 'Admin'; } elseif ($latest_log_user_all === $seller_name) { $latest_log_user_all = 'You'; }
$latest_log_time_all = date('h:i A | d-m-Y', $last_status_log_timestamp);
$balance_change_text_all = '';
if(isset($tran['sale_type']) && strtolower($tran['sale_type']) === 'gsmalo.org') {
list($old_status_latest_all, $new_status_latest_all) = function_exists('bsm_parse_status_from_log') ? bsm_parse_status_from_log($latest_log_all->note_text) : [null, null];
if ($old_status_latest_all !== null && $new_status_latest_all !== null) {
$balance_change_amount_all = function_exists('bsm_calculate_balance_change_for_status_transition') ? bsm_calculate_balance_change_for_status_transition($tran['id'], $old_status_latest_all, $new_status_latest_all) : 0;
if ($balance_change_amount_all > 0) { $balance_change_text_all = ' <span class="balance-change-indicator" style="color:blue;">(Refund +' . number_format($balance_change_amount_all, 2) . '$)</span>'; }
elseif ($balance_change_amount_all < 0) { $balance_change_text_all = ' <span class="balance-change-indicator" style="color:red;">(Cut ' . number_format(abs($balance_change_amount_all), 2) . '$)</span>'; }
}
}
$icon_color_class_all = 'log-count-old';
$log_age = $current_timestamp - $last_status_log_timestamp;
if ($log_age <= 48 * 3600) { if ($log_age <= 24 * 3600) { $icon_color_class_all = 'log-count-recent'; } else { $icon_color_class_all = 'log-count-medium'; } }
$status_log_display = '<div class="latest-log-container">';
$status_log_display .= '<span class="latest-log-info-text">';
$status_log_display .= esc_html($latest_log_user_all);
$status_log_display .= '<small>' . esc_html($latest_log_time_all) . '</small>';
$status_log_display .= '</span>';
$status_log_display .= $balance_change_text_all;
$status_log_display .= '</div>';
if ($log_count_val > 1) { $status_log_display .= '<a href="#" class="more-status-log-icon ' . $icon_color_class_all . '" data-sale-id="' . esc_attr($tran['id']) . '" data-log-type="status" title="View all ' . $log_count_val . ' status logs">+' . ($log_count_val - 1) . '</a>'; }
}
}
?>
<tr class="<?php echo $rowClass; ?>" <?php echo $rowStyle; ?>>
<td class="col-datetime" data-sort-value="<?php echo $primary_timestamp; ?>">
<?php if (!empty($indicator_class)): ?><span class="new-indicator <?php echo $indicator_class; ?>">New</span><?php endif; ?>
<span><?php echo esc_html( date('h:i A | d-m-Y', $primary_timestamp ) ); ?></span>
<small>By: <?php echo esc_html( ($tran['adjuster_name'] === $seller_name) ? 'You' : $tran['adjuster_name'] ); ?></small>
</td>
<td data-sort-value="<?php echo esc_attr($tran['id']); ?>"><?php echo esc_html( $tran['id'] ); ?></td>
<td class="col-order-id"><?php echo esc_html( ($tran['order_id'] !== '-' && !empty($tran['order_id'])) ? substr($tran['order_id'], 0, 8) : '-' ); ?></td>
<td><?php echo esc_html( $tran['sale_type'] ?? 'N/A' ); ?></td>
<td class="col-reason"><?php echo esc_html( $tran['reason'] ); ?></td>
<td data-label="Warning / Fixed By">
<?php echo ($tran['type'] == 'adjustment' && !empty($tran['warning_text'])) ? $tran['warning_text'] : '-'; ?>
</td>
<td class="note-log-cell" data-sort-value="<?php echo $last_note_timestamp; ?>"> <?php echo $last_note_display; ?> </td>
<td class="status-log-cell" data-sort-value="<?php echo $last_status_log_timestamp; ?>"> <?php echo $status_log_display; ?> </td>
<td> <?php echo $status_display_html . $status_change_info; ?> </td>
<td data-sort-value="<?php echo esc_attr(isset($tran['purchase_price']) ? $tran['purchase_price'] : 0); ?>"><?php echo $purchase_price_display; ?></td>
<td data-sort-value="<?php echo esc_attr(isset($tran['selling_price']) ? $tran['selling_price'] : 0); ?>"><?php echo $selling_price_display; ?></td>
<td data-sort-value="<?php echo esc_attr($base_points_value); ?>"><?php echo $base_points_display; ?></td>
<td data-sort-value="<?php echo esc_attr($profit_loss_value); ?>"> <?php echo $profit_loss_display; ?> </td>
</tr>
<?php
}
} else {
echo '<tr><td colspan="13">No transactions found matching your criteria.</td></tr>'; // Colspan updated to 13
}
?>
</tbody>
</table>
</div><?php
// Pagination Links
if ( $total_pages > 1 ) {
echo '<div class="pagination">';
$query_params = $_GET; unset($query_params['all_txn_paged']); $base_url = add_query_arg($query_params, get_permalink());
for ( $i = 1; $i <= $total_pages; $i++ ) {
$url = add_query_arg('all_txn_paged', $i, $base_url);
if ( $i == $page ) { echo '<span class="current-page">' . $i . '</span>'; } else { echo '<a href="' . esc_url($url) . '">' . $i . '</a>'; }
}
echo '</div>';
}
?>
<div class="per-page-form">
<form method="get" action="">
<?php foreach ($_GET as $key => $value) { if (!in_array($key, ['all_txn_per_page', 'all_txn_paged'])) { echo '<input type="hidden" name="' . esc_attr($key) . '" value="' . esc_attr(stripslashes($value)) . '">'; } } ?>
<label for="all_txn_per_page">Records per page: </label>
<select name="all_txn_per_page" id="all_txn_per_page" onchange="this.form.submit();">
<option value="50" <?php selected($per_page, 50); ?>>50</option>
<option value="100" <?php selected($per_page, 100); ?>>100</option>
<option value="200" <?php selected($per_page, 200); ?>>200</option>
<option value="400" <?php selected($per_page, 400); ?>>400</option>
<option value="500" <?php selected($per_page, 500); ?>>500</option>
<option value="1000" <?php selected($per_page, 1000); ?>>1000</option>
</select>
</form>
</div>
</div> <div id="all-txn-status-log-modal" class="log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('all-txn-status-log-modal').style.display='none';">×</span>
<h2>Status Change History</h2>
<div id="all-txn-status-log-modal-content" class="log-modal-content-area"><p>Loading logs...</p></div>
</div>
</div>
<div id="all-txn-note-log-modal" class="log-modal">
<div class="modal-content">
<span class="close" onclick="document.getElementById('all-txn-note-log-modal').style.display='none';">×</span>
<h2>Note History</h2>
<div id="all-txn-note-log-modal-content" class="log-modal-content-area"><p>Loading notes...</p></div>
</div>
</div>
<style>
/* CSS Styles (Updated) */
.bsm-seller-all-txn-history-wrap { padding: 15px; background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 4px; margin: 10px 0; font-family: sans-serif; }
.bsm-seller-all-txn-history-wrap h2 { margin-top: 0; margin-bottom: 15px; color: #333; font-size: 1.5em; }
.seller-balance-info { margin-bottom: 20px; padding: 10px; background: #eef; border: 1px solid #dde; border-radius: 4px; font-size: 1.1em; }
.search-bar-container { background-color: #eaf2f8; padding: 12px 15px; margin-bottom: 20px; border: 1px solid #c5d9e8; border-radius: 6px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.04); }
.search-bar-inner { display: flex; justify-content: center; align-items: center; gap: 8px; flex-wrap: wrap; }
.search-bar-inner form { display: contents; }
.search-bar-inner .search-input { padding: 6px 10px; border: 1px solid #a9c4d0; border-radius: 4px; font-size: 14px; flex: 1 1 250px; max-width: 300px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.search-bar-inner .date-input { padding: 6px 8px; border: 1px solid #a9c4d0; border-radius: 4px; font-size: 14px; flex: 0 1 140px; box-shadow: inset 0 1px 2px rgba(0,0,0,0.06); }
.search-bar-inner .search-button { padding: 7px 14px; border: none; background: #2980b9; color: #fff; border-radius: 4px; cursor: pointer; font-size: 14px; flex-shrink: 0; transition: background-color 0.2s ease; }
.search-bar-inner .search-button:hover { background: #1f648b; }
.warning-box-container { margin-bottom: 15px; }
.warning-box { display: inline-block; margin-right: 10px; padding: 10px 15px; border: 2px solid #d00; background: #ffcccc; border-radius: 5px; font-size: 14px; font-weight: bold; }
.warning-box span { color: red; }
.warning-box.positive { border-color: #0073aa; background: #e0f7fa; }
.warning-box.negative { border-color: #0073aa; background: #e0f7fa; }
.table-responsive-wrapper { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
table#sellerAllTransactionHistoryTable { width: 100%; min-width: 1400px; /* Adjusted */ border-collapse: collapse; table-layout: auto; background: #fff; }
table#sellerAllTransactionHistoryTable td[colspan="13"] { text-align: center; padding: 15px; font-style: italic; color: #666; } /* Colspan updated */
table#sellerAllTransactionHistoryTable th, table#sellerAllTransactionHistoryTable td {
border: 1px solid #ccc; padding: 2px 3px; /* Further reduced padding */ text-align: center; vertical-align: middle;
position: relative; font-size: 10px; /* Further reduced font size */ word-wrap: break-word; overflow-wrap: break-word;
white-space: nowrap;
}
table#sellerAllTransactionHistoryTable th { background: #f1f1f1; font-weight: bold; cursor: pointer; white-space: nowrap; }
table#sellerAllTransactionHistoryTable td.col-reason { white-space: normal; width: 18%; } /* Allow wrapping */
table#sellerAllTransactionHistoryTable td.status-log-cell { white-space: normal; max-width: 110px; min-width: 70px;}
table#sellerAllTransactionHistoryTable td.note-log-cell { white-space: normal; max-width: 110px; min-width: 70px;}
.col-order-id { width: 50px; }
.col-datetime span { display: block; font-size: 9px; color: #555;}
.col-datetime small { display: block; font-size: 8px; color: #0073aa; font-weight: bold; }
table#sellerAllTransactionHistoryTable tbody tr:hover { background-color: #f1f1f1 !important; }
.warning-cell { background: #ffcccc !important; color: #a00; font-weight: bold; }
.warning-cell:hover { background: #ffbbbb !important; }
.fix-note { font-size: 10px; color: #555; line-height:1.2; text-align: center;}
.fix-note small { display: block; font-size:9px; color:#777; }
.pagination { text-align: center; margin-top: 20px; }
.pagination a, .pagination span { display: inline-block; padding: 6px 12px; margin: 0 2px; border: 1px solid #ccc; border-radius: 4px; text-decoration: none; color: #0073aa; }
.pagination span.current-page { background: #0073aa; color: #fff; }
.per-page-form { margin-top: 10px; text-align: center; }
.per-page-form label { margin-right: 5px; }
.per-page-form select { padding: 5px; }
/* New Indicator Styles */
.new-indicator { position: absolute; top: 1px; left: 1px; color: white; font-size: 8px; font-weight: bold; padding: 1px 3px; border-radius: 3px; line-height: 1; z-index: 1; }
.new-indicator-recent { background-color: red; }
.new-indicator-medium { background-color: #DAA520; }
.new-indicator-old { background-color: #808080; }
td.col-datetime { padding-top: 15px !important; }
.status-text { font-weight: bold; }
.status-icon { margin-left: 3px; margin-right: 3px; }
th .sort-indicator { font-size: 9px; margin-left: 4px; }
.amount-plus { color: blue; font-weight: bold; margin-right: 2px;}
.amount-minus { color: red; font-weight: bold; margin-right: 2px;}
/* Status Log & Note Log Column Styles */
td.status-log-cell, td.note-log-cell {
font-size: 9px; line-height: 1.2; text-align: left; position: relative;
padding-right: 20px; /* Reduced space for icon */ vertical-align: top; white-space: normal;
max-width: 110px; min-width: 70px;
}
.latest-log-container, .latest-note-container { margin-bottom: 1px; } /* Reduced margin */
.latest-log-info-text, .latest-note-info-text { display: inline-block; padding: 0; white-space: normal; }
.latest-log-info-text small, .latest-note-info-text small { display: block; font-size: 8px; color: #555; }
.balance-change-indicator { font-size: 9px; font-weight: bold; margin-left: 3px; white-space: nowrap; }
.more-status-log-icon, .more-note-log-icon {
position: absolute; top: 1px; right: 1px; display: inline-block; color: white; /* Smaller icon */
font-size: 8px; font-weight: bold; width: 13px; height: 13px; line-height: 13px;
text-align: center; border-radius: 50%; cursor: pointer; text-decoration: none;
box-shadow: 0 1px 1px rgba(0,0,0,0.2); z-index: 2;
}
.more-status-log-icon.log-count-recent, .more-note-log-icon.log-count-recent { background-color: red; }
.more-status-log-icon.log-count-medium, .more-note-log-icon.log-count-medium { background-color: #DAA520; }
.more-status-log-icon.log-count-old, .more-note-log-icon.log-count-old { background-color: #808080; }
.more-status-log-icon:hover, .more-note-log-icon:hover { filter: brightness(85%); }
/* Log Modal Styles */
.log-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 10001; }
.log-modal .modal-content { background: #fff; width: 700px; max-width: 90%; margin: 80px auto; padding: 20px; border-radius: 5px; position: relative; max-height: 80vh; overflow-y: auto; }
.log-modal .modal-content h2 { margin-top: 0; color: #0073aa; font-size: 1.3em; }
.log-modal .close { position: absolute; top: 10px; right: 15px; cursor: pointer; font-size: 24px; color: #888; font-weight: bold; }
.log-modal-content-area .log-entry { border-bottom: 1px dashed #eee; padding: 8px 0; font-size: 13px; }
.log-modal-content-area .log-entry:last-child { border-bottom: none; }
.log-modal-content-area .log-entry strong { color: #333; }
.log-modal-content-area .log-entry em { color: #777; font-size: 0.9em; }
</style>
<script>
// Pass PHP variables needed for AJAX to JavaScript
var bsm_ajax_obj = {
ajaxurl: '<?php echo admin_url( 'admin-ajax.php' ); ?>',
fetch_nonce: '<?php echo esc_js($log_fetch_nonce); ?>' // Use the nonce generated in PHP
};
// Client-side Table Sorting Function for Seller All Transaction Table
const getCellValueAll = (tr, idx, type) => {
const td = tr.children[idx]; if (!td) return null;
// Use data-sort-value if present, otherwise fallback to text content
const sortValue = td.getAttribute('data-sort-value');
if (sortValue !== null) {
if (type === 'number' || type === 'datetime') { return parseFloat(sortValue) || 0; } // Timestamps are numbers
return sortValue;
}
// Fallback logic (less reliable for complex cells)
const val = (td.innerText || td.textContent).trim();
if (type === 'number') { let numStr = String(val).replace(/[^0-9.-]+/g,"").replace(/,/g, ''); if (String(val).trim().startsWith('-')) { numStr = '-' + numStr.replace('-', ''); } return parseFloat(numStr) || 0; }
if (type === 'datetime') { return new Date(val).getTime() || 0; }
return val;
};
const comparerAll = (idx, asc, type) => (a, b) => ((v1, v2) =>
v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)
)(getCellValueAll(asc ? a : b, idx, type), getCellValueAll(asc ? b : a, idx, type));
function sortTableByColumnAll(columnIndex, type = 'string') {
// Column Indices (Updated): 0=Time, 1=ID, 2=Order, 3=Type, 4=Reason, 5=Warning, 6=NoteLog, 7=StatusLog, 8=Status, 9=P.Price, 10=S.Price, 11=Point, 12=Profit/Loss
// Allow sorting for Note Log (6) and Status Log (7) using 'datetime' type based on data-sort-value
const table = document.getElementById('sellerAllTransactionHistoryTable'); if (!table) return;
const tbody = table.querySelector('tbody'); if (!tbody) return;
const th = table.querySelectorAll('th')[columnIndex]; if (!th) return;
const currentIsAscending = th.classList.contains('sort-asc'); const direction = currentIsAscending ? false : true;
table.querySelectorAll('th').forEach(h => { h.classList.remove('sort-asc', 'sort-desc'); const indicator = h.querySelector('.sort-indicator'); if(indicator) indicator.remove(); });
th.classList.toggle('sort-asc', direction); th.classList.toggle('sort-desc', !direction);
const indicatorSpan = document.createElement('span'); indicatorSpan.className = 'sort-indicator'; indicatorSpan.innerHTML = direction ? ' ▲' : ' ▼'; th.appendChild(indicatorSpan);
Array.from(tbody.querySelectorAll('tr')).sort(comparerAll(columnIndex, direction, type)).forEach(tr => tbody.appendChild(tr) );
}
// Bind log link clicks on DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
rebindAllTxnLogLinks('.more-status-log-icon', 'status'); // Bind status log icons
rebindAllTxnLogLinks('.more-note-log-icon', 'note'); // Bind note log icons
});
// Function to bind click events to log links (status or note)
function rebindAllTxnLogLinks(selector, logTypeToUse) { // Added logTypeToUse parameter
document.querySelectorAll('#sellerAllTransactionHistoryTable ' + selector).forEach(function(link) {
// Make sure we don't add multiple listeners if this is called again
link.removeEventListener('click', handleAllTxnLogClick);
// Add the event listener
link.addEventListener('click', handleAllTxnLogClick);
});
}
// Handler function for log link clicks specific to this page
function handleAllTxnLogClick(e) {
e.preventDefault();
var saleId = this.getAttribute('data-sale-id');
// Ensure logType comes reliably from the clicked element's data-log-type
var logType = this.getAttribute('data-log-type');
if (!logType) { // Fallback if data-log-type is missing
console.warn("Log type missing on clicked element, trying to infer...");
logType = this.classList.contains('more-note-log-icon') ? 'note' : 'status';
}
var modalId = (logType === 'note') ? 'all-txn-note-log-modal' : 'all-txn-status-log-modal';
var modalContentId = (logType === 'note') ? 'all-txn-note-log-modal-content' : 'all-txn-status-log-modal-content';
var modalContentEl = document.getElementById(modalContentId);
var modalEl = document.getElementById(modalId);
if (!modalContentEl || !modalEl) { console.error("All Txn log modal elements not found for type: " + logType); return; }
modalContentEl.innerHTML = '<p>Loading logs...</p>';
modalEl.style.display = 'block';
var fd = new FormData();
fd.append("action", "bsm_fetch_all_logs");
fd.append("sale_id", saleId);
fd.append("log_type", logType);
fd.append("nonce", bsm_ajax_obj.fetch_nonce); // Use localized nonce
fetch(bsm_ajax_obj.ajaxurl, { method: "POST", body: fd, credentials: 'same-origin' })
.then(response => {
if (!response.ok) { return response.text().then(text => { throw new Error(text || response.status); }); }
return response.json();
})
.then(data => {
if (data.success && data.data.html) {
modalContentEl.innerHTML = data.data.html;
} else {
modalContentEl.innerHTML = '<p>Error loading logs: ' + (data.data ? (data.data.message || data.data) : 'Unknown error') + '</p>';
console.error("AJAX Error Response:", data); // Log error details
}
})
.catch(error => {
console.error('Error fetching all txn logs:', error);
modalContentEl.innerHTML = '<p>AJAX Error loading logs: ' + error.message + '</p>';
});
}
// Close Log Modals (Generic)
document.querySelectorAll('.log-modal .close').forEach(function(el) {
el.addEventListener('click', function() {
this.closest('.log-modal').style.display = 'none';
});
});
window.addEventListener('click', function(event) {
if (event.target.classList.contains('log-modal')) {
event.target.style.display = 'none';
}
});
</script>
Comments
Post a Comment