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:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user