Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-11 12:12:40 +08:00
parent 01a2c63b06
commit 71f758fe0b
66 changed files with 3055 additions and 939 deletions
@@ -1,10 +1,15 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { House, Pencil, Bell, Lock, Send, Clock3 } from "lucide-react";
import { House, Pencil, Bell, Lock, Send, Clock3, Plus, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Spinner } from "@/components/ui/spinner";
import { Separator } from "@/components/ui/separator";
import {
AlertDialog, AlertDialogAction, AlertDialogCancel,
AlertDialogContent, AlertDialogDescription,
AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
import { PageMeta } from "@/contexts/MetadataContext";
import {
@@ -15,7 +20,7 @@ import { NOTIFICATION_TEMPLATE_TYPES, getNotificationTemplateType } from "@/data
import { STATUS_META, hasPendingChanges } from "@/data/notificationTemplateStatus.data";
import { cn } from "@/lib/utils";
function TemplateCard({ item, onEdit }) {
function TemplateCard({ item, onEdit, onDelete }) {
const typeMeta = getNotificationTemplateType(item.notify_type);
const TypeIcon = typeMeta?.icon ?? Bell;
const status = STATUS_META[item.status] ?? STATUS_META.draft;
@@ -27,9 +32,16 @@ function TemplateCard({ item, onEdit }) {
<div className="w-10 h-10 rounded-lg border bg-muted flex items-center justify-center shrink-0">
<TypeIcon className="h-4.5 w-4.5 text-muted-foreground" />
</div>
<Button type="button" variant="ghost" size="icon" onClick={() => onEdit(item)}>
<Pencil className="h-4 w-4" />
</Button>
<div className="flex items-center gap-0.5">
{!item.is_system && (
<Button type="button" variant="ghost" size="icon" className="text-destructive hover:text-destructive" onClick={() => onDelete(item)}>
<Trash2 className="h-4 w-4" />
</Button>
)}
<Button type="button" variant="ghost" size="icon" onClick={() => onEdit(item)}>
<Pencil className="h-4 w-4" />
</Button>
</div>
</div>
<div className="min-w-0 flex-1">
@@ -68,11 +80,18 @@ function TemplateCard({ item, onEdit }) {
function NotificationTemplatesInner() {
const navigate = useNavigate();
const { templates, loading, fetchTemplates } = useAdminNotificationTemplates();
const { templates, loading, fetchTemplates, deleteTemplate } = useAdminNotificationTemplates();
const [activeType, setActiveType] = useState("all");
const [deleteTarget, setDeleteTarget] = useState(null);
useEffect(() => { fetchTemplates(); }, []);
const handleDelete = async () => {
if (!deleteTarget) return;
await deleteTemplate(deleteTarget.notification_template_id);
setDeleteTarget(null);
};
const filtered = useMemo(
() => activeType === "all" ? templates : templates.filter((t) => t.notify_type === activeType),
[templates, activeType]
@@ -120,25 +139,30 @@ function NotificationTemplatesInner() {
)}
</div>
</div>
<Button type="button" onClick={() => navigate("/admin/announcement-templates/add")} className="gap-1.5">
<Plus className="h-4 w-4" /> Add Template
</Button>
</div>
<div className="rounded-lg border bg-card p-4 flex items-start gap-3 mb-5">
<Lock className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
<div className="text-xs text-muted-foreground space-y-1">
<p>
Every template here is <strong>system</strong>-triggered — code fires it by referencing its
exact type, so no template can be added or removed from this screen. Only the title and
message wording is editable.
<strong>System</strong> templates are locked — code fires them by referencing their exact
type, so only the title and message wording is editable, never the type itself, and they
can't be deleted.
</p>
<p>
<strong>Draft vs. Sent:</strong> a <strong>Sent</strong> template is the version actually
used for real notifications right now. Editing a Sent template doesn't change what goes out
immediately — it's held as a pending change until you press <strong>Publish</strong> again.
<strong>Custom</strong> templates (no lock icon) are reusable title/message presets you
create — pick one from the "Load from template" dropdown when composing a new announcement
to skip retyping recurring wording. You can freely create, edit, and delete these.
</p>
<p>
Only plain text is supported — no HTML, no conditional logic, just straight{" "}
<code className="bg-muted px-1 rounded">{"{{placeholder}}"}</code> tokens that get swapped
for real values when the notification fires.
<strong>Draft vs. Sent</strong> (system templates only): a <strong>Sent</strong> template
is the version actually used for real notifications right now. Editing a Sent template
doesn't change what goes out immediately — it's held as a pending change until you press{" "}
<strong>Publish</strong> again.
</p>
</div>
</div>
@@ -180,12 +204,31 @@ function NotificationTemplatesInner() {
key={item.notification_template_id}
item={item}
onEdit={(t) => navigate(`/admin/announcement-templates/${t.notification_template_id}/edit`)}
onDelete={setDeleteTarget}
/>
))}
</div>
)}
</div>
</div>
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this template?</AlertDialogTitle>
<AlertDialogDescription>
"{deleteTarget?.label}" will be permanently removed. It won't affect any announcements already sent.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={loading} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
{loading ? <Spinner className="h-4 w-4 mr-2" /> : <Trash2 className="h-4 w-4 mr-2" />}
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
);
}