RJBULK-APP/components/forms/settings-forms.tsx

296 lines
11 KiB
TypeScript
Raw Normal View History

"use client";
import { useActionState, useEffect, useRef, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Plus, Trash2 } from "lucide-react";
import { addUser, deleteUser, sendTestEmail, updateEmailSettings, type ActionResult } from "@/actions/settings";
import { updateDefaultPrice } from "@/actions/pricing";
import { toast } from "sonner";
import type { AppUser, FuelSettings } from "@/lib/types";
const initial: ActionResult = { ok: false, error: "" };
interface Props {
currentUserId: string;
settings: FuelSettings | null;
users: AppUser[];
}
export function SettingsForms({ currentUserId, settings, users }: Props) {
const router = useRouter();
const [defaultPrice, setDefaultPrice] = useState(settings?.default_price_per_litre || 0);
const [emailState, emailAction, emailPending] = useActionState(updateEmailSettings, initial);
const emailFormRef = useRef<HTMLFormElement>(null);
useEffect(() => {
if (emailState.ok) {
toast.success("Email settings saved");
} else if (emailState.error) {
toast.error(emailState.error);
}
}, [emailState]);
function onSaveDefault() {
startTransition(async () => {
const res = await updateDefaultPrice(defaultPrice);
if (res.ok) {
toast.success("Default price saved");
router.refresh();
} else {
toast.error(res.error);
}
});
}
const [, startTransition] = useTransition();
const [testEmail, setTestEmail] = useState(settings?.statement_from_email || "");
function onSendTest() {
if (!testEmail) {
toast.error("Enter a recipient email");
return;
}
startTransition(async () => {
const res = await sendTestEmail(testEmail);
if (res.ok) toast.success("Test email sent");
else toast.error(res.error);
});
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Settings</h1>
<p className="text-sm text-muted-foreground">Fuel price, email, users, and price increase</p>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Default fuel price</CardTitle>
<CardDescription>Used when no per-customer rule applies</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-end gap-2">
<div className="space-y-2 flex-1">
<Label htmlFor="dp">Default price (R / L)</Label>
<Input
id="dp"
type="number"
step="0.01"
min="0"
value={defaultPrice}
onChange={(e) => setDefaultPrice(Number(e.target.value))}
/>
</div>
<Button onClick={onSaveDefault}>Save</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Fuel price increase</CardTitle>
<CardDescription>Quickly apply a cents increase to all customers (use Pricing page for details)</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" asChild>
<a href="/pricing">Go to Pricing →</a>
</Button>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Email settings</CardTitle>
<CardDescription>SMTP / Resend configuration for sending statements</CardDescription>
</CardHeader>
<form ref={emailFormRef} action={emailAction}>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="from">From email</Label>
<Input id="from" name="statement_from_email" type="email" defaultValue={settings?.statement_from_email || ""} />
</div>
<div className="space-y-2">
<Label htmlFor="cc">CC email</Label>
<Input id="cc" name="statement_cc_email" type="email" defaultValue={settings?.statement_cc_email || ""} />
</div>
<div className="space-y-2">
<Label htmlFor="app">App email (fallback)</Label>
<Input id="app" name="statement_app_email" type="email" defaultValue={settings?.statement_app_email || ""} />
</div>
<div className="space-y-2">
<Label htmlFor="pwd">Email password (SMTP)</Label>
<Input
id="pwd"
name="statement_email_password"
type="password"
placeholder="••••••••"
defaultValue={settings?.statement_email_password ? "••••••••" : ""}
/>
</div>
</div>
<div className="flex items-end gap-2 pt-2 border-t">
<div className="space-y-2 flex-1">
<Label htmlFor="test">Send test email to:</Label>
<Input id="test" type="email" value={testEmail} onChange={(e) => setTestEmail(e.target.value)} />
</div>
<Button type="button" variant="outline" onClick={onSendTest}>
Send test
</Button>
</div>
</CardContent>
<div className="px-6 pb-6">
<Button type="submit" disabled={emailPending}>
{emailPending ? "Saving…" : "Save email settings"}
</Button>
</div>
</form>
</Card>
<UsersSection currentUserId={currentUserId} users={users} />
</div>
);
}
function UsersSection({ currentUserId, users }: { currentUserId: string; users: AppUser[] }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [, startTransition] = useTransition();
function onDelete(id: string) {
startTransition(async () => {
const res = await deleteUser(id);
if (res.ok) {
toast.success("User deleted");
setDeletingId(null);
router.refresh();
} else {
toast.error(res.error);
}
});
}
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Users</CardTitle>
<CardDescription>{users.length} user{users.length !== 1 ? "s" : ""}</CardDescription>
</div>
<Button onClick={() => setOpen(true)} size="sm">
<Plus className="h-4 w-4" /> Add user
</Button>
</CardHeader>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Verified</TableHead>
<TableHead className="w-12"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((u) => (
<TableRow key={u.id}>
<TableCell className="font-medium">{u.name || "—"}</TableCell>
<TableCell className="text-muted-foreground">{u.email}</TableCell>
<TableCell>{u.verified ? "Yes" : "No"}</TableCell>
<TableCell>
{u.id !== currentUserId && (
<Button variant="ghost" size="icon" onClick={() => setDeletingId(u.id)}>
<Trash2 className="h-4 w-4" />
</Button>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<AddUserDialog open={open} onOpenChange={setOpen} />
<Dialog open={!!deletingId} onOpenChange={(v) => !v && setDeletingId(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete this user?</DialogTitle>
<DialogDescription>They will lose access immediately. This cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="ghost" onClick={() => setDeletingId(null)}>Cancel</Button>
<Button variant="destructive" onClick={() => deletingId && onDelete(deletingId)}>Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
);
}
function AddUserDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (v: boolean) => void }) {
const router = useRouter();
const ref = useRef<HTMLFormElement>(null);
const [pending, startTransition] = useTransition();
const [state, action] = useActionState(addUser, initial);
useEffect(() => {
if (state.ok) {
toast.success("User created");
onOpenChange(false);
ref.current?.reset();
router.refresh();
} else if (state.error) {
toast.error(state.error);
}
}, [state, router, onOpenChange]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add user</DialogTitle>
<DialogDescription>Create a new login for someone on your team.</DialogDescription>
</DialogHeader>
<form ref={ref} action={action} className="space-y-3">
<div className="space-y-2">
<Label htmlFor="u-name">Name</Label>
<Input id="u-name" name="name" required />
</div>
<div className="space-y-2">
<Label htmlFor="u-username">Username</Label>
<Input id="u-username" name="username" required minLength={3} />
</div>
<div className="space-y-2">
<Label htmlFor="u-email">Email</Label>
<Input id="u-email" name="email" type="email" required />
</div>
<div className="space-y-2">
<Label htmlFor="u-pwd">Password</Label>
<Input id="u-pwd" name="password" type="password" required minLength={8} />
</div>
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button type="submit" disabled={pending}>
{pending ? "Creating…" : "Create user"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}