Yaltopia-Ticket-Admin/src/pages/admin/notifications/broadcast.tsx
“kirukib” 23ab82a726 Add staff roles, subscription txns, system users, issues, FAQ, broadcast
- Introduce SUPER_ADMIN, ADMIN, CUSTOMER_SUPPORT with admin-roles helpers and useAdminRole hook
- New pages: subscription transactions, system members, issues, FAQ & support, notification broadcast
- Services and API paths for admin subscription-transactions, system-members, issues, faq, broadcast
- Nav and quick search filtered by role; login accepts all panel roles

Made-with: Cursor
2026-04-15 10:45:10 +03:00

215 lines
7.1 KiB
TypeScript

import { useState } from "react"
import { Navigate } from "react-router-dom"
import { useMutation } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Bell, Mail, MessageSquare, Send } from "lucide-react"
import { notificationService } from "@/services"
import { useAdminRole } from "@/hooks/use-admin-role"
import { toast } from "sonner"
import { cn } from "@/lib/utils"
export default function NotificationBroadcastPage() {
const { canSendBroadcast } = useAdminRole()
const [title, setTitle] = useState("")
const [message, setMessage] = useState("")
const [audience, setAudience] = useState<
"all_end_users" | "system_users_only" | "everyone_with_access"
>("all_end_users")
const [channels, setChannels] = useState({
push: true,
sms: false,
email: true,
})
const mutation = useMutation({
mutationFn: () =>
notificationService.sendBroadcast({
title,
message,
audience,
channels: (
[
channels.push && "push",
channels.sms && "sms",
channels.email && "email",
].filter(Boolean) as ("push" | "sms" | "email")[]
),
}),
onSuccess: () => {
toast.success("Broadcast queued for delivery")
setTitle("")
setMessage("")
},
onError: () =>
toast.error(
"Could not send. Ensure POST /admin/notifications/broadcast exists.",
),
})
if (!canSendBroadcast) {
return <Navigate to="/admin/dashboard" replace />
}
const toggleChannel = (key: keyof typeof channels) => {
setChannels((c) => ({ ...c, [key]: !c[key] }))
}
const channelActive =
channels.push || channels.sms || channels.email
return (
<div className="space-y-8 max-w-2xl mx-auto bg-white p-4 min-h-screen">
<div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
Send notification
</h1>
<p className="text-gray-500 mt-1">
Super Admins and Admins can broadcast via push, SMS, and email.
Delivery depends on user preferences and channel configuration.
</p>
</div>
<Card className="border shadow-none rounded-none">
<CardHeader className="border-b">
<CardTitle className="text-sm font-bold uppercase tracking-widest text-gray-400">
Channels
</CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-3">
<div className="grid grid-cols-3 gap-2">
<button
type="button"
onClick={() => toggleChannel("push")}
className={cn(
"rounded-none border p-4 text-left transition-colors",
channels.push
? "border-primary bg-primary/5"
: "border-gray-200 opacity-70 hover:opacity-100",
)}
>
<Bell className="h-5 w-5 mb-2 text-gray-700" />
<div className="text-xs font-bold uppercase tracking-wider">
Push
</div>
</button>
<button
type="button"
onClick={() => toggleChannel("sms")}
className={cn(
"rounded-none border p-4 text-left transition-colors",
channels.sms
? "border-primary bg-primary/5"
: "border-gray-200 opacity-70 hover:opacity-100",
)}
>
<MessageSquare className="h-5 w-5 mb-2 text-gray-700" />
<div className="text-xs font-bold uppercase tracking-wider">
SMS
</div>
</button>
<button
type="button"
onClick={() => toggleChannel("email")}
className={cn(
"rounded-none border p-4 text-left transition-colors",
channels.email
? "border-primary bg-primary/5"
: "border-gray-200 opacity-70 hover:opacity-100",
)}
>
<Mail className="h-5 w-5 mb-2 text-gray-700" />
<div className="text-xs font-bold uppercase tracking-wider">
Email
</div>
</button>
</div>
</CardContent>
</Card>
<Card className="border shadow-none rounded-none">
<CardHeader className="border-b">
<CardTitle className="text-sm font-bold uppercase tracking-widest text-gray-400">
Audience
</CardTitle>
</CardHeader>
<CardContent className="pt-6">
<Select
value={audience}
onValueChange={(v) =>
setAudience(
v as
| "all_end_users"
| "system_users_only"
| "everyone_with_access",
)
}
>
<SelectTrigger className="rounded-none">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all_end_users">
All platform customers
</SelectItem>
<SelectItem value="system_users_only">
Panel users only (support &amp; admins)
</SelectItem>
<SelectItem value="everyone_with_access">
Everyone with an account
</SelectItem>
</SelectContent>
</Select>
</CardContent>
</Card>
<Card className="border shadow-none rounded-none">
<CardHeader className="border-b">
<CardTitle className="text-sm font-bold uppercase tracking-widest text-gray-400">
Message
</CardTitle>
</CardHeader>
<CardContent className="pt-6 space-y-4">
<div className="grid gap-1">
<Label htmlFor="bc-title">Title</Label>
<Input
id="bc-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="rounded-none"
/>
</div>
<div className="grid gap-1">
<Label htmlFor="bc-body">Body</Label>
<textarea
id="bc-body"
value={message}
onChange={(e) => setMessage(e.target.value)}
className="flex min-h-[160px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
</div>
<Button
className="rounded-none gap-2 w-full sm:w-auto"
disabled={
mutation.isPending || !title.trim() || !message.trim() || !channelActive
}
onClick={() => mutation.mutate()}
>
<Send className="h-4 w-4" />
Send broadcast
</Button>
</CardContent>
</Card>
</div>
)
}