Reshape the account lifecycle to match how operators actually want to work the system: - Add Account → creates a row with status='unpaired'. No QR yet; the operator lands on the detail page. - Pair / Re-pair → transitions an unpaired account to status='pending' and opens the live QR flow. Works for first-time pair AND for re-pair of an account that was previously unpaired. - Unpair → asks the bot to stop the live Baileys session and clean session files; sets status='unpaired' but KEEPS the row (and its reminders) so the operator can re-pair without retyping anything. - Delete → permanently removes the account and cascades to its groups, reminders, run history. Schema: - whatsapp_groups.account_id and reminders.account_id now have ON DELETE CASCADE so deleting an account fans out cleanly. UI: - /accounts list shows everything except the transient 'pending' state. - /accounts/[id] shows state-aware buttons: Pair (when unpaired/banned/ disconnected), Sync + Unpair (when connected), Delete (always). - /accounts/new is now an "Add Account" form (label only). Other fixes: - next.config.ts: allowedDevOrigins includes 192.168.0.253 + test/rexwa subdomains so Server Actions work across the LAN. - packages/shared/src/rrule.ts: rrule@2.8.1 has no exports field and ships ESM that some bundlers can't resolve via default OR named import. Use createRequire to bridge — works under both NodeNext (bot runtime) and Turbopack (web SSR).
94 lines
2.4 KiB
TypeScript
94 lines
2.4 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 } from "@/actions/reminders";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface ReviewSubmitClientProps {
|
|
accountId: string;
|
|
groupIds: string;
|
|
text?: string;
|
|
mediaId?: string;
|
|
caption?: string;
|
|
scheduledAt: string;
|
|
timezone: string;
|
|
}
|
|
|
|
export function ReviewSubmitClient({
|
|
accountId,
|
|
groupIds,
|
|
text,
|
|
mediaId,
|
|
caption,
|
|
scheduledAt,
|
|
timezone,
|
|
}: ReviewSubmitClientProps) {
|
|
const router = useRouter();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleSchedule() {
|
|
setSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await createReminderAction({
|
|
accountId,
|
|
groupIds: groupIds.split(",").filter(Boolean),
|
|
text: text ?? null,
|
|
mediaId: mediaId ?? null,
|
|
caption: caption ?? null,
|
|
scheduledAtIso: scheduledAt,
|
|
timezone,
|
|
});
|
|
|
|
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" />
|
|
Scheduling…
|
|
</>
|
|
) : (
|
|
<>
|
|
<CalendarCheckIcon className="size-4" />
|
|
Schedule Reminder
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|