154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||
|
|
import { toast } from 'sonner';
|
||
|
|
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Input } from '@/components/ui/input';
|
||
|
|
import { Label } from '@/components/ui/label';
|
||
|
|
import { Textarea } from '@/components/ui/textarea';
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogFooter,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from '@/components/ui/dialog';
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from '@/components/ui/select';
|
||
|
|
import { apiFetch } from '@/lib/api/client';
|
||
|
|
|
||
|
|
interface Account {
|
||
|
|
id: string;
|
||
|
|
emailAddress: string;
|
||
|
|
isActive: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ComposeDialogProps {
|
||
|
|
open: boolean;
|
||
|
|
onOpenChange: (open: boolean) => void;
|
||
|
|
defaultTo?: string;
|
||
|
|
defaultSubject?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ComposeDialog({
|
||
|
|
open,
|
||
|
|
onOpenChange,
|
||
|
|
defaultTo = '',
|
||
|
|
defaultSubject = '',
|
||
|
|
}: ComposeDialogProps) {
|
||
|
|
const qc = useQueryClient();
|
||
|
|
const [accountId, setAccountId] = useState('');
|
||
|
|
const [to, setTo] = useState(defaultTo);
|
||
|
|
const [cc, setCc] = useState('');
|
||
|
|
const [subject, setSubject] = useState(defaultSubject);
|
||
|
|
const [body, setBody] = useState('');
|
||
|
|
|
||
|
|
const { data: accounts = [] } = useQuery<Account[]>({
|
||
|
|
queryKey: ['email', 'accounts'],
|
||
|
|
queryFn: () => apiFetch<{ data: Account[] }>('/api/v1/email/accounts').then((r) => r.data),
|
||
|
|
});
|
||
|
|
|
||
|
|
const activeAccounts = accounts.filter((a) => a.isActive);
|
||
|
|
|
||
|
|
const sendMutation = useMutation({
|
||
|
|
mutationFn: () =>
|
||
|
|
apiFetch('/api/v1/email/compose', {
|
||
|
|
method: 'POST',
|
||
|
|
body: {
|
||
|
|
accountId,
|
||
|
|
to: to
|
||
|
|
.split(',')
|
||
|
|
.map((s) => s.trim())
|
||
|
|
.filter(Boolean),
|
||
|
|
cc: cc
|
||
|
|
? cc
|
||
|
|
.split(',')
|
||
|
|
.map((s) => s.trim())
|
||
|
|
.filter(Boolean)
|
||
|
|
: undefined,
|
||
|
|
subject,
|
||
|
|
bodyHtml: body.replace(/\n/g, '<br>'),
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
onSuccess: () => {
|
||
|
|
toast.success('Email sent');
|
||
|
|
qc.invalidateQueries({ queryKey: ['email', 'threads'] });
|
||
|
|
onOpenChange(false);
|
||
|
|
setTo('');
|
||
|
|
setCc('');
|
||
|
|
setSubject('');
|
||
|
|
setBody('');
|
||
|
|
},
|
||
|
|
onError: (err) => toast.error(err instanceof Error ? err.message : 'Send failed'),
|
||
|
|
});
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
|
|
<DialogContent className="max-w-2xl">
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>Compose email</DialogTitle>
|
||
|
|
</DialogHeader>
|
||
|
|
<div className="space-y-3">
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label>From</Label>
|
||
|
|
<Select value={accountId} onValueChange={setAccountId}>
|
||
|
|
<SelectTrigger>
|
||
|
|
<SelectValue placeholder="Choose account" />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
{activeAccounts.length === 0 ? (
|
||
|
|
<div className="px-2 py-1.5 text-sm text-muted-foreground">
|
||
|
|
No active accounts
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
activeAccounts.map((a) => (
|
||
|
|
<SelectItem key={a.id} value={a.id}>
|
||
|
|
{a.emailAddress}
|
||
|
|
</SelectItem>
|
||
|
|
))
|
||
|
|
)}
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label>To (comma-separated)</Label>
|
||
|
|
<Input value={to} onChange={(e) => setTo(e.target.value)} />
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label>CC</Label>
|
||
|
|
<Input value={cc} onChange={(e) => setCc(e.target.value)} />
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label>Subject</Label>
|
||
|
|
<Input value={subject} onChange={(e) => setSubject(e.target.value)} />
|
||
|
|
</div>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<Label>Message</Label>
|
||
|
|
<Textarea rows={10} value={body} onChange={(e) => setBody(e.target.value)} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<DialogFooter>
|
||
|
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||
|
|
Cancel
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
onClick={() => sendMutation.mutate()}
|
||
|
|
disabled={
|
||
|
|
sendMutation.isPending || !accountId || !to.trim() || !subject.trim() || !body.trim()
|
||
|
|
}
|
||
|
|
>
|
||
|
|
{sendMutation.isPending ? 'Sending…' : 'Send'}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
);
|
||
|
|
}
|