woo-custom-order-status/custom-order-status-for-woocommerce.php
2025-10-17 13:50:29 +06:00

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();