mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
218 lines
11 KiB
React
218 lines
11 KiB
React
import { useEffect, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { ArrowLeft, House, Lock, Send, Clock3 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
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 AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
|
import { PageMeta } from "@/contexts/MetadataContext";
|
|
import {
|
|
AdminNotificationTemplateProvider,
|
|
useAdminNotificationTemplates,
|
|
} from "@/contexts/AdminNotificationTemplateContext";
|
|
import { NOTIFICATION_TEMPLATE_PLACEHOLDERS } from "@/data/notificationTemplatePlaceholders.data";
|
|
import { STATUS_META, hasPendingChanges } from "@/data/notificationTemplateStatus.data";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
function SectionCard({ title, children }) {
|
|
return (
|
|
<div className="rounded-lg border bg-card p-5 space-y-4">
|
|
{title && <p className="text-sm font-semibold border-b pb-3">{title}</p>}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FieldError({ message }) {
|
|
if (!message) return null;
|
|
return <p className="text-xs text-destructive mt-1">{message}</p>;
|
|
}
|
|
|
|
function EditNotificationTemplateInner() {
|
|
const navigate = useNavigate();
|
|
const { id } = useParams();
|
|
const { template, loading, fetchTemplate, updateTemplate } = useAdminNotificationTemplates();
|
|
|
|
const [label, setLabel] = useState("");
|
|
const [title, setTitle] = useState("");
|
|
const [message, setMessage] = useState("");
|
|
const [errors, setErrors] = useState({});
|
|
|
|
useEffect(() => {
|
|
if (id) fetchTemplate(id);
|
|
}, [id]);
|
|
|
|
useEffect(() => {
|
|
if (template) {
|
|
setLabel(template.label ?? "");
|
|
// Prefer whatever's pending (unpublished) over the live version, so
|
|
// reopening a template with pending changes resumes editing them.
|
|
setTitle(template.draft_title ?? template.title ?? "");
|
|
setMessage(template.draft_message ?? template.message ?? "");
|
|
}
|
|
}, [template]);
|
|
|
|
const status = STATUS_META[template?.status] ?? STATUS_META.draft;
|
|
const pending = hasPendingChanges(template);
|
|
const knownPlaceholders = NOTIFICATION_TEMPLATE_PLACEHOLDERS[template?.type] ?? null;
|
|
|
|
const validate = () => {
|
|
const e = {};
|
|
if (!label.trim()) e.label = "Label is required.";
|
|
if (!title.trim()) e.title = "Title is required.";
|
|
if (!message.trim()) e.message = "Message is required.";
|
|
setErrors(e);
|
|
return !Object.keys(e).length;
|
|
};
|
|
|
|
const handleSave = async (publish) => {
|
|
if (!validate()) return;
|
|
|
|
const result = await updateTemplate(id, {
|
|
label: label.trim(),
|
|
title: title.trim(),
|
|
message: message.trim(),
|
|
publish,
|
|
});
|
|
if (result) navigate("/admin/announcement-templates");
|
|
};
|
|
|
|
return (
|
|
<section className="bg-muted/60 min-h-full">
|
|
<PageMeta title="Edit Announcement Template - STARR" />
|
|
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
|
|
<div className="w-full max-w-2xl mx-auto">
|
|
|
|
<div className="flex flex-col gap-2 mb-6">
|
|
<AppBreadcrumb items={[
|
|
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
|
{ label: "Announcements", to: "/admin/announcements" },
|
|
{ label: "Templates", to: "/admin/announcement-templates" },
|
|
{ label: template?.label ?? "Edit" },
|
|
]} />
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<h1 className="text-xl font-semibold">Edit Announcement Template</h1>
|
|
{template && (
|
|
<Badge variant="outline" className={cn("gap-1", status.badgeClass)}>
|
|
<Send className="h-3 w-3" /> {status.label}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">Update this template's title and message.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{pending && (
|
|
<div className="rounded-lg border border-amber-400 bg-amber-50 dark:bg-amber-950/40 dark:border-amber-700 p-4 flex items-start gap-3 mb-5">
|
|
<Clock3 className="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
|
|
<p className="text-xs text-amber-800 dark:text-amber-300">
|
|
This template has <strong>pending changes</strong> that haven't gone out yet — the version
|
|
currently used is the last one you published. Press <strong>Publish</strong> below to
|
|
apply these edits, or <strong>Save as Draft</strong> to keep working without publishing.
|
|
</p>
|
|
</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" />
|
|
<p className="text-xs text-muted-foreground">
|
|
This is a <strong>system</strong> notification — code fires it by referencing this exact type,
|
|
so the type is locked. Label, title and message are still fully editable.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-5">
|
|
|
|
<SectionCard title="Template Details">
|
|
<div className="space-y-1.5">
|
|
<Label>Type</Label>
|
|
<Input value={template?.type ?? ""} disabled />
|
|
<p className="text-xs text-muted-foreground">Cannot be changed — this is what code looks up.</p>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="label">Label <span className="text-destructive">*</span></Label>
|
|
<Input id="label" value={label} onChange={(e) => setLabel(e.target.value)} placeholder="e.g. Tasks Overdue (Admin)" />
|
|
<FieldError message={errors.label} />
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="title">Title <span className="text-destructive">*</span></Label>
|
|
<Input id="title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="e.g. Tasks Overdue" />
|
|
<FieldError message={errors.title} />
|
|
</div>
|
|
</SectionCard>
|
|
|
|
<SectionCard>
|
|
<div className="flex items-center justify-between border-b pb-3">
|
|
<p className="text-sm font-semibold">Message</p>
|
|
</div>
|
|
|
|
<p className="text-xs text-muted-foreground -mt-1">
|
|
Plain text only — no HTML, no conditional logic, just straight{" "}
|
|
<code className="bg-muted px-1 rounded">{"{{placeholder}}"}</code> tokens.
|
|
</p>
|
|
|
|
{(knownPlaceholders !== null) && (
|
|
<div className="space-y-1.5">
|
|
<p className="text-xs font-medium text-muted-foreground">Available placeholders</p>
|
|
{knownPlaceholders.length ? (
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{knownPlaceholders.map((ph) => (
|
|
<Badge key={ph} variant="outline" className="font-mono text-[10px]">
|
|
{`{{${ph}}}`}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">This template has no dynamic placeholders.</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<Textarea
|
|
id="message"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
rows={6}
|
|
className="font-mono text-xs"
|
|
placeholder="{{count}} {{task_word}} automatically marked as overdue."
|
|
/>
|
|
<FieldError message={errors.message} />
|
|
</SectionCard>
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<Button type="button" variant="outline" onClick={() => navigate(-1)} disabled={loading}>Cancel</Button>
|
|
<Button type="button" variant="outline" onClick={() => handleSave(false)} disabled={loading}>
|
|
Save as Draft
|
|
</Button>
|
|
<Button type="button" onClick={() => handleSave(true)} disabled={loading}>
|
|
{loading ? <Spinner className="h-4 w-4 mr-2" /> : <Send className="h-4 w-4 mr-2" />}
|
|
Publish
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export default function EditNotificationTemplate() {
|
|
return (
|
|
<AdminNotificationTemplateProvider>
|
|
<EditNotificationTemplateInner />
|
|
</AdminNotificationTemplateProvider>
|
|
);
|
|
}
|