Add PDF reports, file uploads, admin auth, and comprehensive tests
Features: - PDF service for generating task reports with ReportLab-style formatting - Storage service for file uploads (local and S3-compatible) - Admin authentication middleware with JWT support - Admin user model and repository Infrastructure: - Updated Docker configuration for admin panel builds - Email service enhancements for task notifications - Updated router with admin and file upload routes - Environment configuration updates Tests: - Unit tests for handlers (auth, residence, task) - Unit tests for models (user, residence, task) - Unit tests for repositories (user, residence, task) - Unit tests for services (residence, task) - Integration test setup - Test utilities for mocking database and services Database: - Admin user seed data - Updated test data seeds 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -46,6 +47,43 @@ func (s *EmailService) SendEmail(to, subject, htmlBody, textBody string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmailAttachment represents an email attachment
|
||||
type EmailAttachment struct {
|
||||
Filename string
|
||||
ContentType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// SendEmailWithAttachment sends an email with an attachment
|
||||
func (s *EmailService) SendEmailWithAttachment(to, subject, htmlBody, textBody string, attachment *EmailAttachment) error {
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", s.cfg.From)
|
||||
m.SetHeader("To", to)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.SetBody("text/plain", textBody)
|
||||
m.AddAlternative("text/html", htmlBody)
|
||||
|
||||
if attachment != nil {
|
||||
m.Attach(attachment.Filename,
|
||||
gomail.SetCopyFunc(func(w io.Writer) error {
|
||||
_, err := w.Write(attachment.Data)
|
||||
return err
|
||||
}),
|
||||
gomail.SetHeader(map[string][]string{
|
||||
"Content-Type": {attachment.ContentType},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if err := s.dialer.DialAndSend(m); err != nil {
|
||||
log.Error().Err(err).Str("to", to).Str("subject", subject).Msg("Failed to send email with attachment")
|
||||
return fmt.Errorf("failed to send email: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("to", to).Str("subject", subject).Str("attachment", attachment.Filename).Msg("Email with attachment sent successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendWelcomeEmail sends a welcome email with verification code
|
||||
func (s *EmailService) SendWelcomeEmail(to, firstName, code string) error {
|
||||
subject := "Welcome to MyCrib - Verify Your Email"
|
||||
@@ -342,6 +380,110 @@ The MyCrib Team
|
||||
return s.SendEmail(to, subject, htmlBody, textBody)
|
||||
}
|
||||
|
||||
// SendTasksReportEmail sends a tasks report email with PDF attachment
|
||||
func (s *EmailService) SendTasksReportEmail(to, recipientName, residenceName string, totalTasks, completed, pending, overdue int, pdfData []byte) error {
|
||||
subject := fmt.Sprintf("MyCrib - Tasks Report for %s", residenceName)
|
||||
|
||||
name := recipientName
|
||||
if name == "" {
|
||||
name = "there"
|
||||
}
|
||||
|
||||
htmlBody := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { text-align: center; padding: 20px 0; }
|
||||
.summary-box { background: #f8f9fa; border: 1px solid #e9ecef; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
.summary-grid { display: flex; flex-wrap: wrap; gap: 20px; }
|
||||
.summary-item { flex: 1; min-width: 80px; text-align: center; }
|
||||
.summary-number { font-size: 28px; font-weight: bold; color: #333; }
|
||||
.summary-label { font-size: 12px; color: #666; text-transform: uppercase; }
|
||||
.completed { color: #28a745; }
|
||||
.pending { color: #ffc107; }
|
||||
.overdue { color: #dc3545; }
|
||||
.footer { text-align: center; color: #666; font-size: 12px; margin-top: 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>Tasks Report</h1>
|
||||
<p style="color: #666; margin: 0;">%s</p>
|
||||
</div>
|
||||
<p>Hi %s,</p>
|
||||
<p>Here's your tasks report for <strong>%s</strong>. The full report is attached as a PDF.</p>
|
||||
<div class="summary-box">
|
||||
<h3 style="margin-top: 0;">Summary</h3>
|
||||
<table width="100%%" cellpadding="10" cellspacing="0">
|
||||
<tr>
|
||||
<td align="center" style="border-right: 1px solid #e9ecef;">
|
||||
<div class="summary-number">%d</div>
|
||||
<div class="summary-label">Total Tasks</div>
|
||||
</td>
|
||||
<td align="center" style="border-right: 1px solid #e9ecef;">
|
||||
<div class="summary-number completed">%d</div>
|
||||
<div class="summary-label">Completed</div>
|
||||
</td>
|
||||
<td align="center" style="border-right: 1px solid #e9ecef;">
|
||||
<div class="summary-number pending">%d</div>
|
||||
<div class="summary-label">Pending</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
<div class="summary-number overdue">%d</div>
|
||||
<div class="summary-label">Overdue</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<p>Open the attached PDF for the complete list of tasks with details.</p>
|
||||
<p>Best regards,<br>The MyCrib Team</p>
|
||||
<div class="footer">
|
||||
<p>© %d MyCrib. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, residenceName, name, residenceName, totalTasks, completed, pending, overdue, time.Now().Year())
|
||||
|
||||
textBody := fmt.Sprintf(`
|
||||
Tasks Report for %s
|
||||
|
||||
Hi %s,
|
||||
|
||||
Here's your tasks report for %s. The full report is attached as a PDF.
|
||||
|
||||
Summary:
|
||||
- Total Tasks: %d
|
||||
- Completed: %d
|
||||
- Pending: %d
|
||||
- Overdue: %d
|
||||
|
||||
Open the attached PDF for the complete list of tasks with details.
|
||||
|
||||
Best regards,
|
||||
The MyCrib Team
|
||||
`, residenceName, name, residenceName, totalTasks, completed, pending, overdue)
|
||||
|
||||
// Create filename with timestamp
|
||||
filename := fmt.Sprintf("tasks_report_%s_%s.pdf",
|
||||
residenceName,
|
||||
time.Now().Format("2006-01-02"),
|
||||
)
|
||||
|
||||
attachment := &EmailAttachment{
|
||||
Filename: filename,
|
||||
ContentType: "application/pdf",
|
||||
Data: pdfData,
|
||||
}
|
||||
|
||||
return s.SendEmailWithAttachment(to, subject, htmlBody, textBody, attachment)
|
||||
}
|
||||
|
||||
// EmailTemplate represents an email template
|
||||
type EmailTemplate struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user