Compare commits

...

3 Commits

Author SHA1 Message Date
Yeltsin Batiancila
3b9f1753d6 Merge branch 'develop' of https://code.cannabrands.app/Cannabrands/hub into feature/email-templating 2025-10-29 00:53:07 +08:00
Yeltsin Batiancila
fd1f76ea2d feat: Add business application submitted email notification for admins
- Implemented BusinessApplicationSubmittedMail to handle email notifications for new business applications.
- Updated SellerNotificationService to send email notifications upon business application submission.
- Created EmailTemplateSeeder to seed email templates, including the new business application submitted template.
- Added a console command (TestBusinessApplicationEmail) for testing the business application email notification.
- Updated DatabaseSeeder to include EmailTemplateSeeder.
- Created a Blade view for the business application submitted email.
- Introduced UsesEmailTemplate trait for reusable email template logic.
- Refactored email content rendering to utilize database templates when available.
2025-10-29 00:39:29 +08:00
Yeltsin Batiancila
c4d17e9c22 feat: add EmailTemplate resource with CRUD functionality and email verification template 2025-10-28 02:58:46 +08:00
30 changed files with 2243 additions and 65 deletions

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use App\Models\Business;
use App\Services\SellerNotificationService;
use Illuminate\Console\Command;
class TestBusinessApplicationEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:business-application-email {business_id?}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test the business application email notification';
/**
* Execute the console command.
*/
public function handle()
{
$businessId = $this->argument('business_id');
if ($businessId) {
$business = Business::find($businessId);
if (! $business) {
$this->error("Business with ID {$businessId} not found.");
return 1;
}
} else {
// Get the first business with status 'submitted'
$business = Business::where('status', 'submitted')->first();
if (! $business) {
// Get any business
$business = Business::first();
}
if (! $business) {
$this->error('No businesses found in the database.');
return 1;
}
}
$this->info("Testing business application email for: {$business->name} (ID: {$business->id})");
// Send the notification
$service = app(SellerNotificationService::class);
$service->businessApplicationSubmitted($business);
$this->info('Email notification sent successfully!');
$this->info('Check Mailpit at http://localhost:8025 to view the email.');
return 0;
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\EmailTemplateResource\Pages\CreateEmailTemplate;
use App\Filament\Resources\EmailTemplateResource\Pages\EditEmailTemplate;
use App\Filament\Resources\EmailTemplateResource\Pages\ListEmailTemplates;
use App\Filament\Resources\EmailTemplateResource\Pages\ViewEmailTemplate;
use App\Filament\Resources\EmailTemplateResource\Schemas\EmailTemplateForm;
use App\Filament\Resources\EmailTemplateResource\Schemas\EmailTemplateInfolist;
use App\Filament\Resources\EmailTemplateResource\Tables\EmailTemplatesTable;
use App\Models\EmailTemplate;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
class EmailTemplateResource extends Resource
{
protected static ?string $model = EmailTemplate::class;
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-envelope';
protected static \UnitEnum|string|null $navigationGroup = 'System';
protected static ?int $navigationSort = 10;
protected static ?string $navigationLabel = 'Email Templates';
protected static ?string $modelLabel = 'Email Template';
protected static ?string $pluralModelLabel = 'Email Templates';
public static function getNavigationBadge(): ?string
{
// Count inactive templates
return static::getModel()::where('is_active', false)->count() ?: null;
}
public static function form(Schema $schema): Schema
{
return EmailTemplateForm::configure($schema);
}
public static function infolist(Schema $schema): Schema
{
return EmailTemplateInfolist::configure($schema);
}
public static function table(Table $table): Table
{
return EmailTemplatesTable::configure($table);
}
public static function getPages(): array
{
return [
'index' => ListEmailTemplates::route('/'),
'create' => CreateEmailTemplate::route('/create'),
'view' => ViewEmailTemplate::route('/{record}'),
'edit' => EditEmailTemplate::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Pages;
use App\Filament\Resources\EmailTemplateResource;
use Filament\Resources\Pages\CreateRecord;
class CreateEmailTemplate extends CreateRecord
{
protected static string $resource = EmailTemplateResource::class;
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Pages;
use App\Filament\Resources\EmailTemplateResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ViewAction;
use Filament\Resources\Pages\EditRecord;
class EditEmailTemplate extends EditRecord
{
protected static string $resource = EmailTemplateResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
];
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Pages;
use App\Filament\Resources\EmailTemplateResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListEmailTemplates extends ListRecords
{
protected static string $resource = EmailTemplateResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Pages;
use App\Filament\Resources\EmailTemplateResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;
class ViewEmailTemplate extends ViewRecord
{
protected static string $resource = EmailTemplateResource::class;
protected function getHeaderActions(): array
{
return [
EditAction::make(),
];
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Schemas;
use App\Models\EmailTemplate;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class EmailTemplateForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->columns(1)
->components([
Section::make('Template Details')
->schema([
TextInput::make('key')
->label('Template Key')
->required()
->unique(ignoreRecord: true)
->regex('/^[a-z0-9_-]+$/')
->helperText('Lowercase alphanumeric characters, hyphens and underscores only')
->disabled(fn ($context) => $context === 'edit')
->dehydrated(fn ($context) => $context === 'create')
->columnSpanFull(),
TextInput::make('name')
->label('Template Name')
->required()
->maxLength(255)
->columnSpanFull(),
TextInput::make('subject')
->label('Email Subject')
->required()
->maxLength(255)
->columnSpanFull(),
Textarea::make('description')
->label('Description')
->helperText('Describe when this template is used')
->rows(3)
->columnSpanFull(),
TextInput::make('available_variables')
->label('Available Variables')
->helperText('Comma-separated list (e.g., verification_url, email, logo_url)')
->afterStateHydrated(function (TextInput $component, $state) {
if (is_array($state)) {
$component->state(implode(', ', $state));
}
})
->dehydrateStateUsing(function ($state) {
if (empty($state)) {
return [];
}
return array_map('trim', explode(',', $state));
})
->columnSpanFull(),
Checkbox::make('is_active')
->label('Template is Active')
->default(true)
->inline(false),
])
->columns(2),
Section::make('Email Content')
->schema([
Textarea::make('body_html')
->label('HTML Body')
->required()
->rows(25)
->helperText('Use {{ $variable }} syntax for dynamic content')
->columnSpanFull()
->extraAttributes(['style' => 'font-family: monospace; font-size: 13px;']),
Textarea::make('body_text')
->label('Plain Text Body (Optional)')
->rows(15)
->helperText('Plain text fallback for email clients that don\'t support HTML')
->columnSpanFull()
->extraAttributes(['style' => 'font-family: monospace; font-size: 13px;']),
]),
Section::make('Metadata')
->schema([
Placeholder::make('created_at')
->label('Created At')
->content(fn (?EmailTemplate $record): string => $record?->created_at?->diffForHumans() ?? '-'),
Placeholder::make('updated_at')
->label('Last Updated')
->content(fn (?EmailTemplate $record): string => $record?->updated_at?->diffForHumans() ?? '-'),
])
->columns(2)
->hidden(fn ($context) => $context === 'create'),
]);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Schemas;
use Filament\Infolists\Components\IconEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ViewEntry;
use Filament\Schemas\Schema;
use Illuminate\Support\HtmlString;
class EmailTemplateInfolist
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextEntry::make('name')
->label('Template Name')
->columnSpan(1),
TextEntry::make('key')
->label('Template Key')
->badge()
->copyable()
->copyMessage('Key copied!')
->copyMessageDuration(1500)
->columnSpan(1),
TextEntry::make('subject')
->label('Email Subject')
->columnSpan(2),
TextEntry::make('description')
->label('Description')
->columnSpan(2)
->placeholder('No description provided'),
TextEntry::make('available_variables')
->label('Available Variables')
->badge()
->separator(',')
->columnSpan(2)
->placeholder('No variables defined'),
IconEntry::make('is_active')
->label('Status')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->columnSpan(1),
TextEntry::make('created_at')
->label('Created')
->dateTime()
->since()
->columnSpan(1),
TextEntry::make('updated_at')
->label('Last Updated')
->dateTime()
->since()
->columnSpan(1),
ViewEntry::make('preview')
->label('HTML Preview')
->viewData(fn ($record) => [
'html' => $record->body_html,
])
->view('filament.email-template-preview')
->columnSpan(2),
TextEntry::make('body_html')
->label('HTML Source')
->formatStateUsing(fn ($state) => new HtmlString('<pre class="text-xs font-mono bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-x-auto whitespace-pre-wrap">'.htmlspecialchars($state).'</pre>'))
->columnSpan(2),
TextEntry::make('body_text')
->label('Plain Text Version')
->formatStateUsing(fn ($state) => new HtmlString('<pre class="text-xs font-mono bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-x-auto whitespace-pre-wrap">'.htmlspecialchars($state ?: 'No plain text version').'</pre>'))
->columnSpan(2)
->hidden(fn ($record) => empty($record->body_text)),
])
->columns(2);
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Filament\Resources\EmailTemplateResource\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
class EmailTemplatesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label('Template Name')
->searchable()
->sortable()
->weight('bold'),
TextColumn::make('key')
->label('Key')
->searchable()
->sortable()
->fontFamily('mono')
->size('sm')
->copyable()
->copyMessage('Key copied!')
->copyMessageDuration(1500),
TextColumn::make('subject')
->label('Subject')
->searchable()
->limit(50)
->wrap(),
IconColumn::make('is_active')
->label('Status')
->boolean()
->trueIcon('heroicon-o-check-circle')
->falseIcon('heroicon-o-x-circle')
->trueColor('success')
->falseColor('danger')
->sortable(),
TextColumn::make('updated_at')
->label('Last Updated')
->dateTime()
->sortable()
->since()
->size('sm'),
])
->defaultSort('name')
->filters([
SelectFilter::make('is_active')
->label('Status')
->options([
true => 'Active',
false => 'Inactive',
]),
])
->recordActions([
ViewAction::make(),
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace App\Mail\Admin;
use App\Models\Business;
use App\Models\EmailTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class BusinessApplicationSubmittedMail extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
public Business $business
) {}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
$businessType = $this->business->business_type
? (Business::BUSINESS_TYPES[$this->business->business_type] ?? $this->business->business_type)
: 'Business';
// Check if custom template exists and use its subject
$template = EmailTemplate::getByKey('business_application_submitted');
if ($template) {
$subject = $template->renderSubject($this->getTemplateVariables());
} else {
$subject = "New {$businessType} Application - {$this->business->name}";
}
return new Envelope(
subject: $subject,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
// Check if custom template exists in database
$template = EmailTemplate::getByKey('business_application_submitted');
if ($template) {
// Use custom HTML template from database
return new Content(
htmlString: $template->render($this->getTemplateVariables()),
);
}
// Fall back to blade template
return new Content(
markdown: 'emails.admin.business-application-submitted',
);
}
/**
* Get template variables for rendering
*/
private function getTemplateVariables(): array
{
$businessType = $this->business->business_type
? (Business::BUSINESS_TYPES[$this->business->business_type] ?? $this->business->business_type)
: 'Not specified';
// Format physical address
$physicalAddress = $this->business->physical_address ?? 'Not provided';
if ($this->business->physical_city || $this->business->physical_state || $this->business->physical_zipcode) {
$physicalAddress .= '<br>'.implode(', ', array_filter([
$this->business->physical_city,
$this->business->physical_state,
$this->business->physical_zipcode,
]));
}
// Format billing address
$billingAddress = $this->business->billing_address ?? 'Not provided';
if ($this->business->billing_city || $this->business->billing_state || $this->business->billing_zipcode) {
$billingAddress .= '<br>'.implode(', ', array_filter([
$this->business->billing_city,
$this->business->billing_state,
$this->business->billing_zipcode,
]));
}
// Format shipping address
$shippingAddress = $this->business->shipping_address ?? 'Not provided';
if ($this->business->shipping_city || $this->business->shipping_state || $this->business->shipping_zipcode) {
$shippingAddress .= '<br>'.implode(', ', array_filter([
$this->business->shipping_city,
$this->business->shipping_state,
$this->business->shipping_zipcode,
]));
}
// Format compliance documents status
$documentsStatus = '';
$docs = [
'w9_form_path' => 'W9 Form',
'resale_certificate_path' => 'Resale Certificate',
'ato_document_path' => 'ATO Document',
'tpt_document_path' => 'TPT Document',
'form_5000a_path' => 'Form 5000A',
];
foreach ($docs as $field => $label) {
$status = $this->business->$field
? '<span class="status-badge status-uploaded">✓ Uploaded</span>'
: '<span class="status-badge status-missing">Not uploaded</span>';
$documentsStatus .= "<div class=\"info-row\"><span class=\"info-label\">{$label}:</span> {$status}</div>";
}
return [
'business_name' => $this->business->name ?? 'N/A',
'business_type' => $businessType,
'dba_name' => $this->business->dba_name ?? 'N/A',
'license_number' => $this->business->license_number ?? 'Not provided',
'tin_ein' => $this->business->tin_ein ?? 'Not provided',
'submitted_date' => $this->business->application_submitted_at
? $this->business->application_submitted_at->format('M j, Y \a\t g:i A')
: now()->format('M j, Y \a\t g:i A'),
'business_phone' => $this->business->business_phone ?? 'Not provided',
'business_email' => $this->business->business_email ?? 'Not provided',
'physical_address' => $physicalAddress,
'billing_address' => $billingAddress,
'shipping_address' => $shippingAddress,
'primary_contact_name' => trim(($this->business->primary_contact_first_name ?? '').' '.($this->business->primary_contact_last_name ?? '')) ?: 'Not provided',
'primary_contact_email' => $this->business->primary_contact_email ?? 'Not provided',
'primary_contact_phone' => $this->business->primary_contact_phone ?? 'Not provided',
'ap_contact_name' => trim(($this->business->ap_contact_first_name ?? '').' '.($this->business->ap_contact_last_name ?? '')) ?: 'Not provided',
'ap_contact_email' => $this->business->ap_contact_email ?? 'Not provided',
'ap_contact_phone' => $this->business->ap_contact_phone ?? 'Not provided',
'delivery_contact_name' => trim(($this->business->delivery_contact_first_name ?? '').' '.($this->business->delivery_contact_last_name ?? '')) ?: 'Not provided',
'delivery_contact_phone' => $this->business->delivery_contact_phone ?? 'Not provided',
'payment_method' => $this->business->payment_method ? ucfirst(str_replace('_', ' ', $this->business->payment_method)) : 'Not specified',
'documents_status' => $documentsStatus,
'review_url' => route('filament.admin.resources.businesses.edit', $this->business),
'logo_url' => asset('assets/images/canna_white.png'),
'account_name' => config('app.name'),
];
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Mail\Concerns;
use App\Models\EmailTemplate;
use Illuminate\Mail\Mailables\Content;
trait UsesEmailTemplate
{
/**
* Get the template key for this email.
* Must be implemented by the class using this trait.
*/
abstract protected function getTemplateKey(): string;
/**
* Get the fallback view for this email.
* Must be implemented by the class using this trait.
*/
abstract protected function getFallbackView(): string;
/**
* Get the variables for template rendering.
* Must be implemented by the class using this trait.
*/
abstract protected function getTemplateVariables(): array;
/**
* Get the email content, using database template if available.
*/
protected function getContentWithTemplate(): Content
{
$template = EmailTemplate::getByKey($this->getTemplateKey());
if ($template) {
// Use custom HTML template from database
return new Content(
htmlString: $template->render($this->getTemplateVariables()),
);
}
// Fall back to blade template
$viewType = str_contains($this->getFallbackView(), 'markdown:') ? 'markdown' : 'view';
if ($viewType === 'markdown') {
return new Content(
markdown: str_replace('markdown:', '', $this->getFallbackView()),
);
}
return new Content(
view: $this->getFallbackView(),
);
}
/**
* Get the email subject, using database template if available.
*/
protected function getSubjectWithTemplate(string $fallbackSubject): string
{
$template = EmailTemplate::getByKey($this->getTemplateKey());
if ($template) {
return $template->renderSubject($this->getTemplateVariables());
}
return $fallbackSubject;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Mail;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Contact;
use App\Models\User;
use Illuminate\Bus\Queueable;
@@ -13,7 +14,7 @@ use Illuminate\Queue\SerializesModels;
class ExistingContactWelcome extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
/**
* Create a new message instance.
@@ -29,7 +30,7 @@ class ExistingContactWelcome extends Mailable implements ShouldQueue
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to Cannabrands - Your Account is Ready!',
subject: $this->getSubjectWithTemplate('Welcome to '.config('app.name').' - Your Account is Ready!'),
);
}
@@ -38,16 +39,29 @@ class ExistingContactWelcome extends Mailable implements ShouldQueue
*/
public function content(): Content
{
return new Content(
markdown: 'emails.existing-contact-welcome',
with: [
'user' => $this->user,
'contact' => $this->contact,
'company' => $this->contact->company,
'loginUrl' => route('login'),
'dashboardUrl' => route('buyer.dashboard'),
],
);
return $this->getContentWithTemplate();
}
protected function getTemplateKey(): string
{
return 'existing_contact_welcome';
}
protected function getFallbackView(): string
{
return 'markdown:emails.existing-contact-welcome';
}
protected function getTemplateVariables(): array
{
return [
'contact_first_name' => $this->contact->first_name,
'company_name' => $this->contact->company->name ?? 'your company',
'dashboard_url' => route('buyer.dashboard'),
'account_name' => config('app.name'),
'support_email' => config('mail.from.address'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Invoices;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class InvoiceReadyMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
public function __construct(
public Invoice $invoice
@@ -20,15 +21,39 @@ class InvoiceReadyMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Invoice {$this->invoice->invoice_number} Ready for Approval",
subject: $this->getSubjectWithTemplate("Invoice {$this->invoice->invoice_number} Ready for Approval"),
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.invoices.invoice-ready',
);
return $this->getContentWithTemplate();
}
protected function getTemplateKey(): string
{
return 'invoice_ready';
}
protected function getFallbackView(): string
{
return 'markdown:emails.invoices.invoice-ready';
}
protected function getTemplateVariables(): array
{
return [
'invoice_number' => $this->invoice->invoice_number,
'order_number' => $this->invoice->order->order_number ?? 'N/A',
'invoice_date' => $this->invoice->invoice_date->format('M j, Y'),
'due_date' => $this->invoice->due_date->format('M j, Y'),
'amount_due' => number_format($this->invoice->amount_due, 2),
'payment_terms' => $this->invoice->order->payment_terms ? ucfirst(str_replace('_', ' ', $this->invoice->order->payment_terms)) : 'N/A',
'invoice_url' => route('buyer.invoices.show', $this->invoice),
'account_name' => config('app.name'),
'support_email' => config('mail.from.address'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
public function attachments(): array

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Invoices;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class PaymentReceivedMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
/**
* Create a new message instance.
@@ -27,7 +28,7 @@ class PaymentReceivedMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Payment Received for Invoice {$this->invoice->invoice_number}",
subject: $this->getSubjectWithTemplate("Payment Received for Invoice {$this->invoice->invoice_number}"),
);
}
@@ -36,9 +37,31 @@ class PaymentReceivedMail extends Mailable
*/
public function content(): Content
{
return new Content(
markdown: 'emails.invoices.payment-received',
);
return $this->getContentWithTemplate();
}
protected function getTemplateKey(): string
{
return 'payment_received';
}
protected function getFallbackView(): string
{
return 'markdown:emails.invoices.payment-received';
}
protected function getTemplateVariables(): array
{
return [
'invoice_number' => $this->invoice->invoice_number,
'payment_amount' => number_format($this->paymentAmount, 2),
'payment_date' => now()->format('M j, Y'),
'remaining_balance' => number_format($this->invoice->amount_due, 2),
'is_fully_paid' => $this->invoice->isPaid() ? 'Fully Paid' : 'Partial Payment',
'invoice_url' => route('buyer.invoices.show', $this->invoice),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Orders;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class OrderAcceptedMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
/**
* Create a new message instance.
@@ -26,7 +27,7 @@ class OrderAcceptedMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Order {$this->order->order_number} Accepted",
subject: $this->getSubjectWithTemplate("Order {$this->order->order_number} Accepted"),
);
}
@@ -35,9 +36,39 @@ class OrderAcceptedMail extends Mailable
*/
public function content(): Content
{
return new Content(
markdown: 'emails.orders.order-accepted',
);
return $this->getContentWithTemplate();
}
/**
* Get the template key for this email.
*/
protected function getTemplateKey(): string
{
return 'order_accepted';
}
/**
* Get the fallback view for this email.
*/
protected function getFallbackView(): string
{
return 'markdown:emails.orders.order-accepted';
}
/**
* Get the variables for template rendering.
*/
protected function getTemplateVariables(): array
{
return [
'order_number' => $this->order->order_number,
'order_date' => $this->order->created_at->format('M j, Y'),
'total_items' => $this->order->items->count(),
'order_total' => number_format($this->order->total, 2),
'order_url' => route('buyer.orders.show', $this->order),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Orders;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class OrderDeliveredMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
public function __construct(
public Order $order
@@ -20,15 +21,36 @@ class OrderDeliveredMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Order {$this->order->order_number} Delivered",
subject: $this->getSubjectWithTemplate("Order {$this->order->order_number} Delivered"),
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.orders.order-delivered',
);
return $this->getContentWithTemplate();
}
protected function getTemplateKey(): string
{
return 'order_delivered';
}
protected function getFallbackView(): string
{
return 'markdown:emails.orders.order-delivered';
}
protected function getTemplateVariables(): array
{
return [
'order_number' => $this->order->order_number,
'delivery_date' => $this->order->delivered_at ? $this->order->delivered_at->format('M j, Y') : now()->format('M j, Y'),
'total_items' => $this->order->items->count(),
'order_total' => number_format($this->order->total, 2),
'order_url' => route('buyer.orders.show', $this->order),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
public function attachments(): array

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Orders;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class OrderReadyForDeliveryMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
public function __construct(
public Order $order
@@ -20,15 +21,46 @@ class OrderReadyForDeliveryMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Order {$this->order->order_number} Ready for Delivery",
subject: $this->getSubjectWithTemplate("Order {$this->order->order_number} Ready for Delivery"),
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.orders.ready-for-delivery',
);
return $this->getContentWithTemplate();
}
protected function getTemplateKey(): string
{
return 'order_ready_for_delivery';
}
protected function getFallbackView(): string
{
return 'markdown:emails.orders.ready-for-delivery';
}
protected function getTemplateVariables(): array
{
$deliveryAddress = $this->order->company->shipping_address ?? $this->order->company->physical_address ?? 'Not specified';
if ($this->order->company->shipping_city || $this->order->company->shipping_state) {
$deliveryAddress .= ', '.implode(', ', array_filter([
$this->order->company->shipping_city,
$this->order->company->shipping_state,
$this->order->company->shipping_zipcode,
]));
}
return [
'order_number' => $this->order->order_number,
'order_date' => $this->order->created_at->format('M j, Y'),
'total_items' => $this->order->items->count(),
'order_total' => number_format($this->order->total, 2),
'delivery_address' => $deliveryAddress,
'order_url' => route('buyer.orders.show', $this->order),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
public function attachments(): array

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Seller;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class NewOrderReceivedMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
/**
* Create a new message instance.
@@ -26,7 +27,7 @@ class NewOrderReceivedMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "New Order Received - {$this->order->order_number}",
subject: $this->getSubjectWithTemplate("New Order Received - {$this->order->order_number}"),
);
}
@@ -35,9 +36,42 @@ class NewOrderReceivedMail extends Mailable
*/
public function content(): Content
{
return new Content(
markdown: 'emails.seller.new-order-received',
);
return $this->getContentWithTemplate();
}
/**
* Get the template key for this email.
*/
protected function getTemplateKey(): string
{
return 'seller_new_order_received';
}
/**
* Get the fallback view for this email.
*/
protected function getFallbackView(): string
{
return 'markdown:emails.seller.new-order-received';
}
/**
* Get the variables for template rendering.
*/
protected function getTemplateVariables(): array
{
return [
'order_number' => $this->order->order_number,
'customer_name' => $this->order->company->name ?? 'Unknown',
'order_date' => $this->order->created_at->format('M j, Y \a\t g:i A'),
'total_items' => $this->order->items->count(),
'order_total' => number_format($this->order->total, 2),
'payment_terms' => $this->order->payment_terms ? ucfirst(str_replace('_', ' ', $this->order->payment_terms)) : 'N/A',
'customer_notes' => $this->order->notes ?? 'None',
'order_url' => route('filament.admin.resources.orders.view', $this->order),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Seller;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class OrderCancelledMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
/**
* Create a new message instance.
@@ -26,7 +27,7 @@ class OrderCancelledMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Order Cancelled - {$this->order->order_number}",
subject: $this->getSubjectWithTemplate("Order {$this->order->order_number} Cancelled"),
);
}
@@ -35,9 +36,40 @@ class OrderCancelledMail extends Mailable
*/
public function content(): Content
{
return new Content(
markdown: 'emails.seller.order-cancelled',
);
return $this->getContentWithTemplate();
}
/**
* Get the template key for this email.
*/
protected function getTemplateKey(): string
{
return 'seller_order_cancelled';
}
/**
* Get the fallback view for this email.
*/
protected function getFallbackView(): string
{
return 'markdown:emails.seller.order-cancelled';
}
/**
* Get the variables for template rendering.
*/
protected function getTemplateVariables(): array
{
return [
'order_number' => $this->order->order_number,
'customer_name' => $this->order->company->name ?? 'Unknown',
'order_date' => $this->order->created_at->format('M j, Y'),
'order_total' => number_format($this->order->total, 2),
'cancellation_reason' => $this->order->cancellation_reason ?? 'Not specified',
'order_url' => route('filament.admin.resources.orders.view', $this->order),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Mail\Seller;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\Invoice;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -11,7 +12,7 @@ use Illuminate\Queue\SerializesModels;
class PaymentReceivedMail extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
/**
* Create a new message instance.
@@ -27,7 +28,7 @@ class PaymentReceivedMail extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: "Payment Received - {$this->invoice->invoice_number}",
subject: $this->getSubjectWithTemplate("Payment Received for Invoice {$this->invoice->invoice_number}"),
);
}
@@ -36,9 +37,40 @@ class PaymentReceivedMail extends Mailable
*/
public function content(): Content
{
return new Content(
markdown: 'emails.seller.payment-received',
);
return $this->getContentWithTemplate();
}
/**
* Get the template key for this email.
*/
protected function getTemplateKey(): string
{
return 'seller_payment_received';
}
/**
* Get the fallback view for this email.
*/
protected function getFallbackView(): string
{
return 'markdown:emails.seller.payment-received';
}
/**
* Get the variables for template rendering.
*/
protected function getTemplateVariables(): array
{
return [
'invoice_number' => $this->invoice->invoice_number,
'payment_amount' => number_format($this->paymentAmount, 2),
'remaining_balance' => number_format($this->invoice->amount_due, 2),
'is_fully_paid' => $this->invoice->isPaid() ? 'Fully Paid' : 'Partial Payment',
'order_number' => $this->invoice->order->order_number ?? 'N/A',
'order_url' => route('filament.admin.resources.orders.view', $this->invoice->order),
'account_name' => config('app.name'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Mail;
use App\Mail\Concerns\UsesEmailTemplate;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@@ -13,7 +14,7 @@ use Illuminate\Support\Facades\URL;
class UserApproved extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, UsesEmailTemplate;
public User $user;
@@ -40,7 +41,7 @@ class UserApproved extends Mailable
public function envelope(): Envelope
{
return new Envelope(
subject: 'Welcome to Cannabrands - Your Account is Approved!',
subject: $this->getSubjectWithTemplate('Welcome to '.config('app.name').' - Your Account is Approved!'),
from: config('mail.from.address', 'hello@cannabrands.com'),
);
}
@@ -50,13 +51,28 @@ class UserApproved extends Mailable
*/
public function content(): Content
{
return new Content(
view: 'emails.user-approved',
with: [
'user' => $this->user,
'loginUrl' => $this->loginUrl,
]
);
return $this->getContentWithTemplate();
}
protected function getTemplateKey(): string
{
return 'user_approved';
}
protected function getFallbackView(): string
{
return 'emails.user-approved';
}
protected function getTemplateVariables(): array
{
return [
'user_name' => $this->user->name,
'login_url' => $this->loginUrl,
'account_name' => config('app.name'),
'support_email' => config('mail.from.address'),
'logo_url' => asset('assets/images/canna_white.png'),
];
}
/**

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EmailTemplate extends Model
{
use HasFactory;
protected $fillable = [
'key',
'name',
'subject',
'body_html',
'body_text',
'available_variables',
'description',
'is_active',
];
protected $casts = [
'available_variables' => 'array',
'is_active' => 'boolean',
];
/**
* Get a template by its key
*/
public static function getByKey(string $key): ?self
{
return self::where('key', $key)
->where('is_active', true)
->first();
}
/**
* Render the template with provided variables
*/
public function render(array $variables = []): string
{
$html = $this->body_html;
foreach ($variables as $key => $value) {
$html = str_replace('{{ $'.$key.' }}', $value, $html);
$html = str_replace('{{$'.$key.'}}', $value, $html);
}
return $html;
}
/**
* Render the subject with provided variables
*/
public function renderSubject(array $variables = []): string
{
$subject = $this->subject;
foreach ($variables as $key => $value) {
$subject = str_replace('{{ $'.$key.' }}', $value, $subject);
$subject = str_replace('{{$'.$key.'}}', $value, $subject);
}
return $subject;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\EmailTemplate;
use App\Models\EmailVerification;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
@@ -72,9 +73,29 @@ class EmailVerificationService
*/
public function sendVerificationEmail(string $email, string $token): void
{
$verificationUrl = route('buyer.register.verify', ['token' => $token]);
$verificationUrl = route('buyer.register.complete', ['token' => $token]);
Mail::to($email)->send(new \App\Mail\RegistrationVerificationMail($verificationUrl, $email));
// Try to use database template first, fall back to mailable if not found
$template = EmailTemplate::getByKey('registration_verification');
if ($template) {
$variables = [
'verification_url' => $verificationUrl,
'email' => $email,
'logo_url' => asset('assets/images/canna_white.png'),
'account_name' => 'Cannabrands',
'support_email' => 'hello@cannabrands.com',
];
Mail::send([], [], function ($message) use ($template, $email, $variables) {
$message->to($email)
->subject($template->renderSubject($variables))
->html($template->render($variables));
});
} else {
// Fallback to original mailable
Mail::to($email)->send(new \App\Mail\RegistrationVerificationMail($verificationUrl, $email));
}
}
/**

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Mail\Admin\BusinessApplicationSubmittedMail;
use App\Mail\Seller\NewOrderReceivedMail;
use App\Mail\Seller\OrderCancelledMail;
use App\Mail\Seller\PaymentReceivedMail as SellerPaymentReceivedMail;
@@ -113,6 +114,9 @@ class SellerNotificationService
? (\App\Models\Business::BUSINESS_TYPES[$business->business_type] ?? $business->business_type)
: 'Unknown';
// Send email notification
Mail::to($admin->email)->send(new BusinessApplicationSubmittedMail($business));
// Create in-app notification
$this->notificationService->create(
user: $admin,

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('email_templates', function (Blueprint $table) {
$table->id();
$table->string('key')->unique()->comment('Unique identifier for the template');
$table->string('name')->comment('Human-readable name');
$table->string('subject');
$table->text('body_html')->comment('HTML version of email');
$table->text('body_text')->nullable()->comment('Plain text version');
$table->json('available_variables')->nullable()->comment('List of variables that can be used in this template');
$table->text('description')->nullable()->comment('Description of when this template is used');
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('email_templates');
}
};

View File

@@ -16,6 +16,8 @@ class DatabaseSeeder extends Seeder
$this->call(RoleSeeder::class);
// Always seed super admin after roles exist
$this->call(SuperAdminSeeder::class);
// Always seed email templates
$this->call(EmailTemplateSeeder::class);
// Only seed test/demo users in local or staging
if (app()->environment(['local', 'staging'])) {
$this->call(DevSeeder::class);

View File

@@ -0,0 +1,901 @@
<?php
namespace Database\Seeders;
use App\Models\EmailTemplate;
use Illuminate\Database\Seeder;
class EmailTemplateSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Registration Verification Email
EmailTemplate::updateOrCreate(
['key' => 'registration_verification'],
[
'name' => 'Registration Verification',
'subject' => 'Complete Your Cannabrands Registration',
'body_html' => $this->getRegistrationVerificationHtml(),
'body_text' => $this->getRegistrationVerificationText(),
'available_variables' => [
'verification_url',
'email',
'logo_url',
'account_name',
'support_email',
],
'description' => 'Sent to users when they start the registration process to verify their email address.',
'is_active' => true,
]
);
// Business Application Submitted Email (Admin Notification)
EmailTemplate::updateOrCreate(
['key' => 'business_application_submitted'],
[
'name' => 'Business Application Submitted (Admin)',
'subject' => 'New {{ $business_type }} Application - {{ $business_name }}',
'body_html' => $this->getBusinessApplicationSubmittedHtml(),
'body_text' => $this->getBusinessApplicationSubmittedText(),
'available_variables' => [
'business_name', 'business_type', 'dba_name', 'license_number', 'tin_ein',
'submitted_date', 'business_phone', 'business_email', 'physical_address',
'billing_address', 'shipping_address', 'primary_contact_name',
'primary_contact_email', 'primary_contact_phone', 'ap_contact_name',
'ap_contact_email', 'ap_contact_phone', 'delivery_contact_name',
'delivery_contact_phone', 'payment_method', 'documents_status',
'review_url', 'logo_url', 'account_name',
],
'description' => 'Sent to super admins when a buyer or seller submits their business application for approval.',
'is_active' => true,
]
);
// User Account Approved Email
EmailTemplate::updateOrCreate(
['key' => 'user_approved'],
[
'name' => 'User Account Approved',
'subject' => 'Welcome to {{ $account_name }} - Your Account is Approved!',
'body_html' => $this->getUserApprovedHtml(),
'body_text' => $this->getUserApprovedText(),
'available_variables' => ['user_name', 'login_url', 'account_name', 'support_email', 'logo_url'],
'description' => 'Sent to users when their account is approved by admins.',
'is_active' => true,
]
);
// Existing Contact Welcome Email
EmailTemplate::updateOrCreate(
['key' => 'existing_contact_welcome'],
[
'name' => 'Existing Contact Welcome',
'subject' => 'Welcome to {{ $account_name }} - Your Account is Ready!',
'body_html' => $this->getExistingContactWelcomeHtml(),
'body_text' => $this->getExistingContactWelcomeText(),
'available_variables' => ['contact_first_name', 'company_name', 'dashboard_url', 'account_name', 'support_email', 'logo_url'],
'description' => 'Sent to existing contacts when their user account is created.',
'is_active' => true,
]
);
// Order Accepted Email
EmailTemplate::updateOrCreate(
['key' => 'order_accepted'],
[
'name' => 'Order Accepted',
'subject' => 'Order {{ $order_number }} Accepted',
'body_html' => $this->getOrderAcceptedHtml(),
'body_text' => $this->getOrderAcceptedText(),
'available_variables' => ['order_number', 'order_date', 'total_items', 'order_total', 'order_url', 'account_name', 'logo_url'],
'description' => 'Sent to buyers when their order is accepted.',
'is_active' => true,
]
);
// Order Ready for Delivery Email
EmailTemplate::updateOrCreate(
['key' => 'order_ready_for_delivery'],
[
'name' => 'Order Ready for Delivery',
'subject' => 'Order {{ $order_number }} Ready for Delivery',
'body_html' => $this->getOrderReadyForDeliveryHtml(),
'body_text' => $this->getOrderReadyForDeliveryText(),
'available_variables' => ['order_number', 'order_date', 'total_items', 'order_total', 'delivery_address', 'order_url', 'account_name', 'logo_url'],
'description' => 'Sent to buyers when their order is ready for delivery.',
'is_active' => true,
]
);
// Order Delivered Email
EmailTemplate::updateOrCreate(
['key' => 'order_delivered'],
[
'name' => 'Order Delivered',
'subject' => 'Order {{ $order_number }} Delivered',
'body_html' => $this->getOrderDeliveredHtml(),
'body_text' => $this->getOrderDeliveredText(),
'available_variables' => ['order_number', 'delivery_date', 'total_items', 'order_total', 'order_url', 'account_name', 'logo_url'],
'description' => 'Sent to buyers when their order is delivered.',
'is_active' => true,
]
);
// New Order Received (Seller)
EmailTemplate::updateOrCreate(
['key' => 'seller_new_order_received'],
[
'name' => 'New Order Received (Seller)',
'subject' => 'New Order Received - {{ $order_number }}',
'body_html' => $this->getSellerNewOrderReceivedHtml(),
'body_text' => $this->getSellerNewOrderReceivedText(),
'available_variables' => ['order_number', 'customer_name', 'order_date', 'total_items', 'order_total', 'payment_terms', 'customer_notes', 'order_url', 'account_name', 'logo_url'],
'description' => 'Sent to sellers/admins when a new order is received.',
'is_active' => true,
]
);
// Order Cancelled (Seller)
EmailTemplate::updateOrCreate(
['key' => 'seller_order_cancelled'],
[
'name' => 'Order Cancelled (Seller)',
'subject' => 'Order {{ $order_number }} Cancelled',
'body_html' => $this->getSellerOrderCancelledHtml(),
'body_text' => $this->getSellerOrderCancelledText(),
'available_variables' => ['order_number', 'customer_name', 'order_date', 'order_total', 'cancellation_reason', 'order_url', 'account_name', 'logo_url'],
'description' => 'Sent to sellers/admins when an order is cancelled.',
'is_active' => true,
]
);
// Payment Received (Seller)
EmailTemplate::updateOrCreate(
['key' => 'seller_payment_received'],
[
'name' => 'Payment Received (Seller)',
'subject' => 'Payment Received for Invoice {{ $invoice_number }}',
'body_html' => $this->getSellerPaymentReceivedHtml(),
'body_text' => $this->getSellerPaymentReceivedText(),
'available_variables' => ['invoice_number', 'payment_amount', 'remaining_balance', 'is_fully_paid', 'order_number', 'order_url', 'account_name', 'logo_url'],
'description' => 'Sent to sellers/admins when payment is received for an invoice.',
'is_active' => true,
]
);
// Invoice Ready Email
EmailTemplate::updateOrCreate(
['key' => 'invoice_ready'],
[
'name' => 'Invoice Ready',
'subject' => 'Invoice {{ $invoice_number }} Ready for Approval',
'body_html' => $this->getInvoiceReadyHtml(),
'body_text' => $this->getInvoiceReadyText(),
'available_variables' => ['invoice_number', 'order_number', 'invoice_date', 'due_date', 'amount_due', 'payment_terms', 'invoice_url', 'account_name', 'support_email', 'logo_url'],
'description' => 'Sent to buyers when an invoice is ready for approval.',
'is_active' => true,
]
);
// Payment Received (Buyer)
EmailTemplate::updateOrCreate(
['key' => 'payment_received'],
[
'name' => 'Payment Received (Buyer)',
'subject' => 'Payment Received for Invoice {{ $invoice_number }}',
'body_html' => $this->getPaymentReceivedHtml(),
'body_text' => $this->getPaymentReceivedText(),
'available_variables' => ['invoice_number', 'payment_amount', 'payment_date', 'remaining_balance', 'is_fully_paid', 'invoice_url', 'account_name', 'logo_url'],
'description' => 'Sent to buyers when their payment is received.',
'is_active' => true,
]
);
$this->command->info('Email templates seeded successfully!');
}
private function getRegistrationVerificationHtml(): string
{
return <<<'HTML'
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
background-color: #f4f5f7;
font-family: Arial, sans-serif;
color: #172B4D;
}
.main {
max-width: 600px;
margin: auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #014847 0%, #014342 100%);
padding: 30px;
text-align: center;
}
.header img {
height: 32px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin: 0;
color: #fff;
font-weight: normal;
}
.content {
padding: 30px;
font-size: 16px;
}
.content p {
margin: 20px 0 10px;
}
.highlight-box {
background: #f1f5f9;
padding: 12px 16px;
border-radius: 6px;
margin-top: 20px;
}
.button-container {
text-align: center;
margin-top: 30px;
}
.button {
display: inline-block;
background-color: #015b59;
color: #ffffff;
text-decoration: none;
padding: 14px 28px;
font-weight: bold;
border-radius: 5px;
margin-top: 20px;
}
.button:hover {
background-color: #014847;
}
.footer {
padding: 30px;
font-size: 14px;
text-align: left;
color: #6b7280;
}
.footer a {
color: #015b59;
text-decoration: none;
}
.expiry-note {
font-size: 14px;
color: #6b7280;
text-align: center;
margin-top: 15px;
}
</style>
</head>
<body>
<div class="main">
<div class="header">
<img src="{{ $logo_url }}" alt="{{ $account_name }} Logo" />
<h1>Complete Your Registration</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>Thank you for starting your registration with {{ $account_name }}! To complete your account setup, please click the button below to verify your email address and fill out your profile.</p>
<div class="highlight-box">
<strong>Email:</strong> {{ $email }}
</div>
<div class="button-container">
<a href="{{ $verification_url }}" class="button">Complete Registration</a>
</div>
<p class="expiry-note">
⏱️ This link will expire in 24 hours for security reasons.
</p>
<p>If you didn't request this registration, you can safely ignore this email.</p>
</div>
<div class="footer">
<p>Need help?</p>
<p>Contact us at <a href="mailto:{{ $support_email }}">{{ $support_email }}</a>.</p>
<p> The {{ $account_name }} Team</p>
</div>
</div>
</body>
</html>
HTML;
}
private function getRegistrationVerificationText(): string
{
return <<<'TEXT'
Complete Your Registration
Hello,
Thank you for starting your registration with {{ $account_name }}! To complete your account setup, please visit the link below to verify your email address and fill out your profile.
Email: {{ $email }}
Complete your registration here:
{{ $verification_url }}
⏱️ This link will expire in 24 hours for security reasons.
If you didn't request this registration, you can safely ignore this email.
Need help?
Contact us at {{ $support_email }}.
The {{ $account_name }} Team
TEXT;
}
private function getBusinessApplicationSubmittedHtml(): string
{
return <<<'HTML'
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
background-color: #f4f5f7;
font-family: Arial, sans-serif;
color: #172B4D;
}
.main {
max-width: 700px;
margin: auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #014847 0%, #014342 100%);
padding: 30px;
text-align: center;
}
.header img {
height: 32px;
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin: 0;
color: #fff;
font-weight: normal;
}
.content {
padding: 30px;
font-size: 16px;
}
.content p {
margin: 10px 0;
}
.info-section {
background: #f9fafb;
padding: 20px;
border-radius: 6px;
margin: 20px 0;
border-left: 4px solid #015b59;
}
.info-section h2 {
font-size: 18px;
margin: 0 0 15px 0;
color: #014847;
}
.info-row {
margin: 8px 0;
}
.info-label {
font-weight: bold;
color: #374151;
}
.info-value {
color: #6b7280;
}
.button-container {
text-align: center;
margin: 30px 0;
}
.button {
display: inline-block;
background-color: #015b59;
color: #ffffff;
text-decoration: none;
padding: 14px 28px;
font-weight: bold;
border-radius: 5px;
}
.button:hover {
background-color: #014847;
}
.footer {
padding: 30px;
font-size: 14px;
text-align: left;
color: #6b7280;
background: #f9fafb;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
}
.status-uploaded {
background: #d1fae5;
color: #065f46;
}
.status-missing {
background: #fee2e2;
color: #991b1b;
}
</style>
</head>
<body>
<div class="main">
<div class="header">
<img src="{{ $logo_url }}" alt="{{ $account_name }} Logo" />
<h1>New Business Application</h1>
</div>
<div class="content">
<p>A new <strong>{{ $business_type }}</strong> has submitted their application and is ready for review.</p>
<div class="info-section">
<h2>Business Information</h2>
<div class="info-row">
<span class="info-label">Business Name:</span>
<span class="info-value">{{ $business_name }}</span>
</div>
<div class="info-row">
<span class="info-label">Business Type:</span>
<span class="info-value">{{ $business_type }}</span>
</div>
<div class="info-row">
<span class="info-label">License Number:</span>
<span class="info-value">{{ $license_number }}</span>
</div>
<div class="info-row">
<span class="info-label">TIN/EIN:</span>
<span class="info-value">{{ $tin_ein }}</span>
</div>
<div class="info-row">
<span class="info-label">Submitted:</span>
<span class="info-value">{{ $submitted_date }}</span>
</div>
</div>
<div class="info-section">
<h2>Contact Information</h2>
<div class="info-row">
<span class="info-label">Phone:</span>
<span class="info-value">{{ $business_phone }}</span>
</div>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value">{{ $business_email }}</span>
</div>
<div class="info-row">
<span class="info-label">Physical Address:</span>
<span class="info-value">{{ $physical_address }}</span>
</div>
</div>
<div class="info-section">
<h2>Primary Contact</h2>
<div class="info-row">
<span class="info-label">Name:</span>
<span class="info-value">{{ $primary_contact_name }}</span>
</div>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value">{{ $primary_contact_email }}</span>
</div>
<div class="info-row">
<span class="info-label">Phone:</span>
<span class="info-value">{{ $primary_contact_phone }}</span>
</div>
</div>
<div class="info-section">
<h2>Compliance Documents</h2>
{{ $documents_status }}
</div>
<div class="button-container">
<a href="{{ $review_url }}" class="button">Review & Approve Application</a>
</div>
</div>
<div class="footer">
<p>This application requires your review and approval before the business can begin using the platform.</p>
<p> The {{ $account_name }} Team</p>
</div>
</div>
</body>
</html>
HTML;
}
private function getBusinessApplicationSubmittedText(): string
{
return <<<'TEXT'
New Business Application Submitted
A new {{ $business_type }} has submitted their application and is ready for review.
BUSINESS INFORMATION
--------------------
Business Name: {{ $business_name }}
Business Type: {{ $business_type }}
License Number: {{ $license_number }}
TIN/EIN: {{ $tin_ein }}
Submitted: {{ $submitted_date }}
CONTACT INFORMATION
-------------------
Phone: {{ $business_phone }}
Email: {{ $business_email }}
Physical Address: {{ $physical_address }}
PRIMARY CONTACT
---------------
Name: {{ $primary_contact_name }}
Email: {{ $primary_contact_email }}
Phone: {{ $primary_contact_phone }}
COMPLIANCE DOCUMENTS
--------------------
{{ $documents_status }}
Review and approve this application here:
{{ $review_url }}
This application requires your review and approval before the business can begin using the platform.
The {{ $account_name }} Team
TEXT;
}
private function getUserApprovedHtml(): string
{
return <<<'HTML'
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background-color: #f4f5f7; font-family: Arial, sans-serif; color: #172B4D; }
.main { max-width: 600px; margin: auto; background: #ffffff; border-radius: 8px; overflow: hidden; }
.header { background: linear-gradient(135deg, #014847 0%, #014342 100%); padding: 30px; text-align: center; }
.header img { height: 32px; margin-bottom: 20px; }
.header h1 { font-size: 24px; margin: 0; color: #fff; font-weight: normal; }
.content { padding: 30px; font-size: 16px; }
.button { display: inline-block; background-color: #015b59; color: #ffffff; text-decoration: none; padding: 14px 28px; font-weight: bold; border-radius: 5px; }
.footer { padding: 30px; font-size: 14px; color: #6b7280; background: #f9fafb; }
</style>
</head>
<body>
<div class="main">
<div class="header">
<img src="{{ $logo_url }}" alt="{{ $account_name }} Logo" />
<h1>Account Approved!</h1>
</div>
<div class="content">
<p>Hi {{ $user_name }},</p>
<p>Great news! Your {{ $account_name }} account has been approved. You can now access the platform.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $login_url }}" class="button">Access Your Dashboard</a>
</div>
<p><small>This link expires in 48 hours for security.</small></p>
</div>
<div class="footer">
<p>Need help? Contact us at {{ $support_email }}</p>
<p> The {{ $account_name }} Team</p>
</div>
</div>
</body>
</html>
HTML;
}
private function getUserApprovedText(): string
{
return <<<'TEXT'
Account Approved!
Hi {{ $user_name }},
Great news! Your {{ $account_name }} account has been approved. You can now access the platform.
Access your dashboard here:
{{ $login_url }}
This link expires in 48 hours for security.
Need help? Contact us at {{ $support_email }}
The {{ $account_name }} Team
TEXT;
}
private function getExistingContactWelcomeHtml(): string
{
return <<<'HTML'
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background-color: #f4f5f7; font-family: Arial, sans-serif; color: #172B4D; }
.main { max-width: 600px; margin: auto; background: #ffffff; border-radius: 8px; overflow: hidden; }
.header { background: linear-gradient(135deg, #014847 0%, #014342 100%); padding: 30px; text-align: center; }
.header img { height: 32px; margin-bottom: 20px; }
.header h1 { font-size: 24px; margin: 0; color: #fff; font-weight: normal; }
.content { padding: 30px; font-size: 16px; }
.button { display: inline-block; background-color: #015b59; color: #ffffff; text-decoration: none; padding: 14px 28px; font-weight: bold; border-radius: 5px; }
.footer { padding: 30px; font-size: 14px; color: #6b7280; background: #f9fafb; }
</style>
</head>
<body>
<div class="main">
<div class="header">
<img src="{{ $logo_url }}" alt="{{ $account_name }} Logo" />
<h1>Welcome Back, {{ $contact_first_name }}!</h1>
</div>
<div class="content">
<p>Your account has been created and linked to {{ $company_name }}.</p>
<p>You can now browse products, place orders, and manage your business profile.</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ $dashboard_url }}" class="button">Go to Dashboard</a>
</div>
</div>
<div class="footer">
<p>Need help? Contact us at {{ $support_email }}</p>
<p> The {{ $account_name }} Team</p>
</div>
</div>
</body>
</html>
HTML;
}
private function getExistingContactWelcomeText(): string
{
return <<<'TEXT'
Welcome Back, {{ $contact_first_name }}!
Your account has been created and linked to {{ $company_name }}.
You can now browse products, place orders, and manage your business profile.
Go to your dashboard:
{{ $dashboard_url }}
Need help? Contact us at {{ $support_email }}
The {{ $account_name }} Team
TEXT;
}
private function getOrderAcceptedHtml(): string
{
return $this->getSimpleOrderTemplate(
'Order Accepted',
'Good news! Your order <strong>{{ $order_number }}</strong> has been accepted.',
'Our team is now preparing your order. You\'ll receive another email when it\'s ready for delivery.'
);
}
private function getOrderAcceptedText(): string
{
return $this->getSimpleOrderTextTemplate(
'Order Accepted',
'Good news! Your order {{ $order_number }} has been accepted.',
'Our team is now preparing your order. You\'ll receive another email when it\'s ready for delivery.'
);
}
private function getOrderReadyForDeliveryHtml(): string
{
return $this->getSimpleOrderTemplate(
'Order Ready for Delivery',
'Your order <strong>{{ $order_number }}</strong> is ready for delivery!',
'Delivery Address: {{ $delivery_address }}'
);
}
private function getOrderReadyForDeliveryText(): string
{
return $this->getSimpleOrderTextTemplate(
'Order Ready for Delivery',
'Your order {{ $order_number }} is ready for delivery!',
'Delivery Address: {{ $delivery_address }}'
);
}
private function getOrderDeliveredHtml(): string
{
return $this->getSimpleOrderTemplate(
'Order Delivered',
'Your order <strong>{{ $order_number }}</strong> has been delivered!',
'Delivery Date: {{ $delivery_date }}<br>Thank you for your business!'
);
}
private function getOrderDeliveredText(): string
{
return $this->getSimpleOrderTextTemplate(
'Order Delivered',
'Your order {{ $order_number }} has been delivered!',
'Delivery Date: {{ $delivery_date }}. Thank you for your business!'
);
}
private function getSellerNewOrderReceivedHtml(): string
{
return $this->getSimpleOrderTemplate(
'New Order Received',
'You have received a new order <strong>{{ $order_number }}</strong> from {{ $customer_name }}.',
'Payment Terms: {{ $payment_terms }}<br>Customer Notes: {{ $customer_notes }}'
);
}
private function getSellerNewOrderReceivedText(): string
{
return $this->getSimpleOrderTextTemplate(
'New Order Received',
'You have received a new order {{ $order_number }} from {{ $customer_name }}.',
'Payment Terms: {{ $payment_terms }}. Customer Notes: {{ $customer_notes }}'
);
}
private function getSellerOrderCancelledHtml(): string
{
return $this->getSimpleOrderTemplate(
'Order Cancelled',
'Order <strong>{{ $order_number }}</strong> from {{ $customer_name }} has been cancelled.',
'Cancellation Reason: {{ $cancellation_reason }}'
);
}
private function getSellerOrderCancelledText(): string
{
return $this->getSimpleOrderTextTemplate(
'Order Cancelled',
'Order {{ $order_number }} from {{ $customer_name }} has been cancelled.',
'Cancellation Reason: {{ $cancellation_reason }}'
);
}
private function getSellerPaymentReceivedHtml(): string
{
return $this->getSimpleOrderTemplate(
'Payment Received',
'Payment of <strong>${{ $payment_amount }}</strong> received for invoice {{ $invoice_number }}.',
'Remaining Balance: ${{ $remaining_balance }}<br>Status: {{ $is_fully_paid }}'
);
}
private function getSellerPaymentReceivedText(): string
{
return $this->getSimpleOrderTextTemplate(
'Payment Received',
'Payment of ${{ $payment_amount }} received for invoice {{ $invoice_number }}.',
'Remaining Balance: ${{ $remaining_balance }}. Status: {{ $is_fully_paid }}'
);
}
private function getInvoiceReadyHtml(): string
{
return $this->getSimpleOrderTemplate(
'Invoice Ready',
'Your invoice <strong>{{ $invoice_number }}</strong> for order {{ $order_number }} is ready.',
'Amount Due: ${{ $amount_due }}<br>Due Date: {{ $due_date }}<br>Payment Terms: {{ $payment_terms }}'
);
}
private function getInvoiceReadyText(): string
{
return $this->getSimpleOrderTextTemplate(
'Invoice Ready',
'Your invoice {{ $invoice_number }} for order {{ $order_number }} is ready.',
'Amount Due: ${{ $amount_due }}. Due Date: {{ $due_date }}. Payment Terms: {{ $payment_terms }}'
);
}
private function getPaymentReceivedHtml(): string
{
return $this->getSimpleOrderTemplate(
'Payment Received',
'Your payment of <strong>${{ $payment_amount }}</strong> for invoice {{ $invoice_number }} has been received.',
'Payment Date: {{ $payment_date }}<br>Remaining Balance: ${{ $remaining_balance }}<br>Status: {{ $is_fully_paid }}'
);
}
private function getPaymentReceivedText(): string
{
return $this->getSimpleOrderTextTemplate(
'Payment Received',
'Your payment of ${{ $payment_amount }} for invoice {{ $invoice_number }} has been received.',
'Payment Date: {{ $payment_date }}. Remaining Balance: ${{ $remaining_balance }}. Status: {{ $is_fully_paid }}'
);
}
private function getSimpleOrderTemplate(string $title, string $mainMessage, string $details): string
{
return <<<HTML
<!DOCTYPE html>
<html>
<head>
<style>
body { margin: 0; background-color: #f4f5f7; font-family: Arial, sans-serif; color: #172B4D; }
.main { max-width: 600px; margin: auto; background: #ffffff; border-radius: 8px; overflow: hidden; }
.header { background: linear-gradient(135deg, #014847 0%, #014342 100%); padding: 30px; text-align: center; }
.header img { height: 32px; margin-bottom: 20px; }
.header h1 { font-size: 24px; margin: 0; color: #fff; font-weight: normal; }
.content { padding: 30px; font-size: 16px; }
.info-box { background: #f9fafb; padding: 20px; border-radius: 6px; margin: 20px 0; }
.button { display: inline-block; background-color: #015b59; color: #ffffff; text-decoration: none; padding: 14px 28px; font-weight: bold; border-radius: 5px; }
.footer { padding: 30px; font-size: 14px; color: #6b7280; background: #f9fafb; }
</style>
</head>
<body>
<div class="main">
<div class="header">
<img src="{{ \$logo_url }}" alt="{{ \$account_name }} Logo" />
<h1>{$title}</h1>
</div>
<div class="content">
<p>{$mainMessage}</p>
<div class="info-box">
<p><strong>Order Number:</strong> {{ \$order_number }}</p>
<p><strong>Order Date:</strong> {{ \$order_date }}</p>
<p><strong>Total Items:</strong> {{ \$total_items }}</p>
<p><strong>Order Total:</strong> \${{ \$order_total }}</p>
</div>
<p>{$details}</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{ \$order_url }}" class="button">View Order Details</a>
</div>
</div>
<div class="footer">
<p> The {{ \$account_name }} Team</p>
</div>
</div>
</body>
</html>
HTML;
}
private function getSimpleOrderTextTemplate(string $title, string $mainMessage, string $details): string
{
return <<<TEXT
{$title}
{$mainMessage}
Order Number: {{ \$order_number }}
Order Date: {{ \$order_date }}
Total Items: {{ \$total_items }}
Order Total: \${{ \$order_total }}
{$details}
View order details:
{{ \$order_url }}
The {{ \$account_name }} Team
TEXT;
}
}

View File

@@ -72,11 +72,11 @@
<!-- Registration Form -->
<form
method="POST"
action="{{ route('buyer.register.complete') }}"
action="{{ route('buyer.register.complete.process') }}"
class="space-y-6"
novalidate
x-data="{
form: $form('post', '{{ route('buyer.register.complete') }}', {
form: $form('post', '{{ route('buyer.register.complete.process') }}', {
token: '{{ $token }}',
first_name: '{{ old('first_name', $prefill['first_name'] ?? '') }}',
last_name: '{{ old('last_name', $prefill['last_name'] ?? '') }}',

View File

@@ -0,0 +1,170 @@
@php
$businessTypeLabel = $business->business_type
? (\App\Models\Business::BUSINESS_TYPES[$business->business_type] ?? $business->business_type)
: 'Not specified';
@endphp
@component('mail::message')
# New Business Application Submitted
A new {{ strtolower($businessTypeLabel) }} application has been submitted and is ready for review.
## Business Information
**Business Name:** {{ $business->name }}
@if($business->dba_name)
**DBA Name:** {{ $business->dba_name }}
@endif
**Business Type:** {{ $businessTypeLabel }}
**License Number:** {{ $business->license_number ?? 'Not provided' }}
**TIN/EIN:** {{ $business->tin_ein ?? 'Not provided' }}
**Submitted:** {{ $business->application_submitted_at ? $business->application_submitted_at->format('M j, Y \\a\\t g:i A') : now()->format('M j, Y \\a\\t g:i A') }}
## Contact Information
**Business Phone:** {{ $business->business_phone ?? 'Not provided' }}
**Business Email:** {{ $business->business_email ?? 'Not provided' }}
## Location
**Physical Address:**
{{ $business->physical_address ?? 'Not provided' }}
@if($business->physical_city || $business->physical_state || $business->physical_zipcode)
{{ $business->physical_city }}, {{ $business->physical_state }} {{ $business->physical_zipcode }}
@endif
@if($business->billing_address)
**Billing Address:**
{{ $business->billing_address }}
@if($business->billing_city || $business->billing_state || $business->billing_zipcode)
{{ $business->billing_city }}, {{ $business->billing_state }} {{ $business->billing_zipcode }}
@endif
@endif
@if($business->shipping_address)
**Shipping Address:**
{{ $business->shipping_address }}
@if($business->shipping_city || $business->shipping_state || $business->shipping_zipcode)
{{ $business->shipping_city }}, {{ $business->shipping_state }} {{ $business->shipping_zipcode }}
@endif
@endif
## Primary Contact
@if($business->primary_contact_first_name || $business->primary_contact_last_name)
**Name:** {{ $business->primary_contact_first_name }} {{ $business->primary_contact_last_name }}
@endif
@if($business->primary_contact_email)
**Email:** {{ $business->primary_contact_email }}
@endif
@if($business->primary_contact_phone)
**Phone:** {{ $business->primary_contact_phone }}
@endif
## Accounts Payable Contact
@if($business->ap_contact_first_name || $business->ap_contact_last_name)
**Name:** {{ $business->ap_contact_first_name }} {{ $business->ap_contact_last_name }}
@endif
@if($business->ap_contact_email)
**Email:** {{ $business->ap_contact_email }}
@endif
@if($business->ap_contact_phone)
**Phone:** {{ $business->ap_contact_phone }}
@endif
@if($business->ap_preferred_contact_method)
**Preferred Contact Method:** {{ ucfirst($business->ap_preferred_contact_method) }}
@endif
@if($business->delivery_contact_first_name || $business->delivery_contact_last_name || $business->delivery_contact_phone)
## Delivery Contact
@if($business->delivery_contact_first_name || $business->delivery_contact_last_name)
**Name:** {{ $business->delivery_contact_first_name }} {{ $business->delivery_contact_last_name }}
@endif
@if($business->delivery_contact_phone)
**Phone:** {{ $business->delivery_contact_phone }}
@endif
@if($business->delivery_contact_email)
**Email:** {{ $business->delivery_contact_email }}
@endif
@if($business->delivery_preferences)
**Delivery Preferences:** {{ $business->delivery_preferences }}
@endif
@if($business->delivery_directions)
**Delivery Directions:** {{ $business->delivery_directions }}
@endif
@if($business->delivery_schedule)
**Delivery Schedule:** {{ $business->delivery_schedule }}
@endif
@endif
@if($business->payment_method)
## Payment Information
**Payment Method:** {{ ucfirst(str_replace('_', ' ', $business->payment_method)) }}
@endif
## Compliance Documents
@if($business->w9_form_path)
- W9 Form: Uploaded
@else
- W9 Form: Not uploaded
@endif
@if($business->resale_certificate_path)
- Resale Certificate: Uploaded
@else
- Resale Certificate: Not uploaded
@endif
@if($business->ato_document_path)
- ATO Document: Uploaded
@else
- ATO Document: Not uploaded
@endif
@if($business->tpt_document_path)
- TPT Document: Uploaded
@else
- TPT Document: Not uploaded
@endif
@if($business->form_5000a_path)
- Form 5000A: Uploaded
@else
- Form 5000A: Not uploaded
@endif
@if($business->notes)
## Additional Notes
{{ $business->notes }}
@endif
---
Please review this application and approve or reject it through the admin panel.
@component('mail::button', ['url' => route('filament.admin.resources.businesses.edit', $business)])
Review & Approve Application
@endcomponent
{{ config('app.name') }}
@endcomponent

View File

@@ -0,0 +1,7 @@
<div class="w-full">
<iframe
srcdoc="{{ $html }}"
class="w-full border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white"
style="min-height: 600px; width: 100%;"
></iframe>
</div>