make the send estimate modal more beautifull and informative, also besid
This commit is contained in:
@@ -39,13 +39,14 @@ export default function LeadsPage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleExportCsv = () => {
|
const handleExportCsv = () => {
|
||||||
const headers = ['Name', 'Email', 'Date Submitted'];
|
const headers = ['Name', 'Email', 'Phone', 'Date Submitted'];
|
||||||
const csvContent = [
|
const csvContent = [
|
||||||
headers.join(','),
|
headers.join(','),
|
||||||
...leads.map(lead =>
|
...leads.map(lead =>
|
||||||
[
|
[
|
||||||
`"${lead.name}"`,
|
`"${lead.name}"`,
|
||||||
`"${lead.email}"`,
|
`"${lead.email}"`,
|
||||||
|
`"${lead.phone || ''}"`,
|
||||||
`"${format(new Date(lead.createdAt), 'PPP p')}"`
|
`"${format(new Date(lead.createdAt), 'PPP p')}"`
|
||||||
].join(',')
|
].join(',')
|
||||||
)
|
)
|
||||||
@@ -85,6 +86,7 @@ export default function LeadsPage() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Email</TableHead>
|
<TableHead>Email</TableHead>
|
||||||
|
<TableHead>Phone</TableHead>
|
||||||
<TableHead className="text-right">Date Submitted</TableHead>
|
<TableHead className="text-right">Date Submitted</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -95,6 +97,7 @@ export default function LeadsPage() {
|
|||||||
<TableRow key={i}>
|
<TableRow key={i}>
|
||||||
<TableCell><Skeleton className="h-5 w-24" /></TableCell>
|
<TableCell><Skeleton className="h-5 w-24" /></TableCell>
|
||||||
<TableCell><Skeleton className="h-5 w-40" /></TableCell>
|
<TableCell><Skeleton className="h-5 w-40" /></TableCell>
|
||||||
|
<TableCell><Skeleton className="h-5 w-32" /></TableCell>
|
||||||
<TableCell className="text-right"><Skeleton className="h-5 w-32 ml-auto" /></TableCell>
|
<TableCell className="text-right"><Skeleton className="h-5 w-32 ml-auto" /></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
@@ -104,6 +107,7 @@ export default function LeadsPage() {
|
|||||||
<TableRow key={lead.id}>
|
<TableRow key={lead.id}>
|
||||||
<TableCell className="font-medium">{lead.name}</TableCell>
|
<TableCell className="font-medium">{lead.name}</TableCell>
|
||||||
<TableCell>{lead.email}</TableCell>
|
<TableCell>{lead.email}</TableCell>
|
||||||
|
<TableCell>{lead.phone || 'N/A'}</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{format(new Date(lead.createdAt), 'PPP p')}
|
{format(new Date(lead.createdAt), 'PPP p')}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -111,7 +115,7 @@ export default function LeadsPage() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={3} className="text-center h-24">
|
<TableCell colSpan={4} className="text-center h-24">
|
||||||
No leads yet.
|
No leads yet.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
import type { FormData } from './cost-estimator-form';
|
import type { FormData } from './cost-estimator-form';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Info, Mail } from 'lucide-react';
|
import { Info, Mail, User, Phone, Briefcase, Clock } from 'lucide-react';
|
||||||
import React, { useState, useTransition } from 'react';
|
import React, { useState, useTransition } from 'react';
|
||||||
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +20,7 @@ import { Input } from "@/components/ui/input"
|
|||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { sendEstimateEmail } from '@/lib/actions/send-estimate';
|
import { sendEstimateEmail } from '@/lib/actions/send-estimate';
|
||||||
|
import { Separator } from '../ui/separator';
|
||||||
|
|
||||||
type Step11Props = {
|
type Step11Props = {
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
@@ -123,12 +124,13 @@ export function Step11Results({ onReset, customHours, readyMadeHours, formData }
|
|||||||
const form = new FormData(event.currentTarget);
|
const form = new FormData(event.currentTarget);
|
||||||
const name = form.get('name') as string;
|
const name = form.get('name') as string;
|
||||||
const email = form.get('email') as string;
|
const email = form.get('email') as string;
|
||||||
|
const phone = form.get('phone') as string;
|
||||||
|
|
||||||
if (!name || !email) {
|
if (!name || !email) {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: "Missing Information",
|
title: "Missing Information",
|
||||||
description: "Please enter both your name and email.",
|
description: "Please enter your name and email.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -137,6 +139,7 @@ export function Step11Results({ onReset, customHours, readyMadeHours, formData }
|
|||||||
const result = await sendEstimateEmail({
|
const result = await sendEstimateEmail({
|
||||||
name,
|
name,
|
||||||
email,
|
email,
|
||||||
|
phone,
|
||||||
customHours,
|
customHours,
|
||||||
readyMadeHours,
|
readyMadeHours,
|
||||||
formData,
|
formData,
|
||||||
@@ -212,37 +215,66 @@ export function Step11Results({ onReset, customHours, readyMadeHours, formData }
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button size="lg" variant="outline">
|
<Button size="lg" variant="outline">
|
||||||
<Mail className="mr-2 h-4 w-4" />
|
<Mail className="mr-2 h-4 w-4" />
|
||||||
Send Estimate on Email
|
Send Estimate to Email
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-md">
|
||||||
<form onSubmit={handleSubmit}>
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle className="font-headline text-2xl">Get Your Detailed Estimate</DialogTitle>
|
||||||
<DialogTitle>Send Estimate</DialogTitle>
|
<DialogDescription>
|
||||||
<DialogDescription>
|
Fill out the form below and we'll email you a PDF copy of your estimate.
|
||||||
Enter your name and email to receive the estimate PDF.
|
</DialogDescription>
|
||||||
</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
<div className="space-y-4 py-2">
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<Card className="bg-muted/50 border-dashed">
|
||||||
<Label htmlFor="name" className="text-right">
|
<CardHeader className="flex-row items-center gap-4 space-y-0 p-4">
|
||||||
Name
|
<Briefcase className="h-8 w-8 text-primary" />
|
||||||
</Label>
|
<CardTitle className="text-lg font-medium">Estimate Summary</CardTitle>
|
||||||
<Input id="name" name="name" className="col-span-3" required />
|
</CardHeader>
|
||||||
</div>
|
<CardContent className="p-4 pt-0 space-y-2 text-sm">
|
||||||
<div className="grid grid-cols-4 items-center gap-4">
|
<div className="flex justify-between items-center">
|
||||||
<Label htmlFor="email" className="text-right">
|
<span className="text-muted-foreground flex items-center gap-2"><Clock className="h-4 w-4" /> Custom Development</span>
|
||||||
Email
|
<span className="font-bold">{customHours}+ hours</span>
|
||||||
</Label>
|
</div>
|
||||||
<Input id="email" name="email" type="email" className="col-span-3" required />
|
<div className="flex justify-between items-center">
|
||||||
</div>
|
<span className="text-muted-foreground flex items-center gap-2"><Clock className="h-4 w-4" /> Ready-Made Tools</span>
|
||||||
</div>
|
<span className="font-bold">{readyMadeHours}+ hours</span>
|
||||||
<DialogFooter>
|
</div>
|
||||||
<Button type="submit" disabled={isPending}>
|
</CardContent>
|
||||||
{isPending ? "Sending..." : "Send to Email"}
|
</Card>
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
<Separator />
|
||||||
</form>
|
|
||||||
|
<form id="send-estimate-form" onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Full Name</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input id="name" name="name" placeholder="John Doe" className="pl-10" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email Address</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input id="email" name="email" type="email" placeholder="john.doe@example.com" className="pl-10" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="phone">Phone Number (Optional)</Label>
|
||||||
|
<div className="relative">
|
||||||
|
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input id="phone" name="phone" type="tel" placeholder="(123) 456-7890" className="pl-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="submit" form="send-estimate-form" className="w-full" size="lg" disabled={isPending}>
|
||||||
|
{isPending ? "Sending..." : "Send to Email"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Button onClick={onReset} size="lg" variant="link" className="text-muted-foreground">
|
<Button onClick={onReset} size="lg" variant="link" className="text-muted-foreground">
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ export type Lead = {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
phone: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getLeads(): Promise<Lead[]> {
|
export async function getLeads(): Promise<Lead[]> {
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
'SELECT id, name, email, createdAt FROM leads ORDER BY createdAt DESC'
|
'SELECT id, name, email, phone, createdAt FROM leads ORDER BY createdAt DESC'
|
||||||
);
|
);
|
||||||
const leads = stmt.all() as Lead[];
|
const leads = stmt.all() as Lead[];
|
||||||
return leads;
|
return leads;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { FormData } from '@/components/cost-estimator/cost-estimator-form';
|
|||||||
const inputSchema = z.object({
|
const inputSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
phone: z.string().optional(),
|
||||||
customHours: z.number(),
|
customHours: z.number(),
|
||||||
readyMadeHours: z.number(),
|
readyMadeHours: z.number(),
|
||||||
formData: z.any(), // Keeping this flexible for now
|
formData: z.any(), // Keeping this flexible for now
|
||||||
@@ -39,7 +40,7 @@ async function getSmtpSettings() {
|
|||||||
|
|
||||||
|
|
||||||
async function createEstimatePdf(data: InputType): Promise<Buffer> {
|
async function createEstimatePdf(data: InputType): Promise<Buffer> {
|
||||||
const { name, customHours, readyMadeHours, formData } = data;
|
const { name, email, phone, customHours, readyMadeHours, formData } = data;
|
||||||
const pdfDoc = await PDFDocument.create();
|
const pdfDoc = await PDFDocument.create();
|
||||||
const page = pdfDoc.addPage();
|
const page = pdfDoc.addPage();
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = page.getSize();
|
||||||
@@ -61,10 +62,16 @@ async function createEstimatePdf(data: InputType): Promise<Buffer> {
|
|||||||
y -= 30;
|
y -= 30;
|
||||||
|
|
||||||
// Subheader
|
// Subheader
|
||||||
page.drawText(`Project Estimate for: ${name}`, { x: 50, y, font: boldFont, size: 18 });
|
page.drawText('Project Estimate For:', { x: 50, y, font: boldFont, size: 18 });
|
||||||
|
y -= 25;
|
||||||
|
page.drawText(name, { x: 50, y, font: font, size: 14 });
|
||||||
y -= 20;
|
y -= 20;
|
||||||
page.drawText(`Date: ${new Date().toLocaleDateString()}`, { x: 50, y, font, size: 12, color: rgb(0.3, 0.3, 0.3) });
|
page.drawText(email, { x: 50, y, font: font, size: 12, color: rgb(0.3, 0.3, 0.3) });
|
||||||
y -= 40;
|
y -= 18;
|
||||||
|
if (phone) {
|
||||||
|
page.drawText(phone, { x: 50, y, font: font, size: 12, color: rgb(0.3, 0.3, 0.3) });
|
||||||
|
}
|
||||||
|
y -= 30;
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
page.drawText('Estimate Summary', { x: 50, y, font: boldFont, size: 16 });
|
page.drawText('Estimate Summary', { x: 50, y, font: boldFont, size: 16 });
|
||||||
@@ -109,10 +116,10 @@ async function createEstimatePdf(data: InputType): Promise<Buffer> {
|
|||||||
return Buffer.from(pdfBytes);
|
return Buffer.from(pdfBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLead(name: string, email: string) {
|
async function saveLead(name: string, email: string, phone?: string) {
|
||||||
try {
|
try {
|
||||||
const stmt = db.prepare('INSERT INTO leads (name, email) VALUES (?, ?)');
|
const stmt = db.prepare('INSERT INTO leads (name, email, phone) VALUES (?, ?, ?)');
|
||||||
stmt.run(name, email);
|
stmt.run(name, email, phone || null);
|
||||||
console.log(`Lead saved: ${name}, ${email}`);
|
console.log(`Lead saved: ${name}, ${email}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save lead:', error);
|
console.error('Failed to save lead:', error);
|
||||||
@@ -160,7 +167,7 @@ export async function sendEstimateEmail(data: InputType): Promise<{ success: boo
|
|||||||
await transporter.sendMail(mailOptions);
|
await transporter.sendMail(mailOptions);
|
||||||
|
|
||||||
// Save lead after email is sent successfully
|
// Save lead after email is sent successfully
|
||||||
await saveLead(validation.data.name, validation.data.email);
|
await saveLead(validation.data.name, validation.data.email, validation.data.phone);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ db.exec(`
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
|
phone TEXT,
|
||||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|||||||
Reference in New Issue
Block a user