- 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
215 lines
7.1 KiB
TypeScript
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 & 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>
|
|
)
|
|
}
|