diff --git a/NOTIFICATION_ENHANCEMENTS_PLAN.md b/NOTIFICATION_ENHANCEMENTS_PLAN.md new file mode 100644 index 0000000..41b7728 --- /dev/null +++ b/NOTIFICATION_ENHANCEMENTS_PLAN.md @@ -0,0 +1,428 @@ +# Notification System Enhancements - Implementation Plan + +## Overview + +This document outlines the complete implementation plan for notification system enhancements, including dynamic URLs, activity logging, and customer notifications. + +--- + +## 1. Customer Email Notifications + +### Current State +- WooCommerce handles customer emails automatically +- WooNooW notifications are for admin alerts only +- Recipient field exists but not fully utilized + +### Strategy: Integration, Not Replacement + +**Decision:** Keep WooCommerce's customer email system, add admin notification layer + +**Why:** +- ✅ WooCommerce emails are battle-tested +- ✅ Merchants already customize them +- ✅ Templates, styling, and logic already exist +- ✅ We focus on admin experience + +**What We Add:** +- Admin notifications (email + push) +- Real-time alerts for admins +- Activity logging +- Better UI for managing notifications + +### Implementation + +**No code changes needed!** System already supports: +- `recipient: 'admin'` - Admin notifications +- `recipient: 'customer'` - Customer notifications (via WooCommerce) +- `recipient: 'both'` - Both (admin via WooNooW, customer via WooCommerce) + +**Documentation Update:** +- Clarify that customer emails use WooCommerce +- Document integration points +- Add filter for custom recipient logic + +--- + +## 2. Activity Log System + +### Current State +- WooCommerce has order notes (limited) +- No comprehensive activity log +- No UI for viewing all activities + +### Strategy: Build Custom Activity Log + +**Why Build Our Own:** +- ✅ Full control over what's logged +- ✅ Better UI/UX +- ✅ Searchable and filterable +- ✅ Integration with notifications +- ✅ Real-time updates + +### Data Structure + +```php +// wp_woonoow_activity_log table +CREATE TABLE wp_woonoow_activity_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + action VARCHAR(50) NOT NULL, + object_type VARCHAR(50) NOT NULL, + object_id BIGINT UNSIGNED NOT NULL, + description TEXT, + metadata LONGTEXT, -- JSON + ip_address VARCHAR(45), + user_agent TEXT, + created_at DATETIME NOT NULL, + INDEX idx_user_id (user_id), + INDEX idx_action (action), + INDEX idx_object (object_type, object_id), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### Activity Types + +**Orders:** +- `order.created` - Order created +- `order.updated` - Order updated +- `order.status_changed` - Status changed +- `order.payment_completed` - Payment completed +- `order.refunded` - Order refunded +- `order.deleted` - Order deleted + +**Products:** +- `product.created` - Product created +- `product.updated` - Product updated +- `product.stock_changed` - Stock changed +- `product.deleted` - Product deleted + +**Customers:** +- `customer.created` - Customer registered +- `customer.updated` - Customer updated +- `customer.deleted` - Customer deleted + +**Notifications:** +- `notification.sent` - Notification sent +- `notification.failed` - Notification failed +- `notification.clicked` - Notification clicked + +**Settings:** +- `settings.updated` - Settings changed +- `channel.toggled` - Channel enabled/disabled +- `event.toggled` - Event enabled/disabled + +### Implementation Files + +**Backend:** +1. `includes/Core/ActivityLog/Logger.php` - Main logger class +2. `includes/Core/ActivityLog/ActivityLogTable.php` - Database table +3. `includes/Api/ActivityLogController.php` - REST API +4. Hook into WooCommerce actions + +**Frontend:** +1. `admin-spa/src/routes/ActivityLog/index.tsx` - Activity log page +2. `admin-spa/src/routes/ActivityLog/ActivityItem.tsx` - Single activity +3. `admin-spa/src/routes/ActivityLog/Filters.tsx` - Filter UI + +--- + +## 3. Dynamic Push Notification URLs + +### Current State +- Global URL: `/wp-admin/admin.php?page=woonoow#/orders` +- All notifications go to same page + +### Strategy: Event-Specific Deep Links + +### URL Templates + +```php +$url_templates = [ + // Orders + 'order_placed' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', + 'order_processing' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', + 'order_completed' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', + 'order_cancelled' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', + 'order_refunded' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', + + // Products + 'low_stock' => '/wp-admin/admin.php?page=woonoow#/products/{product_id}', + 'out_of_stock' => '/wp-admin/admin.php?page=woonoow#/products/{product_id}', + + // Customers + 'new_customer' => '/wp-admin/admin.php?page=woonoow#/customers/{customer_id}', + 'customer_note' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', +]; +``` + +### Template Variables + +**Available Variables:** +- `{order_id}` - Order ID +- `{product_id}` - Product ID +- `{customer_id}` - Customer ID +- `{user_id}` - User ID +- `{site_url}` - Site URL +- `{admin_url}` - Admin URL + +### Implementation + +**Backend:** +1. Add URL template field to push settings +2. Parse template variables when sending +3. Store parsed URL in notification metadata + +**Frontend:** +1. Add URL template field to Templates page +2. Show available variables +3. Preview parsed URL + +--- + +## 4. Rich Notification Content + +### Event-Specific Icons + +```php +$notification_icons = [ + 'order_placed' => '🛒', + 'order_processing' => '⚙️', + 'order_completed' => '✅', + 'order_cancelled' => '❌', + 'order_refunded' => '💰', + 'low_stock' => '📦', + 'out_of_stock' => '🚫', + 'new_customer' => '👤', + 'customer_note' => '💬', +]; +``` + +### Event-Specific Images + +**Order Notifications:** +- Show first product image +- Fallback to store logo + +**Product Notifications:** +- Show product image +- Fallback to placeholder + +**Customer Notifications:** +- Show customer avatar (Gravatar) +- Fallback to default avatar + +### Rich Content Structure + +```json +{ + "title": "New Order #1234", + "body": "John Doe ordered 2 items (Rp137.000)", + "icon": "🛒", + "image": "https://example.com/product.jpg", + "badge": "https://example.com/logo.png", + "data": { + "url": "/wp-admin/admin.php?page=woonoow#/orders/1234", + "order_id": 1234, + "customer_name": "John Doe", + "total": 137000 + }, + "actions": [ + { + "action": "view", + "title": "View Order", + "icon": "👁️" + }, + { + "action": "mark_processing", + "title": "Mark Processing", + "icon": "⚙️" + } + ] +} +``` + +--- + +## Implementation Priority + +### Phase 1: Dynamic URLs (Immediate) ✅ +1. Add URL template to push settings +2. Parse template variables +3. Update notification sending logic +4. Test with different events + +### Phase 2: Activity Log (Immediate) ✅ +1. Create database table +2. Implement Logger class +3. Hook into WooCommerce actions +4. Create REST API +5. Build frontend UI + +### Phase 3: Rich Content (Future) 📋 +1. Add icon field to events +2. Add image field to events +3. Implement image fetching logic +4. Update push notification payload +5. Test on different browsers + +### Phase 4: Notification Actions (Future) 📋 +1. Define action types +2. Implement action handlers +3. Update push notification payload +4. Handle action clicks +5. Test on different browsers + +--- + +## Database Schema + +### Activity Log Table + +```sql +CREATE TABLE IF NOT EXISTS wp_woonoow_activity_log ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + user_name VARCHAR(255) NOT NULL, + action VARCHAR(50) NOT NULL, + object_type VARCHAR(50) NOT NULL, + object_id BIGINT UNSIGNED NOT NULL, + object_name VARCHAR(255), + description TEXT, + metadata LONGTEXT, + ip_address VARCHAR(45), + user_agent TEXT, + created_at DATETIME NOT NULL, + + INDEX idx_user_id (user_id), + INDEX idx_action (action), + INDEX idx_object (object_type, object_id), + INDEX idx_created_at (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +``` + +### Push Settings Update + +```php +// Add to woonoow_push_notification_settings +[ + 'enabled' => true, + 'vapid_public_key' => '...', + 'vapid_private_key' => '...', + 'default_url' => '/wp-admin/admin.php?page=woonoow#/orders', + 'url_templates' => [ + 'order_placed' => '/wp-admin/admin.php?page=woonoow#/orders/{order_id}', + // ... more templates + ], + 'show_store_logo' => true, + 'show_product_images' => true, + 'show_customer_avatar' => true, + 'require_interaction' => false, + 'silent_notifications' => false, +] +``` + +--- + +## API Endpoints + +### Activity Log + +``` +GET /woonoow/v1/activity-log + ?page=1&per_page=20&action=order.created&user_id=1&date_from=2025-11-01 + +POST /woonoow/v1/activity-log + { action, object_type, object_id, description, metadata } + +GET /woonoow/v1/activity-log/stats + ?date_from=2025-11-01&date_to=2025-11-30 +``` + +### Push Notification URLs + +``` +GET /woonoow/v1/notifications/push/url-templates + +POST /woonoow/v1/notifications/push/url-templates + { event_id, url_template } + +POST /woonoow/v1/notifications/push/preview-url + { event_id, url_template, variables } +``` + +--- + +## Testing Checklist + +### Dynamic URLs +- [ ] Order notification → Order detail page +- [ ] Product notification → Product edit page +- [ ] Customer notification → Customer page +- [ ] Variables parsed correctly +- [ ] Fallback to default URL + +### Activity Log +- [ ] Activities logged correctly +- [ ] Filtering works +- [ ] Pagination works +- [ ] Search works +- [ ] Real-time updates +- [ ] Performance with 10k+ logs + +### Rich Content +- [ ] Icons display correctly +- [ ] Images load correctly +- [ ] Fallbacks work +- [ ] Different browsers (Chrome, Firefox, Safari) +- [ ] Mobile devices + +--- + +## Success Metrics + +**User Experience:** +- Click-through rate on notifications +- Time to action after notification +- User satisfaction score + +**Technical:** +- Notification delivery rate +- Activity log query performance +- Storage usage + +**Business:** +- Faster response to orders +- Reduced missed notifications +- Better audit trail + +--- + +## Timeline + +**Week 1: Dynamic URLs + Activity Log** +- Day 1-2: Dynamic URLs implementation +- Day 3-5: Activity Log backend +- Day 6-7: Activity Log frontend + +**Week 2: Rich Content** +- Day 1-3: Icons and images +- Day 4-5: Testing and polish +- Day 6-7: Documentation + +**Week 3: Notification Actions** +- Day 1-3: Action handlers +- Day 4-5: Testing +- Day 6-7: Documentation and release + +--- + +## Conclusion + +This plan provides a comprehensive roadmap for enhancing the notification system with: +1. ✅ Customer email clarification (no changes needed) +2. ✅ Activity log system (custom build) +3. ✅ Dynamic push URLs (event-specific) +4. ✅ Rich notification content (icons, images, actions) + +All enhancements are designed to improve admin experience while maintaining compatibility with WooCommerce's existing systems. diff --git a/includes/Api/ActivityLogController.php b/includes/Api/ActivityLogController.php new file mode 100644 index 0000000..40b0fa0 --- /dev/null +++ b/includes/Api/ActivityLogController.php @@ -0,0 +1,145 @@ +namespace, '/' . $this->rest_base, [ + [ + 'methods' => 'GET', + 'callback' => [$this, 'get_activities'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + + // POST /woonoow/v1/activity-log + register_rest_route($this->namespace, '/' . $this->rest_base, [ + [ + 'methods' => 'POST', + 'callback' => [$this, 'create_activity'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + + // GET /woonoow/v1/activity-log/stats + register_rest_route($this->namespace, '/' . $this->rest_base . '/stats', [ + [ + 'methods' => 'GET', + 'callback' => [$this, 'get_stats'], + 'permission_callback' => [$this, 'check_permission'], + ], + ]); + } + + /** + * Get activities + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public function get_activities(WP_REST_Request $request) { + $args = [ + 'page' => $request->get_param('page') ?: 1, + 'per_page' => $request->get_param('per_page') ?: 20, + 'action' => $request->get_param('action'), + 'object_type' => $request->get_param('object_type'), + 'object_id' => $request->get_param('object_id'), + 'user_id' => $request->get_param('user_id'), + 'date_from' => $request->get_param('date_from'), + 'date_to' => $request->get_param('date_to'), + 'search' => $request->get_param('search'), + ]; + + $result = Logger::get_activities($args); + + return new WP_REST_Response($result, 200); + } + + /** + * Create activity + * + * @param WP_REST_Request $request + * @return WP_REST_Response|WP_Error + */ + public function create_activity(WP_REST_Request $request) { + $params = $request->get_json_params(); + + $action = isset($params['action']) ? $params['action'] : null; + $object_type = isset($params['object_type']) ? $params['object_type'] : null; + $object_id = isset($params['object_id']) ? $params['object_id'] : null; + $description = isset($params['description']) ? $params['description'] : ''; + $metadata = isset($params['metadata']) ? $params['metadata'] : []; + + if (empty($action) || empty($object_type) || empty($object_id)) { + return new WP_Error( + 'invalid_params', + __('Action, object_type, and object_id are required', 'woonoow'), + ['status' => 400] + ); + } + + $activity_id = Logger::log($action, $object_type, $object_id, $description, $metadata); + + if ($activity_id === false) { + return new WP_Error( + 'log_failed', + __('Failed to log activity', 'woonoow'), + ['status' => 500] + ); + } + + return new WP_REST_Response([ + 'success' => true, + 'activity_id' => $activity_id, + ], 201); + } + + /** + * Get activity stats + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public function get_stats(WP_REST_Request $request) { + $date_from = $request->get_param('date_from'); + $date_to = $request->get_param('date_to'); + + $stats = Logger::get_stats($date_from, $date_to); + + return new WP_REST_Response($stats, 200); + } + + /** + * Check permission + * + * @return bool + */ + public function check_permission() { + return current_user_can('manage_woocommerce') || current_user_can('manage_options'); + } +} diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php index 9504c7d..2f3e638 100644 --- a/includes/Api/Routes.php +++ b/includes/Api/Routes.php @@ -16,6 +16,7 @@ use WooNooW\Api\EmailController; use WooNooW\API\DeveloperController; use WooNooW\API\SystemController; use WooNooW\Api\NotificationsController; +use WooNooW\Api\ActivityLogController; class Routes { public static function init() { @@ -84,6 +85,10 @@ class Routes { // Notifications controller $notifications_controller = new NotificationsController(); $notifications_controller->register_routes(); + + // Activity Log controller + $activity_log_controller = new ActivityLogController(); + $activity_log_controller->register_routes(); }); } } diff --git a/includes/Core/ActivityLog/ActivityLogTable.php b/includes/Core/ActivityLog/ActivityLogTable.php new file mode 100644 index 0000000..49a64c4 --- /dev/null +++ b/includes/Core/ActivityLog/ActivityLogTable.php @@ -0,0 +1,90 @@ +prefix . self::TABLE_NAME; + } + + /** + * Create or update table + */ + public static function create_table() { + global $wpdb; + + $table_name = self::get_table_name(); + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS {$table_name} ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + user_name VARCHAR(255) NOT NULL, + action VARCHAR(50) NOT NULL, + object_type VARCHAR(50) NOT NULL, + object_id BIGINT UNSIGNED NOT NULL, + object_name VARCHAR(255), + description TEXT, + metadata LONGTEXT, + ip_address VARCHAR(45), + user_agent TEXT, + created_at DATETIME NOT NULL, + + INDEX idx_user_id (user_id), + INDEX idx_action (action), + INDEX idx_object (object_type, object_id), + INDEX idx_created_at (created_at) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql); + + // Store version + update_option('woonoow_activity_log_db_version', self::DB_VERSION); + } + + /** + * Check if table exists + * + * @return bool + */ + public static function table_exists() { + global $wpdb; + $table_name = self::get_table_name(); + return $wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") === $table_name; + } + + /** + * Drop table (for uninstall) + */ + public static function drop_table() { + global $wpdb; + $table_name = self::get_table_name(); + $wpdb->query("DROP TABLE IF EXISTS {$table_name}"); + delete_option('woonoow_activity_log_db_version'); + } +} diff --git a/includes/Core/ActivityLog/Logger.php b/includes/Core/ActivityLog/Logger.php new file mode 100644 index 0000000..75da034 --- /dev/null +++ b/includes/Core/ActivityLog/Logger.php @@ -0,0 +1,287 @@ +display_name : __('System', 'woonoow'); + + // Get object name + $object_name = self::get_object_name($object_type, $object_id); + + // Get IP and user agent + $ip_address = self::get_client_ip(); + $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + + // Prepare data + $data = [ + 'user_id' => $user_id, + 'user_name' => $user_name, + 'action' => $action, + 'object_type' => $object_type, + 'object_id' => $object_id, + 'object_name' => $object_name, + 'description' => $description, + 'metadata' => json_encode($metadata), + 'ip_address' => $ip_address, + 'user_agent' => $user_agent, + 'created_at' => current_time('mysql'), + ]; + + // Insert + $table_name = ActivityLogTable::get_table_name(); + $result = $wpdb->insert($table_name, $data); + + if ($result === false) { + return false; + } + + return $wpdb->insert_id; + } + + /** + * Get object name based on type and ID + * + * @param string $object_type + * @param int $object_id + * @return string + */ + private static function get_object_name($object_type, $object_id) { + switch ($object_type) { + case 'order': + return sprintf(__('Order #%d', 'woonoow'), $object_id); + + case 'product': + $product = wc_get_product($object_id); + return $product ? $product->get_name() : sprintf(__('Product #%d', 'woonoow'), $object_id); + + case 'customer': + $customer = get_userdata($object_id); + return $customer ? $customer->display_name : sprintf(__('Customer #%d', 'woonoow'), $object_id); + + case 'notification': + return sprintf(__('Notification #%d', 'woonoow'), $object_id); + + case 'settings': + return __('Settings', 'woonoow'); + + default: + return sprintf(__('%s #%d', 'woonoow'), ucfirst($object_type), $object_id); + } + } + + /** + * Get client IP address + * + * @return string + */ + private static function get_client_ip() { + $ip_keys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; + + foreach ($ip_keys as $key) { + if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP)) { + return $_SERVER[$key]; + } + } + + return '0.0.0.0'; + } + + /** + * Get activities + * + * @param array $args Query arguments + * @return array + */ + public static function get_activities($args = []) { + global $wpdb; + + $defaults = [ + 'page' => 1, + 'per_page' => 20, + 'action' => null, + 'object_type' => null, + 'object_id' => null, + 'user_id' => null, + 'date_from' => null, + 'date_to' => null, + 'search' => null, + ]; + + $args = wp_parse_args($args, $defaults); + + $table_name = ActivityLogTable::get_table_name(); + $where = ['1=1']; + $where_values = []; + + // Filters + if ($args['action']) { + $where[] = 'action = %s'; + $where_values[] = $args['action']; + } + + if ($args['object_type']) { + $where[] = 'object_type = %s'; + $where_values[] = $args['object_type']; + } + + if ($args['object_id']) { + $where[] = 'object_id = %d'; + $where_values[] = $args['object_id']; + } + + if ($args['user_id']) { + $where[] = 'user_id = %d'; + $where_values[] = $args['user_id']; + } + + if ($args['date_from']) { + $where[] = 'created_at >= %s'; + $where_values[] = $args['date_from']; + } + + if ($args['date_to']) { + $where[] = 'created_at <= %s'; + $where_values[] = $args['date_to']; + } + + if ($args['search']) { + $where[] = '(description LIKE %s OR object_name LIKE %s OR user_name LIKE %s)'; + $search_term = '%' . $wpdb->esc_like($args['search']) . '%'; + $where_values[] = $search_term; + $where_values[] = $search_term; + $where_values[] = $search_term; + } + + // Build query + $where_clause = implode(' AND ', $where); + $offset = ($args['page'] - 1) * $args['per_page']; + + // Count total + $count_query = "SELECT COUNT(*) FROM {$table_name} WHERE {$where_clause}"; + if (!empty($where_values)) { + $count_query = $wpdb->prepare($count_query, $where_values); + } + $total = (int) $wpdb->get_var($count_query); + + // Get activities + $query = "SELECT * FROM {$table_name} WHERE {$where_clause} ORDER BY created_at DESC LIMIT %d OFFSET %d"; + $where_values[] = $args['per_page']; + $where_values[] = $offset; + + $query = $wpdb->prepare($query, $where_values); + $activities = $wpdb->get_results($query, ARRAY_A); + + // Parse metadata + foreach ($activities as &$activity) { + $activity['metadata'] = json_decode($activity['metadata'], true); + } + + return [ + 'activities' => $activities, + 'total' => $total, + 'page' => $args['page'], + 'per_page' => $args['per_page'], + 'total_pages' => ceil($total / $args['per_page']), + ]; + } + + /** + * Get activity stats + * + * @param string $date_from + * @param string $date_to + * @return array + */ + public static function get_stats($date_from = null, $date_to = null) { + global $wpdb; + + $table_name = ActivityLogTable::get_table_name(); + $where = ['1=1']; + $where_values = []; + + if ($date_from) { + $where[] = 'created_at >= %s'; + $where_values[] = $date_from; + } + + if ($date_to) { + $where[] = 'created_at <= %s'; + $where_values[] = $date_to; + } + + $where_clause = implode(' AND ', $where); + + // Total activities + $total_query = "SELECT COUNT(*) FROM {$table_name} WHERE {$where_clause}"; + if (!empty($where_values)) { + $total_query = $wpdb->prepare($total_query, $where_values); + } + $total = (int) $wpdb->get_var($total_query); + + // By action + $by_action_query = "SELECT action, COUNT(*) as count FROM {$table_name} WHERE {$where_clause} GROUP BY action ORDER BY count DESC LIMIT 10"; + if (!empty($where_values)) { + $by_action_query = $wpdb->prepare($by_action_query, $where_values); + } + $by_action = $wpdb->get_results($by_action_query, ARRAY_A); + + // By user + $by_user_query = "SELECT user_id, user_name, COUNT(*) as count FROM {$table_name} WHERE {$where_clause} GROUP BY user_id ORDER BY count DESC LIMIT 10"; + if (!empty($where_values)) { + $by_user_query = $wpdb->prepare($by_user_query, $where_values); + } + $by_user = $wpdb->get_results($by_user_query, ARRAY_A); + + return [ + 'total' => $total, + 'by_action' => $by_action, + 'by_user' => $by_user, + ]; + } + + /** + * Delete old activities + * + * @param int $days Days to keep + * @return int Number of deleted rows + */ + public static function cleanup($days = 90) { + global $wpdb; + + $table_name = ActivityLogTable::get_table_name(); + $date = date('Y-m-d H:i:s', strtotime("-{$days} days")); + + return $wpdb->query($wpdb->prepare( + "DELETE FROM {$table_name} WHERE created_at < %s", + $date + )); + } +} diff --git a/includes/Core/Bootstrap.php b/includes/Core/Bootstrap.php index 17e5a64..f832339 100644 --- a/includes/Core/Bootstrap.php +++ b/includes/Core/Bootstrap.php @@ -20,6 +20,7 @@ use WooNooW\Core\Mail\WooEmailOverride; use WooNooW\Core\DataStores\OrderStore; use WooNooW\Core\MediaUpload; use WooNooW\Core\Notifications\PushNotificationHandler; +use WooNooW\Core\ActivityLog\ActivityLogTable; use WooNooW\Branding; class Bootstrap { @@ -33,6 +34,9 @@ class Bootstrap { MediaUpload::init(); PushNotificationHandler::init(); + // Activity Log + ActivityLogTable::create_table(); + // Addon system (order matters: Registry → Routes → Navigation) AddonRegistry::init(); RouteRegistry::init();