通过强大的定价谈判系统赋予您的WooCommerce商店。让客户在您控制的边界内提出自己的价格。自动或手动批准提案,以创造更具吸引力的购物体验。
**关键功能:**
*允许客户提出自己的产品价格
*设置最低和最大折扣阈值
*可接受范围内的自动宣传提案
*管理仪表板管理所有价格建议
*新建议的电子邮件通知
*有关提案活动的详细统计数据
*与WooCommerce Cart无缝集成并结帐
相关文章: WooCommerce 高级产品定价和折扣管理
<?php /** * Plugin Name: BDFG Flexible Pricing * Plugin URI: https://beiduofengou.net/2024/12/25/bdfg-flexible-pricing/ * Description: Allows customers to propose their own prices for products, with admin-defined rules for approval. * Version: 2.5.1 * Author: beiduofengou * Author URI: https://beiduofengou.net * Text Domain: bdfg-flexible-pricing * Domain Path: /languages * Requires at least: 5.6 * Requires PHP: 7.2 * WC requires at least: 5.0 * WC tested up to: 8.0 * License: GPL v2 or later */ // Prevent direct access defined('ABSPATH') || exit; // Define plugin constants define('BDFG_FLEXIBLE_PRICING_VERSION', '2.5.1'); define('BDFG_FLEXIBLE_PRICING_FILE', __FILE__); define('BDFG_FLEXIBLE_PRICING_PATH', plugin_dir_path(__FILE__)); define('BDFG_FLEXIBLE_PRICING_URL', plugin_dir_url(__FILE__)); define('BDFG_FLEXIBLE_PRICING_BASENAME', plugin_basename(__FILE__)); /** * Check if WooCommerce is active * * @return bool True if WooCommerce is active, false otherwise */ function bdfg_flexible_pricing_check_wc() { if (!class_exists('WooCommerce')) { add_action('admin_notices', 'bdfg_flexible_pricing_wc_missing_notice'); return false; } return true; } /** * Display notice when WooCommerce is missing */ function bdfg_flexible_pricing_wc_missing_notice() { ?> <div class="error"> <p><?php _e('BDFG Flexible Pricing requires WooCommerce to be installed and active.', 'bdfg-flexible-pricing'); ?></p> </div> <?php } /** * Plugin initialization */ function bdfg_flexible_pricing_init() { if (!bdfg_flexible_pricing_check_wc()) { return; } // Load language files load_plugin_textdomain('bdfg-flexible-pricing', false, dirname(plugin_basename(__FILE__)) . '/languages'); // Include required files require_once BDFG_FLEXIBLE_PRICING_PATH . 'includes/class-bdfg-flexible-pricing.php'; // Initialize the plugin BDFG_Flexible_Pricing::get_instance(); } // Register activation hook register_activation_hook(__FILE__, 'bdfg_flexible_pricing_activate'); function bdfg_flexible_pricing_activate() { // Create required database tables if (class_exists('BDFG_Flexible_Pricing')) { BDFG_Flexible_Pricing::get_instance()->create_tables(); } // Create required directories $upload_dir = wp_upload_dir(); $bdfg_dir = $upload_dir['basedir'] . '/bdfg-flexible-pricing'; if (!file_exists($bdfg_dir)) { wp_mkdir_p($bdfg_dir); } // Add default settings add_option('bdfg_min_discount', 0); add_option('bdfg_max_discount', 50); add_option('bdfg_allow_auto_approval', 'yes'); add_option('bdfg_admin_email', get_option('admin_email')); } // Start the plugin add_action('plugins_loaded', 'bdfg_flexible_pricing_init'); // Register deactivation hook register_deactivation_hook(__FILE__, 'bdfg_flexible_pricing_deactivate'); /** * Plugin deactivation actions */ function bdfg_flexible_pricing_deactivate() { // Clear scheduled hooks wp_clear_scheduled_hook('bdfg_clean_expired_proposals'); // Keep database tables intact for future reactivation // You may want to add an option to delete data upon uninstall } /** * Add settings link on plugin page */ function bdfg_flexible_pricing_settings_link($links) { $settings_link = '<a href="admin.php?page=bdfg-flexible-pricing&tab=settings">' . __('Settings', 'bdfg-flexible-pricing') . '</a>'; array_unshift($links, $settings_link); return $links; } add_filter('plugin_action_links_' . BDFG_FLEXIBLE_PRICING_BASENAME, 'bdfg_flexible_pricing_settings_link');
includes/class-bdfg-flexible-pricing.php
<?php /** * BDFG Flexible Pricing Main Class * * @package BDFG_Flexible_Pricing * @since 2.0.0 */ // Prevent direct access defined('ABSPATH') || exit; /** * Main plugin class */ class BDFG_Flexible_Pricing { /** * Single instance of the class * * @var BDFG_Flexible_Pricing */ private static $instance = null; /** * Minimum discount percentage allowed * * @var int */ private $min_discount = 0; /** * Maximum discount percentage allowed * * @var int */ private $max_discount = 50; /** * Whether to allow automatic approval for proposals within discount range * * @var bool */ private $allow_auto_approval = true; /** * Email to send notifications to * * @var string */ private $admin_email = ''; /** * Get single instance of the class * * @return BDFG_Flexible_Pricing */ public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Class constructor */ private function __construct() { // Initialize settings $this->init_settings(); // Frontend hooks add_action('woocommerce_single_product_summary', array($this, 'display_price_proposal_form'), 25); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); add_action('wp_ajax_bdfg_submit_price_proposal', array($this, 'handle_price_proposal')); add_action('wp_ajax_nopriv_bdfg_submit_price_proposal', array($this, 'handle_price_proposal')); // Admin hooks add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); add_action('admin_enqueue_scripts', array($this, 'admin_scripts')); // Proposal management add_action('wp_ajax_bdfg_approve_proposal', array($this, 'approve_proposal')); add_action('wp_ajax_bdfg_reject_proposal', array($this, 'reject_proposal')); // Cart and checkout hooks add_action('woocommerce_add_to_cart', array($this, 'check_approved_price'), 10, 6); add_filter('woocommerce_add_cart_item_data', array($this, 'add_price_proposal_to_cart_item'), 10, 3); add_filter('woocommerce_get_cart_item_from_session', array($this, 'get_cart_item_proposal_from_session'), 10, 2); add_filter('woocommerce_cart_item_price', array($this, 'show_proposed_price_in_cart'), 10, 3); add_filter('woocommerce_cart_item_subtotal', array($this, 'show_proposed_subtotal_in_cart'), 10, 3); add_action('woocommerce_before_calculate_totals', array($this, 'calculate_custom_price'), 10, 1); // Order hooks add_action('woocommerce_checkout_create_order_line_item', array($this, 'add_proposal_data_to_order_items'), 10, 4); // Custom order statuses add_action('init', array($this, 'register_custom_order_statuses')); add_filter('wc_order_statuses', array($this, 'add_custom_order_statuses')); // Schedule cleanup task if (!wp_next_scheduled('bdfg_clean_expired_proposals')) { wp_schedule_event(time(), 'daily', 'bdfg_clean_expired_proposals'); } add_action('bdfg_clean_expired_proposals', array($this, 'clean_expired_proposals')); } /** * Initialize settings */ public function init_settings() { $this->min_discount = get_option('bdfg_min_discount', 0); $this->max_discount = get_option('bdfg_max_discount', 50); $this->allow_auto_approval = get_option('bdfg_allow_auto_approval', 'yes') === 'yes'; $this->admin_email = get_option('bdfg_admin_email', get_option('admin_email')); } /** * Create required database tables */ public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Only create table if it doesn't exist if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") != $table_name) { $sql = "CREATE TABLE {$table_name} ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, product_id bigint(20) NOT NULL, original_price decimal(10,2) NOT NULL, proposed_price decimal(10,2) NOT NULL, status varchar(20) NOT NULL DEFAULT 'pending', created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT NULL, notes text DEFAULT NULL, token varchar(64) DEFAULT NULL, PRIMARY KEY (id), KEY user_product (user_id,product_id), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // Log table creation error_log('BDFG Flexible Pricing: Created ' . $table_name); } } /** * Enqueue frontend scripts and styles */ public function enqueue_scripts() { if (is_product()) { wp_enqueue_style( 'bdfg-flexible-pricing', BDFG_FLEXIBLE_PRICING_URL . 'assets/css/bdfg-flexible-pricing.css', array(), BDFG_FLEXIBLE_PRICING_VERSION ); wp_enqueue_script( 'bdfg-flexible-pricing', BDFG_FLEXIBLE_PRICING_URL . 'assets/js/bdfg-flexible-pricing.js', array('jquery'), BDFG_FLEXIBLE_PRICING_VERSION, true ); // Pass variables to JS wp_localize_script('bdfg-flexible-pricing', 'bdfg_pricing_data', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bdfg_price_proposal'), 'currency_symbol' => get_woocommerce_currency_symbol(), 'min_discount' => $this->min_discount, 'max_discount' => $this->max_discount, 'i18n' => array( 'submit' => __('Submit Proposal', 'bdfg-flexible-pricing'), 'proposing' => __('Submitting...', 'bdfg-flexible-pricing'), 'success' => __('Price proposal submitted successfully!', 'bdfg-flexible-pricing'), 'error' => __('Error submitting proposal. Please try again.', 'bdfg-flexible-pricing'), 'invalid_price' => __('Please enter a valid price.', 'bdfg-flexible-pricing'), 'price_too_low' => __('The price is too low. Please increase your offer.', 'bdfg-flexible-pricing'), 'price_too_high' => __('The price is too high. Original price is better.', 'bdfg-flexible-pricing'), ) )); } } /** * Enqueue admin scripts and styles */ public function admin_scripts($hook) { if ('woocommerce_page_bdfg-flexible-pricing' !== $hook) { return; } wp_enqueue_style( 'bdfg-admin-css', BDFG_FLEXIBLE_PRICING_URL . 'assets/css/bdfg-admin.css', array(), BDFG_FLEXIBLE_PRICING_VERSION ); wp_enqueue_script( 'bdfg-admin-js', BDFG_FLEXIBLE_PRICING_URL . 'assets/js/bdfg-admin.js', array('jquery'), BDFG_FLEXIBLE_PRICING_VERSION, true ); wp_localize_script('bdfg-admin-js', 'bdfg_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bdfg_admin_nonce'), 'i18n' => array( 'approve_confirm' => __('Are you sure you want to approve this price proposal?', 'bdfg-flexible-pricing'), 'reject_confirm' => __('Are you sure you want to reject this price proposal?', 'bdfg-flexible-pricing'), 'approving' => __('Approving...', 'bdfg-flexible-pricing'), 'rejecting' => __('Rejecting...', 'bdfg-flexible-pricing'), 'success' => __('Action completed successfully.', 'bdfg-flexible-pricing'), 'error' => __('An error occurred. Please try again.', 'bdfg-flexible-pricing'), ) )); } /** * Display price proposal form on product page */ public function display_price_proposal_form() { global $product; // Only show for simple products if (!$product || $product->get_type() !== 'simple') { return; } // Get price and currency $regular_price = $product->get_regular_price(); if (empty($regular_price) || $regular_price <= 0) { return; // Don't display for free products } $currency = get_woocommerce_currency_symbol(); // Calculate minimum allowed price $min_allowed = $regular_price * (1 - ($this->max_discount / 100)); $min_allowed = round($min_allowed, 2); // Get user's existing proposal if any $user_id = get_current_user_id(); $proposal = $this->get_user_proposal($user_id, $product->get_id()); // Load template include(BDFG_FLEXIBLE_PRICING_PATH . 'templates/proposal-form.php'); } /** * Get user's proposal for a product * * @param int $user_id User ID * @param int $product_id Product ID * @return object|null Proposal data or null */ private function get_user_proposal($user_id, $product_id) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Get user's latest price proposal if ($user_id > 0) { $proposal = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table_name} WHERE user_id = %d AND product_id = %d ORDER BY created_at DESC LIMIT 1", $user_id, $product_id )); } else { // Try to get guest proposal using cookie if (isset($_COOKIE['bdfg_guest_id'])) { $guest_id = sanitize_text_field($_COOKIE['bdfg_guest_id']); $proposal = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table_name} WHERE notes = %s AND product_id = %d ORDER BY created_at DESC LIMIT 1", $guest_id, $product_id )); } else { $proposal = null; } } return $proposal; } /** * Handle price proposal submission via AJAX */ public function handle_price_proposal() { // Security check if (!check_ajax_referer('bdfg_price_proposal', 'nonce', false)) { wp_send_json_error(array('message' => __('Security check failed. Please refresh the page and try again.', 'bdfg-flexible-pricing'))); } // Get parameters $product_id = isset($_POST['product_id']) ? intval($_POST['product_id']) : 0; $proposed_price = isset($_POST['price']) ? floatval($_POST['price']) : 0; // Validate product ID $product = wc_get_product($product_id); if (!$product) { wp_send_json_error(array('message' => __('Invalid product. Please try again.', 'bdfg-flexible-pricing'))); return; } // Get original price $original_price = floatval($product->get_regular_price()); // Validate price if ($proposed_price <= 0) { wp_send_json_error(array('message' => __('Price must be greater than zero.', 'bdfg-flexible-pricing'))); return; } // Calculate discount percentage $discount_percent = (($original_price - $proposed_price) / $original_price) * 100; // Check if price is within allowed range if ($discount_percent < $this->min_discount || $discount_percent > $this->max_discount) { $message = sprintf( __('Price must be between %s and %s (discount range: %s%% to %s%%).', 'bdfg-flexible-pricing'), wc_price($original_price * (1 - $this->max_discount / 100)), wc_price($original_price * (1 - $this->min_discount / 100)), $this->min_discount, $this->max_discount ); wp_send_json_error(array('message' => $message)); return; } // Get user ID $user_id = get_current_user_id(); $guest_id = ''; if (!$user_id) { // Create temporary identifier for guest if (!isset($_COOKIE['bdfg_guest_id'])) { $guest_id = 'guest_' . md5(uniqid('', true)); setcookie('bdfg_guest_id', $guest_id, time() + 86400 * 30, COOKIEPATH, COOKIE_DOMAIN, is_ssl()); } else { $guest_id = sanitize_text_field($_COOKIE['bdfg_guest_id']); } } // Determine status $status = 'pending'; if ($this->allow_auto_approval && $discount_percent <= $this->max_discount) { $status = 'approved'; } // Generate unique token $token = md5(uniqid(($user_id ? $user_id : $guest_id) . '_' . $product_id, true)); // Save proposal $result = $this->save_proposal($user_id, $product_id, $original_price, $proposed_price, $status, $token, $guest_id); if (!$result) { wp_send_json_error(array('message' => __('Failed to save proposal. Please try again.', 'bdfg-flexible-pricing'))); return; } // Send email notification to admin if ($status === 'pending') { $this->send_admin_notification($user_id, $product_id, $original_price, $proposed_price, $token, $guest_id); } // Return response wp_send_json_success(array( 'message' => $status === 'approved' ? __('Your price proposal has been automatically approved! You can now add this product to your cart at your proposed price.', 'bdfg-flexible-pricing') : __('Your price proposal has been submitted! We will review it shortly.', 'bdfg-flexible-pricing'), 'status' => $status, 'token' => $token )); } /** * Save price proposal to database * * @param int $user_id User ID * @param int $product_id Product ID * @param float $original_price Original product price * @param float $proposed_price Proposed price * @param string $status Proposal status * @param string $token Unique token * @param string $guest_id Guest identifier * @return bool True on success, false on failure */ private function save_proposal($user_id, $product_id, $original_price, $proposed_price, $status, $token, $guest_id = '') { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Insert new record $result = $wpdb->insert( $table_name, array( 'user_id' => $user_id ? $user_id : 0, 'product_id' => $product_id, 'original_price' => $original_price, 'proposed_price' => $proposed_price, 'status' => $status, 'created_at' => current_time('mysql'), 'token' => $token, 'notes' => $guest_id ? $guest_id : '' ), array('%d', '%d', '%f', '%f', '%s', '%s', '%s', '%s') ); return $result !== false; } /** * Send notification email to admin * * @param int $user_id User ID * @param int $product_id Product ID * @param float $original_price Original product price * @param float $proposed_price Proposed price * @param string $token Unique token * @param string $guest_id Guest identifier */ private function send_admin_notification($user_id, $product_id, $original_price, $proposed_price, $token, $guest_id = '') { $product = wc_get_product($product_id); $product_name = $product ? $product->get_name() : sprintf(__('Product #%d', 'bdfg-flexible-pricing'), $product_id); // Get user information if ($user_id > 0) { $user = get_user_by('id', $user_id); $user_info = $user ? $user->user_email . ' (' . $user->display_name . ')' : sprintf(__('User #%d', 'bdfg-flexible-pricing'), $user_id); } else { $user_info = sprintf(__('Guest user (%s)', 'bdfg-flexible-pricing'), substr($guest_id, 0, 8) . '...'); } // Build URLs for quick actions $admin_url = admin_url('admin.php?page=bdfg-flexible-pricing'); $approval_url = add_query_arg(array( 'action' => 'approve', 'token' => $token, 'nonce' => wp_create_nonce('bdfg_approve_' . $token) ), $admin_url); $rejection_url = add_query_arg(array( 'action' => 'reject', 'token' => $token, 'nonce' => wp_create_nonce('bdfg_reject_' . $token) ), $admin_url); $discount_percent = round((($original_price - $proposed_price) / $original_price) * 100, 2); $subject = sprintf( __('[%s] New Price Proposal for %s', 'bdfg-flexible-pricing'), get_bloginfo('name'), $product_name ); $message = sprintf( __("Hello, A new price proposal has been submitted: Product: %s Original Price: %s Proposed Price: %s Discount: %s%% By: %s Date: %s To approve: %s To reject: %s Or you can manage all proposals from the admin panel: %s Regards, BDFG Flexible Pricing %s", 'bdfg-flexible-pricing'), $product_name, wc_price($original_price), wc_price($proposed_price), $discount_percent, $user_info, current_time(get_option('date_format') . ' ' . get_option('time_format')), $approval_url, $rejection_url, $admin_url, home_url() ); // Send email $headers = array('Content-Type: text/plain; charset=UTF-8'); wp_mail($this->admin_email, $subject, $message, $headers); } /** * Add admin menu */ public function add_admin_menu() { add_submenu_page( 'woocommerce', __('BDFG Flexible Pricing', 'bdfg-flexible-pricing'), __('Flexible Pricing', 'bdfg-flexible-pricing'), 'manage_woocommerce', 'bdfg-flexible-pricing', array($this, 'render_admin_page') ); } /** * Register plugin settings */ public function register_settings() { register_setting('bdfg_pricing_options', 'bdfg_min_discount', array( 'type' => 'integer', 'sanitize_callback' => array($this, 'sanitize_discount_value'), 'default' => 0, )); register_setting('bdfg_pricing_options', 'bdfg_max_discount', array( 'type' => 'integer', 'sanitize_callback' => array($this, 'sanitize_discount_value'), 'default' => 50, )); register_setting('bdfg_pricing_options', 'bdfg_allow_auto_approval', array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => 'yes', )); register_setting('bdfg_pricing_options', 'bdfg_admin_email', array( 'type' => 'string', 'sanitize_callback' => 'sanitize_email', 'default' => get_option('admin_email'), )); add_settings_section( 'bdfg_pricing_settings', __('Flexible Pricing Settings', 'bdfg-flexible-pricing'), array($this, 'settings_section_callback'), 'bdfg_pricing_options' ); add_settings_field( 'bdfg_min_discount', __('Minimum Discount (%)', 'bdfg-flexible-pricing'), array($this, 'min_discount_callback'), 'bdfg_pricing_options', 'bdfg_pricing_settings' ); add_settings_field( 'bdfg_max_discount', __('Maximum Discount (%)', 'bdfg-flexible-pricing'), array($this, 'max_discount_callback'), 'bdfg_pricing_options', 'bdfg_pricing_settings' ); add_settings_field( 'bdfg_allow_auto_approval', __('Auto-Approve Eligible Proposals', 'bdfg-flexible-pricing'), array($this, 'auto_approval_callback'), 'bdfg_pricing_options', 'bdfg_pricing_settings' ); add_settings_field( 'bdfg_admin_email', __('Notification Email', 'bdfg-flexible-pricing'), array($this, 'admin_email_callback'), 'bdfg_pricing_options', 'bdfg_pricing_settings' ); } /** * Sanitize discount values */ public function sanitize_discount_value($value) { $value = intval($value); return max(0, min(100, $value)); // Value between 0 and 100 } /** * Settings section description */ public function settings_section_callback() { echo '<p>' . __('Configure how customer price proposals are handled. Set the discount range that customers can propose and whether proposals should be automatically approved.', 'bdfg-flexible-pricing') . '</p>'; } /** * Minimum discount field */ public function min_discount_callback() { $min_discount = get_option('bdfg_min_discount', 0); echo '<input type="number" min="0" max="99" name="bdfg_min_discount" value="' . esc_attr($min_discount) . '" />'; echo '<p class="description">' . __('Minimum discount allowed (0 = no discount, customers can offer full price or higher)', 'bdfg-flexible-pricing') . '</p>'; } /** * Maximum discount field */ public function max_discount_callback() { $max_discount = get_option('bdfg_max_discount', 50); echo '<input type="number" min="1" max="100" name="bdfg_max_discount" value="' . esc_attr($max_discount) . '" />'; echo '<p class="description">' . __('Maximum discount allowed (e.g., 50 means customers can offer up to 50% off)', 'bdfg-flexible-pricing') . '</p>'; } /** * Auto approval field */ public function auto_approval_callback() { $allow_auto_approval = get_option('bdfg_allow_auto_approval', 'yes'); echo '<input type="checkbox" name="bdfg_allow_auto_approval" value="yes" ' . checked($allow_auto_approval, 'yes', false) . ' />'; echo '<p class="description">' . __('Automatically approve proposals that meet the discount criteria', 'bdfg-flexible-pricing') . '</p>'; } /** * Admin email field */ public function admin_email_callback() { $admin_email = get_option('bdfg_admin_email', get_option('admin_email')); echo '<input type="email" name="bdfg_admin_email" value="' . esc_attr($admin_email) . '" class="regular-text" />'; echo '<p class="description">' . __('Email address for price proposal notifications', 'bdfg-flexible-pricing') . '</p>'; } /** * Render admin page */ public function render_admin_page() { // Handle direct approve/reject links if (isset($_GET['action']) && isset($_GET['token']) && isset($_GET['nonce'])) { $action = sanitize_text_field($_GET['action']); $token = sanitize_text_field($_GET['token']); $nonce = sanitize_text_field($_GET['nonce']); if (($action === 'approve' && wp_verify_nonce($nonce, 'bdfg_approve_' . $token)) || ($action === 'reject' && wp_verify_nonce($nonce, 'bdfg_reject_' . $token))) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; $proposal = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table_name} WHERE token = %s", $token )); if ($proposal) { $status = $action === 'approve' ? 'approved' : 'rejected'; $updated = $wpdb->update( $table_name, array( 'status' => $status, 'updated_at' => current_time('mysql') ), array('id' => $proposal->id), array('%s', '%s'), array('%d') ); if ($updated) { // Also notify the customer if we have their email if ($proposal->user_id > 0) { $this->notify_customer_of_decision($proposal, $status); } echo '<div class="notice notice-success"><p>' . sprintf(__('Proposal has been %s.', 'bdfg-flexible-pricing'), $status) . '</p></div>'; } else { echo '<div class="notice notice-error"><p>' . __('Failed to update proposal status.', 'bdfg-flexible-pricing') . '</p></div>'; } } } } // Display tabs $active_tab = isset($_GET['tab']) ? sanitize_text_field($_GET['tab']) : 'proposals'; ?> <div class="wrap bdfg-admin-wrap"> <h1><?php _e('BDFG Flexible Pricing', 'bdfg-flexible-pricing'); ?></h1> <h2 class="nav-tab-wrapper"> <a href="?page=bdfg-flexible-pricing&tab=proposals" class="nav-tab <?php echo $active_tab === 'proposals' ? 'nav-tab-active' : ''; ?>"> <?php _e('Price Proposals', 'bdfg-flexible-pricing'); ?> </a> <a href="?page=bdfg-flexible-pricing&tab=settings" class="nav-tab <?php echo $active_tab === 'settings' ? 'nav-tab-active' : ''; ?>"> <?php _e('Settings', 'bdfg-flexible-pricing'); ?> </a> <a href="?page=bdfg-flexible-pricing&tab=stats" class="nav-tab <?php echo $active_tab === 'stats' ? 'nav-tab-active' : ''; ?>"> <?php _e('Statistics', 'bdfg-flexible-pricing'); ?> </a> </h2> <div class="tab-content"> <?php if ($active_tab === 'proposals') { $this->render_proposals_tab(); } elseif ($active_tab === 'stats') { $this->render_stats_tab(); } else { $this->render_settings_tab(); } ?> </div> <div class="bdfg-footer"> <p> <?php printf( __('BDFG Flexible Pricing v%s | Made with ❤️ by <a href="%s" target="_blank">beiduofengou</a>', 'bdfg-flexible-pricing'), BDFG_FLEXIBLE_PRICING_VERSION, 'https://beiduofengou.net' ); ?> </p> </div> </div> <?php } /** * Render proposals tab */ private function render_proposals_tab() { // Create custom list table require_once(BDFG_FLEXIBLE_PRICING_PATH . 'includes/class-bdfg-proposals-list-table.php'); $proposals_table = new BDFG_Proposals_List_Table(); $proposals_table->prepare_items(); echo '<div class="bdfg-proposals-container">'; // Add a filter form echo '<div class="bdfg-filter-form">'; $proposals_table->views(); $proposals_table->display_tablenav('top'); echo '</div>'; $proposals_table->display(); echo '</div>'; } /** * Render settings tab */ private function render_settings_tab() { ?> <div class="bdfg-settings-container"> <form method="post" action="options.php"> <?php settings_fields('bdfg_pricing_options'); do_settings_sections('bdfg_pricing_options'); submit_button(__('Save Settings', 'bdfg-flexible-pricing')); ?> </form> <div class="bdfg-settings-sidebar"> <div class="bdfg-settings-box"> <h3><?php _e('Need Help?', 'bdfg-flexible-pricing'); ?></h3> <p><?php _e('Check out our documentation for tips on how to get the most out of BDFG Flexible Pricing.', 'bdfg-flexible-pricing'); ?></p> <a href="https://beiduofengou.net/docs/flexible-pricing/" class="button button-secondary" target="_blank"> <?php _e('View Documentation', 'bdfg-flexible-pricing'); ?> </a> </div> <div class="bdfg-settings-box"> <h3><?php _e('Rate Our Plugin', 'bdfg-flexible-pricing'); ?></h3> <p><?php _e('If you enjoy using BDFG Flexible Pricing, please take a moment to rate it. It helps us a lot!', 'bdfg-flexible-pricing'); ?></p> <a href="https://wordpress.org/plugins/bdfg-flexible-pricing/reviews/" class="button button-secondary" target="_blank"> <?php _e('Leave a Review', 'bdfg-flexible-pricing'); ?> </a> </div> </div> </div> <?php } /** * Render statistics tab */ private function render_stats_tab() { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Get statistics $total_proposals = $wpdb->get_var("SELECT COUNT(id) FROM $table_name"); $approved_proposals = $wpdb->get_var("SELECT COUNT(id) FROM $table_name WHERE status = 'approved'"); $rejected_proposals = $wpdb->get_var("SELECT COUNT(id) FROM $table_name WHERE status = 'rejected'"); $pending_proposals = $wpdb->get_var("SELECT COUNT(id) FROM $table_name WHERE status = 'pending'"); $used_proposals = $wpdb->get_var("SELECT COUNT(id) FROM $table_name WHERE status = 'used'"); // Get average discount $avg_discount = $wpdb->get_var( "SELECT AVG((original_price - proposed_price) / original_price * 100) FROM $table_name WHERE status IN ('approved', 'used')" ); // Get most active products $popular_products = $wpdb->get_results( "SELECT product_id, COUNT(id) as proposal_count FROM $table_name GROUP BY product_id ORDER BY proposal_count DESC LIMIT 5" ); // Date range for recent stats $thirty_days_ago = date('Y-m-d H:i:s', strtotime('-30 days')); $recent_proposals = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(id) FROM $table_name WHERE created_at >= %s", $thirty_days_ago )); ?> <div class="bdfg-stats-container"> <div class="bdfg-stats-summary"> <div class="bdfg-stat-box"> <h3><?php _e('Total Proposals', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo esc_html($total_proposals); ?></div> </div> <div class="bdfg-stat-box bdfg-stat-approved"> <h3><?php _e('Approved', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo esc_html($approved_proposals); ?></div> </div> <div class="bdfg-stat-box bdfg-stat-rejected"> <h3><?php _e('Rejected', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo esc_html($rejected_proposals); ?></div> </div> <div class="bdfg-stat-box bdfg-stat-pending"> <h3><?php _e('Pending', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo esc_html($pending_proposals); ?></div> </div> <div class="bdfg-stat-box bdfg-stat-used"> <h3><?php _e('Used in Orders', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo esc_html($used_proposals); ?></div> </div> <div class="bdfg-stat-box bdfg-stat-discount"> <h3><?php _e('Avg. Discount', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo number_format($avg_discount, 2); ?>%</div> </div> <div class="bdfg-stat-box bdfg-stat-recent"> <h3><?php _e('Last 30 Days', 'bdfg-flexible-pricing'); ?></h3> <div class="bdfg-stat-number"><?php echo esc_html($recent_proposals); ?></div> </div> </div> <div class="bdfg-stats-details"> <div class="bdfg-popular-products"> <h3><?php _e('Most Popular Products', 'bdfg-flexible-pricing'); ?></h3> <table class="widefat"> <thead> <tr> <th><?php _e('Product', 'bdfg-flexible-pricing'); ?></th> <th><?php _e('Proposals', 'bdfg-flexible-pricing'); ?></th> <th><?php _e('Actions', 'bdfg-flexible-pricing'); ?></th> </tr> </thead> <tbody> <?php if ($popular_products): ?> <?php foreach ($popular_products as $item): ?> <?php $product = wc_get_product($item->product_id); ?> <tr> <td> <?php if ($product): ?> <a href="<?php echo get_edit_post_link($item->product_id); ?>"> <?php echo esc_html($product->get_name()); ?> </a> <?php else: ?> <?php printf(__('Product #%d (Deleted)', 'bdfg-flexible-pricing'), $item->product_id); ?> <?php endif; ?> </td> <td><?php echo esc_html($item->proposal_count); ?></td> <td> <?php if ($product): ?> <a href="<?php echo esc_url(admin_url('admin.php?page=bdfg-flexible-pricing&product_id=' . $item->product_id)); ?>" class="button button-small"> <?php _e('View Proposals', 'bdfg-flexible-pricing'); ?> </a> <?php endif; ?> </td> </tr> <?php endforeach; ?> <?php else: ?> <tr> <td colspan="3"><?php _e('No proposals yet.', 'bdfg-flexible-pricing'); ?></td> </tr> <?php endif; ?> </tbody> </table> </div> </div> </div> <?php } /** * Notify customer of proposal decision * * @param object $proposal Proposal data * @param string $status New status */ private function notify_customer_of_decision($proposal, $status) { if ($proposal->user_id <= 0) { return; // No user to notify } $user = get_user_by('id', $proposal->user_id); if (!$user || !$user->user_email) { return; } $product = wc_get_product($proposal->product_id); $product_name = $product ? $product->get_name() : sprintf(__('Product #%d', 'bdfg-flexible-pricing'), $proposal->product_id); $product_url = $product ? get_permalink($proposal->product_id) : ''; $subject = sprintf( $status === 'approved' ? __('[%s] Your Price Proposal for %s has been Approved', 'bdfg-flexible-pricing') : __('[%s] Your Price Proposal for %s has been Declined', 'bdfg-flexible-pricing'), get_bloginfo('name'), $product_name ); if ($status === 'approved') { $message = sprintf( __("Hello %s, Good news! Your price proposal for %s has been approved. Product: %s Your Proposed Price: %s Original Price: %s Discount: %.2f%% You can now purchase this product at your proposed price by adding it to your cart. Product link: %s Thank you for shopping with us! Regards, %s %s", 'bdfg-flexible-pricing'), $user->display_name, $product_name, $product_name, wc_price($proposal->proposed_price), wc_price($proposal->original_price), (($proposal->original_price - $proposal->proposed_price) / $proposal->original_price) * 100, $product_url, get_bloginfo('name'), home_url() ); } else { $message = sprintf( __("Hello %s, Thank you for your interest in our products. Unfortunately, we are unable to accept your price proposal for %s at this time. Product: %s Your Proposed Price: %s Original Price: %s You are welcome to submit a new proposal or purchase the product at the regular price. Product link: %s Thank you for your understanding. Regards, %s %s", 'bdfg-flexible-pricing'), $user->display_name, $product_name, $product_name, wc_price($proposal->proposed_price), wc_price($proposal->original_price), $product_url, get_bloginfo('name'), home_url() ); } $headers = array('Content-Type: text/plain; charset=UTF-8'); wp_mail($user->user_email, $subject, $message, $headers); } /** * Approve a price proposal via AJAX */ public function approve_proposal() { // Check security check_ajax_referer('bdfg_admin_nonce', 'nonce'); if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('You do not have permission to do this', 'bdfg-flexible-pricing'))); } $proposal_id = isset($_POST['proposal_id']) ? intval($_POST['proposal_id']) : 0; global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Get proposal details before updating $proposal = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $proposal_id)); if (!$proposal) { wp_send_json_error(array('message' => __('Proposal not found', 'bdfg-flexible-pricing'))); return; } $result = $wpdb->update( $table_name, array( 'status' => 'approved', 'updated_at' => current_time('mysql') ), array('id' => $proposal_id), array('%s', '%s'), array('%d') ); if ($result) { // Notify customer $this->notify_customer_of_decision($proposal, 'approved'); // Add admin note $this->add_admin_action_log('Approved price proposal #' . $proposal_id); wp_send_json_success(array( 'message' => __('Proposal approved successfully', 'bdfg-flexible-pricing') )); } else { wp_send_json_error(array('message' => __('Failed to approve proposal', 'bdfg-flexible-pricing'))); } } /** * Reject a price proposal via AJAX */ public function reject_proposal() { // Check security check_ajax_referer('bdfg_admin_nonce', 'nonce'); if (!current_user_can('manage_woocommerce')) { wp_send_json_error(array('message' => __('You do not have permission to do this', 'bdfg-flexible-pricing'))); } $proposal_id = isset($_POST['proposal_id']) ? intval($_POST['proposal_id']) : 0; global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Get proposal details before updating $proposal = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table_name} WHERE id = %d", $proposal_id)); if (!$proposal) { wp_send_json_error(array('message' => __('Proposal not found', 'bdfg-flexible-pricing'))); return; } $result = $wpdb->update( $table_name, array( 'status' => 'rejected', 'updated_at' => current_time('mysql') ), array('id' => $proposal_id), array('%s', '%s'), array('%d') ); if ($result) { // Notify customer $this->notify_customer_of_decision($proposal, 'rejected'); // Add admin note $this->add_admin_action_log('Rejected price proposal #' . $proposal_id); wp_send_json_success(array( 'message' => __('Proposal rejected successfully', 'bdfg-flexible-pricing') )); } else { wp_send_json_error(array('message' => __('Failed to reject proposal', 'bdfg-flexible-pricing'))); } } /** * Add proposal data to cart item */ public function add_price_proposal_to_cart_item($cart_item_data, $product_id, $variation_id) { $user_id = get_current_user_id(); // Guest user if (!$user_id && isset($_COOKIE['bdfg_guest_id'])) { $guest_id = sanitize_text_field($_COOKIE['bdfg_guest_id']); } // Check for approved price proposal if (isset($user_id) || isset($guest_id)) { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Get user's latest approved price proposal if (isset($user_id) && $user_id > 0) { $proposal = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table_name} WHERE user_id = %d AND product_id = %d AND status = 'approved' ORDER BY created_at DESC LIMIT 1", $user_id, $product_id )); } else { $proposal = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$table_name} WHERE notes = %s AND product_id = %d AND status = 'approved' ORDER BY created_at DESC LIMIT 1", $guest_id, $product_id )); } if ($proposal) { // Save proposal info to cart item $cart_item_data['bdfg_proposal'] = array( 'id' => $proposal->id, 'original_price' => $proposal->original_price, 'custom_price' => $proposal->proposed_price ); } } return $cart_item_data; } /** * Get cart item proposal from session */ public function get_cart_item_proposal_from_session($cart_item, $values) { if (isset($values['bdfg_proposal'])) { $cart_item['bdfg_proposal'] = $values['bdfg_proposal']; } return $cart_item; } /** * Show proposed price in cart */ public function show_proposed_price_in_cart($price, $cart_item, $cart_item_key) { if (isset($cart_item['bdfg_proposal'])) { $custom_price = $cart_item['bdfg_proposal']['custom_price']; $original_price = $cart_item['bdfg_proposal']['original_price']; // Show original and custom price $formatted_price = '<del>' . wc_price($original_price) . '</del> <ins>' . wc_price($custom_price) . '</ins>'; return $formatted_price; } return $price; } /** * Show proposed subtotal in cart */ public function show_proposed_subtotal_in_cart($subtotal, $cart_item, $cart_item_key) { if (isset($cart_item['bdfg_proposal'])) { $custom_price = $cart_item['bdfg_proposal']['custom_price']; $custom_subtotal = $custom_price * $cart_item['quantity']; return wc_price($custom_subtotal); } return $subtotal; } /** * Calculate custom price for cart items */ public function calculate_custom_price($cart) { if (is_admin() && !defined('DOING_AJAX')) { return; } if (did_action('woocommerce_before_calculate_totals') >= 2) { return; } foreach ($cart->get_cart() as $cart_item_key => $cart_item) { if (isset($cart_item['bdfg_proposal'])) { $cart_item['data']->set_price($cart_item['bdfg_proposal']['custom_price']); } } } /** * Add proposal data to order items */ public function add_proposal_data_to_order_items($item, $cart_item_key, $values, $order) { if (isset($values['bdfg_proposal'])) { $item->add_meta_data('_bdfg_original_price', $values['bdfg_proposal']['original_price'], true); $item->add_meta_data('_bdfg_proposed_price', $values['bdfg_proposal']['custom_price'], true); $item->add_meta_data('_bdfg_proposal_id', $values['bdfg_proposal']['id'], true); // Calculate discount percentage $original = $values['bdfg_proposal']['original_price']; $custom = $values['bdfg_proposal']['custom_price']; $discount = round((($original - $custom) / $original) * 100, 2); $item->add_meta_data('_bdfg_discount_percent', $discount, true); // Visible order item metadata $item->add_meta_data( __('Custom Price', 'bdfg-flexible-pricing'), sprintf(__('Customer proposed price: %s (%.2f%% off)', 'bdfg-flexible-pricing'), wc_price($custom), $discount), true ); } } /** * Register custom order statuses */ public function register_custom_order_statuses() { register_post_status('wc-proposal-pending', array( 'label' => __('Proposal Pending', 'bdfg-flexible-pricing'), 'public' => true, 'show_in_admin_status_list' => true, 'show_in_admin_all_list' => true, 'exclude_from_search' => false, 'label_count' => _n_noop('Proposal Pending <span class="count">(%s)</span>', 'Proposal Pending <span class="count">(%s)</span>', 'bdfg-flexible-pricing') )); } /** * Add custom order statuses */ public function add_custom_order_statuses($order_statuses) { $new_statuses = array(); // Insert our custom status after processing foreach ($order_statuses as $key => $status) { $new_statuses[$key] = $status; if ($key === 'wc-processing') { $new_statuses['wc-proposal-pending'] = __('Proposal Pending', 'bdfg-flexible-pricing'); } } return $new_statuses; } /** * Check approved price on add to cart */ public function check_approved_price($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) { // Check if added product has approved price proposal if (isset($cart_item_data['bdfg_proposal'])) { // Update price proposal status to used global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; $wpdb->update( $table_name, array( 'status' => 'used', 'updated_at' => current_time('mysql'), 'notes' => sprintf(__('Used in cart item %s on %s', 'bdfg-flexible-pricing'), $cart_item_key, current_time(get_option('date_format') . ' ' . get_option('time_format'))) ), array('id' => $cart_item_data['bdfg_proposal']['id']), array('%s', '%s', '%s'), array('%d') ); } } /** * Clean expired proposals */ public function clean_expired_proposals() { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Clean up proposals older than 30 days that are pending $thirty_days_ago = date('Y-m-d H:i:s', strtotime('-30 days')); $wpdb->query($wpdb->prepare( "UPDATE {$table_name} SET status = 'expired', updated_at = %s WHERE status = 'pending' AND created_at < %s", current_time('mysql'), $thirty_days_ago )); // Log cleanup $this->add_admin_action_log('Cleaned expired price proposals'); } /** * Add admin action log */ private function add_admin_action_log($message) { $current_user = wp_get_current_user(); $username = $current_user->exists() ? $current_user->user_login : 'system'; $log_message = sprintf( '[%s] %s: %s', current_time('Y-m-d H:i:s'), $username, $message ); $log_file = WP_CONTENT_DIR . '/uploads/bdfg-flexible-pricing/admin-actions.log'; // Create directory if it doesn't exist $log_dir = dirname($log_file); if (!file_exists($log_dir)) { wp_mkdir_p($log_dir); } // Append to log file file_put_contents($log_file, $log_message . PHP_EOL, FILE_APPEND); } }
includes/class-bdfg-proposals-list-table.php
相关文章: WooCommerce 产品过滤器
<?php /** * BDFG Proposals List Table * * @package BDFG_Flexible_Pricing * @since 2.0.0 */ // Prevent direct access defined('ABSPATH') || exit; // Check for WP_List_Table class if (!class_exists('WP_List_Table')) { require_once(ABSPATH . 'wp-admin/includes/class-wp-list-table.php'); } /** * Proposals List Table */ class BDFG_Proposals_List_Table extends WP_List_Table { /** * Constructor */ public function __construct() { parent::__construct(array( 'singular' => 'proposal', 'plural' => 'proposals', 'ajax' => false )); } /** * Get columns * * @return array */ public function get_columns() { return array( 'cb' => '<input type="checkbox" />', 'id' => __('ID', 'bdfg-flexible-pricing'), 'product' => __('Product', 'bdfg-flexible-pricing'), 'user' => __('Customer', 'bdfg-flexible-pricing'), 'original_price' => __('Original Price', 'bdfg-flexible-pricing'), 'proposed_price' => __('Proposed Price', 'bdfg-flexible-pricing'), 'discount' => __('Discount %', 'bdfg-flexible-pricing'), 'status' => __('Status', 'bdfg-flexible-pricing'), 'date' => __('Date', 'bdfg-flexible-pricing'), 'actions' => __('Actions', 'bdfg-flexible-pricing') ); } /** * Get sortable columns * * @return array */ public function get_sortable_columns() { return array( 'id' => array('id', true), 'product' => array('product_id', false), 'original_price' => array('original_price', false), 'proposed_price' => array('proposed_price', false), 'discount' => array('discount', false), 'status' => array('status', false), 'date' => array('created_at', true) ); } /** * Column default * * @param object $item Item data * @param string $column_name Column name * @return string */ public function column_default($item, $column_name) { switch ($column_name) { case 'id': return $item->id; case 'product': $product = wc_get_product($item->product_id); if ($product) { $image = $product->get_image(array(32, 32)); return sprintf( '<div class="bdfg-product-cell"><div class="bdfg-product-thumb">%s</div><div class="bdfg-product-info"><a href="%s">%s</a> <span class="bdfg-product-id">#%d</span></div></div>', $image, get_edit_post_link($item->product_id), $product->get_name(), $item->product_id ); } return sprintf('<span class="bdfg-deleted-product">' . __('Product #%d (Deleted)', 'bdfg-flexible-pricing') . '</span>', $item->product_id); case 'user': if (is_numeric($item->user_id) && $item->user_id > 0) { $user = get_user_by('id', $item->user_id); if ($user) { return sprintf( '<a href="%s">%s</a><br><span class="bdfg-user-email">%s</span>', get_edit_user_link($item->user_id), $user->display_name, $user->user_email ); } return sprintf(__('User #%d', 'bdfg-flexible-pricing'), $item->user_id); } elseif (!empty($item->notes) && strpos($item->notes, 'guest_') === 0) { return sprintf('<span class="bdfg-guest-user">' . __('Guest (%s)', 'bdfg-flexible-pricing') . '</span>', substr($item->notes, 0, 15) . '...'); } return '<span class="bdfg-guest-user">' . __('Guest', 'bdfg-flexible-pricing') . '</span>'; case 'original_price': return wc_price($item->original_price); case 'proposed_price': return wc_price($item->proposed_price); case 'discount': $discount = (($item->original_price - $item->proposed_price) / $item->original_price) * 100; $class = $discount >= 30 ? 'bdfg-high-discount' : ($discount >= 15 ? 'bdfg-medium-discount' : 'bdfg-low-discount'); return '<span class="' . $class . '">' . number_format($discount, 2) . '%</span>'; case 'status': $statuses = array( 'pending' => __('Pending', 'bdfg-flexible-pricing'), 'approved' => __('Approved', 'bdfg-flexible-pricing'), 'rejected' => __('Rejected', 'bdfg-flexible-pricing'), 'used' => __('Used', 'bdfg-flexible-pricing'), 'expired' => __('Expired', 'bdfg-flexible-pricing') ); $status_class = 'bdfg-status-' . $item->status; $status_text = isset($statuses[$item->status]) ? $statuses[$item->status] : $item->status; return sprintf('<span class="%s">%s</span>', $status_class, $status_text); case 'date': $date_created = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($item->created_at)); $date_updated = !empty($item->updated_at) ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($item->updated_at)) : ''; $output = sprintf('<span class="bdfg-date-created">%s</span>', $date_created); if ($date_updated && $item->status !== 'pending') { $output .= sprintf('<br><span class="bdfg-date-updated">' . __('Updated: %s', 'bdfg-flexible-pricing') . '</span>', $date_updated); } return $output; case 'actions': $actions = array(); if ($item->status === 'pending') { $actions['approve'] = sprintf( '<a href="#" class="bdfg-approve button button-small" data-id="%d">%s</a>', $item->id, __('Approve', 'bdfg-flexible-pricing') ); $actions['reject'] = sprintf( '<a href="#" class="bdfg-reject button button-small button-link-delete" data-id="%d">%s</a>', $item->id, __('Reject', 'bdfg-flexible-pricing') ); } if ($item->status === 'approved') { $actions['reject'] = sprintf( '<a href="#" class="bdfg-reject button button-small button-link-delete" data-id="%d">%s</a>', $item->id, __('Reject', 'bdfg-flexible-pricing') ); } if ($item->status === 'rejected') { $actions['approve'] = sprintf( '<a href="#" class="bdfg-approve button button-small" data-id="%d">%s</a>', $item->id, __('Approve', 'bdfg-flexible-pricing') ); } return implode(' ', $actions); default: return print_r($item, true); } } /** * Column checkbox * * @param object $item Item data * @return string */ public function column_cb($item) { return sprintf( '<input type="checkbox" name="proposals[]" value="%d" />', $item->id ); } /** * Get views * * @return array */ public function get_views() { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; $status_links = array(); $current_status = isset($_REQUEST['status']) ? sanitize_text_field($_REQUEST['status']) : ''; // All link $all_count = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); $class = empty($current_status) ? 'current' : ''; $status_links['all'] = "<a href='" . admin_url('admin.php?page=bdfg-flexible-pricing') . "' class='$class'>" . __('All', 'bdfg-flexible-pricing') . " <span class='count'>($all_count)</span></a>"; // Status specific links $statuses = array( 'pending' => __('Pending', 'bdfg-flexible-pricing'), 'approved' => __('Approved', 'bdfg-flexible-pricing'), 'rejected' => __('Rejected', 'bdfg-flexible-pricing'), 'used' => __('Used', 'bdfg-flexible-pricing'), 'expired' => __('Expired', 'bdfg-flexible-pricing') ); foreach ($statuses as $status => $label) { $count = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $table_name WHERE status = %s", $status)); if ($count > 0) { $class = ($current_status === $status) ? 'current' : ''; $status_links[$status] = "<a href='" . admin_url('admin.php?page=bdfg-flexible-pricing&status=' . $status) . "' class='$class'>" . $label . " <span class='count'>($count)</span></a>"; } } return $status_links; } /** * Prepare items */ public function prepare_items() { global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; $per_page = 20; $current_page = $this->get_pagenum(); // Process filter options $where = '1=1'; // Filter by status if set if (isset($_REQUEST['status']) && !empty($_REQUEST['status'])) { $status = sanitize_text_field($_REQUEST['status']); $where .= $wpdb->prepare(" AND status = %s", $status); } // Filter by product ID if set if (isset($_REQUEST['product_id']) && !empty($_REQUEST['product_id'])) { $product_id = intval($_REQUEST['product_id']); $where .= $wpdb->prepare(" AND product_id = %d", $product_id); } // Filter by user ID if set if (isset($_REQUEST['user_id']) && !empty($_REQUEST['user_id'])) { $user_id = intval($_REQUEST['user_id']); $where .= $wpdb->prepare(" AND user_id = %d", $user_id); } // Filter by date range if set if (isset($_REQUEST['date_from']) && !empty($_REQUEST['date_from'])) { $date_from = sanitize_text_field($_REQUEST['date_from']); $where .= $wpdb->prepare(" AND created_at >= %s", $date_from); } if (isset($_REQUEST['date_to']) && !empty($_REQUEST['date_to'])) { $date_to = sanitize_text_field($_REQUEST['date_to']); $where .= $wpdb->prepare(" AND created_at <= %s", $date_to); } // Get total items count $total_items = $wpdb->get_var("SELECT COUNT(id) FROM $table_name WHERE $where"); // Process ordering $orderby = !empty($_REQUEST['orderby']) ? sanitize_text_field($_REQUEST['orderby']) : 'created_at'; $order = !empty($_REQUEST['order']) ? sanitize_text_field($_REQUEST['order']) : 'DESC'; // Handle special orderby cases if ($orderby === 'discount') { // Special handling for discount sorting $orderby = '((original_price - proposed_price) / original_price * 100)'; } // Prepare the SQL query $sql = $wpdb->prepare( "SELECT * FROM $table_name WHERE $where ORDER BY $orderby $order LIMIT %d OFFSET %d", $per_page, ($current_page - 1) * $per_page ); // Get the data $this->items = $wpdb->get_results($sql); // Set pagination arguments $this->set_pagination_args(array( 'total_items' => $total_items, 'per_page' => $per_page, 'total_pages' => ceil($total_items / $per_page) )); // Set column headers $this->_column_headers = array( $this->get_columns(), array(), $this->get_sortable_columns() ); } /** * Get bulk actions * * @return array */ public function get_bulk_actions() { return array( 'approve' => __('Approve', 'bdfg-flexible-pricing'), 'reject' => __('Reject', 'bdfg-flexible-pricing'), 'delete' => __('Delete', 'bdfg-flexible-pricing') ); } /** * Process bulk actions */ public function process_bulk_action() { // Security check if (isset($_POST['_wpnonce']) && !empty($_POST['_wpnonce'])) { $nonce = filter_input(INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING); if (!wp_verify_nonce($nonce, 'bulk-' . $this->_args['plural'])) { wp_die(__('Security check failed', 'bdfg-flexible-pricing')); } } $action = $this->current_action(); if (!$action) { return; } // Make sure we have proposals to process if (!isset($_POST['proposals']) || empty($_POST['proposals'])) { return; } global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; // Process each selected proposal $proposals = array_map('intval', $_POST['proposals']); switch ($action) { case 'approve': foreach ($proposals as $id) { // Get proposal before updating to notify customer $proposal = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id)); if ($proposal && $proposal->status !== 'approved') { $wpdb->update( $table_name, array( 'status' => 'approved', 'updated_at' => current_time('mysql') ), array('id' => $id), array('%s', '%s'), array('%d') ); // Notify customer if logged in user if ($proposal->user_id > 0) { $bdfg = BDFG_Flexible_Pricing::get_instance(); $bdfg->notify_customer_of_decision($proposal, 'approved'); } } } break; case 'reject': foreach ($proposals as $id) { // Get proposal before updating to notify customer $proposal = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id)); if ($proposal && $proposal->status !== 'rejected') { $wpdb->update( $table_name, array( 'status' => 'rejected', 'updated_at' => current_time('mysql') ), array('id' => $id), array('%s', '%s'), array('%d') ); // Notify customer if logged in user if ($proposal->user_id > 0) { $bdfg = BDFG_Flexible_Pricing::get_instance(); $bdfg->notify_customer_of_decision($proposal, 'rejected'); } } } break; case 'delete': foreach ($proposals as $id) { $wpdb->delete( $table_name, array('id' => $id), array('%d') ); } break; } } /** * Extra tablenav * * @param string $which Top or bottom */ public function extra_tablenav($which) { if ($which !== 'top') { return; } global $wpdb; $table_name = $wpdb->prefix . 'bdfg_price_proposals'; $product_id = isset($_REQUEST['product_id']) ? intval($_REQUEST['product_id']) : 0; $date_from = isset($_REQUEST['date_from']) ? sanitize_text_field($_REQUEST['date_from']) : ''; $date_to = isset($_REQUEST['date_to']) ? sanitize_text_field($_REQUEST['date_to']) : ''; echo '<div class="alignleft actions">'; // Product dropdown echo '<select name="product_id" id="filter-by-product">'; echo '<option value="">' . __('All products', 'bdfg-flexible-pricing') . '</option>'; // Get products that have proposals $products = $wpdb->get_results("SELECT DISTINCT product_id FROM $table_name ORDER BY product_id"); foreach ($products as $prod) { $product = wc_get_product($prod->product_id); $selected = selected($product_id, $prod->product_id, false); $name = $product ? $product->get_name() : sprintf(__('Product #%d (Deleted)', 'bdfg-flexible-pricing'), $prod->product_id); printf( '<option value="%d" %s>%s</option>', $prod->product_id, $selected, $name ); } echo '</select>'; // Date range echo '<input type="date" name="date_from" value="' . esc_attr($date_from) . '" placeholder="' . __('From date', 'bdfg-flexible-pricing') . '" class="bdfg-date-input" />'; echo '<input type="date" name="date_to" value="' . esc_attr($date_to) . '" placeholder="' . __('To date', 'bdfg-flexible-pricing') . '" class="bdfg-date-input" />'; submit_button(__('Filter', 'bdfg-flexible-pricing'), '', 'filter_action', false); // Add a reset filters button if any filter is active if (!empty($product_id) || !empty($date_from) || !empty($date_to) || isset($_REQUEST['status'])) { echo '<a href="' . esc_url(admin_url('admin.php?page=bdfg-flexible-pricing')) . '" class="button bdfg-reset-filters">' . __('Reset Filters', 'bdfg-flexible-pricing') . '</a>'; } echo '</div>'; } /** * Display the list table */ public function display() { // Process any bulk actions before displaying $this->process_bulk_action(); // Display the table parent::display(); } }
templates/proposal-form.php
<?php /** * BDFG Flexible Pricing - Price Proposal Form * * @package BDFG_Flexible_Pricing * @version 2.5.1 */ defined('ABSPATH') || exit; ?> <div class="bdfg-price-proposal-form"> <h4><?php _e('Make a Price Proposal', 'bdfg-flexible-pricing'); ?></h4> <div class="bdfg-proposal-info"> <p><?php printf(__('Regular price: <span class="bdfg-original-price">%s</span>', 'bdfg-flexible-pricing'), wc_price($regular_price)); ?></p> <?php if ($proposal): ?> <?php if ($proposal->status === 'approved'): ?> <div class="bdfg-proposal-approved"> <p class="bdfg-success-message"> <?php _e('Your price proposal has been approved!', 'bdfg-flexible-pricing'); ?> </p> <p class="bdfg-approved-price"> <?php printf(__('Approved price: <strong>%s</strong>', 'bdfg-flexible-pricing'), wc_price($proposal->proposed_price)); ?> <span class="bdfg-discount-badge"> <?php $discount = (($proposal->original_price - $proposal->proposed_price) / $proposal->original_price) * 100; printf(__('%.2f%% off', 'bdfg-flexible-pricing'), $discount); ?> </span> </p> <p class="bdfg-approved-note"> <?php _e('This price will be applied when you add this product to your cart.', 'bdfg-flexible-pricing'); ?> </p> </div> <?php elseif ($proposal->status === 'pending'): ?> <div class="bdfg-proposal-pending"> <p class="bdfg-info-message"> <?php _e('Your price proposal is being reviewed.', 'bdfg-flexible-pricing'); ?> </p> <p> <?php printf(__('Proposed price: <strong>%s</strong>', 'bdfg-flexible-pricing'), wc_price($proposal->proposed_price)); ?> </p> <p class="bdfg-submission-date"> <?php printf(__('Submitted: %s', 'bdfg-flexible-pricing'), date_i18n(get_option('date_format'), strtotime($proposal->created_at))); ?> </p> </div> <?php elseif ($proposal->status === 'rejected'): ?> <div class="bdfg-proposal-rejected"> <p class="bdfg-error-message"> <?php _e('Your price proposal was not accepted.', 'bdfg-flexible-pricing'); ?> </p> <p> <?php printf(__('You can submit a new proposal below.', 'bdfg-flexible-pricing')); ?> </p> <form class="bdfg-proposal-form"> <div class="bdfg-form-row"> <label for="bdfg-proposed-price"><?php _e('Your Offer:', 'bdfg-flexible-pricing'); ?></label> <div class="bdfg-price-input"> <span class="bdfg-currency-symbol"><?php echo $currency; ?></span> <input type="number" id="bdfg-proposed-price" name="proposed_price" min="<?php echo esc_attr($min_allowed); ?>" step="0.01" placeholder="<?php echo esc_attr($min_allowed); ?>"> </div> </div> <div class="bdfg-message-container"></div> <div class="bdfg-form-row bdfg-form-actions"> <input type="hidden" name="product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <button type="submit" class="button bdfg-submit-proposal"><?php _e('Submit Proposal', 'bdfg-flexible-pricing'); ?></button> </div> </form> </div> <?php else: ?> <form class="bdfg-proposal-form"> <div class="bdfg-form-row"> <label for="bdfg-proposed-price"><?php _e('Your Offer:', 'bdfg-flexible-pricing'); ?></label> <div class="bdfg-price-input"> <span class="bdfg-currency-symbol"><?php echo $currency; ?></span> <input type="number" id="bdfg-proposed-price" name="proposed_price" min="<?php echo esc_attr($min_allowed); ?>" step="0.01" placeholder="<?php echo esc_attr($min_allowed); ?>"> </div> </div> <div class="bdfg-message-container"></div> <div class="bdfg-form-row bdfg-form-actions"> <input type="hidden" name="product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <button type="submit" class="button bdfg-submit-proposal"><?php _e('Submit Proposal', 'bdfg-flexible-pricing'); ?></button> </div> </form> <?php endif; ?> <?php else: ?> <form class="bdfg-proposal-form"> <div class="bdfg-form-row"> <label for="bdfg-proposed-price"><?php _e('Your Offer:', 'bdfg-flexible-pricing'); ?></label> <div class="bdfg-price-input"> <span class="bdfg-currency-symbol"><?php echo $currency; ?></span> <input type="number" id="bdfg-proposed-price" name="proposed_price" min="<?php echo esc_attr($min_allowed); ?>" step="0.01" placeholder="<?php echo esc_attr($min_allowed); ?>"> </div> </div> <div class="bdfg-message-container"></div> <div class="bdfg-form-row bdfg-form-actions"> <input type="hidden" name="product_id" value="<?php echo esc_attr($product->get_id()); ?>"> <button type="submit" class="button bdfg-submit-proposal"><?php _e('Submit Proposal', 'bdfg-flexible-pricing'); ?></button> </div> </form> <?php endif; ?> </div> <div class="bdfg-proposal-guidelines"> <p class="bdfg-guidelines-text"> <?php if ($this->min_discount > 0) { printf( __('Offer between %s and %s (discount range: %s%% to %s%%).', 'bdfg-flexible-pricing'), wc_price($regular_price * (1 - $this->max_discount / 100)), wc_price($regular_price * (1 - $this->min_discount / 100)), $this->min_discount, $this->max_discount ); } else { printf( __('Maximum discount allowed: %s%% (minimum price: %s).', 'bdfg-flexible-pricing'), $this->max_discount, wc_price($regular_price * (1 - $this->max_discount / 100)) ); } ?> </p> </div> </div>
assets/js/bdfg-flexible-pricing.js
相关文章: WordPress 智能缓存管理
/** * BDFG Flexible Pricing - Frontend Script * * @package BDFG_Flexible_Pricing * @version 2.5.1 */ (function($) { 'use strict'; // Initialize the plugin when the DOM is fully loaded $(document).ready(function() { // Form submission handling $('.bdfg-proposal-form').on('submit', function(e) { e.preventDefault(); var $form = $(this), $submitButton = $form.find('.bdfg-submit-proposal'), $messageContainer = $form.find('.bdfg-message-container'), proposedPrice = $form.find('#bdfg-proposed-price').val(), productId = $form.find('input[name="product_id"]').val(); // Basic validation if (!proposedPrice || isNaN(parseFloat(proposedPrice)) || parseFloat(proposedPrice) <= 0) { $messageContainer.html('<div class="bdfg-error">' + bdfg_pricing_data.i18n.invalid_price + '</div>'); return false; } // Show loading state $submitButton.prop('disabled', true).text(bdfg_pricing_data.i18n.proposing); $messageContainer.html('<div class="bdfg-info">Processing...</div>'); // Send AJAX request $.ajax({ url: bdfg_pricing_data.ajax_url, type: 'POST', data: { action: 'bdfg_submit_price_proposal', nonce: bdfg_pricing_data.nonce, product_id: productId, price: proposedPrice }, success: function(response) { if (response.success) { // Success $messageContainer.html('<div class="bdfg-success">' + response.data.message + '</div>'); // If approved, reload the page after a delay to show the updated form if (response.data.status === 'approved') { setTimeout(function() { window.location.reload(); }, 2000); } else { // Reset form after a delay setTimeout(function() { $form[0].reset(); $submitButton.prop('disabled', false).text(bdfg_pricing_data.i18n.submit); }, 2000); } } else { // Error $messageContainer.html('<div class="bdfg-error">' + response.data.message + '</div>'); $submitButton.prop('disabled', false).text(bdfg_pricing_data.i18n.submit); } }, error: function() { // AJAX error $messageContainer.html('<div class="bdfg-error">' + bdfg_pricing_data.i18n.error + '</div>'); $submitButton.prop('disabled', false).text(bdfg_pricing_data.i18n.submit); } }); }); // Price input validation $('#bdfg-proposed-price').on('input', function() { var $input = $(this), value = parseFloat($input.val()); // Ensure valid number if (isNaN(value) || value <= 0) { return; } // Validate against min price if available var minPrice = parseFloat($input.attr('min')); if (!isNaN(minPrice) && value < minPrice) { $input.addClass('bdfg-invalid-price'); $input.parent().siblings('.bdfg-price-warning').remove(); $input.parent().after('<div class="bdfg-price-warning">' + bdfg_pricing_data.i18n.price_too_low + '</div>'); } else { $input.removeClass('bdfg-invalid-price'); $input.parent().siblings('.bdfg-price-warning').remove(); } }); // Highlight price input on focus $('#bdfg-proposed-price').on('focus', function() { $(this).parent().addClass('bdfg-input-focused'); }).on('blur', function() { $(this).parent().removeClass('bdfg-input-focused'); }); }); })(jQuery);
assets/js/bdfg-admin.js
/** * BDFG Flexible Pricing - Admin Script * * @package BDFG_Flexible_Pricing * @version 2.5.1 */ (function($) { 'use strict'; // Run when document is ready $(document).ready(function() { // Approve proposal $('.bdfg-proposals-container').on('click', '.bdfg-approve', function(e) { e.preventDefault(); if (!confirm(bdfg_admin.i18n.approve_confirm)) { return false; } var $button = $(this), proposalId = $button.data('id'), $row = $button.closest('tr'); $button.text(bdfg_admin.i18n.approving).attr('disabled', true); // Send AJAX request $.ajax({ url: bdfg_admin.ajax_url, type: 'POST', data: { action: 'bdfg_approve_proposal', nonce: bdfg_admin.nonce, proposal_id: proposalId }, success: function(response) { if (response.success) { // Update the status cell $row.find('td.column-status').html('<span class="bdfg-status-approved">Approved</span>'); // Update the actions cell $row.find('td.column-actions').html('<a href="#" class="bdfg-reject button button-small button-link-delete" data-id="' + proposalId + '">' + 'Reject' + '</a>'); // Flash the row $row.addClass('bdfg-updated').delay(1500).queue(function(next) { $(this).removeClass('bdfg-updated'); next(); }); } else { alert(response.data.message || bdfg_admin.i18n.error); $button.text('Approve').attr('disabled', false); } }, error: function() { alert(bdfg_admin.i18n.error); $button.text('Approve').attr('disabled', false); } }); }); // Reject proposal $('.bdfg-proposals-container').on('click', '.bdfg-reject', function(e) { e.preventDefault(); if (!confirm(bdfg_admin.i18n.reject_confirm)) { return false; } var $button = $(this), proposalId = $button.data('id'), $row = $button.closest('tr'); $button.text(bdfg_admin.i18n.rejecting).attr('disabled', true); // Send AJAX request $.ajax({ url: bdfg_admin.ajax_url, type: 'POST', data: { action: 'bdfg_reject_proposal', nonce: bdfg_admin.nonce, proposal_id: proposalId }, success: function(response) { if (response.success) { // Update the status cell $row.find('td.column-status').html('<span class="bdfg-status-rejected">Rejected</span>'); // Update the actions cell $row.find('td.column-actions').html('<a href="#" class="bdfg-approve button button-small" data-id="' + proposalId + '">' + 'Approve' + '</a>'); // Flash the row $row.addClass('bdfg-updated').delay(1500).queue(function(next) { $(this).removeClass('bdfg-updated'); next(); }); } else { alert(response.data.message || bdfg_admin.i18n.error); $button.text('Reject').attr('disabled', false); } }, error: function() { alert(bdfg_admin.i18n.error); $button.text('Reject').attr('disabled', false); } }); }); // Date range picker functionality if ($.fn.datepicker) { $('.bdfg-date-input').datepicker({ dateFormat: 'yy-mm-dd', changeMonth: true, changeYear: true }); } // Statistics tab - toggle details $('.bdfg-stat-box').on('click', function() { $(this).toggleClass('bdfg-stat-expanded'); }); // Settings tab - min discount should not be greater than max discount $('input[name="bdfg_min_discount"], input[name="bdfg_max_discount"]').on('change', function() { var minDiscount = parseInt($('input[name="bdfg_min_discount"]').val()) || 0, maxDiscount = parseInt($('input[name="bdfg_max_discount"]').val()) || 0; if (minDiscount > maxDiscount) { alert('Minimum discount cannot be greater than maximum discount'); $(this).val(maxDiscount); } // Ensure values are between 0 and 100 if (minDiscount < 0) { $('input[name="bdfg_min_discount"]').val(0); } else if (minDiscount > 100) { $('input[name="bdfg_min_discount"]').val(100); } if (maxDiscount < 0) { $('input[name="bdfg_max_discount"]').val(0); } else if (maxDiscount > 100) { $('input[name="bdfg_max_discount"]').val(100); } }); // Log current admin console.log('BDFG Flexible Pricing initialized by: beiduofengou on ' + getCurrentDateTime()); }); /** * Get current datetime in YYYY-MM-DD HH:MM:SS format */ function getCurrentDateTime() { var now = new Date(); var year = now.getUTCFullYear(); var month = ('0' + (now.getUTCMonth() + 1)).slice(-2); var day = ('0' + now.getUTCDate()).slice(-2); var hours = ('0' + now.getUTCHours()).slice(-2); var minutes = ('0' + now.getUTCMinutes()).slice(-2); var seconds = ('0' + now.getUTCSeconds()).slice(-2); return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds; } })(jQuery);
assets/css/bdfg-flexible-pricing.css
相关文章: WooCommerce 预定功能插件
/** * BDFG Flexible Pricing - Frontend Styles * * @package BDFG_Flexible_Pricing * @version 2.5.1 * @author beiduofengou */ /* Main container */ .bdfg-price-proposal-form { margin: 1.5em 0; padding: 1.2em; border: 1px solid #eaeaea; border-radius: 4px; background-color: #f9f9f9; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } .bdfg-price-proposal-form h4 { margin: 0 0 1em; padding-bottom: 0.5em; border-bottom: 1px solid #eaeaea; font-size: 1.2em; font-weight: 600; color: #333; } /* Form elements */ .bdfg-proposal-form { margin-top: 1em; } .bdfg-form-row { margin-bottom: 1em; display: flex; align-items: center; } .bdfg-form-row label { width: 30%; margin-right: 1em; font-weight: 600; } .bdfg-price-input { position: relative; display: flex; align-items: center; width: 60%; } .bdfg-currency-symbol { position: absolute; left: 10px; font-weight: bold; color: #555; z-index: 1; } #bdfg-proposed-price { padding-left: 25px; width: 100%; border-radius: 3px; border: 1px solid #ddd; padding: 8px 8px 8px 25px; font-size: 1em; } .bdfg-input-focused { box-shadow: 0 0 0 1px rgba(30, 140, 190, 0.8); } .bdfg-form-actions { margin-top: 1.5em; } .bdfg-submit-proposal { background-color: #0073aa !important; color: #fff !important; border-color: #0073aa !important; padding: 0.5em 1em !important; cursor: pointer; } .bdfg-submit-proposal:hover { background-color: #006799 !important; } .bdfg-submit-proposal:disabled { opacity: 0.7; cursor: not-allowed; } /* Messages */ .bdfg-message-container { margin: 1em 0; } .bdfg-success { padding: 0.75em 1em; border-left: 4px solid #46b450; background-color: #f7fff7; color: #0a3622; } .bdfg-error { padding: 0.75em 1em; border-left: 4px solid #dc3232; background-color: #fdf2f2; color: #8a1f11; } .bdfg-info { padding: 0.75em 1em; border-left: 4px solid #00a0d2; background-color: #f3f9fb; color: #0a769d; } .bdfg-price-warning { color: #d63638; font-size: 0.9em; margin-top: 0.5em; padding-left: 30%; } .bdfg-invalid-price { border-color: #d63638 !important; } /* Proposal statuses */ .bdfg-proposal-approved { background-color: #f0f9e8; border-left: 4px solid #46b450; padding: 1em; margin-bottom: 1.5em; } .bdfg-proposal-pending { background-color: #f7fcfe; border-left: 4px solid #00a0d2; padding: 1em; margin-bottom: 1.5em; } .bdfg-proposal-rejected { background-color: #fef7f1; border-left: 4px solid #ffb900; padding: 1em; margin-bottom: 1.5em; } .bdfg-success-message { color: #46b450; font-weight: 600; margin-top: 0; } .bdfg-info-message { color: #00a0d2; font-weight: 600; margin-top: 0; } .bdfg-error-message { color: #dc3232; font-weight: 600; margin-top: 0; } .bdfg-discount-badge { display: inline-block; margin-left: 10px; padding: 2px 8px; border-radius: 3px; background-color: #46b450; color: white; font-size: 0.85em; font-weight: bold; } .bdfg-approved-price { font-size: 1.1em; } .bdfg-approved-note { color: #555; font-style: italic; margin-bottom: 0; } .bdfg-submission-date { color: #777; font-size: 0.9em; font-style: italic; margin-bottom: 0; } /* Guidelines */ .bdfg-proposal-guidelines { margin-top: 1.5em; padding-top: 1em; border-top: 1px dashed #eaeaea; font-size: 0.9em; color: #666; } .bdfg-guidelines-text { margin-bottom: 0; } .bdfg-original-price { font-weight: bold; color: #333; } /* Responsive */ @media screen and (max-width: 768px) { .bdfg-form-row { flex-direction: column; align-items: flex-start; } .bdfg-form-row label { width: 100%; margin-bottom: 0.5em; } .bdfg-price-input { width: 100%; } .bdfg-price-warning { padding-left: 0; } } /* BDFG Branding */ .bdfg-price-proposal-form::after { content: "Powered by BDFG Flexible Pricing"; display: block; text-align: right; font-size: 0.75em; color: #999; margin-top: 1.5em; font-style: italic; }
assets/css/bdfg-admin.css
/** * BDFG Flexible Pricing - Admin Styles * * @package BDFG_Flexible_Pricing * @version 2.5.1 * @author beiduofengou */ /* Admin Container */ .bdfg-admin-wrap { max-width: 100%; margin-top: 20px; } /* Tabs Content */ .tab-content { margin-top: 20px; background: #fff; border: 1px solid #ccd0d4; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); padding: 20px; } /* Settings Layout */ .bdfg-settings-container { display: flex; flex-wrap: wrap; } .bdfg-settings-container form { flex: 1 1 65%; min-width: 500px; margin-right: 30px; } .bdfg-settings-sidebar { flex: 1 1 25%; min-width: 250px; } .bdfg-settings-box { background: #f8f9fa; border: 1px solid #e2e4e7; border-radius: 3px; padding: 15px; margin-bottom: 20px; } .bdfg-settings-box h3 { margin-top: 0; border-bottom: 1px solid #e2e4e7; padding-bottom: 10px; color: #23282d; } /* Footer */ .bdfg-footer { margin-top: 30px; padding-top: 15px; border-top: 1px solid #eee; color: #777; font-size: 13px; text-align: right; } /* Proposals Table */ .bdfg-proposals-container { position: relative; } .bdfg-filter-form { margin-bottom: 20px; } .bdfg-date-input { margin: 0 5px; vertical-align: middle; } .bdfg-reset-filters { margin-left: 5px !important; } /* Product Cell */ .bdfg-product-cell { display: flex; align-items: center; } .bdfg-product-thumb { margin-right: 10px; } .bdfg-product-thumb img { max-width: 32px; height: auto; vertical-align: middle; border: 1px solid #eee; border-radius: 2px; } .bdfg-product-info { display: flex; flex-direction: column; } .bdfg-product-id { color: #777; font-size: 12px; } /* Deleted Items */ .bdfg-deleted-product { color: #a00; font-style: italic; } /* User Cell */ .bdfg-user-email { color: #777; font-size: 12px; } .bdfg-guest-user { color: #666; font-style: italic; } /* Discount formatting */ .bdfg-high-discount { color: #d63638; font-weight: bold; } .bdfg-medium-discount { color: #b26200; font-weight: bold; } .bdfg-low-discount { color: #007017; } /* Status labels */ .bdfg-status-pending { background-color: #f1f1f1; border: 1px solid #ccd0d4; color: #222; display: inline-block; padding: 3px 7px; border-radius: 3px; } .bdfg-status-approved { background-color: #dff0d8; border: 1px solid #c3e6cb; color: #3c763d; display: inline-block; padding: 3px 7px; border-radius: 3px; } .bdfg-status-rejected { background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; display: inline-block; padding: 3px 7px; border-radius: 3px; } .bdfg-status-used { background-color: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; display: inline-block; padding: 3px 7px; border-radius: 3px; } .bdfg-status-expired { background-color: #ffeeba; border: 1px solid #ffeeba; color: #856404; display: inline-block; padding: 3px 7px; border-radius: 3px; } /* Row highlighting */ .bdfg-updated { background-color: #fffbdd !important; transition: background-color 1.5s ease-in-out; } /* Statistics Tab */ .bdfg-stats-container { margin-top: 20px; } .bdfg-stats-summary { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 30px; } .bdfg-stat-box { background: #fff; border: 1px solid #e2e4e7; border-radius: 5px; padding: 15px; flex: 1 1 calc(25% - 15px); min-width: 150px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); text-align: center; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; } .bdfg-stat-box:hover { transform: translateY(-2px); box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1); } .bdfg-stat-box h3 { margin: 0 0 10px 0; color: #23282d; font-size: 1em; } .bdfg-stat-number { font-size: 2em; font-weight: 600; } .bdfg-stat-approved .bdfg-stat-number { color: #46b450; } .bdfg-stat-rejected .bdfg-stat-number { color: #dc3232; } .bdfg-stat-pending .bdfg-stat-number { color: #ffb900; } .bdfg-stat-discount .bdfg-stat-number { color: #00a0d2; } .bdfg-stats-details { background: #fff; border: 1px solid #e2e4e7; border-radius: 5px; padding: 20px; margin-top: 20px; } .bdfg-popular-products h3 { margin-top: 0; border-bottom: 1px solid #e2e4e7; padding-bottom: 10px; } /* Custom WP Admin Branding */ #wpadminbar #wp-admin-bar-site-name>.ab-item:before { content: "\f227"; top: 2px; } /* Version info */ .bdfg-admin-wrap::after { content: "BDFG Flexible Pricing v2.5.1 - Last updated: 2025-03-05 16:04:03 by beiduofengou"; display: block; text-align: right; font-size: 11px; color: #999; margin-top: 30px; }