feat: Add Store link to admin header and notification activity log
- Add Store link to admin header (visible when customer SPA is enabled) - Add storeUrl and customerSpaEnabled to WNW_CONFIG in Assets.php and StandaloneAdmin.php - Update window.d.ts with new WNW_CONFIG properties - Create ActivityLog.tsx component with search, filters, and pagination - Add /notifications/logs API endpoint to NotificationsController - Update Notifications.tsx to link to activity log page - Add ActivityLog route to App.tsx
This commit is contained in:
@@ -243,6 +243,7 @@ import EmailConfiguration from '@/routes/Settings/Notifications/EmailConfigurati
|
|||||||
import PushConfiguration from '@/routes/Settings/Notifications/PushConfiguration';
|
import PushConfiguration from '@/routes/Settings/Notifications/PushConfiguration';
|
||||||
import EmailCustomization from '@/routes/Settings/Notifications/EmailCustomization';
|
import EmailCustomization from '@/routes/Settings/Notifications/EmailCustomization';
|
||||||
import EditTemplate from '@/routes/Settings/Notifications/EditTemplate';
|
import EditTemplate from '@/routes/Settings/Notifications/EditTemplate';
|
||||||
|
import ActivityLog from '@/routes/Settings/Notifications/ActivityLog';
|
||||||
import SettingsDeveloper from '@/routes/Settings/Developer';
|
import SettingsDeveloper from '@/routes/Settings/Developer';
|
||||||
import SettingsModules from '@/routes/Settings/Modules';
|
import SettingsModules from '@/routes/Settings/Modules';
|
||||||
import ModuleSettings from '@/routes/Settings/ModuleSettings';
|
import ModuleSettings from '@/routes/Settings/ModuleSettings';
|
||||||
@@ -462,6 +463,17 @@ function Header({ onFullscreen, fullscreen, showToggle = true, scrollContainerRe
|
|||||||
>
|
>
|
||||||
<span>{__('WordPress')}</span>
|
<span>{__('WordPress')}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{window.WNW_CONFIG?.customerSpaEnabled && (
|
||||||
|
<a
|
||||||
|
href={window.WNW_CONFIG?.storeUrl || '/store/'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
||||||
|
title="Open Store"
|
||||||
|
>
|
||||||
|
<span>{__('Store')}</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
||||||
@@ -471,6 +483,17 @@ function Header({ onFullscreen, fullscreen, showToggle = true, scrollContainerRe
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!isStandalone && window.WNW_CONFIG?.customerSpaEnabled && (
|
||||||
|
<a
|
||||||
|
href={window.WNW_CONFIG?.storeUrl || '/store/'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 border rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground"
|
||||||
|
title="Open Store"
|
||||||
|
>
|
||||||
|
<span>{__('Store')}</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
{showToggle && (
|
{showToggle && (
|
||||||
<button
|
<button
|
||||||
@@ -560,6 +583,7 @@ function AppRoutes() {
|
|||||||
<Route path="/settings/notifications/channels/push" element={<PushConfiguration />} />
|
<Route path="/settings/notifications/channels/push" element={<PushConfiguration />} />
|
||||||
<Route path="/settings/notifications/email-customization" element={<EmailCustomization />} />
|
<Route path="/settings/notifications/email-customization" element={<EmailCustomization />} />
|
||||||
<Route path="/settings/notifications/edit-template" element={<EditTemplate />} />
|
<Route path="/settings/notifications/edit-template" element={<EditTemplate />} />
|
||||||
|
<Route path="/settings/notifications/activity-log" element={<ActivityLog />} />
|
||||||
<Route path="/settings/brand" element={<SettingsIndex />} />
|
<Route path="/settings/brand" element={<SettingsIndex />} />
|
||||||
<Route path="/settings/developer" element={<SettingsDeveloper />} />
|
<Route path="/settings/developer" element={<SettingsDeveloper />} />
|
||||||
<Route path="/settings/modules" element={<SettingsModules />} />
|
<Route path="/settings/modules" element={<SettingsModules />} />
|
||||||
|
|||||||
@@ -208,9 +208,6 @@ export default function OrderShow() {
|
|||||||
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printLabel} title={__('Print shipping label')}>
|
<button className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" onClick={printLabel} title={__('Print shipping label')}>
|
||||||
<Ticket className="w-4 h-4" /> {__('Label')}
|
<Ticket className="w-4 h-4" /> {__('Label')}
|
||||||
</button>
|
</button>
|
||||||
<Link className="border rounded-md px-3 py-2 text-sm flex items-center gap-2 no-print" to={`/orders`} title={__('Back to orders list')}>
|
|
||||||
<ExternalLink className="w-4 h-4" /> {__('Orders')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -154,12 +154,14 @@ export default function NotificationsSettings() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-between pt-2">
|
<div className="flex items-center justify-between pt-2">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{__('Coming soon')}
|
{__('Sent, Failed, Pending')}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" disabled>
|
<Link to="/settings/notifications/activity-log">
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
{__('View Log')}
|
{__('View Log')}
|
||||||
<ChevronRight className="ml-2 h-4 w-4" />
|
<ChevronRight className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
241
admin-spa/src/routes/Settings/Notifications/ActivityLog.tsx
Normal file
241
admin-spa/src/routes/Settings/Notifications/ActivityLog.tsx
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { __ } from '@/lib/i18n';
|
||||||
|
import { SettingsLayout } from '../components/SettingsLayout';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Mail,
|
||||||
|
Bell,
|
||||||
|
MessageCircle,
|
||||||
|
Send,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
RefreshCw,
|
||||||
|
Filter,
|
||||||
|
Search
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
|
interface NotificationLogEntry {
|
||||||
|
id: number;
|
||||||
|
channel: 'email' | 'push' | 'whatsapp' | 'telegram';
|
||||||
|
event: string;
|
||||||
|
recipient: string;
|
||||||
|
subject?: string;
|
||||||
|
status: 'sent' | 'failed' | 'pending' | 'queued';
|
||||||
|
created_at: string;
|
||||||
|
sent_at?: string;
|
||||||
|
error_message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationLogsResponse {
|
||||||
|
logs: NotificationLogEntry[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelIcons: Record<string, React.ReactNode> = {
|
||||||
|
email: <Mail className="h-4 w-4" />,
|
||||||
|
push: <Bell className="h-4 w-4" />,
|
||||||
|
whatsapp: <MessageCircle className="h-4 w-4" />,
|
||||||
|
telegram: <Send className="h-4 w-4" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusConfig: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
|
||||||
|
sent: { icon: <CheckCircle2 className="h-4 w-4" />, color: 'text-green-600 bg-green-50', label: 'Sent' },
|
||||||
|
failed: { icon: <XCircle className="h-4 w-4" />, color: 'text-red-600 bg-red-50', label: 'Failed' },
|
||||||
|
pending: { icon: <Clock className="h-4 w-4" />, color: 'text-yellow-600 bg-yellow-50', label: 'Pending' },
|
||||||
|
queued: { icon: <RefreshCw className="h-4 w-4" />, color: 'text-blue-600 bg-blue-50', label: 'Queued' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ActivityLog() {
|
||||||
|
const [search, setSearch] = React.useState('');
|
||||||
|
const [channelFilter, setChannelFilter] = React.useState('all');
|
||||||
|
const [statusFilter, setStatusFilter] = React.useState('all');
|
||||||
|
const [page, setPage] = React.useState(1);
|
||||||
|
|
||||||
|
const { data, isLoading, error, refetch } = useQuery<NotificationLogsResponse>({
|
||||||
|
queryKey: ['notification-logs', page, channelFilter, statusFilter, search],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set('page', page.toString());
|
||||||
|
params.set('per_page', '20');
|
||||||
|
if (channelFilter !== 'all') params.set('channel', channelFilter);
|
||||||
|
if (statusFilter !== 'all') params.set('status', statusFilter);
|
||||||
|
if (search) params.set('search', search);
|
||||||
|
return api.get(`/notifications/logs?${params.toString()}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
return new Date(dateStr).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsLayout
|
||||||
|
title={__('Activity Log')}
|
||||||
|
description={__('View notification history and delivery status')}
|
||||||
|
action={
|
||||||
|
<Link to="/settings/notifications">
|
||||||
|
<Button variant="outline">
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
{__('Back')}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Filters */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder={__('Search by recipient or subject...')}
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className="!pl-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select value={channelFilter} onValueChange={setChannelFilter}>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder={__('Channel')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{__('All Channels')}</SelectItem>
|
||||||
|
<SelectItem value="email">{__('Email')}</SelectItem>
|
||||||
|
<SelectItem value="push">{__('Push')}</SelectItem>
|
||||||
|
<SelectItem value="whatsapp">{__('WhatsApp')}</SelectItem>
|
||||||
|
<SelectItem value="telegram">{__('Telegram')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-[150px]">
|
||||||
|
<SelectValue placeholder={__('Status')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">{__('All Status')}</SelectItem>
|
||||||
|
<SelectItem value="sent">{__('Sent')}</SelectItem>
|
||||||
|
<SelectItem value="failed">{__('Failed')}</SelectItem>
|
||||||
|
<SelectItem value="pending">{__('Pending')}</SelectItem>
|
||||||
|
<SelectItem value="queued">{__('Queued')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="outline" size="icon" onClick={() => refetch()}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Activity Log Table */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{__('Recent Activity')}</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{data?.total ? `${data.total} ${__('notifications found')}` : __('Loading...')}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<RefreshCw className="h-8 w-8 mx-auto mb-2 animate-spin" />
|
||||||
|
<p>{__('Loading activity log...')}</p>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<XCircle className="h-8 w-8 mx-auto mb-2 text-red-500" />
|
||||||
|
<p>{__('Failed to load activity log')}</p>
|
||||||
|
<Button variant="outline" className="mt-4" onClick={() => refetch()}>
|
||||||
|
{__('Try Again')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : !data?.logs?.length ? (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<Bell className="h-12 w-12 mx-auto mb-2 opacity-30" />
|
||||||
|
<p className="text-lg font-medium">{__('No notifications yet')}</p>
|
||||||
|
<p className="text-sm">{__('Notification activities will appear here once sent.')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{data.logs.map((log) => (
|
||||||
|
<div
|
||||||
|
key={log.id}
|
||||||
|
className="flex items-start gap-4 p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* Channel Icon */}
|
||||||
|
<div className="p-2 bg-muted rounded-lg">
|
||||||
|
{channelIcons[log.channel] || <Bell className="h-4 w-4" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="font-medium truncate">{log.event}</span>
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs ${statusConfig[log.status]?.color || 'text-gray-600 bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
{statusConfig[log.status]?.icon}
|
||||||
|
{statusConfig[log.status]?.label || log.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground truncate">
|
||||||
|
{__('To')}: {log.recipient}
|
||||||
|
{log.subject && ` — ${log.subject}`}
|
||||||
|
</p>
|
||||||
|
{log.error_message && (
|
||||||
|
<p className="text-sm text-red-600 mt-1">
|
||||||
|
{__('Error')}: {log.error_message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timestamp */}
|
||||||
|
<div className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
|
{formatDate(log.sent_at || log.created_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{data.total > 20 && (
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page <= 1}
|
||||||
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
{__('Previous')}
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{__('Page')} {page} {__('of')} {Math.ceil(data.total / 20)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page >= Math.ceil(data.total / 20)}
|
||||||
|
onClick={() => setPage(p => p + 1)}
|
||||||
|
>
|
||||||
|
{__('Next')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</SettingsLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
admin-spa/src/types/window.d.ts
vendored
4
admin-spa/src/types/window.d.ts
vendored
@@ -41,6 +41,10 @@ interface WNW_CONFIG {
|
|||||||
decimalSeparator: string;
|
decimalSeparator: string;
|
||||||
decimals: number;
|
decimals: number;
|
||||||
};
|
};
|
||||||
|
storeUrl?: string;
|
||||||
|
customerSpaEnabled?: boolean;
|
||||||
|
nonce?: string;
|
||||||
|
pluginUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ class Assets
|
|||||||
'wpAdminUrl' => admin_url('admin.php?page=woonoow'),
|
'wpAdminUrl' => admin_url('admin.php?page=woonoow'),
|
||||||
'isAuthenticated' => is_user_logged_in(),
|
'isAuthenticated' => is_user_logged_in(),
|
||||||
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
||||||
|
'storeUrl' => home_url('/store/'),
|
||||||
|
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
||||||
]);
|
]);
|
||||||
wp_add_inline_script($handle, 'window.WNW_CONFIG = window.WNW_CONFIG || WNW_CONFIG;', 'after');
|
wp_add_inline_script($handle, 'window.WNW_CONFIG = window.WNW_CONFIG || WNW_CONFIG;', 'after');
|
||||||
|
|
||||||
@@ -195,6 +197,8 @@ class Assets
|
|||||||
'wpAdminUrl' => admin_url('admin.php?page=woonoow'),
|
'wpAdminUrl' => admin_url('admin.php?page=woonoow'),
|
||||||
'isAuthenticated' => is_user_logged_in(),
|
'isAuthenticated' => is_user_logged_in(),
|
||||||
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
||||||
|
'storeUrl' => home_url('/store/'),
|
||||||
|
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// WordPress REST API settings (for media upload compatibility)
|
// WordPress REST API settings (for media upload compatibility)
|
||||||
|
|||||||
@@ -132,7 +132,9 @@ class StandaloneAdmin {
|
|||||||
currentUser: <?php echo wp_json_encode( $current_user ); ?>,
|
currentUser: <?php echo wp_json_encode( $current_user ); ?>,
|
||||||
locale: <?php echo wp_json_encode( get_locale() ); ?>,
|
locale: <?php echo wp_json_encode( get_locale() ); ?>,
|
||||||
siteUrl: <?php echo wp_json_encode( home_url() ); ?>,
|
siteUrl: <?php echo wp_json_encode( home_url() ); ?>,
|
||||||
siteName: <?php echo wp_json_encode( get_bloginfo( 'name' ) ); ?>
|
siteName: <?php echo wp_json_encode( get_bloginfo( 'name' ) ); ?>,
|
||||||
|
storeUrl: <?php echo wp_json_encode( home_url( '/store/' ) ); ?>,
|
||||||
|
customerSpaEnabled: <?php echo get_option( 'woonoow_customer_spa_enabled', false ) ? 'true' : 'false'; ?>
|
||||||
};
|
};
|
||||||
|
|
||||||
// Also set WNW_API for API compatibility
|
// Also set WNW_API for API compatibility
|
||||||
|
|||||||
@@ -217,6 +217,15 @@ class NotificationsController {
|
|||||||
'permission_callback' => [$this, 'check_permission'],
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// GET /woonoow/v1/notifications/logs
|
||||||
|
register_rest_route($this->namespace, '/' . $this->rest_base . '/logs', [
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [$this, 'get_logs'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -872,4 +881,54 @@ class NotificationsController {
|
|||||||
),
|
),
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get notification activity logs
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request object
|
||||||
|
* @return WP_REST_Response
|
||||||
|
*/
|
||||||
|
public function get_logs(WP_REST_Request $request) {
|
||||||
|
$page = (int) $request->get_param('page') ?: 1;
|
||||||
|
$per_page = (int) $request->get_param('per_page') ?: 20;
|
||||||
|
$channel = $request->get_param('channel');
|
||||||
|
$status = $request->get_param('status');
|
||||||
|
$search = $request->get_param('search');
|
||||||
|
|
||||||
|
// Get logs from option (in a real app, use a custom table)
|
||||||
|
$all_logs = get_option('woonoow_notification_logs', []);
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if ($channel && $channel !== 'all') {
|
||||||
|
$all_logs = array_filter($all_logs, fn($log) => $log['channel'] === $channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status && $status !== 'all') {
|
||||||
|
$all_logs = array_filter($all_logs, fn($log) => $log['status'] === $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($search) {
|
||||||
|
$search_lower = strtolower($search);
|
||||||
|
$all_logs = array_filter($all_logs, function($log) use ($search_lower) {
|
||||||
|
return strpos(strtolower($log['recipient'] ?? ''), $search_lower) !== false ||
|
||||||
|
strpos(strtolower($log['subject'] ?? ''), $search_lower) !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by date descending
|
||||||
|
usort($all_logs, function($a, $b) {
|
||||||
|
return strtotime($b['created_at'] ?? '') - strtotime($a['created_at'] ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
$total = count($all_logs);
|
||||||
|
$offset = ($page - 1) * $per_page;
|
||||||
|
$logs = array_slice(array_values($all_logs), $offset, $per_page);
|
||||||
|
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'logs' => $logs,
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'per_page' => $per_page,
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user