feat: complete email infrastructure with queue, templates, logging, and API
Adds the full transactional email system:
- Redis queue (QUEUE_CONNECTION=redis), SES config in .env.example
- 3 migrations: organisation_email_settings, organisation_email_templates, email_logs
- EmailTemplateType and EmailLogStatus enums with Dutch defaults
- EmailService as central entry point for all email sending
- SendTransactionalEmail queued job with retries and idempotency
- TransactionalMail mailable with responsive HTML + plain text templates
- Organisation-level branding (colors, logo, footer, reply-to)
- Per-type template overrides with {variable} substitution
- Email log with filtering by status, type, date range, recipient
- Preview and send-test endpoints for template management
- API endpoints: email-settings, email-templates (CRUD), email-logs (read-only)
- Integrated into existing flows: invitations, password reset, email
verification, registration approval/rejection
- 37 new tests across 4 test files, all existing tests updated
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
api/resources/views/emails/transactional.blade.php
Normal file
81
api/resources/views/emails/transactional.blade.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ $heading ?? '' }}</title>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f4f5;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 16px;">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width: 600px; width: 100%; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08);">
|
||||
{{-- Accent line --}}
|
||||
<tr>
|
||||
<td style="height: 4px; background-color: {{ $primaryColor }};"></td>
|
||||
</tr>
|
||||
|
||||
{{-- Header: logo or Crewli text --}}
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 40px 24px;">
|
||||
@if($logoUrl)
|
||||
<img src="{{ $logoUrl }}" alt="Logo" style="max-height: 60px; max-width: 250px; display: block;">
|
||||
@else
|
||||
<p style="margin: 0; font-size: 20px; color: #1f2937; font-weight: 700;">Crewli</p>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Body --}}
|
||||
<tr>
|
||||
<td style="padding: 0 40px 32px;">
|
||||
{{-- Heading --}}
|
||||
@if($heading)
|
||||
<h1 style="margin: 0 0 20px; font-size: 22px; font-weight: 700; color: #1f2937; line-height: 1.3;">
|
||||
{{ $heading }}
|
||||
</h1>
|
||||
@endif
|
||||
|
||||
{{-- Body text --}}
|
||||
<div style="font-size: 16px; color: #1f2937; line-height: 1.6;">
|
||||
{!! nl2br(e($bodyText)) !!}
|
||||
</div>
|
||||
|
||||
{{-- CTA button --}}
|
||||
@if($buttonText && $actionUrl)
|
||||
<div style="margin-top: 28px; text-align: center;">
|
||||
<a href="{{ $actionUrl }}" style="display: inline-block; padding: 12px 24px; background-color: {{ $primaryColor }}; color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 15px; font-weight: 600;">{{ $buttonText }}</a>
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- Footer --}}
|
||||
<tr>
|
||||
<td style="padding: 24px 40px; border-top: 1px solid #e5e7eb;">
|
||||
@if($footerText)
|
||||
<p style="margin: 0 0 16px; font-size: 13px; color: #6b7280; text-align: center; line-height: 1.5;">
|
||||
{{ $footerText }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<p style="margin: 0; font-size: 11px; color: #9ca3af; text-align: center; line-height: 1.5;">
|
||||
Powered by Crewli
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
12
api/resources/views/emails/transactional_text.blade.php
Normal file
12
api/resources/views/emails/transactional_text.blade.php
Normal file
@@ -0,0 +1,12 @@
|
||||
@if($heading)
|
||||
{{ $heading }}
|
||||
|
||||
@endif
|
||||
{{ $bodyText }}
|
||||
@if($buttonText && $actionUrl)
|
||||
|
||||
{{ $buttonText }}: {{ $actionUrl }}
|
||||
@endif
|
||||
|
||||
---
|
||||
{{ $footerText ?? 'Crewli' }}
|
||||
Reference in New Issue
Block a user