806 lines
43 KiB
TypeScript
806 lines
43 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
import { ColumnDef } from "@tanstack/react-table";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { DataTable } from "@/components/data-table/data-table";
|
|
import { EmptyState } from "@/components/empty-state";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { useCampaignList, useCancelCampaign, useCampaignReport, useUpdateCampaign, useDeleteCampaign } from "@/modules/campaigns/hooks";
|
|
import { Campaign } from "@/modules/campaigns/schemas";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { MoreHorizontal, Plus, Send, Trash, Calendar, Filter, Eye, CheckCircle2, XCircle, Clock, AlertCircle, FileText, TrendingUp, Target, Users, RefreshCw } from "lucide-react";
|
|
import { formatDateTime } from "@/lib/utils";
|
|
|
|
export default function CampaignsPage() {
|
|
const router = useRouter();
|
|
const { toast } = useToast();
|
|
const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
|
|
const [isRefetching, setIsRefetching] = useState(false);
|
|
const { data, isLoading, refetch } = useCampaignList(statusFilter);
|
|
const cancelMutation = useCancelCampaign();
|
|
const updateMutation = useUpdateCampaign();
|
|
const deleteMutation = useDeleteCampaign();
|
|
const [cancelCampaign, setCancelCampaign] = useState<{ id: string; title: string } | null>(null);
|
|
const [selectedCampaign, setSelectedCampaign] = useState<Campaign | null>(null);
|
|
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
const [editFormData, setEditFormData] = useState<{ title?: string; content?: string; date?: string; status?: "pending" | "completed" | "cancelled" | "failed" }>({});
|
|
const [reportCampaignId, setReportCampaignId] = useState<string | null>(null);
|
|
const [isReportOpen, setIsReportOpen] = useState(false);
|
|
|
|
const handleRefetch = async () => {
|
|
setIsRefetching(true);
|
|
try {
|
|
await refetch();
|
|
} finally {
|
|
setIsRefetching(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async () => {
|
|
if (!cancelCampaign) return;
|
|
|
|
try {
|
|
await cancelMutation.mutateAsync(cancelCampaign.id);
|
|
toast({
|
|
title: "Campaign cancelled",
|
|
description: "Campaign has been cancelled successfully",
|
|
});
|
|
setCancelCampaign(null);
|
|
} catch (error) {
|
|
toast({
|
|
title: "Cancel failed",
|
|
description: "Failed to cancel campaign",
|
|
variant: "destructive",
|
|
});
|
|
setCancelCampaign(null);
|
|
}
|
|
};
|
|
|
|
const handleEdit = async () => {
|
|
if (!selectedCampaign || !editFormData.title) return;
|
|
|
|
try {
|
|
await updateMutation.mutateAsync({
|
|
id: selectedCampaign.UUID_ACP,
|
|
data: editFormData,
|
|
});
|
|
toast({
|
|
title: "Campaign updated",
|
|
description: "Campaign has been updated successfully",
|
|
});
|
|
setIsEditOpen(false);
|
|
setEditFormData({});
|
|
refetch();
|
|
} catch (error) {
|
|
toast({
|
|
title: "Update failed",
|
|
description: "Failed to update campaign",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (campaignId: string) => {
|
|
try {
|
|
await deleteMutation.mutateAsync(campaignId);
|
|
toast({
|
|
title: "Campaign deleted",
|
|
description: "Campaign has been cancelled successfully",
|
|
});
|
|
refetch();
|
|
} catch (error) {
|
|
toast({
|
|
title: "Delete failed",
|
|
description: "Failed to delete campaign",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
const getStatusBadge = (status: string | undefined) => {
|
|
if (!status) {
|
|
return <Badge variant="default">Unknown</Badge>;
|
|
}
|
|
const variants: Record<string, "default" | "secondary" | "destructive"> = {
|
|
pending: "secondary",
|
|
completed: "default",
|
|
cancelled: "destructive",
|
|
failed: "destructive",
|
|
};
|
|
return (
|
|
<Badge variant={variants[status] || "default"}>
|
|
{status.charAt(0).toUpperCase() + status.slice(1)}
|
|
</Badge>
|
|
);
|
|
};
|
|
|
|
const columns: ColumnDef<Campaign>[] = [
|
|
{
|
|
accessorKey: "Title_ACP",
|
|
header: "Title",
|
|
cell: ({ row }) => (
|
|
<div className="font-medium">{row.original.Title_ACP || "-"}</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "Content_ACP",
|
|
header: "Content",
|
|
cell: ({ row }) => (
|
|
<div className="max-w-[200px] truncate text-muted-foreground">
|
|
{row.original.Content_ACP || "-"}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "Date_ACP",
|
|
header: "Scheduled Date",
|
|
cell: ({ row }) => row.original.Date_ACP ? formatDateTime(row.original.Date_ACP) : "-",
|
|
},
|
|
{
|
|
accessorKey: "Status_ACP",
|
|
header: "Status",
|
|
cell: ({ row }) => getStatusBadge(row.original.Status_ACP),
|
|
},
|
|
{
|
|
accessorKey: "TargetUsers_ACP",
|
|
header: "Target",
|
|
cell: ({ row }) => (
|
|
<div className="text-center font-mono">{row.original.TargetUsers_ACP}</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "SentCount_ACP",
|
|
header: "Sent",
|
|
cell: ({ row }) => (
|
|
<div className="text-center font-mono">{row.original.SentCount_ACP}</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "DeliveryRate_ACP",
|
|
header: "Delivery Rate",
|
|
cell: ({ row }) => {
|
|
const rate = row.original.DeliveryRate_ACP;
|
|
return (
|
|
<div className="text-center">
|
|
<span className={`font-semibold ${rate >= 90 ? 'text-green-600' : rate >= 70 ? 'text-yellow-600' : 'text-red-600'}`}>
|
|
{rate}%
|
|
</span>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "CreatedAt_ACP",
|
|
header: "Created",
|
|
cell: ({ row }) => (
|
|
<div className="text-sm text-muted-foreground">
|
|
{row.original.CreatedAt_ACP ? formatDateTime(row.original.CreatedAt_ACP) : "-"}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setSelectedCampaign(row.original);
|
|
setIsDetailOpen(true);
|
|
}}
|
|
>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
View Details
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setReportCampaignId(row.original.UUID_ACP);
|
|
setIsReportOpen(true);
|
|
}}
|
|
>
|
|
<FileText className="mr-2 h-4 w-4" />
|
|
View Report
|
|
</DropdownMenuItem>
|
|
{row.original.Status_ACP === "pending" && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setSelectedCampaign(row.original);
|
|
setEditFormData({
|
|
title: row.original.Title_ACP,
|
|
content: row.original.Content_ACP,
|
|
date: row.original.Date_ACP,
|
|
});
|
|
setIsEditOpen(true);
|
|
}}
|
|
>
|
|
<MoreHorizontal className="mr-2 h-4 w-4" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => setCancelCampaign({ id: row.original.UUID_ACP, title: row.original.Title_ACP })}
|
|
className="text-destructive"
|
|
>
|
|
<Trash className="mr-2 h-4 w-4" />
|
|
Cancel
|
|
</DropdownMenuItem>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
),
|
|
},
|
|
];
|
|
|
|
if (isLoading) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
const campaigns = data?.data || [];
|
|
|
|
const filterLabel = statusFilter
|
|
? statusFilter.charAt(0).toUpperCase() + statusFilter.slice(1)
|
|
: "All Status";
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Campaign Management</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage and schedule notification campaigns
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => handleRefetch()}
|
|
title="Refresh campaigns"
|
|
disabled={isRefetching || isLoading}
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isRefetching || isLoading ? 'animate-spin-smooth' : ''}`} />
|
|
</Button>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline">
|
|
<Filter className="mr-2 h-4 w-4" />
|
|
{filterLabel}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => setStatusFilter(undefined)}>
|
|
All Status
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter("pending")}>
|
|
Pending
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter("completed")}>
|
|
Completed
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter("cancelled")}>
|
|
Cancelled
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setStatusFilter("failed")}>
|
|
Failed
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => router.push("/campaigns/send")}
|
|
>
|
|
<Send className="mr-2 h-4 w-4" />
|
|
Send Single
|
|
</Button>
|
|
<Button onClick={() => router.push("/campaigns/setup")}>
|
|
<Calendar className="mr-2 h-4 w-4" />
|
|
Setup Campaign
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{campaigns.length === 0 ? (
|
|
<EmptyState
|
|
title="No campaigns yet"
|
|
description="Create your first campaign to get started"
|
|
action={
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => router.push("/campaigns/send")}
|
|
>
|
|
<Send className="mr-2 h-4 w-4" />
|
|
Send Single
|
|
</Button>
|
|
<Button onClick={() => router.push("/campaigns/setup")}>
|
|
<Calendar className="mr-2 h-4 w-4" />
|
|
Setup Campaign
|
|
</Button>
|
|
</div>
|
|
}
|
|
/>
|
|
) : (
|
|
<DataTable columns={columns} data={campaigns} searchKey="Title_ACP" />
|
|
)}
|
|
|
|
<AlertDialog open={!!cancelCampaign} onOpenChange={(open) => !open && setCancelCampaign(null)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Cancel Campaign</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to cancel the campaign "{cancelCampaign?.title}"? This action cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Keep Campaign</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleCancel}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
Cancel Campaign
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Campaign</DialogTitle>
|
|
<DialogDescription>
|
|
Update campaign details
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{selectedCampaign && (
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Title</label>
|
|
<input
|
|
type="text"
|
|
value={editFormData.title || ""}
|
|
onChange={(e) => setEditFormData({ ...editFormData, title: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-md border-input bg-background"
|
|
placeholder="Campaign title"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Content</label>
|
|
<textarea
|
|
value={editFormData.content || ""}
|
|
onChange={(e) => setEditFormData({ ...editFormData, content: e.target.value })}
|
|
className="w-full px-3 py-2 border rounded-md border-input bg-background min-h-24"
|
|
placeholder="Campaign content"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium">Scheduled Date</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={editFormData.date ? new Date(editFormData.date).toISOString().slice(0, 16) : ""}
|
|
onChange={(e) => setEditFormData({ ...editFormData, date: new Date(e.target.value).toISOString() })}
|
|
className="w-full px-3 py-2 border rounded-md border-input bg-background"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2 justify-end mt-6">
|
|
<Button variant="outline" onClick={() => setIsEditOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleEdit} disabled={updateMutation.isPending}>
|
|
{updateMutation.isPending ? "Updating..." : "Update Campaign"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
|
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
Campaign Details
|
|
{selectedCampaign && getStatusBadge(selectedCampaign.Status_ACP)}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Complete information about this campaign
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{selectedCampaign && (
|
|
<div className="space-y-6">
|
|
{/* Basic Info */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Basic Information</h3>
|
|
<div className="grid gap-3">
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<span className="text-sm text-muted-foreground">Campaign ID:</span>
|
|
<span className="col-span-2 text-sm font-mono">{selectedCampaign.UUID_ACP}</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<span className="text-sm text-muted-foreground">Title:</span>
|
|
<span className="col-span-2 text-sm font-medium">{selectedCampaign.Title_ACP}</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<span className="text-sm text-muted-foreground">Content:</span>
|
|
<span className="col-span-2 text-sm">{selectedCampaign.Content_ACP}</span>
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<span className="text-sm text-muted-foreground">Scheduled:</span>
|
|
<span className="col-span-2 text-sm">{formatDateTime(selectedCampaign.Date_ACP)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delivery Stats */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Delivery Statistics</h3>
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Target Users</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold">{selectedCampaign.TargetUsers_ACP}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Send className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Sent Count</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold">{selectedCampaign.SentCount_ACP}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
<span className="text-xs text-muted-foreground">Success</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold text-green-600">{selectedCampaign.SuccessCount_ACP}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<XCircle className="h-4 w-4 text-red-600" />
|
|
<span className="text-xs text-muted-foreground">Failures</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold text-red-600">{selectedCampaign.FailureCount_ACP}</p>
|
|
</div>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">Delivery Rate</span>
|
|
<span className={`text-2xl font-bold ${selectedCampaign.DeliveryRate_ACP >= 90 ? 'text-green-600' :
|
|
selectedCampaign.DeliveryRate_ACP >= 70 ? 'text-yellow-600' :
|
|
'text-red-600'
|
|
}`}>
|
|
{selectedCampaign.DeliveryRate_ACP}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timestamps */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Timeline</h3>
|
|
<div className="grid gap-2">
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground">Created:</span>
|
|
<span className="font-mono">{formatDateTime(selectedCampaign.CreatedAt_ACP)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground">Updated:</span>
|
|
<span className="font-mono">{formatDateTime(selectedCampaign.UpdatedAt_ACP)}</span>
|
|
</div>
|
|
{selectedCampaign.SentAt_ACP && (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<Send className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground">Sent At:</span>
|
|
<span className="font-mono">{formatDateTime(selectedCampaign.SentAt_ACP)}</span>
|
|
</div>
|
|
)}
|
|
{selectedCampaign.CompletedAt_ACP && (
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
<span className="text-muted-foreground">Completed:</span>
|
|
<span className="font-mono">{formatDateTime(selectedCampaign.CompletedAt_ACP)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{selectedCampaign.ErrorMessage_ACP && (
|
|
<div className="space-y-2">
|
|
<h3 className="text-sm font-semibold text-muted-foreground flex items-center gap-2">
|
|
<AlertCircle className="h-4 w-4 text-red-600" />
|
|
Error Message
|
|
</h3>
|
|
<div className="glass border border-red-600/20 rounded-lg p-4 bg-red-50/50 dark:bg-red-950/20">
|
|
<p className="text-sm text-red-600 dark:text-red-400 font-mono">
|
|
{selectedCampaign.ErrorMessage_ACP}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<CampaignReportDialog
|
|
campaignId={reportCampaignId}
|
|
isOpen={isReportOpen}
|
|
onClose={() => {
|
|
setIsReportOpen(false);
|
|
setReportCampaignId(null);
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CampaignReportDialog({ campaignId, isOpen, onClose }: { campaignId: string | null; isOpen: boolean; onClose: () => void }) {
|
|
const { data: reportData, isLoading } = useCampaignReport(campaignId || "");
|
|
const report = reportData?.data;
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-4xl max-h-[85vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<FileText className="h-5 w-5" />
|
|
Campaign Report
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Detailed performance and delivery report
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="text-center space-y-4">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
|
<p className="text-sm text-muted-foreground">Loading report...</p>
|
|
</div>
|
|
</div>
|
|
) : report ? (
|
|
<div className="space-y-6">
|
|
{/* Campaign Info */}
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-lg font-semibold">{report.campaign.title}</h3>
|
|
<p className="text-sm text-muted-foreground mt-1">{report.campaign.content}</p>
|
|
</div>
|
|
<Badge variant={report.campaign.status === "completed" ? "default" : report.campaign.status === "failed" ? "destructive" : "secondary"}>
|
|
{report.campaign.status}
|
|
</Badge>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-muted-foreground">Campaign ID:</span>
|
|
<p className="font-mono mt-1">{report.campaign.id}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-muted-foreground">Scheduled Date:</span>
|
|
<p className="mt-1">{formatDateTime(report.campaign.scheduledDate)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metrics */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Performance Metrics</h3>
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Lead Time</span>
|
|
</div>
|
|
<p className="text-xl font-semibold">{report.metrics.leadTime}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Execution Time</span>
|
|
</div>
|
|
<p className="text-xl font-semibold">{report.metrics.executionTime}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Target className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Time Until Execution</span>
|
|
</div>
|
|
<p className="text-xl font-semibold">{report.metrics.timeUntilExecution}</p>
|
|
</div>
|
|
</div>
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<div className="glass border rounded-lg p-3 flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">Scheduled</span>
|
|
<Badge variant={report.metrics.isScheduled ? "default" : "secondary"}>
|
|
{report.metrics.isScheduled ? "Yes" : "No"}
|
|
</Badge>
|
|
</div>
|
|
<div className="glass border rounded-lg p-3 flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">Overdue</span>
|
|
<Badge variant={report.metrics.isOverdue ? "destructive" : "default"}>
|
|
{report.metrics.isOverdue ? "Yes" : "No"}
|
|
</Badge>
|
|
</div>
|
|
<div className="glass border rounded-lg p-3 flex items-center justify-between">
|
|
<span className="text-sm text-muted-foreground">Avg Delivery</span>
|
|
<span className="text-sm font-semibold">{report.metrics.avgDeliveryTime}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delivery Stats */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Delivery Statistics</h3>
|
|
<div className="grid gap-3 md:grid-cols-4">
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Target Users</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold">{report.delivery.targetUsers}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Send className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-xs text-muted-foreground">Sent</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold">{report.delivery.sentCount}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
<span className="text-xs text-muted-foreground">Success</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold text-green-600">{report.delivery.successCount}</p>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<XCircle className="h-4 w-4 text-red-600" />
|
|
<span className="text-xs text-muted-foreground">Failed</span>
|
|
</div>
|
|
<p className="text-2xl font-semibold text-red-600">{report.delivery.failureCount}</p>
|
|
</div>
|
|
</div>
|
|
<div className="glass border rounded-lg p-4">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium">Overall Delivery Rate</span>
|
|
<span className="text-2xl font-bold">{report.delivery.deliveryRate}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Breakdown */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Status Breakdown</h3>
|
|
<div className="grid gap-3 md:grid-cols-4">
|
|
<div className="border rounded-lg p-3 text-center">
|
|
<p className="text-2xl font-semibold">{report.delivery.statusBreakdown.pending}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Pending</p>
|
|
</div>
|
|
<div className="border rounded-lg p-3 text-center">
|
|
<p className="text-2xl font-semibold">{report.delivery.statusBreakdown.sent}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Sent</p>
|
|
</div>
|
|
<div className="border rounded-lg p-3 text-center">
|
|
<p className="text-2xl font-semibold text-green-600">{report.delivery.statusBreakdown.delivered}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Delivered</p>
|
|
</div>
|
|
<div className="border rounded-lg p-3 text-center">
|
|
<p className="text-2xl font-semibold text-red-600">{report.delivery.statusBreakdown.failed}</p>
|
|
<p className="text-xs text-muted-foreground mt-1">Failed</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timeline */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Timeline</h3>
|
|
<div className="space-y-2 border rounded-lg p-4">
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground w-24">Created:</span>
|
|
<span className="font-mono">{formatDateTime(report.timeline.created)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground w-24">Scheduled:</span>
|
|
<span className="font-mono">{formatDateTime(report.timeline.scheduled)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground w-24">Executed:</span>
|
|
<span className="font-mono">{formatDateTime(report.timeline.executed)}</span>
|
|
</div>
|
|
{report.timeline.sent && (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<Send className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground w-24">Sent:</span>
|
|
<span className="font-mono">{formatDateTime(report.timeline.sent)}</span>
|
|
</div>
|
|
)}
|
|
{report.timeline.completed && (
|
|
<div className="flex items-center gap-3 text-sm">
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
<span className="text-muted-foreground w-24">Completed:</span>
|
|
<span className="font-mono">{formatDateTime(report.timeline.completed)}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Delivery Records */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground">Delivery Records</h3>
|
|
<div className="glass border rounded-lg p-4 text-center">
|
|
<p className="text-3xl font-bold">{report.deliveryRecords.total}</p>
|
|
<p className="text-sm text-muted-foreground mt-1">Total Delivery Records</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{report.campaign.errorMessage && (
|
|
<div className="space-y-2">
|
|
<h3 className="text-sm font-semibold text-muted-foreground flex items-center gap-2">
|
|
<AlertCircle className="h-4 w-4 text-red-600" />
|
|
Error Message
|
|
</h3>
|
|
<div className="glass border border-red-600/20 rounded-lg p-4 bg-red-50/50 dark:bg-red-950/20">
|
|
<p className="text-sm text-red-600 dark:text-red-400 font-mono">
|
|
{report.campaign.errorMessage}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No report data available
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|