// modules/admin/pages/notifications/AddNotificationBroadcast.jsx import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { format } from "date-fns"; import { House, Check, ArrowLeft, ArrowRight } from "lucide-react"; import { useNotificationBroadcasts } from "@/contexts/AdminNotificationBroadcastContext"; import { useAuth } from "@/contexts/AuthContext"; import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; import { BroadcastTargetPicker } from "@/components/generic/BroadcastTargetPicker"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Spinner } from "@/components/ui/spinner"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DateTimePicker } from "@/components/ui/date-time-picker"; import { TARGET_TYPE_OPTIONS, TARGET_TYPE_MAP } from "@/data/notificationBroadcast.data"; import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard"; import { TIER_COLOR_OPTIONS, getTierColor, getContrastText } from "@/utils/tierColors"; import { normalizeExternalUrl } from "@/components/generic/notificationDisplay"; import { isValidLink, LINK_ERROR } from "@/utils/link.util"; // Internal paths ("/course/123") pass through untouched — everything else // gets a scheme so the saved URL always matches what goToLink() will open, // instead of relying on client-side normalization to paper over a bare // "example.com" the admin typed. const normalizeLinkUrl = (raw) => (!raw ? null : raw.startsWith("/") ? raw : normalizeExternalUrl(raw)); // ─── Schema ───────────────────────────────────────────────────────────────── const schema = z.object({ title: z.string().min(1, "Title is required."), message: z.string().optional(), target_type: z.enum(["admin", "user", "both", "task_list", "course", "tier_plan"], { message: "Please select a target." }), target_id: z.string().nullable().optional(), show_in_sticky: z.boolean().optional(), show_in_notifications: z.boolean().optional(), link_url: z.string().trim().optional(), color: z.string().optional(), start_date: z.string().optional(), end_date: z.string().optional(), }).superRefine((data, ctx) => { if (TARGET_TYPE_MAP[data.target_type]?.needsTarget && !data.target_id) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Please select a specific target.", path: ["target_id"], }); } if (!data.show_in_sticky && !data.show_in_notifications) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Select where to show this notification (Sticky or Notifications).", path: ["show_in_sticky"], }); } if (data.show_in_sticky && data.show_in_notifications) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Choose only one: Sticky or Notifications.", path: ["show_in_notifications"], }); } if (data.show_in_notifications && !data.message?.trim()) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Message is required for Notifications alerts.", path: ["message"], }); } if (data.link_url && !isValidLink(data.link_url)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: LINK_ERROR, path: ["link_url"], }); } if (data.start_date && data.end_date && new Date(data.start_date) > new Date(data.end_date)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "End date must be after the start date.", path: ["end_date"], }); } }); // ─── Steps config ─────────────────────────────────────────────────────────── const STEPS = [ { label: "Content", description: "What it says & how it's shown" }, { label: "Target", description: "Who receives it" }, { label: "Review", description: "Confirm & save" }, ]; // ─── Helpers ──────────────────────────────────────────────────────────────── function FieldError({ message }) { if (!message) return null; return
{message}
; } function SectionCard({ title, description, children }) { return ({description}
}Compose an alert. It's saved as a draft until you send it.