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,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { deleteReminderAction } from "@/actions/reminders";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
deleteAction: () => Promise<void>;
|
||||
reminderId: string;
|
||||
}
|
||||
|
||||
export function DeleteDialog({ deleteAction }: DeleteDialogProps) {
|
||||
export function DeleteDialog({ reminderId }: DeleteDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
async function handleConfirm() {
|
||||
setPending(true);
|
||||
try {
|
||||
await deleteAction();
|
||||
} finally {
|
||||
setPending(false);
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
@ -48,14 +38,16 @@ export function DeleteDialog({ deleteAction }: DeleteDialogProps) {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<form action={deleteReminderAction} className="flex gap-2">
|
||||
<input type="hidden" name="reminderId" value={reminderId} />
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={pending}
|
||||
onClick={handleConfirm}
|
||||
type="submit"
|
||||
>
|
||||
{pending ? "Deleting…" : "Delete"}
|
||||
Delete
|
||||
</Button>
|
||||
</form>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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
|
||||
@ -242,7 +235,7 @@ export default async function ReminderDetailPage({ params }: Props) {
|
||||
|
||||
{/* Action footer */}
|
||||
<div className="flex items-center justify-end pt-2 border-t">
|
||||
<DeleteDialog deleteAction={_deleteReminderStub} />
|
||||
<DeleteDialog reminderId={reminder.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user