function decreasec_stock($product, $quantity) { $current_stock = get_field("stock_$product", 'option'); // Ambil stok dari Option Page if ($current_stock === false || $current_stock === null) { error_log("Stok untuk $product tidak ditemukan atau null."); return; } // Pastikan stok adalah integer $current_stock = (int) $current_stock; // Log stok yang ditemukan error_log("Current stock for $product: $current_stock"); if ($current_stock > 0 && $current_stock >= $quantity) { $new_stock = $current_stock - $quantity; // Kurangi stok update_field("stock_$product", $new_stock, 'option'); // Perbarui stok di Option Page error_log("Stok untuk $product dikurangi menjadi $new_stock"); } else { error_log("Tidak cukup stok untuk $product. Stok saat ini: $current_stock"); } } function get_monthly_sales_report($month, $year) { // Query untuk mengambil data dari CPT rental-order (penjualan) $sales_args = array( 'post_type' => 'rental-order', 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => 'payment_status', 'value' => 'Sudah Bayar', 'compare' => '=' ), ), 'date_query' => array( array( 'year' => $year, 'month' => $month, ), ), ); $sales_query = new WP_Query($sales_args); $total_sales = 0; $transaction_count = 0; $total_shipping_cost = 0; if ($sales_query->have_posts()) { while ($sales_query->have_posts()) { $sales_query->the_post(); $total_cost = get_post_meta(get_the_ID(), 'total_cost', true); $shipping_cost = get_post_meta(get_the_ID(), 'shipping_cost', true); $total_sales += (float) $total_cost; $total_shipping_cost += (float) $shipping_cost; $transaction_count++; } } wp_reset_postdata(); // Query untuk mengambil data dari CPT rental-expense (pengeluaran) $expense_args = array( 'post_type' => 'rental-expense', 'post_status' => 'publish', 'posts_per_page' => -1, 'date_query' => array( array( 'year' => $year, 'month' => $month, ), ), ); $expense_query = new WP_Query($expense_args); $total_expense = 0; if ($expense_query->have_posts()) { while ($expense_query->have_posts()) { $expense_query->the_post(); $cost_expense = get_post_meta(get_the_ID(), 'cost-expense', true); $total_expense += (float) $cost_expense; } } wp_reset_postdata(); // Menghitung net profit $net_profit = $total_sales - $total_expense - $total_shipping_cost; return array( 'total_sales' => $total_sales, 'transaction_count' => $transaction_count, 'total_shipping_cost' => $total_shipping_cost, 'total_expense' => $total_expense, 'net_profit' => $net_profit ); } // Shortcode untuk menampilkan total sales, transaction count, shipping cost, expenses, dan net profit add_shortcode('monthly_sales_report', function($atts) { // Atribut default (default "total_sales") $atts = shortcode_atts( array( 'type' => 'total_sales', // Menentukan apa yang ditampilkan: "total_sales", "transaction_count", "shipping_cost", "total_expense", atau "net_profit" ), $atts, 'monthly_sales_report' ); $current_month = date('m'); // Bulan saat ini $current_year = date('Y'); // Tahun saat ini $report = get_monthly_sales_report($current_month, $current_year); // Memilih data sesuai dengan atribut "type" if ($atts['type'] == 'total_sales') { return 'Rp' . number_format($report['total_sales'], 0, ',', '.'); } elseif ($atts['type'] == 'transaction_count') { return $report['transaction_count']; } elseif ($atts['type'] == 'shipping_cost') { return 'Rp' . number_format($report['total_shipping_cost'], 0, ',', '.'); } elseif ($atts['type'] == 'total_expense') { return 'Rp' . number_format($report['total_expense'], 0, ',', '.'); } elseif ($atts['type'] == 'net_profit') { // Jika net profit negatif, tampilkan - Rp100.000 (tanpa spasi, tanpa tanda minus ganda) if ($report['net_profit'] < 0) { return '- Rp' . number_format(abs($report['net_profit']), 0, ',', '.'); } return 'Rp' . number_format($report['net_profit'], 0, ',', '.'); } return ''; // Mengembalikan kosong jika tidak ada tipe yang valid }); add_filter( 'bricks/dynamic_tags_list', 'add_bulan_indo_tag_to_builder' ); function add_bulan_indo_tag_to_builder( $tags ) { // Menambahkan tag dinamis baru untuk bulan dalam bahasa Indonesia $tags[] = [ 'name' => '{bulan_indo}', 'label' => 'Bulan Indonesia', 'group' => 'Custom Dynamic Data', ]; return $tags; } add_filter( 'bricks/dynamic_data/render_tag', 'get_bulan_indo_value', 20, 3 ); function get_bulan_indo_value( $tag, $post, $context = 'text' ) { // Memeriksa apakah tag adalah {bulan_indo} if ( $tag !== '{bulan_indo}' ) { return $tag; } // Mendapatkan bulan dalam format Indonesia $value = run_bulan_indo_logic(); return $value; } function run_bulan_indo_logic() { // Array bulan dalam bahasa Indonesia $bulan_indo = [ 1 => 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' ]; // Mendapatkan bulan saat ini $bulan = date('n'); // Mengambil bulan dalam angka (1-12) // Mengembalikan nama bulan dalam bahasa Indonesia return $bulan_indo[$bulan]; } add_filter( 'bricks/dynamic_data/render_content', 'render_bulan_indo_tag', 20, 3 ); add_filter( 'bricks/frontend/render_data', 'render_bulan_indo_tag', 20, 2 ); function render_bulan_indo_tag( $content, $post, $context = 'text' ) { // Memeriksa apakah tag {bulan_indo} ada dalam konten if ( strpos( $content, '{bulan_indo}' ) === false ) { return $content; } // Menjalankan logika untuk mendapatkan bulan dalam bahasa Indonesia $bulan_indo = run_bulan_indo_logic(); // Mengganti tag {bulan_indo} dengan bulan dalam bahasa Indonesia $content = str_replace( '{bulan_indo}', $bulan_indo, $content ); return $content; } /** * Rental System API for WordPress * Handles stock updates, order management, and WhatsApp notifications. * * @package RentalSystem * @version 1.0.11 */ defined('ABSPATH') || exit; /** * Rental System class encapsulating all functionality */ class RentalSystem { private const UNIT_MAPPING = [ 'PS3TV' => ['ps3' => 1, 'tv' => 1], 'PS4TV' => ['ps4' => 1, 'tv' => 1], 'PS3' => ['ps3' => 1], 'PS4' => ['ps4' => 1], 'TV' => ['tv' => 1], ]; private const DURATION_MAPPING = [ '6H' => 6, '6 JAM' => 6, '12H' => 12, '12 JAM' => 12, '24H' => 24, '1 HARI' => 24, '48H' => 48, '2 HARI' => 48, '72H' => 72, '3 HARI' => 72, '96H' => 96, '4 HARI' => 96, '120H' => 120, '5 HARI' => 120, ]; /** * Initialize the plugin */ public static function init() { add_action('rest_api_init', [__CLASS__, 'register_endpoints']); add_action('decrease_stock_scheduled', [__CLASS__, 'execute_stock_decrease'], 10, 2); add_action('set_status_pending_scheduled', [__CLASS__, 'execute_set_status_pending'], 10, 1); add_action('send_whatsapp_message_template_2', [__CLASS__, 'send_whatsapp_template_2'], 10, 3); add_action('send_whatsapp_message_template_3', [__CLASS__, 'send_whatsapp_template_3'], 10, 3); add_action('wp_cron', [__CLASS__, 'log_cron_execution']); } /** * Register REST API endpoints */ public static function register_endpoints() { register_rest_route('websee/v1', '/sync-stock', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [__CLASS__, 'handle_sync_stock_request'], 'permission_callback' => '__return_true', ]); } /** * Handle webhook request for stock sync * * @param WP_REST_Request $request * @return WP_REST_Response */ public static function handle_sync_stock_request(WP_REST_Request $request) { $data = $request->get_json_params(); self::log_message('INFO', 'Webhook received', ['data' => $data]); // Validate required fields $validation = self::validate_request_data($data); if (is_array($validation)) { self::log_message('ERROR', 'Missing required fields', ['fields' => $validation]); return new WP_REST_Response(['message' => 'Missing fields: ' . implode(', ', $validation)], 400); } $post_id = intval($data['post-id']); $order_status = sanitize_text_field($data['order-status-api']); $payment_status = sanitize_text_field($data['payment-status-api']); $unit_name = trim(sanitize_text_field($data['unit-name-api'])); $pickup_date = isset($data['pickup-date']) && is_string($data['pickup-date']) ? sanitize_text_field(trim(str_replace(' ,', ',', $data['pickup-date']))) : ''; $return_date = isset($data['return-date']) && is_string($data['return-date']) ? sanitize_text_field(trim(str_replace(' ,', ',', $data['return-date']))) : ''; $phone_number = self::normalize_phone_number( isset($data['phone_number-api']) && !empty(trim($data['phone_number-api'])) ? (string) $data['phone_number-api'] : get_field('phone_number', $post_id) ); self::log_message('INFO', 'Processing order', [ 'post_id' => $post_id, 'unit' => $unit_name, 'order_status' => $order_status, 'payment_status' => $payment_status, 'pickup_date' => $pickup_date, 'return_date' => $return_date, 'phone_number' => $phone_number, ]); // Validate post and unit if (!get_post($post_id)) { self::log_message('ERROR', 'Invalid post ID', ['post_id' => $post_id]); return new WP_REST_Response(['message' => 'Invalid post ID'], 404); } if (!isset(self::UNIT_MAPPING[$unit_name])) { self::log_message('ERROR', 'Invalid unit name', ['unit_name' => $unit_name]); return new WP_REST_Response(['message' => 'Invalid unit name'], 400); } // Update basic fields $fields = [ 'status-order' => $order_status, 'payment_status' => $payment_status, 'unit' => $unit_name, 'phone_number' => $phone_number, ]; foreach ($fields as $key => $value) { update_field($key, $value, $post_id); } // Check if payment status is valid for scheduling //$is_paid = in_array($payment_status, ['Sudah_DP', 'Sudah Bayar', ]); $is_paid = in_array($payment_status, ['Sudah_DP', 'Sudah Bayar', 'Belum Bayar']); // Handle status-specific logic switch ($order_status) { case 'Running': self::handle_running_status($post_id, $unit_name, $phone_number, $payment_status, $is_paid); break; case 'Selesai': self::handle_completed_status($post_id, $unit_name, $phone_number, $is_paid); break; case 'Dibatalkan': self::handle_cancelled_status($post_id, $unit_name); break; case 'Dipesan': case 'Pending': self::handle_pending_status($post_id, $unit_name, $phone_number, $payment_status, $pickup_date, $return_date, $is_paid, $order_status); break; default: self::log_message('ERROR', 'Unknown order status', ['status' => $order_status]); return new WP_REST_Response(['message' => 'Unknown order status'], 400); } self::log_message('INFO', 'Stock scheduling completed', ['post_id' => $post_id]); return new WP_REST_Response(['message' => 'Stock update scheduled successfully'], 200); } /** * Validate request data * * @param array $data * @return array|true */ private static function validate_request_data($data) { $required = ['post-id', 'order-status-api', 'payment-status-api', 'unit-name-api']; $errors = []; foreach ($required as $field) { if (empty(trim($data[$field] ?? ''))) { $errors[] = $field; } } return $errors ?: true; } /** * Normalize phone number to E.164 format * * @param string $phone * @return string */ private static function normalize_phone_number($phone) { $phone = trim(sanitize_text_field($phone)); if (empty($phone)) { return ''; } if (substr($phone, 0, 2) === '08') { $phone = '628' . substr($phone, 2); } elseif (substr($phone, 0, 1) === '8') { $phone = '628' . substr($phone, 1); } elseif (substr($phone, 0, 3) !== '628') { $phone = '628' . $phone; } if (!preg_match('/^628[0-9]{8,12}$/', $phone)) { self::log_message('WARNING', 'Invalid phone number format', ['phone' => $phone]); return ''; } return $phone; } /** * Parse date in WIB timezone * * @param string $date_str * @return DateTime|false */ private static function parse_wib_date($date_str) { if (empty($date_str) || !is_string($date_str)) { self::log_message('ERROR', 'Invalid date string', ['date' => $date_str]); return false; } $clean_date = trim(str_replace(' ,', ',', $date_str)); $date = DateTime::createFromFormat('d-m-Y, H:i', $clean_date, new DateTimeZone('Asia/Jakarta')); if (!$date) { self::log_message('ERROR', 'Failed to parse date', ['date' => $clean_date, 'format' => 'd-m-Y, H:i']); return false; } return $date; } /** * Handle Running status * * @param int $post_id * @param string $unit_name * @param string $phone_number * @param string $payment_status * @param bool $is_paid */ private static function handle_running_status($post_id, $unit_name, $phone_number, $payment_status, $is_paid) { // Set pickup_date to now in WIB $wib_timezone = new DateTimeZone('Asia/Jakarta'); $current_datetime = new DateTime('now', $wib_timezone); $pickup_date = $current_datetime->format('d-m-Y, H:i'); // Calculate return_date based on duration $duration_value = get_field('duration', $post_id); if (is_array($duration_value)) { $duration_value = reset($duration_value); } $duration_value = trim(sanitize_text_field($duration_value)); if (empty($duration_value)) { self::log_message('WARNING', 'No duration found, defaulting to 6 hours', ['post_id' => $post_id]); } $hours_to_add = self::DURATION_MAPPING[strtoupper($duration_value)] ?? 6; $return_datetime = clone $current_datetime; $return_datetime->modify("+{$hours_to_add} hours"); $return_date = $return_datetime->format('d-m-Y, H:i'); self::log_message('INFO', 'Running status dates updated', [ 'post_id' => $post_id, 'pickup_date' => $pickup_date, 'return_date' => $return_date, ]); // Update dates update_post_meta($post_id, 'pickup_date', $pickup_date); update_post_meta($post_id, 'return_date', $return_date); // Cancel all existing schedulers self::cancel_all_order_schedulers($post_id); // Proceed only if payment is valid if ($is_paid) { // Schedule stock decrease if (!get_post_meta($post_id, 'has-stock-decreased', true)) { self::schedule_stock_decrease($post_id, $unit_name, $pickup_date); } // Send template_1 and schedule WhatsApp messages if ($phone_number) { self::send_whatsapp_message('template_1', $post_id, 'Running', $payment_status, $phone_number); self::schedule_whatsapp_message($post_id, $unit_name, $phone_number, $return_date, $payment_status); } else { self::log_message('WARNING', 'No phone number for WhatsApp', ['post_id' => $post_id]); } // Schedule Pending status self::schedule_pending_status($post_id, $return_date); } else { self::log_message('INFO', 'Skipped scheduling due to unpaid status', [ 'post_id' => $post_id, 'payment_status' => $payment_status, ]); } } /** * Handle Completed status * * @param int $post_id * @param string $unit_name * @param string $phone_number * @param bool $is_paid */ private static function handle_completed_status($post_id, $unit_name, $phone_number, $is_paid) { self::cancel_all_order_schedulers($post_id); if ($is_paid && get_post_meta($post_id, 'has-stock-decreased', true)) { self::restore_stock($unit_name); delete_post_meta($post_id, 'has-stock-decreased'); self::log_message('INFO', 'Stock restored', ['post_id' => $post_id, 'unit' => $unit_name]); } if ($is_paid && $phone_number) { self::send_whatsapp_message('template_4', $post_id, 'Selesai', 'Sudah Bayar', $phone_number); } self::log_message('INFO', 'Processed Selesai status', ['post_id' => $post_id]); } /** * Handle Cancelled status * * @param int $post_id * @param string $unit_name */ private static function handle_cancelled_status($post_id, $unit_name) { self::cancel_all_order_schedulers($post_id); if (get_post_meta($post_id, 'has-stock-decreased', true)) { self::restore_stock($unit_name); delete_post_meta($post_id, 'has-stock-decreased'); self::log_message('INFO', 'Stock restored', ['post_id' => $post_id, 'unit' => $unit_name]); } $updated_post = wp_update_post([ 'ID' => $post_id, 'post_status' => 'trash', ]); if (!is_wp_error($updated_post)) { clean_post_cache($post_id); wp_cache_delete($post_id, 'posts'); self::log_message('INFO', 'Post moved to trash', ['post_id' => $post_id]); } else { self::log_message('ERROR', 'Failed to trash post', ['post_id' => $post_id]); } } /** * Handle Pending/Dipesan status * * @param int $post_id * @param string $unit_name * @param string $phone_number * @param string $payment_status * @param string $pickup_date * @param string $return_date * @param bool $is_paid * @param string $order_status */ private static function handle_pending_status($post_id, $unit_name, $phone_number, $payment_status, $pickup_date, $return_date, $is_paid, $order_status) { // Use existing pickup_date and return_date from post meta if input is empty $existing_pickup_date = $pickup_date ?: get_post_meta($post_id, 'pickup_date', true) ?: ''; $existing_return_date = $return_date ?: get_post_meta($post_id, 'return_date', true) ?: ''; // Update only if new values are provided if ($pickup_date) { update_post_meta($post_id, 'pickup_date', $pickup_date); $existing_pickup_date = $pickup_date; } if ($return_date) { update_post_meta($post_id, 'return_date', $return_date); $existing_return_date = $return_date; } // Cancel existing schedulers to ensure clean state self::cancel_all_order_schedulers($post_id); if ($is_paid) { // If no pickup_date exists, set to current time if (empty($existing_pickup_date)) { $wib_timezone = new DateTimeZone('Asia/Jakarta'); $current_datetime = new DateTime('now', $wib_timezone); $existing_pickup_date = $current_datetime->format('d-m-Y, H:i'); update_post_meta($post_id, 'pickup_date', $existing_pickup_date); self::log_message('INFO', 'Set default pickup_date for Dipesan', [ 'post_id' => $post_id, 'pickup_date' => $existing_pickup_date, ]); } // If no return_date exists, calculate based on duration if (empty($existing_return_date)) { $duration_value = get_field('duration', $post_id); if (is_array($duration_value)) { $duration_value = reset($duration_value); } $duration_value = trim(sanitize_text_field($duration_value)); $hours_to_add = self::DURATION_MAPPING[strtoupper($duration_value)] ?? 6; $pickup_date_obj = self::parse_wib_date($existing_pickup_date); if ($pickup_date_obj) { $return_datetime = clone $pickup_date_obj; $return_datetime->modify("+{$hours_to_add} hours"); $existing_return_date = $return_datetime->format('d-m-Y, H:i'); update_post_meta($post_id, 'return_date', $existing_return_date); self::log_message('INFO', 'Set default return_date for Dipesan', [ 'post_id' => $post_id, 'return_date' => $existing_return_date, ]); } else { self::log_message('WARNING', 'Failed to parse pickup_date for return_date calculation', [ 'post_id' => $post_id, 'pickup_date' => $existing_pickup_date, ]); return; // Skip scheduling if dates are invalid } } // Schedule stock decrease using existing pickup_date if (!get_post_meta($post_id, 'has-stock-decreased', true) && $existing_pickup_date) { self::schedule_stock_decrease($post_id, $unit_name, $existing_pickup_date); } // Send WhatsApp and schedule messages for Dipesan/Pending if ($phone_number && $existing_return_date) { if ($order_status === 'Pending') { self::send_whatsapp_message('template_2', $post_id, 'Pending', $payment_status, $phone_number); } self::schedule_whatsapp_message($post_id, $unit_name, $phone_number, $existing_return_date, $payment_status); } else { self::log_message('WARNING', 'No phone number or return_date for WhatsApp scheduling', [ 'post_id' => $post_id, 'phone_number' => $phone_number, 'return_date' => $existing_return_date, ]); } // Schedule Pending status if ($existing_return_date) { self::schedule_pending_status($post_id, $existing_return_date); } } else { self::log_message('INFO', 'Skipped scheduling due to unpaid status', [ 'post_id' => $post_id, 'payment_status' => $payment_status, ]); } } /** * Schedule stock decrease * * @param int $post_id * @param string $unit_name * @param string $pickup_date */ public static function schedule_stock_decrease($post_id, $unit_name, $pickup_date) { self::log_message('INFO', 'Scheduling stock decrease', [ 'post_id' => $post_id, 'unit' => $unit_name, 'pickup_date' => $pickup_date, ]); $date_obj = self::parse_wib_date($pickup_date); if (!$date_obj) { return; } $date_obj->setTimezone(new DateTimeZone('UTC')); $timestamp = $date_obj->getTimestamp(); $current_time = time(); $tolerance = 120; // 2 minutes if ($timestamp < $current_time - $tolerance) { self::log_message('ERROR', 'Pickup date too far in the past', ['post_id' => $post_id, 'timestamp' => $timestamp]); return; } elseif ($timestamp <= $current_time) { self::log_message('INFO', 'Pickup date is now, executing stock decrease immediately', ['post_id' => $post_id]); self::execute_stock_decrease($post_id, $unit_name); return; } as_unschedule_action('decrease_stock_scheduled', [$post_id, $unit_name], "stock_decrease_{$post_id}"); as_schedule_single_action($timestamp, 'decrease_stock_scheduled', [$post_id, $unit_name], "stock_decrease_{$post_id}"); self::log_message('INFO', 'Scheduled stock decrease', [ 'post_id' => $post_id, 'timestamp' => gmdate('Y-m-d H:i:s', $timestamp), ]); } /** * Execute stock decrease * * @param int $post_id * @param string $unit_name */ public static function execute_stock_decrease($post_id, $unit_name) { $lock_key = "stock_decrease_{$post_id}"; if (get_transient($lock_key)) { self::log_message('WARNING', 'Stock decrease locked', ['post_id' => $post_id]); return; } set_transient($lock_key, true, 10); if (empty($post_id) || empty($unit_name)) { self::log_message('ERROR', 'Invalid parameters for stock decrease', ['post_id' => $post_id, 'unit' => $unit_name]); delete_transient($lock_key); return; } if (get_post_meta($post_id, 'has-stock-decreased', true)) { self::log_message('WARNING', 'Stock already decreased, skipping', ['post_id' => $post_id]); delete_transient($lock_key); return; } self::handle_unit_stock_decrease($unit_name); update_post_meta($post_id, 'has-stock-decreased', true); delete_transient($lock_key); self::log_message('INFO', 'Stock decreased', ['post_id' => $post_id, 'unit' => $unit_name]); } /** * Decrease stock for unit * * @param string $unit_name */ private static function handle_unit_stock_decrease($unit_name) { if (!isset(self::UNIT_MAPPING[$unit_name])) { self::log_message('ERROR', 'Unknown unit', ['unit' => $unit_name]); return; } foreach (self::UNIT_MAPPING[$unit_name] as $product => $qty) { $field = "stock_{$product}"; $current_stock = (int) get_field($field, 'option'); $new_stock = $current_stock - $qty; update_field($field, $new_stock, 'option'); self::log_message('INFO', 'Stock updated', [ 'field' => $field, 'old' => $current_stock, 'new' => $new_stock, ]); if ($new_stock < 0) { self::log_message('WARNING', 'Negative stock', ['field' => $field, 'stock' => $new_stock]); } } } /** * Restore stock for unit * * @param string $unit_name */ private static function restore_stock($unit_name) { if (!isset(self::UNIT_MAPPING[$unit_name])) { self::log_message('ERROR', 'Unknown unit', ['unit' => $unit_name]); return; } foreach (self::UNIT_MAPPING[$unit_name] as $product => $qty) { $field = "stock_{$product}"; $current_stock = (int) get_field($field, 'option'); $new_stock = $current_stock + $qty; update_field($field, $new_stock, 'option'); self::log_message('INFO', 'Stock restored', [ 'field' => $field, 'old' => $current_stock, 'new' => $new_stock, ]); } } /** * Schedule WhatsApp messages * * @param int $post_id * @param string $unit_name * @param string $phone_number * @param string $return_date * @param string $payment_status */ public static function schedule_whatsapp_message($post_id, $unit_name, $phone_number, $return_date, $payment_status) { if (empty($phone_number)) { self::log_message('WARNING', 'Cannot schedule WhatsApp: empty phone number', ['post_id' => $post_id]); return; } //if (!in_array($payment_status, ['Sudah_DP', 'Sudah Bayar'])) { if (!in_array($payment_status, ['Sudah_DP', 'Sudah Bayar', 'Belum Bayar'])) { self::log_message('INFO', 'Skipped WhatsApp scheduling due to unpaid status', [ 'post_id' => $post_id, 'payment_status' => $payment_status, ]); return; } $order_status = get_field('status-order', $post_id); if ($order_status === 'Dibatalkan') { self::log_message('INFO', 'Skipped WhatsApp scheduling due to cancelled order', [ 'post_id' => $post_id, 'order_status' => $order_status, ]); return; } $return_date_obj = self::parse_wib_date($return_date); if (!$return_date_obj) { return; } $current_time = time(); $return_timestamp = $return_date_obj->setTimezone(new DateTimeZone('UTC'))->getTimestamp(); if ($return_timestamp < $current_time - 300) { // 5-minute tolerance self::log_message('WARNING', 'Return date too far in the past', ['post_id' => $post_id, 'date' => $return_date]); return; } $message_2_time = (clone $return_date_obj)->modify('-1 hour'); $timestamps = [ 'template_2' => $message_2_time->setTimezone(new DateTimeZone('UTC'))->getTimestamp(), 'template_3' => $return_timestamp, ]; foreach ($timestamps as $template => $timestamp) { if ($timestamp < $current_time) { self::log_message('WARNING', "Skipping $template scheduling: timestamp in past", [ 'post_id' => $post_id, 'timestamp' => gmdate('Y-m-d H:i:s', $timestamp), ]); continue; } as_unschedule_action("send_whatsapp_message_{$template}", [$post_id, $unit_name, $phone_number], "whatsapp_{$post_id}_{$template}"); as_schedule_single_action($timestamp, "send_whatsapp_message_{$template}", [$post_id, $unit_name, $phone_number], "whatsapp_{$post_id}_{$template}"); self::log_message('INFO', "Scheduled $template", [ 'post_id' => $post_id, 'timestamp' => gmdate('Y-m-d H:i:s', $timestamp), ]); } } /** * Send WhatsApp message with template * * @param string $template * @param int $post_id * @param string $order_status * @param string $payment_status * @param string $phone_number */ private static function send_whatsapp_message($template, $post_id, $order_status, $payment_status, $phone_number) { if (empty($phone_number)) { self::log_message('WARNING', 'Cannot send WhatsApp: empty phone number', ['post_id' => $post_id]); return; } //if (!in_array($payment_status, ['Sudah_DP', 'Sudah Bayar'])) { if (!in_array($payment_status, ['Sudah_DP', 'Sudah Bayar', 'Belum Bayar'])) { self::log_message('INFO', 'Skipped WhatsApp sending due to unpaid status', [ 'post_id' => $post_id, 'payment_status' => $payment_status, ]); return; } // Get ACF fields, default to '-' if empty $acf_fields = [ 'customer_name' => get_field('customer_name', $post_id) ?: '-', 'locations' => get_field('locations', $post_id) ?: '-', 'unit' => get_field('unit', $post_id) ?: '-', 'duration' => get_field('duration', $post_id) ?: '-', 'pickup_date' => get_field('pickup_date', $post_id) ?: '-', 'return_date' => get_field('return_date', $post_id) ?: '-', ]; // Log if any fields are defaulted to '-' $defaulted_fields = array_keys(array_filter($acf_fields, fn($value) => $value === '-')); if ($defaulted_fields) { self::log_message('INFO', 'Using default value for ACF fields', [ 'post_id' => $post_id, 'fields' => $defaulted_fields, ]); } $max_retries = 3; $attempt = 1; while ($attempt <= $max_retries) { try { $response = send_message_with_template_zapin($template, $post_id, $order_status, $payment_status, $phone_number); self::log_message('INFO', "WhatsApp $template sent", [ 'post_id' => $post_id, 'attempt' => $attempt, 'response' => $response, ]); return; } catch (Exception $e) { self::log_message('ERROR', "WhatsApp $template failed", [ 'post_id' => $post_id, 'attempt' => $attempt, 'error' => $e->getMessage(), ]); } $attempt++; sleep(1); } self::log_message('ERROR', "WhatsApp $template failed after $max_retries attempts", ['post_id' => $post_id]); } /** * Send WhatsApp template 2 * * @param int $post_id * @param string $unit_name * @param string $phone_number */ public static function send_whatsapp_template_2($post_id, $unit_name, $phone_number) { if (empty($post_id) || empty($phone_number)) { self::log_message('ERROR', 'Invalid parameters for template_2', [ 'post_id' => $post_id, 'phone_number' => $phone_number, ]); return; } $order_status = get_field('status-order', $post_id) ?: 'Pending'; $payment_status = get_field('payment_status', $post_id) ?: ''; self::send_whatsapp_message('template_2', $post_id, $order_status, $payment_status, $phone_number); self::log_message('INFO', 'Executed template_2', ['post_id' => $post_id]); } /** * Send WhatsApp template 3 * * @param int $post_id * @param string $unit_name * @param string $phone_number */ public static function send_whatsapp_template_3($post_id, $unit_name, $phone_number) { if (empty($post_id) || empty($phone_number)) { self::log_message('ERROR', 'Invalid parameters for template_3', [ 'post_id' => $post_id, 'phone_number' => $phone_number, ]); return; } $order_status = get_field('status-order', $post_id) ?: 'Pending'; $payment_status = get_field('payment_status', $post_id) ?: ''; self::send_whatsapp_message('template_3', $post_id, $order_status, $payment_status, $phone_number); self::log_message('INFO', 'Executed template_3', ['post_id' => $post_id]); } /** * Schedule Pending status * * @param int $post_id * @param string $return_date */ private static function schedule_pending_status($post_id, $return_date) { $date_obj = self::parse_wib_date($return_date); if (!$date_obj) { return; } $timestamp = $date_obj->setTimezone(new DateTimeZone('UTC'))->getTimestamp(); as_unschedule_action('set_status_pending_scheduled', [$post_id], "pending_{$post_id}"); as_schedule_single_action($timestamp, 'set_status_pending_scheduled', [$post_id], "pending_{$post_id}"); self::log_message('INFO', 'Scheduled Pending status', [ 'post_id' => $post_id, 'timestamp' => gmdate('Y-m-d H:i:s', $timestamp), ]); } /** * Execute set status to Pending * * @param int $post_id */ public static function execute_set_status_pending($post_id) { if (empty($post_id)) { self::log_message('ERROR', 'Invalid post ID for Pending status'); return; } $payment_status = get_field('payment_status', $post_id); //if (!in_array($payment_status, ['Sudah_DP', 'Sudah Bayar'])) { if (!in_array($payment_status, ['Sudah_DP', 'Sudah Bayar', 'Belum Bayar'])) { self::log_message('INFO', 'Skipped setting Pending status due to unpaid status', [ 'post_id' => $post_id, 'payment_status' => $payment_status, ]); return; } update_field('status-order', 'Pending', $post_id); $phone_number = get_field('phone_number', $post_id); if ($phone_number) { self::send_whatsapp_message('template_2', $post_id, 'Pending', $payment_status, $phone_number); } self::log_message('INFO', 'Set status to Pending', ['post_id' => $post_id]); } /** * Cancel all schedulers for an order * * @param int $post_id * @return bool */ private static function cancel_all_order_schedulers($post_id) { $unit_name = get_field('unit', $post_id) ?: ''; $phone_number = get_field('phone_number', $post_id) ?: ''; $total_cancelled = 0; $actions = [ ['hook' => 'decrease_stock_scheduled', 'args' => [$post_id, $unit_name], 'group' => "stock_decrease_{$post_id}"], ['hook' => 'set_status_pending_scheduled', 'args' => [$post_id], 'group' => "pending_{$post_id}"], ['hook' => 'send_whatsapp_message_template_2', 'args' => [$post_id, $unit_name, $phone_number], 'group' => "whatsapp_{$post_id}_template_2"], ['hook' => 'send_whatsapp_message_template_3', 'args' => [$post_id, $unit_name, $phone_number], 'group' => "whatsapp_{$post_id}_template_3"], ]; foreach ($actions as $action) { $cancelled = as_unschedule_action($action['hook'], $action['args'], $action['group']); $total_cancelled += $cancelled ? 1 : 0; self::log_message('INFO', $cancelled ? "Cancelled {$action['hook']}" : "No task found for {$action['hook']}", [ 'post_id' => $post_id, 'args' => $action['args'], ]); } self::log_message('INFO', 'Total schedulers cancelled', ['post_id' => $post_id, 'count' => $total_cancelled]); return $total_cancelled > 0; } /** * Log cron execution */ public static function log_cron_execution() { self::log_message('INFO', 'WP-Cron executed', ['time' => gmdate('Y-m-d H:i:s')]); } /** * Log message with structured format * * @param string $level * @param string $message * @param array $context */ private static function log_message($level, $message, $context = []) { error_log(sprintf( '[RentalSystem][%s] %s %s', strtoupper($level), $message, !empty($context) ? json_encode($context) : '' )); } } // Initialize the plugin RentalSystem::init();function send_whatsapp_message_zapin($phone_number, $message) { $api_key = 'wyj9HRX8kKqTE0hQKAI1WGAbGfJVNV'; // API key Anda $sender = '6287856161934'; // Nomor pengirim WhatsApp // Data yang akan dikirim ke API $data = array( 'api_key' => $api_key, 'sender' => $sender, 'number' => $phone_number, 'message' => $message, ); // Endpoint API $url = 'https://zapin.my.id/send-message'; // Inisialisasi cURL $ch = curl_init(); // Set opsi untuk permintaan cURL curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); // Mengirim data dalam format JSON curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); // Set header Content-Type ke application/json // Eksekusi cURL dan ambil respons $result = curl_exec($ch); // Cek apakah ada kesalahan saat eksekusi cURL // Cek apakah ada kesalahan saat eksekusi cURL if(curl_errno($ch)) { error_log('❌ Gagal mengirim pesan WhatsApp ke nomor ' . $phone_number . ': ' . curl_error($ch)); // Log error jika terjadi masalah } else { error_log('✅ Pesan WhatsApp berhasil dikirim ke nomor ' . $phone_number . '.'); } // Tutup cURL curl_close($ch); } function send_message_with_template_zapin($template, $post_id, $order_status, $payment_status, $phone_number) { // Ambil data ACF berdasarkan post_id $customer_name = get_field('customer_name', $post_id); // Nama customer $location = get_field('locations', $post_id); // Lokasi $unit_name = get_field('unit', $post_id); // Nama unit $duration = get_field('duration', $post_id); // Durasi $pickup_date = get_field('pickup_date', $post_id); // Tanggal pengambilan $return_date = get_field('return_date', $post_id); // Tanggal pengembalian $status_order = get_field('status-order', $post_id); // Status order $shipping_cost = get_field('shipping_cost', $post_id); // Biaya ongkir $unit_cost = get_field('unit_cost', $post_id); // Biaya unit $status_payment = get_field('payment_status', $post_id); // Status pembayaran // **Konversi Nomor Telepon**: Ganti awalan dengan '628' jika nomor dimulai dengan '8' atau '08' if (substr($phone_number, 0, 2) === '08') { $phone_number = '628' . substr($phone_number, 2); // Mengganti awalan '08' menjadi '628' } elseif (substr($phone_number, 0, 1) === '8') { $phone_number = '628' . substr($phone_number, 1); // Mengganti awalan '8' menjadi '628' } elseif (substr($phone_number, 0, 3) !== '628') { // Jika nomor tidak dimulai dengan '628', tambahkan '628' di depan $phone_number = '628' . $phone_number; } // Tentukan pesan berdasarkan template // Tentukan pesan berdasarkan template $message = ''; switch ($template) { case 'template_1': // Pesanan Baru $message = "*Haloo Kak $customer_name di $location,* 🎉\n\n"; $message .= "Terimakasih atas pesanan Anda! Kami sangat senang bisa melayani Anda. 🙌\n\n"; $message .= "*Unit Name*: $unit_name 📦\n"; $message .= "*Durasi*: $duration ⏰\n"; $message .= "*Pengambilan*: $pickup_date 📅\n"; $message .= "*Pengembalian*: $return_date 🔄\n"; $message .= "*Status Order*: $status_order ✅\n"; $message .= "*Biaya Ongkir*: Rp " . number_format($shipping_cost, 0, ',', '.') . " 💵\n"; $message .= "*Biaya Unit*: Rp " . number_format($unit_cost, 0, ',', '.') . " 💸\n"; $message .= "*Status Pembayaran*: $status_payment 💳\n\n"; $message .= "Kami berharap kakak enjoy bermain PSnya dan Jangan ragu untuk menghubungi kami jika ada pertanyaan lebih lanjut. 😄\n\n"; $message .= "*Have fun, Kak!* 🎮✨"; break; case 'template_2': // Reminder $message = "*Reminder untuk Kak $customer_name di $location* ⏰\n\n"; $message .= "Mimin mau mengingatkan nih, PS-nya akan habis *1 JAM* lagi ya, kak, di *$return_date* ⏳📅\n\n"; $message .= "Jika ada pertanyaan atau butuh bantuan, termasuk jika kakak ingin menambah durasi lagi, langsung hubungi Mimin ya! 😊\n\n"; $message .= "Terima kasih sudah memilih kami! 🤙🏻"; break; case 'template_3': // Pengembalian $message = "*Halo Kak $customer_name,* 👋\n\n"; $message .= "Masa sewanya *sudah habis nih kak*. 🕒\n"; $message .= "Tolong dipastikan barang sudah disiapkan ya kak. 🙏\n\n"; $message .= "Terimakasih untuk sewanya, semoga kakak puas! 💖\n"; $message .= "Jika mau *order lagi*, bisa hubungi mimin yaa kak! 📱✨\n\n"; $message .= "Jangan lupa untuk *bintang 5* di google maps ya kakk😄\n\n"; $message .= "Instagram : sewapsmalang_\n"; $message .= "Maps : https://maps.app.goo.gl/TYDbJpahDiX7cj3A8"; break; case 'template_4': // Pengembalian $message = "*Halo Kak $customer_name,* 👋\n\n"; $message .= "Terimakasih untuk sewanya, semoga kakak puas! 💖\n"; $message .= "Jika mau *order lagi*, bisa hubungi mimin yaa kak! 📱✨\n\n"; $message .= "Jangan lupa untuk *bintang 5* di google maps ya kakk😄\n\n"; $message .= "Instagram : sewapsmalang_\n"; $message .= "Maps : https://maps.app.goo.gl/TYDbJpahDiX7cj3A8"; break; default: $message = "Pesan tidak dikenali."; break; } // Kirim pesan menggunakan API send_whatsapp_message_zapin($phone_number, $message); } // Menjadwalkan event cron untuk pengecekan status if ( ! wp_next_scheduled( 'check_and_update_order_status' ) ) { wp_schedule_event( time(), 'hourly', 'check_and_update_order_status' ); // Setiap jam } // Hook ke dalam cron job untuk mengecek status add_action( 'check_and_update_order_status', 'check_and_update_status' ); function check_and_update_status() { // Query untuk mencari semua post 'rental-order' yang status-nya 'Running' dan payment_status 'Sudah Bayar' $args = array( 'post_type' => 'rental-order', 'posts_per_page' => -1, // Ambil semua post 'meta_query' => array( array( 'key' => 'payment_status', 'value' => 'Sudah Bayar', 'compare' => '=' ), array( 'key' => 'status-order', 'value' => 'Running', 'compare' => '=' ), array( 'key' => 'return_date', 'value' => date('Y-m-d H:i:s'), // Bandingkan dengan waktu sekarang 'compare' => '<', // Mengambil semua post dengan return_date yang sudah lewat 'type' => 'DATETIME' ), ) ); // Query WP untuk mengambil post yang relevan $query = new WP_Query($args); // Loop semua post dan ubah status-nya if ($query->have_posts()) { while ($query->have_posts()) { $query->the_post(); $post_id = get_the_ID(); // Update status-order menjadi 'Selesai' update_field('status-order', 'Selesai', $post_id); error_log("Status-order untuk post ID $post_id telah diubah menjadi Selesai."); } wp_reset_postdata(); // Reset postdata setelah query } } function get_daily_sales_expenses_report($month, $year) { // Initialize the result array $report_data = []; // Loop through the days of the month (from 1 to 31) for ($day = 1; $day <= 31; $day++) { $day_padded = str_pad($day, 2, '0', STR_PAD_LEFT); // Format the day (e.g., 01, 02, ..., 31) // Query for sales (income) for a specific day $sales_args = array( 'post_type' => 'rental-order', // Sales CPT 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => array( array( 'key' => 'payment_status', 'value' => 'Sudah Bayar', // Only consider paid orders 'compare' => '=' ), ), 'date_query' => array( array( 'year' => $year, 'month' => $month, 'day' => $day, ), ), ); $sales_query = new WP_Query($sales_args); $total_sales = 0; if ($sales_query->have_posts()) { while ($sales_query->have_posts()) { $sales_query->the_post(); $total_sales += (float) get_post_meta(get_the_ID(), 'total_cost', true); // Add total sales } } wp_reset_postdata(); // Query for expenses for the specific day $expense_args = array( 'post_type' => 'rental-expense', // Expense CPT 'post_status' => 'publish', 'posts_per_page' => -1, 'date_query' => array( array( 'year' => $year, 'month' => $month, 'day' => $day, ), ), ); $expense_query = new WP_Query($expense_args); $total_expense = 0; if ($expense_query->have_posts()) { while ($expense_query->have_posts()) { $expense_query->the_post(); $total_expense += (float) get_post_meta(get_the_ID(), 'cost-expense', true); // Add expense } } wp_reset_postdata(); // Store the data for this day $report_data[] = array( 'date' => "$day_padded-$month-$year", 'sales' => $total_sales, 'expenses' => $total_expense ); } return $report_data; } // Registering the shortcode add_shortcode('daily_sales_expenses_report', function($atts) { // Default attributes (month and year as current date) $atts = shortcode_atts( array( 'month' => date('m'), // Current month 'year' => date('Y'), // Current year ), $atts, 'daily_sales_expenses_report' ); // Fetching the report data $report_data = get_daily_sales_expenses_report($atts['month'], $atts['year']); // Starting the table HTML output $output = ''; $output .= ''; $output .= ''; // Loop through the report data and populate the table rows foreach ($report_data as $data) { $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; } $output .= ''; $output .= '
TanggalPemasukanPengeluaran
' . $data['date'] . 'Rp ' . number_format($data['sales'], 0, ',', '.') . 'Rp ' . number_format($data['expenses'], 0, ',', '.') . '
'; return $output; // Return the table HTML }); function get_daily_sales_expenses() { $month = date('m'); // Current month $year = date('Y'); // Current year $report_data = []; // Loop through the days of the month (1 to 31) for ($day = 1; $day <= 31; $day++) { $day_padded = str_pad($day, 2, '0', STR_PAD_LEFT); // Format day (01, 02, ..., 31) // Query for sales data for this day $sales_query = new WP_Query([ 'post_type' => 'rental-order', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => 'payment_status', 'value' => 'Sudah Bayar', 'compare' => '=' ] ], 'date_query' => [ [ 'year' => $year, 'month' => $month, 'day' => $day, ] ] ]); $total_sales = 0; while ($sales_query->have_posts()) { $sales_query->the_post(); $total_sales += (float) get_post_meta(get_the_ID(), 'total_cost', true); } wp_reset_postdata(); // Query for expenses data for this day $expense_query = new WP_Query([ 'post_type' => 'rental-expense', 'posts_per_page' => -1, 'date_query' => [ [ 'year' => $year, 'month' => $month, 'day' => $day, ] ] ]); $total_expense = 0; while ($expense_query->have_posts()) { $expense_query->the_post(); $total_expense += (float) get_post_meta(get_the_ID(), 'cost-expense', true); } wp_reset_postdata(); // Combine sales and expenses for the given day $report_data[] = [ 'date' => "$day_padded-$month-$year", 'sales' => $total_sales, 'expenses' => $total_expense ]; } // Generate HTML output for the table rows $output = ''; foreach ($report_data as $index => $data) { $output .= ''; $output .= '' . ($index + 1) . ''; $output .= '' . $data['date'] . ''; $output .= '' . 'Rp ' . number_format($data['expenses'], 0, ',', '.') . ''; $output .= '' . 'Rp ' . number_format($data['sales'], 0, ',', '.') . ''; $output .= ''; } return $output; } add_shortcode('daily_sales_expenses', 'get_daily_sales_expenses'); function restrict_content_to_logged_in_users() { // Check if user is not logged in if (!is_user_logged_in() && !is_page('login')) { // Redirect to the login page wp_redirect('/login/'); exit(); } } add_action('template_redirect', 'restrict_content_to_logged_in_users'); // Mendaftarkan endpoint API untuk update stok add_action('rest_api_init', function() { register_rest_route('websee/v1', '/update-stock', array( 'methods' => 'POST', 'callback' => 'websee_handle_update_stock', // Handler baru 'permission_callback' => '__return_true', )); }); // Fungsi untuk menangani permintaan update stok function websee_handle_update_stock(WP_REST_Request $request) { $data = $request->get_json_params(); error_log('Update stock received: ' . print_r($data, true)); if (isset($data['stoku_ps3']) && isset($data['stoku_ps4']) && isset($data['stoku_tv']) && isset($data['stoku_ps3-mines']) && isset($data['stoku_ps4-mines']) && isset($data['stoku_tv-mines'])) { websee_increase_stock('ps3', intval($data['stoku_ps3'])); websee_increase_stock('ps4', intval($data['stoku_ps4'])); websee_increase_stock('tv', intval($data['stoku_tv'])); websee_decrease_stock('ps3', intval($data['stoku_ps3-mines'])); websee_decrease_stock('ps4', intval($data['stoku_ps4-mines'])); websee_decrease_stock('tv', intval($data['stoku_tv-mines'])); return new WP_REST_Response('Stock updated successfully', 200); } error_log('Invalid data received: ' . print_r($data, true)); return new WP_REST_Response('Invalid data', 400); } // Fungsi untuk mengurangi stok produk function websee_decrease_stock($product, $quantity) { $field_name = "stock_$product"; $current_stock = (int) get_field($field_name, 'option'); $new_stock = $current_stock - $quantity; if ($new_stock < 0) { $new_stock = 0; } update_field($field_name, $new_stock, 'option'); error_log("✅ Stok $product berkurang menjadi $new_stock."); } // Fungsi untuk menambah stok produk function websee_increase_stock($product, $quantity) { $field_name = "stock_$product"; $current_stock = (int) get_field($field_name, 'option'); $new_stock = $current_stock + $quantity; update_field($field_name, $new_stock, 'option'); error_log("✅ Stok $product bertambah menjadi $new_stock."); } // Disable the WordPress admin bar for all users add_filter('show_admin_bar', '__return_false'); Saber – Laman 12
Nama Pemesan :
Lokasi :
Unit :
Jarak Antar:
Phone :
Tanggal
Pengambilan :
Durasi :
Tanggal
Pengembalian :
Status :
Biaya Unit :
Biaya Ongkir :
Total :
Status
Pembayaran :
Sisa Unit
Update Unit
UnitJumlah
PS 32
PS 4-2
TV5
UnitTambahKurang
PS 3
PS 4
TV
Riwayat Pemesanan
NoNamaLokasiTanggal PengambilanUnitDurasiHargaStatus UnitTanggal PengembalianStatus PembayaranOngkir
111Achmad Irfan01-06-2026, 20:26PS41 Hari75000
02-06-2026, 20:26
25000
112GanevIkan Piranha01-06-2026, 19:25PS4TV1 Hari100000
02-06-2026, 19:25
113FarizJawara Land01-06-2026, 16:23PS41 Hari75000
02-06-2026, 16:23
114Diva Alsabillah01-06-2026, 11:36PS41 Hari75000
02-06-2026, 11:36
115Bima01-06-2026, 10:48PS3TV1 Hari75000
02-06-2026, 10:48
116tes01-06-2026, 07:05PS36 Jam25000
01-06-2026, 13:05
10000
117Affan31-05-2026, 00:55PS4TV12 Jam80000
31-05-2026, 12:55
118Fernanda30-05-2026, 23:24PS42 Hari150000
01-06-2026, 23:24
10000
119Fery Irianto01-06-2026, 03:02PS31 Hari45000
02-06-2026, 03:02
120Edo Kurni30-05-2026, 19:57PS41 Hari75000
31-05-2026, 19:57
10000
[frontend_indexing_buttons]