- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.3 KiB
PHP
115 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Mail;
|
|
|
|
use App\Mail\Concerns\AttachesStoredFiles;
|
|
use App\Models\IpPool;
|
|
use App\Support\EmailTemplateShell;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Mail\Mailables\Envelope;
|
|
use Illuminate\Bus\Queueable;
|
|
|
|
class IpAllocationMail extends TemplateMailable
|
|
{
|
|
use Queueable, AttachesStoredFiles;
|
|
|
|
public const ALLOCATED = 'ip_allocated';
|
|
public const REVOKED = 'ip_revoked';
|
|
|
|
private const CELL = "padding: 13px 14px; font-size: 15px; text-align: center; font-family: 'Times New Roman', Times, serif; color: #2b2b2b;";
|
|
|
|
private const HEAD_CELL = "padding: 11px 14px; color: #c2410c; font-size: 13px; font-weight: bold; letter-spacing: .4px; border-bottom: 1px solid #f6d2b8; background: #fff5ee; text-align: center; font-family: 'Times New Roman', Times, serif;";
|
|
|
|
/**
|
|
* @param array<int, array{ip_address: string, cidr: ?int, gateway: ?string, service_name: ?string}> $ips
|
|
*/
|
|
public function __construct(
|
|
public readonly string $type,
|
|
public readonly string $clientName,
|
|
public readonly array $ips,
|
|
public readonly string $date,
|
|
public readonly ?string $note = null,
|
|
public readonly ?string $subjectOverride = null,
|
|
public readonly array $storedFiles = [],
|
|
) {}
|
|
|
|
/**
|
|
* @param iterable<int, IpPool> $pools
|
|
* @return array<int, array{ip_address: string, cidr: ?int, gateway: ?string, service_name: ?string}>
|
|
*/
|
|
public static function ipsFrom(iterable $pools): array
|
|
{
|
|
return collect($pools)
|
|
->map(fn (IpPool $pool) => $pool->only(['ip_address', 'cidr', 'gateway', 'service_name']))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public function attachments(): array
|
|
{
|
|
return $this->storedFileAttachments($this->storedFiles);
|
|
}
|
|
|
|
public function envelope(): Envelope
|
|
{
|
|
$envelope = parent::envelope();
|
|
if (filled($this->subjectOverride)) {
|
|
$envelope->subject = trim($this->subjectOverride);
|
|
}
|
|
|
|
return $envelope;
|
|
}
|
|
|
|
protected function scenario(): string
|
|
{
|
|
return $this->type;
|
|
}
|
|
|
|
protected function variables(): array
|
|
{
|
|
$carbon = Carbon::parse($this->date);
|
|
$date = $carbon->format('d/m/Y');
|
|
|
|
return [
|
|
'client_name' => $this->clientName,
|
|
'ip_count' => (string) count($this->ips),
|
|
'ip_rows' => collect($this->ips)->map(fn (array $ip) => EmailTemplateShell::row(
|
|
e($ip['ip_address']),
|
|
e(implode(' · ', array_filter([
|
|
$ip['cidr'] ? '/' . $ip['cidr'] : null,
|
|
$ip['gateway'] ? 'Gateway ' . $ip['gateway'] : null,
|
|
])) ?: '—'),
|
|
))->implode(''),
|
|
'ip_table' => $this->ipTable($date),
|
|
'issued_on' => "ngày {$carbon->day} tháng {$carbon->month} năm {$carbon->year}",
|
|
'allocated_at' => $date,
|
|
'revoked_at' => $date,
|
|
'note_block' => $this->note ? EmailTemplateShell::note(nl2br(e($this->note)), 'Ghi chú:') : '',
|
|
];
|
|
}
|
|
|
|
private function ipTable(string $date): string
|
|
{
|
|
$last = count($this->ips) - 1;
|
|
$rows = collect($this->ips)->values()->map(function (array $ip, int $index) use ($date, $last) {
|
|
$border = $index === $last ? '' : ' border-bottom: 1px solid #f6d2b8;';
|
|
$cell = fn (string $value, string $extra = '') => '<td style="' . self::CELL . $border . $extra . '">' . $value . '</td>';
|
|
|
|
return '<tr>'
|
|
. $cell((string) ($index + 1), ' width: 60px;')
|
|
. $cell(e($ip['ip_address']), " font-family: Consolas, 'Courier New', monospace; font-weight: bold; color: #c2410c;")
|
|
. $cell(e($ip['service_name'] ?: '—'))
|
|
. $cell($date)
|
|
. '</tr>';
|
|
})->implode('');
|
|
|
|
$head = collect(['STT', 'IP Address', 'Dịch vụ', 'Ngày cấp phát'])
|
|
->map(fn (string $title) => '<td style="' . self::HEAD_CELL . '">' . $title . '</td>')
|
|
->implode('');
|
|
|
|
return '<table style="border: 1px solid #f6d2b8; border-radius: 10px; border-collapse: separate; overflow: hidden;" role="presentation" width="100%" cellspacing="0" cellpadding="0"><tbody>'
|
|
. '<tr>' . $head . '</tr>' . $rows
|
|
. '</tbody></table>';
|
|
}
|
|
}
|