Yaltopia-Ticket-Admin/src/pages/notifications/index.tsx
2026-06-11 10:48:11 +03:00

563 lines
24 KiB
TypeScript

import { useState, useMemo } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
import {
Search,
CheckCheck,
Send,
BellRing,
Mail,
MessageSquare,
History,
Target,
ArrowRight,
Loader2,
Calendar,
} from "lucide-react";
import { notificationService } from "@/services/notification.service";
import type {
SendPushNotificationRequest,
SendSmsNotificationRequest,
SendEmailNotificationRequest,
} from "@/services/notification.service";
import { toast } from "sonner";
import { format } from "date-fns";
import { cn } from "@/lib/utils";
type Channel = "PUSH" | "SMS" | "EMAIL";
export default function NotificationsPage() {
const queryClient = useQueryClient();
const [searchQuery, setSearchQuery] = useState("");
const [activeChannel, setActiveChannel] = useState<Channel>("PUSH");
const [isSendModalOpen, setIsSendModalOpen] = useState(false);
// Combined form state
const [pushForm, setPushForm] = useState<SendPushNotificationRequest>({
title: "",
body: "",
recipientId: "",
url: "",
icon: "/assets/icon.png",
});
const [smsForm, setSmsForm] = useState<SendSmsNotificationRequest>({
body: "",
recipientPhone: "",
});
const [emailForm, setEmailForm] = useState<SendEmailNotificationRequest>({
subject: "",
body: "",
recipientEmail: "",
});
const { data: notifications, isLoading } = useQuery({
queryKey: ["notifications"],
queryFn: () => notificationService.getNotifications(),
});
const { data: unreadCount } = useQuery({
queryKey: ["notifications", "unread-count"],
queryFn: () => notificationService.getUnreadCount(),
});
const pushMutation = useMutation({
mutationFn: (data: SendPushNotificationRequest) =>
notificationService.sendPushNotification(data),
onSuccess: () => {
toast.success("Network transmission: Push packet delivered to gateway");
setIsSendModalOpen(false);
queryClient.invalidateQueries({ queryKey: ["notifications"] });
},
});
const smsMutation = useMutation({
mutationFn: (data: SendSmsNotificationRequest) =>
notificationService.sendSmsNotification(data),
onSuccess: () => {
toast.success("Cellular uplink: SMS payload queued for broadcast");
setIsSendModalOpen(false);
queryClient.invalidateQueries({ queryKey: ["notifications"] });
},
});
const emailMutation = useMutation({
mutationFn: (data: SendEmailNotificationRequest) =>
notificationService.sendEmailNotification(data),
onSuccess: () => {
toast.success("SMTP Handshake: Email broadcast initiated");
setIsSendModalOpen(false);
queryClient.invalidateQueries({ queryKey: ["notifications"] });
},
});
const filteredNotifications = useMemo(() => {
if (!notifications) return [];
return notifications.filter((n) => {
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
return (
n.title?.toLowerCase().includes(q) || n.body.toLowerCase().includes(q)
);
});
}, [notifications, searchQuery]);
const handleSend = () => {
if (activeChannel === "PUSH") pushMutation.mutate(pushForm);
else if (activeChannel === "SMS") smsMutation.mutate(smsForm);
else if (activeChannel === "EMAIL") emailMutation.mutate(emailForm);
};
const isPending =
pushMutation.isPending || smsMutation.isPending || emailMutation.isPending;
return (
<div className="space-y-8 animate-in fade-in duration-500">
{/* Header Section */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 pb-2">
<div className="space-y-1">
<div className="flex items-center gap-2 text-primary mb-1">
<div className="p-2 bg-primary/10 rounded-lg">
<BellRing className="w-5 h-5" />
</div>
<span className="text-xs font-black uppercase tracking-widest opacity-70">
Messaging Hub
</span>
</div>
<h1 className="text-4xl font-black tracking-tighter text-slate-900 uppercase italic">
Command <span className="text-primary NOT-italic">Center</span>
</h1>
<p className="text-slate-500 text-sm font-medium max-w-xl leading-relaxed">
Dispatch multi-channel broadcasts and monitor real-time network
telemetry across the Yaltopia mesh.
</p>
</div>
<div className="flex items-center gap-3">
<Button
variant="ghost"
className="h-12 px-6 rounded-2xl text-slate-400 hover:text-slate-900 hover:bg-slate-100 font-black uppercase text-[10px] tracking-widest transition-all"
onClick={() => notificationService.markAllAsRead()}
>
<CheckCheck className="w-4 h-4 mr-2" />
Clear Signal
</Button>
<Button
className="h-12 px-8 rounded-2xl bg-slate-900 hover:bg-slate-800 text-white font-black uppercase text-xs tracking-[0.1em] shadow-xl shadow-slate-200 transition-all hover:-translate-y-0.5"
onClick={() => setIsSendModalOpen(true)}
>
<Send className="h-4 w-4 mr-2" />
New Broadcast
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Stats / Quick Info */}
<div className="lg:col-span-1 space-y-6">
<Card className="border-none shadow-[0_8px_30px_rgb(0,0,0,0.04)] bg-slate-900 text-white rounded-3xl overflow-hidden p-8">
<div className="space-y-6">
<div className="flex items-center justify-between">
<span className="text-[10px] font-black uppercase tracking-[0.2em] opacity-40">
System Status
</span>
<Badge className="bg-emerald-500 text-white border-none text-[10px] rounded-full px-2 py-0">
Active
</Badge>
</div>
<div>
<span className="text-5xl font-black italic tracking-tighter leading-none">
{unreadCount ?? 0}
</span>
<p className="text-slate-400 text-xs font-medium mt-2">
Active notifications in current window.
</p>
</div>
<div className="grid grid-cols-3 gap-4 border-t border-white/10 pt-6">
<div className="text-center">
<div className="text-lg font-black tracking-tight">
{notifications?.length ?? 0}
</div>
<div className="text-[8px] font-black uppercase tracking-widest opacity-40 mt-1 uppercase">
Push
</div>
</div>
<div className="text-center border-x border-white/10 px-2">
<div className="text-lg font-black tracking-tight"></div>
<div className="text-[8px] font-black uppercase tracking-widest opacity-40 mt-1 uppercase">
SMS
</div>
</div>
<div className="text-center">
<div className="text-lg font-black tracking-tight"></div>
<div className="text-[8px] font-black uppercase tracking-widest opacity-40 mt-1 uppercase">
Mail
</div>
</div>
</div>
</div>
</Card>
<Card className="border-none shadow-[0_8px_30px_rgb(0,0,0,0.04)] bg-white/50 backdrop-blur-xl rounded-3xl p-6">
<h3 className="text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 mb-4 px-2">
Operator Directives
</h3>
<div className="space-y-4">
{[
{
icon: Target,
label: "Audience Segmentation",
desc: "Filter by role",
},
{
icon: Calendar,
label: "Scheduled Dispatch",
desc: "Queue for later",
},
{
icon: History,
label: "Audit Integrity",
desc: "Full log access",
},
].map((item, idx) => (
<div
key={idx}
className="flex items-center gap-4 p-3 rounded-2xl hover:bg-slate-50 transition-colors group cursor-default"
>
<div className="p-2.5 bg-slate-100 rounded-xl group-hover:bg-primary/10 transition-colors">
<item.icon className="w-4 h-4 text-slate-400 group-hover:text-primary transition-colors" />
</div>
<div>
<p className="text-xs font-black text-slate-900 tracking-tight">
{item.label}
</p>
<p className="text-[10px] text-slate-400 font-medium">
{item.desc}
</p>
</div>
</div>
))}
</div>
</Card>
</div>
{/* History Feed */}
<div className="lg:col-span-2 space-y-6">
<Card className="border-none shadow-[0_8px_30px_rgb(0,0,0,0.04)] bg-white/50 backdrop-blur-xl rounded-3xl overflow-hidden">
<CardHeader className="p-8 border-b border-slate-100/50 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6 uppercase">
<h2 className="text-xs font-black tracking-[0.2em] text-slate-400">
Transmission Log
</h2>
<div className="relative group min-w-[280px]">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 group-focus-within:text-primary transition-colors" />
<Input
className="pl-11 h-11 bg-slate-50 border-slate-200/60 rounded-xl text-sm focus:bg-white transition-all shadow-none placeholder:text-slate-400 font-medium"
placeholder="Search signal history..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto min-h-[400px]">
<div className="p-4 space-y-4">
{isLoading ? (
Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="h-24 bg-slate-100/50 animate-pulse rounded-2xl"
/>
))
) : filteredNotifications.length ? (
filteredNotifications.map((n) => (
<div
key={n.id}
className="p-6 bg-white border border-slate-100 rounded-2xl hover:border-primary/20 hover:shadow-lg hover:shadow-primary/5 transition-all group relative overflow-hidden"
>
<div className="absolute top-0 right-0 w-1 p-1 h-full bg-slate-100 group-hover:bg-primary transition-colors" />
<div className="flex items-start justify-between gap-6">
<div className="flex gap-4">
<div className="p-3 bg-slate-50 rounded-xl group-hover:bg-primary/5 transition-colors">
<BellRing className="w-5 h-5 text-slate-400 group-hover:text-primary transition-colors" />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-black text-slate-900 tracking-tight">
{n.title}
</span>
<Badge
variant="outline"
className="text-[9px] font-black uppercase tracking-tighter opacity-50 px-1.5 py-0 border-slate-200"
>
Push
</Badge>
</div>
<p className="text-xs text-slate-500 font-medium leading-relaxed max-w-lg">
{n.body}
</p>
</div>
</div>
<div className="flex flex-col items-end gap-2 shrink-0">
<span className="text-[10px] font-bold text-slate-400">
{format(
new Date(n.createdAt),
"HH:mm · MMM d, yyyy",
)}
</span>
<Badge
className={cn(
"text-[9px] font-black uppercase tracking-widest rounded-lg px-2 border-none",
n.isSent
? "bg-emerald-500 text-white"
: "bg-slate-200 text-slate-500",
)}
>
{n.isSent ? "Delivered" : "Queued"}
</Badge>
</div>
</div>
</div>
))
) : (
<div className="py-24 text-center">
<div className="flex flex-col items-center justify-center space-y-4 opacity-20 grayscale">
<History className="w-16 h-16" />
<div className="flex flex-col">
<span className="text-sm font-black uppercase tracking-[0.2em]">
Zero Telemetry
</span>
<span className="text-xs font-medium italic mt-1">
No transmissions detected in the current signal
range.
</span>
</div>
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
</div>
{/* Dispatch Dialog */}
<Dialog open={isSendModalOpen} onOpenChange={setIsSendModalOpen}>
<DialogContent className="rounded-3xl max-w-2xl p-0 border-none shadow-2xl overflow-hidden">
<div className="p-8 bg-slate-900 text-white overflow-hidden relative">
{/* Decorative element */}
<div className="absolute -top-12 -right-12 w-48 h-48 bg-primary/20 rounded-full blur-3xl" />
<div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-white/10 rounded-xl">
<Target className="w-5 h-5 text-primary" />
</div>
<span className="text-[10px] font-black uppercase tracking-[0.2em] opacity-60 italic">
Signal Transmission
</span>
</div>
<DialogTitle className="text-3xl font-black italic tracking-tighter uppercase leading-none">
Dispatch{" "}
<span className="text-primary NOT-italic">Broadcast</span>
</DialogTitle>
<DialogDescription className="text-slate-400 text-xs font-medium mt-2 leading-relaxed">
Authoritative platform-wide signal broadcast. Choose delivery
channels and construct the payload with precision.
</DialogDescription>
</div>
<div className="p-8">
<div className="space-y-8">
{/* Channel Selector */}
<div className="space-y-4">
<Label className="text-[10px] font-black uppercase text-slate-400 tracking-widest">
Select Uplink Channels
</Label>
<div className="grid grid-cols-3 gap-4">
{[
{ id: "PUSH", icon: BellRing, label: "Push Notification" },
{ id: "SMS", icon: MessageSquare, label: "SMS Gateway" },
{ id: "EMAIL", icon: Mail, label: "Email Relay" },
].map((c) => (
<button
key={c.id}
type="button"
onClick={() => setActiveChannel(c.id as Channel)}
className={cn(
"flex flex-col items-center justify-center gap-3 p-6 rounded-2xl border-2 transition-all group",
activeChannel === c.id
? "border-primary bg-primary/5 text-primary shadow-lg shadow-primary/10"
: "border-slate-100 bg-white text-slate-400 hover:border-slate-200",
)}
>
<c.icon
className={cn(
"w-6 h-6 transition-transform group-active:scale-90",
activeChannel === c.id
? "text-primary"
: "text-slate-300",
)}
/>
<span className="text-[10px] font-black uppercase tracking-widest">
{c.id}
</span>
</button>
))}
</div>
</div>
<Separator className="bg-slate-100" />
{/* Dynamic Form Area */}
<div className="space-y-6">
{activeChannel === "PUSH" && (
<div className="space-y-4 animate-in slide-in-from-right-2 duration-300">
<div className="grid gap-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">
Notification Title
</Label>
<Input
value={pushForm.title}
onChange={(e) =>
setPushForm({ ...pushForm, title: e.target.value })
}
placeholder="Critical System Patch Available"
className="h-12 rounded-xl bg-slate-50 border-slate-200/60 font-bold"
/>
</div>
<div className="grid gap-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">
Message Body
</Label>
<Textarea
value={pushForm.body}
onChange={(e) =>
setPushForm({ ...pushForm, body: e.target.value })
}
placeholder="Update your client to version 4.2 now..."
className="min-h-[100px] rounded-xl bg-slate-50 border-slate-200/60"
/>
</div>
</div>
)}
{activeChannel === "SMS" && (
<div className="space-y-4 animate-in slide-in-from-right-2 duration-300">
<div className="grid gap-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">
Target Phone (Optional)
</Label>
<Input
value={smsForm.recipientPhone}
onChange={(e) =>
setSmsForm({
...smsForm,
recipientPhone: e.target.value,
})
}
placeholder="+1 (555) 000-0000"
className="h-12 rounded-xl bg-slate-50 border-slate-200/60 font-bold"
/>
<p className="text-[9px] text-slate-400 font-medium italic">
Leave empty for multi-user broadcast.
</p>
</div>
<div className="grid gap-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">
SMS Payload
</Label>
<Textarea
value={smsForm.body}
onChange={(e) =>
setSmsForm({ ...smsForm, body: e.target.value })
}
placeholder="Your Yaltopia ticket code is XYZ-123..."
className="min-h-[100px] rounded-xl bg-slate-50 border-slate-200/60"
/>
</div>
</div>
)}
{activeChannel === "EMAIL" && (
<div className="space-y-4 animate-in slide-in-from-right-2 duration-300">
<div className="grid gap-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">
Email Subject
</Label>
<Input
value={emailForm.subject}
onChange={(e) =>
setEmailForm({
...emailForm,
subject: e.target.value,
})
}
placeholder="Important Account Update"
className="h-12 rounded-xl bg-slate-50 border-slate-200/60 font-bold"
/>
</div>
<div className="grid gap-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">
HTML Content
</Label>
<Textarea
value={emailForm.body}
onChange={(e) =>
setEmailForm({ ...emailForm, body: e.target.value })
}
placeholder="<h1>Welcome to Yaltopia</h1>..."
className="min-h-[160px] rounded-xl bg-slate-50 border-slate-200/60"
/>
</div>
</div>
)}
</div>
</div>
</div>
<DialogFooter className="p-8 pt-4 bg-slate-50 border-t border-slate-100 flex items-center justify-between">
<Button
variant="ghost"
className="h-12 px-6 rounded-xl font-black uppercase text-xs tracking-widest text-slate-400 hover:text-slate-900"
onClick={() => setIsSendModalOpen(false)}
>
Abort Mission
</Button>
<Button
className="h-12 px-10 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-black uppercase text-xs tracking-[0.1em] shadow-lg shadow-slate-200 transition-all active:scale-95"
disabled={isPending}
onClick={handleSend}
>
{isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Broadcasting...
</>
) : (
<>
Commit {activeChannel} Signal
<ArrowRight className="w-4 h-4 ml-2" />
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}