feat: add MFA/TOTP auth, PII encryption, agents/leads/inquiries modules, and comprehensive tests
- 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>
This commit is contained in:
153
apps/web/components/leads/create-lead-dialog.tsx
Normal file
153
apps/web/components/leads/create-lead-dialog.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user