units,lesson as standalone

This commit is contained in:
2026-07-10 11:45:02 +08:00
parent 182bd93d10
commit 01a2c63b06
60 changed files with 3217 additions and 606 deletions
@@ -11,6 +11,7 @@ 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";
@@ -26,6 +27,8 @@ const schema = z.object({
message: z.string().min(1, "Message is required."),
target_type: z.enum(["admin", "user", "both", "task_list", "course", "tier_plan"], { required_error: "Target is required." }),
target_id: z.string().nullable().optional(),
show_in_sticky: z.boolean().optional(),
show_in_notifications: z.boolean().optional(),
}).superRefine((data, ctx) => {
if (TARGET_TYPE_MAP[data.target_type]?.needsTarget && !data.target_id) {
ctx.addIssue({
@@ -34,6 +37,14 @@ const schema = z.object({
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"],
});
}
});
// ─── Helpers ────────────────────────────────────────────────────────────────
@@ -77,16 +88,20 @@ export default function AddNotificationBroadcast() {
message: "",
target_type: undefined,
target_id: null,
show_in_sticky: false,
show_in_notifications: true,
},
});
const targetType = watch("target_type");
const targetId = watch("target_id");
const needsTarget = TARGET_TYPE_MAP[targetType]?.needsTarget;
const showInSticky = watch("show_in_sticky");
const showInNotifications = watch("show_in_notifications");
const breadcrumbItems = [
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
{ label: "Notifications", to: "/admin/notifications" },
{ label: "Announcements", to: "/admin/announcements" },
{ label: "New" },
];
@@ -94,11 +109,13 @@ export default function AddNotificationBroadcast() {
const payload = {
...values,
target_id: TARGET_TYPE_MAP[values.target_type]?.needsTarget ? values.target_id : null,
show_in_sticky: values.show_in_sticky ?? false,
show_in_notifications: values.show_in_notifications ?? true,
createdBy: user?.user_id ?? null,
};
const res = await createBroadcast(payload);
if (res) navigate("/admin/notifications");
if (res) navigate("/admin/announcements");
};
return (
@@ -109,7 +126,7 @@ export default function AddNotificationBroadcast() {
</div>
<div className="w-full max-w-2xl pb-10">
<h1 className="text-2xl font-semibold tracking-tight mb-1">New notification</h1>
<h1 className="text-2xl font-semibold tracking-tight mb-1">New announcement</h1>
<p className="text-sm text-muted-foreground mb-6">Compose an announcement. It's saved as a draft until you send it.</p>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
@@ -127,7 +144,7 @@ export default function AddNotificationBroadcast() {
</div>
</SectionCard>
<SectionCard title="Target" description="Who receives this notification when it's sent.">
<SectionCard title="Target" description="Who receives this announcement when it's sent.">
<div>
<Select
value={targetType}
@@ -165,6 +182,32 @@ export default function AddNotificationBroadcast() {
)}
</SectionCard>
<SectionCard title="Display" description="Where clients/admins can see this announcement.">
<div className="space-y-3">
<div className="flex items-center gap-3">
<Checkbox
id="show_in_sticky"
checked={showInSticky === true}
onCheckedChange={(v) => setValue("show_in_sticky", v === true, { shouldValidate: true })}
/>
<Label htmlFor="show_in_sticky" className="cursor-pointer">
Show in Sticky Announcements
</Label>
</div>
<div className="flex items-center gap-3">
<Checkbox
id="show_in_notifications"
checked={showInNotifications === true}
onCheckedChange={(v) => setValue("show_in_notifications", v === true, { shouldValidate: true })}
/>
<Label htmlFor="show_in_notifications" className="cursor-pointer">
Show in Notifications
</Label>
</div>
</div>
</SectionCard>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={() => navigate(-1)} disabled={loading}>
Cancel