feat(api): organisation email branding and shared mail layout

- Add email branding columns to organisations table (logo, color, reply-to, sender name, footer)
- Create MailBrandingService for resolving per-org branding with defaults
- Create CrewliMailable abstract base class with branded from/reply-to
- Create shared Blade layout (mail.layouts.crewli) with inline CSS
- Refactor Registration*Mail and InvitationMail to extend CrewliMailable
- Add config/crewli.php for platform-wide defaults (portal_url, app_url, logo)
- Add dev-only /mail-preview/{type} route for browser email previewing
- Update Organisation model, resource, and form requests with branding fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 00:44:34 +02:00
parent de8ebf724b
commit ec4ba8733d
21 changed files with 739 additions and 60 deletions

View File

@@ -0,0 +1,15 @@
@extends('mail.layouts.crewli')
@section('title')
Je bent uitgenodigd!
@endsection
@section('content')
<p style="margin: 0 0 16px;">{{ $inviterName }} heeft je uitgenodigd om deel te nemen als <strong>{{ $role }}</strong>.</p>
<p style="margin: 0;">Deze uitnodiging verloopt op <strong>{{ $expiresAt->format('d-m-Y H:i') }}</strong> (over 7 dagen).</p>
@endsection
@section('action')
<a href="{{ $acceptUrl }}" style="display: inline-block; padding: 12px 24px; background-color: {{ $branding['primary_color'] }}; color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 15px; font-weight: 600;">Uitnodiging accepteren</a>
@endsection

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title')</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<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: {{ $branding['primary_color'] }};"></td>
</tr>
{{-- Header: logo + org name --}}
<tr>
<td align="center" style="padding: 32px 40px 24px;">
@if($branding['logo_url'])
<img src="{{ $branding['logo_url'] }}" alt="{{ $branding['organisation_name'] }}" style="max-height: 60px; max-width: 200px; display: block; margin-bottom: 12px;">
@endif
<p style="margin: 0; font-size: 15px; color: #6b7280; font-weight: 500;">{{ $branding['organisation_name'] }}</p>
</td>
</tr>
{{-- Body --}}
<tr>
<td style="padding: 0 40px 32px;">
{{-- Title --}}
<h1 style="margin: 0 0 20px; font-size: 22px; font-weight: 700; color: #1f2937; line-height: 1.3;">
@yield('title')
</h1>
{{-- Content --}}
<div style="font-size: 16px; color: #1f2937; line-height: 1.6;">
@yield('content')
</div>
{{-- Action button --}}
@hasSection('action')
<div style="margin-top: 28px;">
@yield('action')
</div>
@endif
</td>
</tr>
{{-- Footer --}}
<tr>
<td style="padding: 24px 40px; border-top: 1px solid #e5e7eb;">
@if($branding['footer_text'])
<p style="margin: 0 0 16px; font-size: 13px; color: #6b7280; text-align: center; line-height: 1.5;">
{{ $branding['footer_text'] }}
</p>
@endif
<p style="margin: 0; font-size: 11px; color: #9ca3af; text-align: center; line-height: 1.5;">
Powered by Crewli<br>
Je ontvangt deze email omdat je bent aangemeld bij {{ $branding['organisation_name'] }}.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,17 @@
@extends('mail.layouts.crewli')
@section('title')
Goed nieuws!
@endsection
@section('content')
<p style="margin: 0 0 16px;">Beste {{ $person->first_name }},</p>
<p style="margin: 0 0 16px;">Goed nieuws! Je bent goedgekeurd als vrijwilliger voor <strong>{{ $event->name }}</strong>.</p>
<p style="margin: 0;">Log in op het portaal om je shifts te bekijken en te claimen.</p>
@endsection
@section('action')
<a href="{{ $portalUrl }}" style="display: inline-block; padding: 12px 24px; background-color: {{ $branding['primary_color'] }}; color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 15px; font-weight: 600;">Naar het portaal</a>
@endsection

View File

@@ -0,0 +1,24 @@
@extends('mail.layouts.crewli')
@section('title')
Bedankt voor je aanmelding!
@endsection
@section('content')
<p style="margin: 0 0 16px;">Beste {{ $person->first_name }},</p>
<p style="margin: 0 0 16px;">Bedankt voor je aanmelding als vrijwilliger voor <strong>{{ $event->name }}</strong>!</p>
<p style="margin: 0 0 16px;">Je registratie wordt beoordeeld door de organisatie. Je ontvangt een e-mail zodra je aanmelding is verwerkt.</p>
<p style="margin: 0 0 16px;">Je kunt inloggen op het vrijwilligersportaal om je status te volgen.</p>
<p style="margin: 0;">
<strong>Evenement:</strong> {{ $event->name }}<br>
<strong>Datum:</strong> {{ $event->start_date->format('d-m-Y') }} t/m {{ $event->end_date->format('d-m-Y') }}
</p>
@endsection
@section('action')
<a href="{{ $portalUrl }}" style="display: inline-block; padding: 12px 24px; background-color: {{ $branding['primary_color'] }}; color: #ffffff; text-decoration: none; border-radius: 6px; font-size: 15px; font-weight: 600;">Naar het portaal</a>
@endsection

View File

@@ -0,0 +1,17 @@
@extends('mail.layouts.crewli')
@section('title')
Update over je aanmelding
@endsection
@section('content')
<p style="margin: 0 0 16px;">Beste {{ $person->first_name }},</p>
<p style="margin: 0 0 16px;">Helaas hebben we je aanmelding voor <strong>{{ $event->name }}</strong> niet kunnen goedkeuren.</p>
@if($reason)
<p style="margin: 0 0 16px;"><strong>Reden:</strong> {{ $reason }}</p>
@endif
<p style="margin: 0;">Neem contact op met de organisatie als je vragen hebt.</p>
@endsection