feat(web): delete reminder server action wired to detail page
This commit is contained in:
parent
83a19d4800
commit
6916f5a0ed
42
apps/web/src/actions/reminders.ts
Normal file
42
apps/web/src/actions/reminders.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { reminders } from "@cmbot/db";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { getSeededOperator } from "@/lib/operator";
|
||||||
|
import { checkRateLimit } from "@/lib/rate-limit";
|
||||||
|
|
||||||
|
async function rateLimit(key: string) {
|
||||||
|
const h = await headers();
|
||||||
|
const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? h.get("x-real-ip") ?? "unknown";
|
||||||
|
const r = await checkRateLimit(`${key}:${ip}`, { max: 30, windowSec: 10 });
|
||||||
|
if (r.limited) throw new Error("Too many requests");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteReminderAction(formData: FormData): Promise<void> {
|
||||||
|
await rateLimit("delete-reminder");
|
||||||
|
const reminderId = formData.get("reminderId");
|
||||||
|
if (typeof reminderId !== "string") return;
|
||||||
|
|
||||||
|
const op = await getSeededOperator();
|
||||||
|
const reminder = await db.query.reminders.findFirst({
|
||||||
|
where: (r, { eq }) => eq(r.id, reminderId),
|
||||||
|
});
|
||||||
|
if (!reminder) return;
|
||||||
|
|
||||||
|
// Verify ownership via the account
|
||||||
|
const account = await db.query.whatsappAccounts.findFirst({
|
||||||
|
where: (a, { eq, and }) => and(eq(a.id, reminder.accountId), eq(a.operatorId, op.id)),
|
||||||
|
});
|
||||||
|
if (!account) return;
|
||||||
|
|
||||||
|
// Cascading FKs (reminder_runs + reminder_targets + reminder_messages) clean up.
|
||||||
|
// pg-boss job for this reminder will fire and find the row gone (soft cancel).
|
||||||
|
await db.delete(reminders).where(eq(reminders.id, reminderId));
|
||||||
|
|
||||||
|
revalidatePath("/reminders" as any);
|
||||||
|
redirect("/reminders" as any);
|
||||||
|
}
|
||||||
@ -12,24 +12,14 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { deleteReminderAction } from "@/actions/reminders";
|
||||||
|
|
||||||
interface DeleteDialogProps {
|
interface DeleteDialogProps {
|
||||||
deleteAction: () => Promise<void>;
|
reminderId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteDialog({ deleteAction }: DeleteDialogProps) {
|
export function DeleteDialog({ reminderId }: DeleteDialogProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [pending, setPending] = useState(false);
|
|
||||||
|
|
||||||
async function handleConfirm() {
|
|
||||||
setPending(true);
|
|
||||||
try {
|
|
||||||
await deleteAction();
|
|
||||||
} finally {
|
|
||||||
setPending(false);
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@ -48,14 +38,16 @@ export function DeleteDialog({ deleteAction }: DeleteDialogProps) {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
<form action={deleteReminderAction} className="flex gap-2">
|
||||||
|
<input type="hidden" name="reminderId" value={reminderId} />
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={pending}
|
type="submit"
|
||||||
onClick={handleConfirm}
|
|
||||||
>
|
>
|
||||||
{pending ? "Deleting…" : "Delete"}
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</form>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -73,13 +73,6 @@ function StatusPill({ status }: { status: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Server action (no-op placeholder — real delete wired in Task 19)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
async function _deleteReminderStub() {
|
|
||||||
"use server";
|
|
||||||
// wired in Task 19
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Page
|
// Page
|
||||||
@ -242,7 +235,7 @@ export default async function ReminderDetailPage({ params }: Props) {
|
|||||||
|
|
||||||
{/* Action footer */}
|
{/* Action footer */}
|
||||||
<div className="flex items-center justify-end pt-2 border-t">
|
<div className="flex items-center justify-end pt-2 border-t">
|
||||||
<DeleteDialog deleteAction={_deleteReminderStub} />
|
<DeleteDialog reminderId={reminder.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user