mirror of
https://gh.wpcy.net/https://github.com/absoftlabs/woo-custom-order-status.git
synced 2026-04-23 19:52:21 +08:00
475 lines
22 KiB
PHP
475 lines
22 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Custom Order Status for WooCommerce
|
|
* Description: Custom order statuses + per-status SMS via Alpha SMS (api.sms.net.bd) with shortcode-enabled templates and admin bulk/row actions.
|
|
* Plugin URI: https://absoftlab.com
|
|
* Version: 1.2.2
|
|
* Author: absoftlab
|
|
* Author URI: https://absoftlab.com
|
|
* License: GPL2
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) exit;
|
|
|
|
class Absoftlab_Custom_Order_Status_Manager {
|
|
private $option_key_statuses = 'absoftlab_custom_order_statuses'; // array: slug => ['label'=>..., 'sms'=>...]
|
|
private $option_key_settings = 'absoftlab_sms_settings'; // array: api_url, api_key, sender_id, enabled, content_id (opt), schedule (opt)
|
|
|
|
public function __construct() {
|
|
// Admin UI
|
|
add_action('admin_menu', [$this, 'add_admin_menu']);
|
|
add_action('admin_init', [$this, 'handle_form_submit']);
|
|
|
|
// Data + WC integrate
|
|
add_action('init', [$this, 'maybe_migrate_statuses']);
|
|
add_action('init', [$this, 'register_custom_statuses']);
|
|
add_filter('wc_order_statuses', [$this, 'add_to_wc_statuses']);
|
|
|
|
// SMS on status change
|
|
add_action('woocommerce_order_status_changed', [$this, 'maybe_send_sms'], 10, 4);
|
|
|
|
// === Orders list: Bulk actions "Change status to …"
|
|
// Legacy orders list
|
|
add_filter('bulk_actions-edit-shop_order', [$this, 'register_bulk_status_actions'], 20);
|
|
// New HPOS orders screen
|
|
add_filter('bulk_actions-woocommerce_page_wc-orders', [$this, 'register_bulk_status_actions'], 20);
|
|
|
|
add_action('admin_notices', [$this, 'bulk_changed_notice']);
|
|
|
|
// === Orders list: per-row action button "Change status to …" (legacy table)
|
|
add_filter('woocommerce_admin_order_actions', [$this, 'add_row_action_change_status'], 10, 2);
|
|
add_action('admin_post_absoftlab_mark_status', [$this, 'handle_single_mark_status']); // secure handler
|
|
|
|
// Default settings if empty
|
|
add_action('plugins_loaded', function(){
|
|
$s = $this->get_settings();
|
|
if (empty($s['api_url'])) {
|
|
$s['api_url'] = 'https://api.sms.net.bd/sendsms';
|
|
$this->save_settings($s);
|
|
}
|
|
});
|
|
}
|
|
|
|
/*============================= Helpers: Options =============================*/
|
|
private function get_statuses() {
|
|
$val = get_option($this->option_key_statuses, []);
|
|
return is_array($val) ? $val : [];
|
|
}
|
|
private function save_statuses($arr) { update_option($this->option_key_statuses, $arr); }
|
|
|
|
private function get_settings() {
|
|
$defaults = [
|
|
'enabled' => 1,
|
|
'api_url' => 'https://api.sms.net.bd/sendsms',
|
|
'api_key' => '',
|
|
'sender_id' => '',
|
|
'content_id' => '',
|
|
'schedule' => '', // Y-m-d H:i:s (optional)
|
|
];
|
|
$val = get_option($this->option_key_settings, []);
|
|
if (!is_array($val)) $val = [];
|
|
return wp_parse_args($val, $defaults);
|
|
}
|
|
private function save_settings($arr) { update_option($this->option_key_settings, $arr); }
|
|
|
|
/*============================= Migration =============================*/
|
|
public function maybe_migrate_statuses() {
|
|
$raw = get_option($this->option_key_statuses, []);
|
|
if (!is_array($raw)) return;
|
|
$changed = false;
|
|
foreach ($raw as $slug => $val) {
|
|
if (is_string($val)) {
|
|
$raw[$slug] = ['label' => $val, 'sms' => ''];
|
|
$changed = true;
|
|
} elseif (is_array($val) && !isset($val['sms'])) {
|
|
$raw[$slug]['sms'] = '';
|
|
$changed = true;
|
|
}
|
|
}
|
|
if ($changed) $this->save_statuses($raw);
|
|
}
|
|
|
|
/*============================= WC Status integrate =============================*/
|
|
public function register_custom_statuses() {
|
|
foreach ($this->get_statuses() as $slug => $data) {
|
|
$label = is_array($data) ? ($data['label'] ?? $slug) : (string)$data;
|
|
register_post_status($slug, [
|
|
'label' => $label,
|
|
'public' => true,
|
|
'exclude_from_search' => false,
|
|
'show_in_admin_all_list' => true,
|
|
'show_in_admin_status_list' => true,
|
|
'label_count' => _n_noop("$label (%s)", "$label (%s)")
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function add_to_wc_statuses($statuses) {
|
|
foreach ($this->get_statuses() as $slug => $data) {
|
|
$label = is_array($data) ? ($data['label'] ?? $slug) : (string)$data;
|
|
$statuses[$slug] = $label;
|
|
}
|
|
return $statuses;
|
|
}
|
|
|
|
/*============================= Admin Page =============================*/
|
|
public function add_admin_menu() {
|
|
add_submenu_page(
|
|
'woocommerce',
|
|
'Custom Order Status',
|
|
'Custom Order Status',
|
|
'manage_woocommerce',
|
|
'absoftlab-order-status',
|
|
[$this, 'settings_page']
|
|
);
|
|
}
|
|
|
|
public function settings_page() {
|
|
if (!current_user_can('manage_woocommerce')) return;
|
|
$statuses = $this->get_statuses();
|
|
$settings = $this->get_settings();
|
|
?>
|
|
<div class="wrap">
|
|
<h1>Custom Order Status Manager</h1>
|
|
|
|
<?php if (isset($_GET['added'])): ?>
|
|
<div class="notice notice-success"><p>Status added successfully!</p></div>
|
|
<?php elseif (isset($_GET['deleted'])): ?>
|
|
<div class="notice notice-success"><p>Status deleted successfully!</p></div>
|
|
<?php elseif (isset($_GET['settings_saved'])): ?>
|
|
<div class="notice notice-success"><p>Alpha SMS settings saved!</p></div>
|
|
<?php endif; ?>
|
|
|
|
<h2 style="margin-top:24px;">Add New Status</h2>
|
|
<form method="post">
|
|
<?php wp_nonce_field('absoftlab_add_status'); ?>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th><label for="status_label">Status Label</label></th>
|
|
<td><input type="text" name="status_label" id="status_label" class="regular-text" placeholder="e.g., Order Confirm" required /></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="status_sms">SMS Message</label></th>
|
|
<td>
|
|
<textarea name="status_sms" id="status_sms" class="large-text code" rows="4" placeholder="Hello [billing_first_name], your order #[order_id] is now [order_status]. Amount: [order_amount] [order_currency]"></textarea>
|
|
<p class="description">
|
|
Placeholders (both styles):<br>
|
|
<code>{order_id}</code>, <code>{order_key}</code>, <code>{status}</code>, <code>{first_name}</code>, <code>{last_name}</code>, <code>{total}</code>, <code>{currency}</code>, <code>{site}</code><br>
|
|
Shortcodes: <code>[store_name]</code>, <code>[billing_first_name]</code>, <code>[order_id]</code>, <code>[order_status]</code>, <code>[order_date_created]</code>, <code>[order_date_completed]</code>, <code>[order_currency]</code>, <code>[order_amount]</code>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p><input type="submit" name="absoftlab_add_status" class="button button-primary" value="Add Status" /></p>
|
|
</form>
|
|
|
|
<h2 style="margin-top:32px;">Existing Statuses</h2>
|
|
<table class="widefat striped">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:20%;">Slug</th>
|
|
<th style="width:20%;">Label</th>
|
|
<th>SMS Message</th>
|
|
<th style="width:10%;">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($statuses)): ?>
|
|
<tr><td colspan="4">No custom statuses yet.</td></tr>
|
|
<?php else: foreach ($statuses as $slug => $data):
|
|
$label = is_array($data) ? ($data['label'] ?? $slug) : (string)$data;
|
|
$sms = is_array($data) ? ($data['sms'] ?? '') : '';
|
|
?>
|
|
<tr>
|
|
<td><?php echo esc_html($slug); ?></td>
|
|
<td><?php echo esc_html($label); ?></td>
|
|
<td><code style="display:inline-block;max-width:100%;white-space:pre-wrap;"><?php echo esc_html($sms); ?></code></td>
|
|
<td>
|
|
<a href="<?php echo wp_nonce_url(admin_url('admin.php?page=absoftlab-order-status&absoftlab_delete=' . rawurlencode($slug)), 'absoftlab_delete_status'); ?>"
|
|
class="button button-small delete" onclick="return confirm('Delete this status?');">Delete</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; endif; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<hr style="margin:36px 0;">
|
|
|
|
<h2>Alpha SMS Settings</h2>
|
|
<form method="post">
|
|
<?php wp_nonce_field('absoftlab_save_sms_settings'); ?>
|
|
<table class="form-table">
|
|
<tr>
|
|
<th>Enable SMS</th>
|
|
<td>
|
|
<label><input type="checkbox" name="enabled" value="1" <?php checked(!empty($settings['enabled'])); ?>> Send SMS on status change</label>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="api_url">API URL</label></th>
|
|
<td>
|
|
<input type="url" name="api_url" id="api_url" class="regular-text" value="<?php echo esc_attr($settings['api_url']); ?>" placeholder="https://api.sms.net.bd/sendsms" />
|
|
<p class="description">We POST: <code>api_key</code>, <code>msg</code>, <code>to</code>, <code>sender_id</code> (optional), <code>schedule</code> (optional), <code>content_id</code> (optional).</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="api_key">API Key</label></th>
|
|
<td><input type="text" name="api_key" id="api_key" class="regular-text" value="<?php echo esc_attr($settings['api_key']); ?>" /></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="sender_id">Sender ID</label></th>
|
|
<td><input type="text" name="sender_id" id="sender_id" class="regular-text" value="<?php echo esc_attr($settings['sender_id']); ?>" /></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="content_id">Content ID (optional)</label></th>
|
|
<td><input type="text" name="content_id" id="content_id" class="regular-text" value="<?php echo esc_attr($settings['content_id']); ?>" /></td>
|
|
</tr>
|
|
<tr>
|
|
<th><label for="schedule">Schedule (optional)</label></th>
|
|
<td>
|
|
<input type="text" name="schedule" id="schedule" class="regular-text" placeholder="YYYY-mm-dd HH:ii:ss" value="<?php echo esc_attr($settings['schedule']); ?>" />
|
|
<p class="description">Format: <code>Y-m-d H:i:s</code> (e.g., 2025-10-17 11:21:23). Leave empty for immediate send.</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<p><input type="submit" name="absoftlab_save_sms_settings" class="button button-primary" value="Save Settings" /></p>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/*============================= Handle Admin Forms =============================*/
|
|
public function handle_form_submit() {
|
|
if (!current_user_can('manage_woocommerce')) return;
|
|
|
|
if (isset($_POST['absoftlab_add_status'])) {
|
|
check_admin_referer('absoftlab_add_status');
|
|
$label = sanitize_text_field($_POST['status_label'] ?? '');
|
|
$sms = wp_kses_post($_POST['status_sms'] ?? '');
|
|
if ($label !== '') {
|
|
$slug = 'wc-' . sanitize_title($label);
|
|
$statuses = $this->get_statuses();
|
|
$statuses[$slug] = ['label' => $label, 'sms' => $sms];
|
|
$this->save_statuses($statuses);
|
|
wp_redirect(admin_url('admin.php?page=absoftlab-order-status&added=1'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (isset($_GET['absoftlab_delete']) && isset($_GET['_wpnonce'])) {
|
|
if (wp_verify_nonce($_GET['_wpnonce'], 'absoftlab_delete_status')) {
|
|
$slug = sanitize_text_field($_GET['absoftlab_delete']);
|
|
$statuses = $this->get_statuses();
|
|
if (isset($statuses[$slug])) {
|
|
unset($statuses[$slug]);
|
|
$this->save_statuses($statuses);
|
|
}
|
|
wp_redirect(admin_url('admin.php?page=absoftlab-order-status&deleted=1'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (isset($_POST['absoftlab_save_sms_settings'])) {
|
|
check_admin_referer('absoftlab_save_sms_settings');
|
|
$settings = [
|
|
'enabled' => !empty($_POST['enabled']) ? 1 : 0,
|
|
'api_url' => esc_url_raw($_POST['api_url'] ?? ''),
|
|
'api_key' => sanitize_text_field($_POST['api_key'] ?? ''),
|
|
'sender_id' => sanitize_text_field($_POST['sender_id'] ?? ''),
|
|
'content_id' => sanitize_text_field($_POST['content_id'] ?? ''),
|
|
'schedule' => sanitize_text_field($_POST['schedule'] ?? ''),
|
|
];
|
|
$this->save_settings($settings);
|
|
wp_redirect(admin_url('admin.php?page=absoftlab-order-status&settings_saved=1'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/*============================= SMS on Status Change =============================*/
|
|
public function maybe_send_sms($order_id, $old_status, $new_status, $order) {
|
|
try {
|
|
if (!is_a($order, 'WC_Order')) {
|
|
$order = wc_get_order($order_id);
|
|
if (!$order) return;
|
|
}
|
|
$settings = $this->get_settings();
|
|
if (empty($settings['enabled'])) return;
|
|
|
|
$statuses = $this->get_statuses();
|
|
$slug = 'wc-' . sanitize_title($new_status);
|
|
if (!isset($statuses[$slug])) return;
|
|
|
|
$data = $statuses[$slug];
|
|
$sms_template = is_array($data) ? ($data['sms'] ?? '') : '';
|
|
if ($sms_template === '') return;
|
|
|
|
$phone = $order->get_billing_phone();
|
|
if (!$phone) return;
|
|
|
|
$label = is_array($data) ? ($data['label'] ?? $slug) : $slug;
|
|
$message = $this->fill_placeholders($sms_template, $order, $slug, $label);
|
|
|
|
$this->send_via_alpha_sms($settings, $phone, $message, $order_id);
|
|
} catch (\Throwable $e) {
|
|
$this->log('SMS error: ' . $e->getMessage(), 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fill both curly placeholders and shortcode style.
|
|
*/
|
|
private function fill_placeholders($tpl, WC_Order $order, $slug, $label) {
|
|
$created = $order->get_date_created();
|
|
$completed = $order->get_date_completed();
|
|
$decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
|
|
$amount_numeric = function_exists('wc_format_decimal') ? wc_format_decimal($order->get_total(), $decimals) : number_format((float)$order->get_total(), $decimals, '.', '');
|
|
|
|
$pairs = [
|
|
'{order_id}' => (string)$order->get_id(),
|
|
'{order_key}' => (string)$order->get_order_key(),
|
|
'{status}' => (string)$label,
|
|
'{first_name}' => (string)$order->get_billing_first_name(),
|
|
'{last_name}' => (string)$order->get_billing_last_name(),
|
|
'{total}' => (string)$amount_numeric,
|
|
'{currency}' => (string)$order->get_currency(),
|
|
'{site}' => (string)wp_parse_url(home_url(), PHP_URL_HOST),
|
|
|
|
'[store_name]' => get_bloginfo('name'),
|
|
'[billing_first_name]' => (string)$order->get_billing_first_name(),
|
|
'[order_id]' => (string)$order->get_id(),
|
|
'[order_status]' => (string)$label,
|
|
'[order_date_created]' => $created ? $created->date_i18n('Y-m-d H:i') : '',
|
|
'[order_date_completed]' => $completed ? $completed->date_i18n('Y-m-d H:i') : '',
|
|
'[order_currency]' => (string)$order->get_currency(),
|
|
'[order_amount]' => (string)$amount_numeric,
|
|
];
|
|
|
|
$pairs = apply_filters('absoftlab_sms_placeholders', $pairs, $order, $slug, $label);
|
|
return strtr($tpl, $pairs);
|
|
}
|
|
|
|
/*============================= Alpha SMS sender =============================*/
|
|
private function send_via_alpha_sms($settings, $to, $message, $order_id = 0) {
|
|
$api_url = trim($settings['api_url'] ?? '');
|
|
$api_key = trim($settings['api_key'] ?? '');
|
|
$sender_id = trim($settings['sender_id'] ?? '');
|
|
$content_id = trim($settings['content_id'] ?? '');
|
|
$schedule = trim($settings['schedule'] ?? '');
|
|
|
|
if ($api_url === '' || $api_key === '') {
|
|
$this->log('SMS not sent: API URL / KEY missing.', 'warning');
|
|
return;
|
|
}
|
|
|
|
$body = [
|
|
'api_key' => $api_key,
|
|
'msg' => $message,
|
|
'to' => $to,
|
|
];
|
|
if ($sender_id !== '') $body['sender_id'] = $sender_id;
|
|
if ($content_id !== '') $body['content_id'] = $content_id;
|
|
if ($schedule !== '') $body['schedule'] = $schedule;
|
|
|
|
$args = [
|
|
'timeout' => 25,
|
|
'headers' => ['Accept' => 'application/json'],
|
|
'body' => $body,
|
|
];
|
|
|
|
$args = apply_filters('absoftlab_sms_request_args', $args, $settings, $to, $message, $order_id);
|
|
$res = wp_remote_post($api_url, $args);
|
|
|
|
if (is_wp_error($res)) { $this->log('SMS WP_Error: ' . $res->get_error_message(), 'error'); return; }
|
|
|
|
$code = wp_remote_retrieve_response_code($res);
|
|
$body_text = wp_remote_retrieve_body($res);
|
|
|
|
if ($code >= 200 && $code < 300) {
|
|
$ok = false; $rid = '';
|
|
$j = json_decode($body_text, true);
|
|
if (is_array($j) && isset($j['error']) && intval($j['error']) === 0) {
|
|
$ok = true;
|
|
if (isset($j['data']['request_id'])) $rid = (string)$j['data']['request_id'];
|
|
}
|
|
if ($ok) $this->log("SMS sent to {$to} for order #{$order_id}. Request ID: {$rid}", 'info');
|
|
else $this->log("SMS response (HTTP {$code}) but non-zero error. Body: {$body_text}", 'warning');
|
|
} else {
|
|
$this->log("SMS failed ({$code}) to {$to}. Body: {$body_text}", 'error');
|
|
}
|
|
}
|
|
|
|
private function log($msg, $level = 'info') {
|
|
if (!function_exists('wc_get_logger')) return;
|
|
$logger = wc_get_logger();
|
|
$context = ['source' => 'absoftlab-sms'];
|
|
switch ($level) {
|
|
case 'error': $logger->error($msg, $context); break;
|
|
case 'warning': $logger->warning($msg, $context); break;
|
|
default: $logger->info($msg, $context); break;
|
|
}
|
|
}
|
|
|
|
/*============================= Bulk actions (both screens) =============================*/
|
|
public function register_bulk_status_actions($actions) {
|
|
// Add "Change status to X" items for each custom status.
|
|
foreach ($this->get_statuses() as $slug => $data) {
|
|
$label = is_array($data) ? ($data['label'] ?? $slug) : (string)$data;
|
|
$status_key = preg_replace('/^wc-/', '', $slug); // e.g. wc-confirm -> confirm
|
|
$actions['mark_' . $status_key] = sprintf(__('Change status to %s', 'woocommerce'), $label);
|
|
}
|
|
return $actions;
|
|
}
|
|
|
|
public function bulk_changed_notice() {
|
|
if (!isset($_REQUEST['post_type']) || $_REQUEST['post_type'] !== 'shop_order') return;
|
|
if (empty($_REQUEST['changed'])) return;
|
|
$changed = absint($_REQUEST['changed']);
|
|
if ($changed > 0) {
|
|
echo '<div class="notice notice-success is-dismissible"><p>'
|
|
. esc_html(sprintf(_n('%s order updated.', '%s orders updated.', $changed, 'woocommerce'), number_format_i18n($changed)))
|
|
. '</p></div>';
|
|
}
|
|
}
|
|
|
|
/*============================= Row action (legacy table) =============================*/
|
|
public function add_row_action_change_status($actions, $order) {
|
|
if (!current_user_can('edit_shop_order', $order->get_id())) return $actions;
|
|
|
|
foreach ($this->get_statuses() as $slug => $data) {
|
|
$label = is_array($data) ? ($data['label'] ?? $slug) : (string)$data;
|
|
$status_key = preg_replace('/^wc-/', '', $slug);
|
|
|
|
$url = wp_nonce_url(
|
|
admin_url('admin-post.php?action=absoftlab_mark_status&order_id=' . $order->get_id() . '&status=' . $status_key),
|
|
'absoftlab_mark_status_' . $order->get_id()
|
|
);
|
|
|
|
$actions['absoftlab_mark_' . $status_key] = [
|
|
'url' => $url,
|
|
'name' => sprintf(__('Change status to %s', 'woocommerce'), $label),
|
|
'action' => 'status-' . sanitize_html_class($status_key),
|
|
];
|
|
}
|
|
return $actions;
|
|
}
|
|
|
|
public function handle_single_mark_status() {
|
|
if (!current_user_can('edit_shop_order')) wp_die('Not allowed');
|
|
$order_id = isset($_GET['order_id']) ? absint($_GET['order_id']) : 0;
|
|
$status = isset($_GET['status']) ? sanitize_title($_GET['status']) : '';
|
|
check_admin_referer('absoftlab_mark_status_' . $order_id);
|
|
|
|
if ($order_id && $status) {
|
|
$order = wc_get_order($order_id);
|
|
if ($order) {
|
|
$order->set_status($status, 'Status changed via row action.');
|
|
$order->save();
|
|
}
|
|
}
|
|
$ref = wp_get_referer();
|
|
wp_safe_redirect($ref ? $ref : admin_url('edit.php?post_type=shop_order'));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
new Absoftlab_Custom_Order_Status_Manager();
|