the email template itself has to be able to get modified by admin

This commit is contained in:
Leon Serfaty G
2025-07-18 03:21:13 +00:00
parent c56316c740
commit 4cf4c0ccbe
3 changed files with 210 additions and 22 deletions
+35
View File
@@ -13,6 +13,15 @@ function seed() {
)
`);
// Create email_templates table
db.exec(`
CREATE TABLE IF NOT EXISTS email_templates (
id INTEGER PRIMARY KEY,
subject TEXT,
body TEXT
)
`);
// Check if the hourly_rate setting already exists
const settingStmt = db.prepare('SELECT * FROM settings WHERE key = ?');
const hourlyRateSetting = settingStmt.get('hourly_rate');
@@ -28,6 +37,32 @@ function seed() {
console.log('Hourly rate setting already exists.');
}
// Check if default email template exists
const templateStmt = db.prepare('SELECT * FROM email_templates WHERE id = ?');
const defaultTemplate = templateStmt.get(1);
if (!defaultTemplate) {
const insertTemplate = db.prepare(
"INSERT INTO email_templates (id, subject, body) VALUES (?, ?, ?)"
);
const defaultSubject = "Your Project Estimate is Ready!";
const defaultBody = `Hello, [User Name],
Thank you for using EstimateFlow. We've prepared a rough estimate for your project based on your selections.
[EstimateDetails]
Please note that this is a preliminary estimate. For a more detailed quote and to discuss your project further, please don't hesitate to contact us.
Best regards,
The EstimateFlow Team`;
insertTemplate.run(1, defaultSubject, defaultBody);
console.log('Default email template created.');
} else {
console.log('Default email template already exists.');
}
console.log('Seeding complete.');
}
+121 -22
View File
@@ -1,8 +1,95 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { useToast } from '@/hooks/use-toast';
import { getEmailTemplate, updateEmailTemplate } from '@/lib/actions/email';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
type EmailTemplate = {
subject: string;
body: string;
};
function EmailPreview({ template }: { template: EmailTemplate }) {
const previewBody = template.body.replace(
'[EstimateDetails]',
`<div class="my-6 space-y-4 rounded-lg border bg-background p-4">
<div>
<h3 class="font-semibold">Custom Development Estimate</h3>
<p class="text-2xl font-bold">[Custom Hours]+ hours</p>
</div>
<div>
<h3 class="font-semibold">Ready-Made Tools Estimate</h3>
<p class="text-2xl font-bold">[Ready-Made Hours]+ hours</p>
</div>
</div>`
).replace(/\n/g, '<br />');
return (
<div className="w-full rounded-lg border bg-muted p-6">
<div className="font-sans text-sm text-foreground">
<h2 className="text-xl font-bold">{template.subject}</h2>
<div className="mt-4" dangerouslySetInnerHTML={{ __html: previewBody }} />
</div>
</div>
);
}
export default function EmailTemplatesPage() {
const [isEditing, setIsEditing] = useState(false);
const [template, setTemplate] = useState<EmailTemplate>({ subject: '', body: '' });
const [isLoading, setIsLoading] = useState(true);
const { toast } = useToast();
useEffect(() => {
async function fetchTemplate() {
setIsLoading(true);
try {
const fetchedTemplate = await getEmailTemplate();
if (fetchedTemplate) {
setTemplate(fetchedTemplate);
} else {
throw new Error('Could not find email template.');
}
} catch (error: any) {
toast({
variant: 'destructive',
title: 'Error',
description: error.message || 'Failed to load email template.',
});
} finally {
setIsLoading(false);
}
}
fetchTemplate();
}, [toast]);
const handleSave = async () => {
try {
const result = await updateEmailTemplate(template);
if (result.success) {
toast({
title: 'Success',
description: 'Email template updated successfully.',
});
setIsEditing(false);
} else {
throw new Error(result.message);
}
} catch (error: any) {
toast({
variant: 'destructive',
title: 'Error',
description: error.message || 'Failed to save template.',
});
}
};
return (
<div className="space-y-8">
<div>
@@ -18,33 +105,45 @@ export default function EmailTemplatesPage() {
<CardTitle>Estimate Completion Email</CardTitle>
<CardDescription>This is the email users receive.</CardDescription>
</div>
<Button variant="outline">Edit Template</Button>
{!isEditing ? (
<Button variant="outline" onClick={() => setIsEditing(true)}>Edit Template</Button>
) : (
<div className="flex gap-2">
<Button variant="outline" onClick={() => setIsEditing(false)}>Cancel</Button>
<Button onClick={handleSave}>Save Changes</Button>
</div>
)}
</div>
</CardHeader>
<CardContent>
<div className="w-full rounded-lg border bg-muted p-6">
<div className="font-sans text-sm text-foreground">
<h2 className="text-xl font-bold">Your Project Estimate is Ready!</h2>
<p className="mt-4">Hello, [User Name],</p>
<p className="mt-2">Thank you for using EstimateFlow. We've prepared a rough estimate for your project based on your selections.</p>
<div className="my-6 space-y-4 rounded-lg border bg-background p-4">
<div>
<h3 className="font-semibold">Custom Development Estimate</h3>
<p className="text-2xl font-bold">[Custom Hours]+ hours</p>
</div>
<div>
<h3 className="font-semibold">Ready-Made Tools Estimate</h3>
<p className="text-2xl font-bold">[Ready-Made Hours]+ hours</p>
</div>
{isLoading ? (
<p>Loading template...</p>
) : isEditing ? (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="subject">Subject</Label>
<Input
id="subject"
value={template.subject}
onChange={(e) => setTemplate({ ...template, subject: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="body">Body</Label>
<Textarea
id="body"
value={template.body}
onChange={(e) => setTemplate({ ...template, body: e.target.value })}
className="h-64 font-mono"
/>
<p className="text-xs text-muted-foreground">
Use placeholders like [User Name] and [EstimateDetails]. They will be replaced with actual values.
</p>
</div>
<p className="mt-2">Please note that this is a preliminary estimate. For a more detailed quote and to discuss your project further, please don't hesitate to contact us.</p>
<p className="mt-6">Best regards,</p>
<p className="font-semibold">The EstimateFlow Team</p>
</div>
</div>
) : (
<EmailPreview template={template} />
)}
</CardContent>
</Card>
</div>
+54
View File
@@ -0,0 +1,54 @@
'use server';
import { z } from 'zod';
import db from '@/lib/db';
import { revalidatePath } from 'next/cache';
const emailTemplateSchema = z.object({
subject: z.string().min(1, 'Subject is required.'),
body: z.string().min(1, 'Body is required.'),
});
/**
* Gets the email template from the database.
*/
export async function getEmailTemplate(): Promise<{ subject: string; body: string } | null> {
try {
const stmt = db.prepare('SELECT subject, body FROM email_templates WHERE id = ?');
// We assume there is only one template with id = 1
const template = stmt.get(1) as { subject: string; body: string } | undefined;
if (!template) {
return null;
}
return template;
} catch (error) {
console.error('Failed to get email template:', error);
return null;
}
}
/**
* Updates the email template in the database.
*/
export async function updateEmailTemplate(data: { subject: string; body: string }): Promise<{ success: boolean; message: string }> {
const validation = emailTemplateSchema.safeParse(data);
if (!validation.success) {
const errorMessages = validation.error.issues.map(issue => issue.message).join(' ');
return { success: false, message: `Invalid data provided: ${errorMessages}` };
}
const { subject, body } = validation.data;
try {
const stmt = db.prepare('UPDATE email_templates SET subject = ?, body = ? WHERE id = ?');
// We assume we're updating the template with id = 1
stmt.run(subject, body, 1);
revalidatePath('/admin/settings/email-templates');
return { success: true, message: 'Email template updated successfully!' };
} catch (error) {
console.error('Failed to update email template:', error);
return { success: false, message: 'An unexpected error occurred.' };
}
}