Wizard When-step and the per-section Edit When page now expose an optional "Pause sending by" hour. Fire time IS the implicit start, so the deadline is the only thing the operator sets. When the bot's fan-out hasn't finished by that hour (in the reminder's timezone) the run pauses for resume — that runtime gating lands in a later phase; this commit just persists the hour and threads it through the wizard. HourSelect splits hour and AM/PM into two side-by-side <select>s and emits a single 0..23 value. to12Hour / from12Hour are pure helpers covered by 11 round-trip tests. Dashboard adjustments: * "WhatsApp accounts" card now reads Connected / Unpaired / Total. * "Reminders" card reads Active / Paused / Ended / Total. * "Recent runs" stat card removed (the Recent activity section below shows the same info). * Activity rows show absolute timestamp with AM/PM and relative time in tandem. Accounts list: * The page-level <h1>Accounts</h1> is hidden on mobile (the top bar already shows it), matching the Dashboard pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { CalendarCheckIcon, AlertCircleIcon, Loader2Icon } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { createReminderAction, updateReminderAction } from "@/actions/reminders";
|
|
import { cn } from "@/lib/utils";
|
|
import type { MessagePart } from "@/lib/reminder-messages";
|
|
|
|
interface ReviewSubmitClientProps {
|
|
accountId: string;
|
|
groupIds?: string;
|
|
name?: string;
|
|
messages: MessagePart[];
|
|
scheduledAt: string;
|
|
rrule?: string;
|
|
editReminderId?: string;
|
|
timezone: string;
|
|
deliveryEndHour?: number;
|
|
}
|
|
|
|
export function ReviewSubmitClient({
|
|
accountId,
|
|
groupIds,
|
|
name,
|
|
messages,
|
|
scheduledAt,
|
|
rrule,
|
|
editReminderId,
|
|
timezone,
|
|
deliveryEndHour,
|
|
}: ReviewSubmitClientProps) {
|
|
const router = useRouter();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleSchedule() {
|
|
const trimmedName = name?.trim();
|
|
if (!trimmedName) {
|
|
// The wizard's compose step now blocks Continue when the name is
|
|
// blank, so the only way to land here without one is a stale
|
|
// bookmarked URL. Bounce the operator back to step 2 with a
|
|
// clear error rather than letting the server reject it.
|
|
setError("Give the reminder a name (back on the Message step).");
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const payload = {
|
|
accountId,
|
|
groupIds: groupIds ? groupIds.split(",").filter(Boolean) : [],
|
|
name: trimmedName,
|
|
messages,
|
|
scheduledAtIso: scheduledAt,
|
|
rrule: rrule ?? null,
|
|
timezone,
|
|
deliveryWindowEndHour: deliveryEndHour,
|
|
};
|
|
const result = editReminderId
|
|
? await updateReminderAction({ ...payload, reminderId: editReminderId })
|
|
: await createReminderAction(payload);
|
|
|
|
if (result.ok) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
router.push(`/reminders/${result.reminderId}` as any);
|
|
} else {
|
|
setError(result.error);
|
|
setSubmitting(false);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "An unexpected error occurred.");
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3 pt-2">
|
|
{error && (
|
|
<div className="flex items-start gap-2 rounded-lg bg-destructive/10 px-3 py-2.5 text-sm text-destructive">
|
|
<AlertCircleIcon className="size-4 shrink-0 mt-0.5" />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
size="lg"
|
|
onClick={handleSchedule}
|
|
disabled={submitting}
|
|
className={cn("gap-2", submitting && "cursor-wait")}
|
|
>
|
|
{submitting ? (
|
|
<>
|
|
<Loader2Icon className="size-4 animate-spin" />
|
|
{editReminderId ? "Saving…" : "Scheduling…"}
|
|
</>
|
|
) : (
|
|
<>
|
|
<CalendarCheckIcon className="size-4" />
|
|
{editReminderId ? "Save changes" : "Schedule Reminder"}
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|