yiekheng 9437df74ee feat(web): split Add Account from Pair; add Unpair/Re-pair/Delete actions
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).
2026-05-10 00:27:33 +08:00

77 lines
2.3 KiB
TypeScript

"use client";
import { useActionState } from "react";
import Link from "next/link";
import { ArrowRightIcon, Loader2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { addAccountAction, type AddAccountResult } from "@/actions/accounts";
const initialState: AddAccountResult = { ok: true, accountId: "" };
export function PairForm() {
const [state, action, isPending] = useActionState(addAccountAction, initialState);
return (
<form action={action} className="space-y-5">
<div className="space-y-1.5">
<Label htmlFor="label" className="text-sm font-medium">
Account label
</Label>
<Input
id="label"
name="label"
type="text"
placeholder="e.g. Personal, Work, Support"
maxLength={60}
required
aria-invalid={state.ok === false ? true : undefined}
aria-describedby={state.ok === false ? "label-error" : undefined}
className="h-9"
autoFocus
/>
{state.ok === false && (
<p
id="label-error"
role="alert"
className="flex items-center gap-1.5 text-xs text-destructive"
>
<span className="inline-block size-1 rounded-full bg-destructive shrink-0" />
{state.error}
</p>
)}
<p className="text-xs text-muted-foreground">
A short name to identify this WhatsApp account. You can have multiple accounts.
</p>
</div>
<div className="flex items-center gap-3 pt-1">
<Button
type="submit"
disabled={isPending}
className="gap-2"
size="default"
>
{isPending ? (
<>
<Loader2Icon className="size-3.5 animate-spin" />
Adding
</>
) : (
<>
Add Account
<ArrowRightIcon className="size-3.5" />
</>
)}
</Button>
<Button asChild variant="ghost" size="default">
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Link href={"/accounts" as any}>Cancel</Link>
</Button>
</div>
</form>
);
}