feat: Implement activity log system
## ✅ Activity Log System - Complete
### Backend Implementation
**1. Database Table**
- `ActivityLogTable.php` - Table creation and management
- Auto-creates on plugin init
- Indexed for performance (user_id, action, object, created_at)
**2. Logger Class**
- `Logger.php` - Main logging functionality
- `log()` - Log activities
- `get_activities()` - Query with filters
- `get_stats()` - Activity statistics
- `cleanup()` - Delete old logs
**3. REST API**
- `ActivityLogController.php` - REST endpoints
- GET `/activity-log` - List activities
- POST `/activity-log` - Create activity
- GET `/activity-log/stats` - Get statistics
### Features
**Logging:**
- User ID and name
- Action type (order.created, product.updated, etc.)
- Object type and ID
- Object name (auto-resolved)
- Description
- Metadata (JSON)
- IP address
- User agent
- Timestamp
**Querying:**
- Pagination
- Filter by action, object, user, date
- Search by description, object name, user name
- Sort by date (newest first)
**Statistics:**
- Total activities
- By action (top 10)
- By user (top 10)
- Date range filtering
### Activity Types
**Orders:**
- order.created, order.updated, order.status_changed
- order.payment_completed, order.refunded, order.deleted
**Products:**
- product.created, product.updated
- product.stock_changed, product.deleted
**Customers:**
- customer.created, customer.updated, customer.deleted
**Notifications:**
- notification.sent, notification.failed, notification.clicked
**Settings:**
- settings.updated, channel.toggled, event.toggled
### Integration
- Registered in Bootstrap
- REST API routes registered
- Ready for WooCommerce hooks
- Ready for frontend UI
---
**Next:** Frontend UI + WooCommerce hooks
This commit is contained in:
428
NOTIFICATION_ENHANCEMENTS_PLAN.md
Normal file
428
NOTIFICATION_ENHANCEMENTS_PLAN.md
Normal file
@@ -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.
|
||||||
145
includes/Api/ActivityLogController.php
Normal file
145
includes/Api/ActivityLogController.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Activity Log REST API Controller
|
||||||
|
*
|
||||||
|
* @package WooNooW\Api
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WooNooW\Api;
|
||||||
|
|
||||||
|
use WooNooW\Core\ActivityLog\Logger;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_REST_Response;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
class ActivityLogController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API namespace
|
||||||
|
*/
|
||||||
|
private $namespace = 'woonoow/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REST API base
|
||||||
|
*/
|
||||||
|
private $rest_base = 'activity-log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes
|
||||||
|
*/
|
||||||
|
public function register_routes() {
|
||||||
|
// GET /woonoow/v1/activity-log
|
||||||
|
register_rest_route($this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use WooNooW\Api\EmailController;
|
|||||||
use WooNooW\API\DeveloperController;
|
use WooNooW\API\DeveloperController;
|
||||||
use WooNooW\API\SystemController;
|
use WooNooW\API\SystemController;
|
||||||
use WooNooW\Api\NotificationsController;
|
use WooNooW\Api\NotificationsController;
|
||||||
|
use WooNooW\Api\ActivityLogController;
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
public static function init() {
|
public static function init() {
|
||||||
@@ -84,6 +85,10 @@ class Routes {
|
|||||||
// Notifications controller
|
// Notifications controller
|
||||||
$notifications_controller = new NotificationsController();
|
$notifications_controller = new NotificationsController();
|
||||||
$notifications_controller->register_routes();
|
$notifications_controller->register_routes();
|
||||||
|
|
||||||
|
// Activity Log controller
|
||||||
|
$activity_log_controller = new ActivityLogController();
|
||||||
|
$activity_log_controller->register_routes();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
includes/Core/ActivityLog/ActivityLogTable.php
Normal file
90
includes/Core/ActivityLog/ActivityLogTable.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Activity Log Database Table
|
||||||
|
*
|
||||||
|
* Creates and manages the activity log database table.
|
||||||
|
*
|
||||||
|
* @package WooNooW\Core\ActivityLog
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WooNooW\Core\ActivityLog;
|
||||||
|
|
||||||
|
class ActivityLogTable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name (without prefix)
|
||||||
|
*/
|
||||||
|
const TABLE_NAME = 'woonoow_activity_log';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database version
|
||||||
|
*/
|
||||||
|
const DB_VERSION = '1.0.0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full table name with prefix
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function get_table_name() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
287
includes/Core/ActivityLog/Logger.php
Normal file
287
includes/Core/ActivityLog/Logger.php
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Activity Logger
|
||||||
|
*
|
||||||
|
* Logs user activities and system events.
|
||||||
|
*
|
||||||
|
* @package WooNooW\Core\ActivityLog
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WooNooW\Core\ActivityLog;
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log an activity
|
||||||
|
*
|
||||||
|
* @param string $action Action type (e.g., 'order.created')
|
||||||
|
* @param string $object_type Object type (e.g., 'order', 'product')
|
||||||
|
* @param int $object_id Object ID
|
||||||
|
* @param string $description Human-readable description
|
||||||
|
* @param array $metadata Additional metadata
|
||||||
|
* @param int|null $user_id User ID (null = current user)
|
||||||
|
* @return int|false Activity ID or false on failure
|
||||||
|
*/
|
||||||
|
public static function log($action, $object_type, $object_id, $description = '', $metadata = [], $user_id = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Get user info
|
||||||
|
if ($user_id === null) {
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = get_userdata($user_id);
|
||||||
|
$user_name = $user ? $user->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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ use WooNooW\Core\Mail\WooEmailOverride;
|
|||||||
use WooNooW\Core\DataStores\OrderStore;
|
use WooNooW\Core\DataStores\OrderStore;
|
||||||
use WooNooW\Core\MediaUpload;
|
use WooNooW\Core\MediaUpload;
|
||||||
use WooNooW\Core\Notifications\PushNotificationHandler;
|
use WooNooW\Core\Notifications\PushNotificationHandler;
|
||||||
|
use WooNooW\Core\ActivityLog\ActivityLogTable;
|
||||||
use WooNooW\Branding;
|
use WooNooW\Branding;
|
||||||
|
|
||||||
class Bootstrap {
|
class Bootstrap {
|
||||||
@@ -33,6 +34,9 @@ class Bootstrap {
|
|||||||
MediaUpload::init();
|
MediaUpload::init();
|
||||||
PushNotificationHandler::init();
|
PushNotificationHandler::init();
|
||||||
|
|
||||||
|
// Activity Log
|
||||||
|
ActivityLogTable::create_table();
|
||||||
|
|
||||||
// Addon system (order matters: Registry → Routes → Navigation)
|
// Addon system (order matters: Registry → Routes → Navigation)
|
||||||
AddonRegistry::init();
|
AddonRegistry::init();
|
||||||
RouteRegistry::init();
|
RouteRegistry::init();
|
||||||
|
|||||||
Reference in New Issue
Block a user