Files
portal-api/app/Mail/IpAllocationMail.php
T
baodev0andClaude Opus 5.5 46aa138eec feat(admin): IP pool allocation, billing exports, service attachments
- 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>
2026-10-15 19:58:36 +07:00

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>';
}
}