- Add TOTP-based MFA with setup, verify, disable, backup codes, and challenge flow - Add PII field encryption middleware with AES-256-GCM and deterministic search hashes - Add agents, inquiries, and leads domain modules with entities, events, value objects - Add web dashboard pages for inquiries and leads with detail dialogs - Add 30+ component tests (valuation, charts, listings, search, providers, UI) - Add Prisma migrations for encryption hash columns and MFA TOTP support - Fix all ESLint errors (unused imports, duplicate imports, lint auto-fixes) - Update dependencies and lock file - Clean up obsolete exploration/QA docs, add audit documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
154 lines
4.8 KiB
TypeScript
154 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select } from '@/components/ui/select';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { useCreateLead } from '@/lib/hooks/use-leads';
|
|
import { LEAD_SOURCES } from '@/lib/leads-api';
|
|
|
|
interface CreateLeadDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
export function CreateLeadDialog({ open, onOpenChange }: CreateLeadDialogProps) {
|
|
const createLead = useCreateLead();
|
|
const [form, setForm] = React.useState({
|
|
name: '',
|
|
phone: '',
|
|
email: '',
|
|
source: 'website',
|
|
score: '',
|
|
notes: '',
|
|
});
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
createLead.mutate(
|
|
{
|
|
name: form.name,
|
|
phone: form.phone,
|
|
email: form.email || undefined,
|
|
source: form.source,
|
|
score: form.score ? Number(form.score) : undefined,
|
|
notes: form.notes ? { text: form.notes } : undefined,
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
setForm({ name: '', phone: '', email: '', source: 'website', score: '', notes: '' });
|
|
onOpenChange(false);
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-md sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>Thêm lead mới</DialogTitle>
|
|
<DialogDescription>
|
|
Nhập thông tin khách hàng tiềm năng
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4 py-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="lead-name">Tên khách hàng *</Label>
|
|
<Input
|
|
id="lead-name"
|
|
value={form.name}
|
|
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
|
placeholder="Nguyễn Văn A"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="lead-phone">Số điện thoại *</Label>
|
|
<Input
|
|
id="lead-phone"
|
|
value={form.phone}
|
|
onChange={(e) => setForm((f) => ({ ...f, phone: e.target.value }))}
|
|
placeholder="0901234567"
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="lead-email">Email</Label>
|
|
<Input
|
|
id="lead-email"
|
|
type="email"
|
|
value={form.email}
|
|
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
|
placeholder="email@example.com"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="lead-source">Nguồn</Label>
|
|
<Select
|
|
id="lead-source"
|
|
value={form.source}
|
|
onChange={(e) => setForm((f) => ({ ...f, source: e.target.value }))}
|
|
>
|
|
{LEAD_SOURCES.map((s) => (
|
|
<option key={s.value} value={s.value}>
|
|
{s.label}
|
|
</option>
|
|
))}
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="lead-score">Điểm (0-100)</Label>
|
|
<Input
|
|
id="lead-score"
|
|
type="number"
|
|
min={0}
|
|
max={100}
|
|
value={form.score}
|
|
onChange={(e) => setForm((f) => ({ ...f, score: e.target.value }))}
|
|
placeholder="75"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="lead-notes">Ghi chú</Label>
|
|
<Textarea
|
|
id="lead-notes"
|
|
value={form.notes}
|
|
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
|
placeholder="Thông tin bổ sung về khách hàng..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
Hủy
|
|
</Button>
|
|
<Button type="submit" disabled={createLead.isPending}>
|
|
{createLead.isPending ? 'Đang tạo...' : 'Tạo lead'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|