update
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\Admin\Customer\Concerns;
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Support\AutoEmailGate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -34,6 +35,7 @@ trait ManagesCustomerServices
|
||||
'requirements' => 'nullable|string',
|
||||
'feedback' => 'nullable|string',
|
||||
'price' => 'nullable|numeric|min:0',
|
||||
'virtual_machine_id' => 'nullable|integer|exists:virtual_machines,id',
|
||||
'os_id' => 'nullable|integer|exists:os,id',
|
||||
'vm_cpu' => 'nullable|integer|min:1',
|
||||
'vm_ram_mb' => 'nullable|integer|min:1',
|
||||
@@ -74,6 +76,7 @@ trait ManagesCustomerServices
|
||||
|
||||
public function notifyServiceRenewal(string $uuid, int $serviceId): JsonResponse
|
||||
{
|
||||
AutoEmailGate::markManual();
|
||||
$this->serviceProvisioning->notifyRenewal($uuid, $serviceId);
|
||||
|
||||
return response()->json(['error' => false, 'message' => 'Đã gửi thông báo gia hạn đến khách hàng.']);
|
||||
|
||||
@@ -238,6 +238,8 @@ class EmailTemplateController extends Controller
|
||||
'news_template_id' => 'nullable|integer|exists:template,id',
|
||||
'invoice_template_id' => 'nullable|integer|exists:template,id',
|
||||
'mail_enabled' => 'sometimes|boolean',
|
||||
'auto_disabled' => 'sometimes|array',
|
||||
'auto_disabled.*' => ['string', Rule::in(array_keys(EmailScenarioRegistry::SCENARIOS))],
|
||||
]);
|
||||
|
||||
$setting = Setting::firstOrNew(['key' => 'email_settings']);
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api\Admin\Invoice\Concerns;
|
||||
use App\Mail\InvoiceNotificationMail;
|
||||
use App\Models\Customer;
|
||||
use App\Services\InvoiceReminderService;
|
||||
use App\Support\AutoEmailGate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -36,6 +37,7 @@ trait SendsInvoiceNotifications
|
||||
}
|
||||
|
||||
try {
|
||||
AutoEmailGate::markManual();
|
||||
$reminders->remind($invoice->load('customer'), force: true);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
@@ -79,6 +81,7 @@ trait SendsInvoiceNotifications
|
||||
);
|
||||
$paymentUrl = url('/client/invoices/' . $invoice->invoice_number . '/payment');
|
||||
try {
|
||||
AutoEmailGate::markManual();
|
||||
$invoicePdf = $this->invoicePdfService->generate($invoice);
|
||||
$invoicePdfFileName = $this->invoicePdfService->generateFileName($invoice);
|
||||
|
||||
|
||||
@@ -44,6 +44,30 @@ class ServiceController extends Controller
|
||||
return response()->json(['error' => false, 'data' => $service]);
|
||||
}
|
||||
|
||||
public function bulkAction(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'integer|distinct|exists:services,id',
|
||||
'action' => 'required|in:activate,suspend,cancel,detach-customer',
|
||||
]);
|
||||
|
||||
$updates = match ($data['action']) {
|
||||
'activate' => ['status' => 'active', 'suspended_at' => null, 'cancelled_at' => null],
|
||||
'suspend' => ['status' => 'suspended', 'suspended_at' => now()],
|
||||
'cancel' => ['status' => 'cancelled', 'cancelled_at' => now()],
|
||||
'detach-customer' => ['customer_id' => null],
|
||||
};
|
||||
|
||||
$updated = Service::whereIn('id', $data['ids'])->update($updates);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => "Đã cập nhật {$updated} dịch vụ.",
|
||||
'data' => ['updated' => $updated],
|
||||
]);
|
||||
}
|
||||
|
||||
public function detachCustomer(int $id): JsonResponse
|
||||
{
|
||||
$service = Service::findOrFail($id);
|
||||
|
||||
@@ -129,7 +129,7 @@ class ServiceController extends Controller
|
||||
{
|
||||
$customer = $request->user();
|
||||
|
||||
$service = Service::with(['orderItem.order.vmWare.vCenter'])
|
||||
$service = Service::with(['vmware.vCenter', 'orderItem.order.vmWare.vCenter'])
|
||||
->where('uuid', $uuid)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
@@ -138,7 +138,7 @@ class ServiceController extends Controller
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy dịch vụ'], 404);
|
||||
}
|
||||
|
||||
$vm = $service->orderItem?->order?->vmWare?->first();
|
||||
$vm = $service->resolveVmware();
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Dịch vụ chưa được cấp phát VM'], 404);
|
||||
|
||||
@@ -31,7 +31,7 @@ trait ManagesVmNetwork
|
||||
return $service;
|
||||
}
|
||||
|
||||
$vm = $service->orderItem?->order?->vmWare?->first();
|
||||
$vm = $service->resolveVmware();
|
||||
if (!$vm) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ trait ManagesVmNetwork
|
||||
return $service;
|
||||
}
|
||||
|
||||
$vm = $service->orderItem?->order?->vmWare?->first();
|
||||
$vm = $service->resolveVmware();
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['success' => false, 'message' => 'Không tìm thấy VM cho dịch vụ này.'], 404);
|
||||
@@ -219,8 +219,9 @@ trait ManagesVmNetwork
|
||||
{
|
||||
$customer = $request->user();
|
||||
|
||||
// VM chain: Service → orderItem → order → vmWare (same as ServiceClientDetailResource)
|
||||
$service = Service::with([
|
||||
'vmware.vCenter',
|
||||
'vmware.ipAddress.host.datacenter.vCenter',
|
||||
'orderItem.order.vmWare.vCenter',
|
||||
'orderItem.order.vmWare.ipAddress.host.datacenter.vCenter',
|
||||
'orderItem.os.osGroup',
|
||||
|
||||
@@ -65,7 +65,9 @@ class ExpiryMail implements ShouldQueue
|
||||
|
||||
try {
|
||||
$email = new ExpiryService($this->template_id,$data);
|
||||
Mail::to($product->Customer->email)->send($email);
|
||||
if (Mail::to($product->Customer->email)->send($email) === null) {
|
||||
return;
|
||||
}
|
||||
\App\Models\Mail::create([
|
||||
'template_id' => $this->template_id,
|
||||
'subject' => $email->envelope()->subject,
|
||||
|
||||
@@ -49,11 +49,11 @@ class SendMailCampaignRecipient implements ShouldQueue
|
||||
if ($bcc = EmailRecipients::parse($campaign->bcc)) {
|
||||
$mailer->bcc($bcc);
|
||||
}
|
||||
$mailer->send(new GenericTemplateMail(
|
||||
$failed = $mailer->send(new GenericTemplateMail(
|
||||
$rendered['subject'],
|
||||
$rendered['html'],
|
||||
Arr::only($campaign->template?->mailConfig() ?? [], ['from_email', 'from_name', 'reply_to', 'reply_to_name']),
|
||||
));
|
||||
)) === null;
|
||||
} catch (\Throwable $e) {
|
||||
$failed = true;
|
||||
Log::warning('Gửi email chiến dịch thất bại', [
|
||||
|
||||
@@ -37,6 +37,7 @@ class SendTemplateEmailJob implements ShouldQueue
|
||||
$rendered['subject'],
|
||||
$rendered['html'],
|
||||
$template->mailConfig(),
|
||||
$this->scenario,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Listeners\Mail;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Support\AutoEmailGate;
|
||||
use Illuminate\Mail\Events\MessageSending;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -26,6 +27,15 @@ class CheckMailEnabled
|
||||
return false;
|
||||
}
|
||||
|
||||
$scenario = $event->data[AutoEmailGate::SCENARIO_DATA_KEY] ?? null;
|
||||
if (AutoEmailGate::blocks($scenario)) {
|
||||
Log::info('Mail sending blocked: automatic sending is off for this scenario.', [
|
||||
'scenario' => $scenario,
|
||||
'subject' => $event->message->getSubject(),
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
@@ -26,11 +27,11 @@ class AdminOrderNotification extends TemplateMailable
|
||||
|
||||
protected function variables(): array
|
||||
{
|
||||
$vmFields = ['ip' => '🌐 IP Address', 'username' => '👤 Username', 'password' => '🔑 Password'];
|
||||
$vmFields = ['ip' => 'IP Address', 'username' => 'Username', 'password' => 'Password'];
|
||||
$vmRows = '';
|
||||
foreach ($vmFields as $field => $label) {
|
||||
if (!empty($this->data[$field])) {
|
||||
$vmRows .= '<div class="vm-row"><span class="vm-label">' . $label . '</span><span class="vm-value">' . e($this->data[$field]) . '</span></div>';
|
||||
$vmRows .= EmailTemplateShell::row($label, e($this->data[$field]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,14 +41,14 @@ class AdminOrderNotification extends TemplateMailable
|
||||
'customer_name' => $this->data['customer_name'] ?? 'N/A',
|
||||
'customer_email' => $this->data['customer_email'] ?? 'N/A',
|
||||
'client_ip_row' => !empty($this->data['client_ip'])
|
||||
? '<tr><td>IP đặt hàng</td><td>' . e($this->data['client_ip']) . '</td></tr>'
|
||||
? EmailTemplateShell::row('IP đặt hàng', e($this->data['client_ip']))
|
||||
: '',
|
||||
'product_name' => $this->data['product_name'] ?? 'N/A',
|
||||
'product_type' => $this->data['product_type'] ?? 'N/A',
|
||||
'billing_cycle' => $this->data['billing_cycle'] ?? 'N/A',
|
||||
'total_formatted' => number_format((float) ($this->data['total'] ?? 0), 0, ',', '.') . ' VNĐ',
|
||||
'vm_block' => $vmRows
|
||||
? '<div class="section-label">Thông tin dịch vụ Cloud VPS</div><div class="vm-box">' . $vmRows . '</div>'
|
||||
? EmailTemplateShell::heading('Thông tin dịch vụ Cloud VPS') . EmailTemplateShell::infoTable([], $vmRows)
|
||||
: '',
|
||||
'company_name' => $this->data['company_name'] ?? 'Portal Admin',
|
||||
];
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
@@ -27,7 +28,7 @@ class HostingAccountCreatedAdminNotification extends TemplateMailable
|
||||
protected function variables(): array
|
||||
{
|
||||
$row = fn (string $label, ?string $value) => $value
|
||||
? '<div class="cred-row"><span class="cred-label">' . $label . '</span><span class="cred-value">' . e($value) . '</span></div>'
|
||||
? EmailTemplateShell::row($label, e($value))
|
||||
: '';
|
||||
|
||||
return [
|
||||
@@ -41,10 +42,10 @@ class HostingAccountCreatedAdminNotification extends TemplateMailable
|
||||
'plesk_client_id' => $this->data['plesk_client_id'] ?? 'N/A',
|
||||
'plesk_subscription_id' => $this->data['plesk_subscription_id'] ?? 'N/A',
|
||||
'ftp_username' => $this->data['ftp_username'] ?? 'N/A',
|
||||
'ftp_password_row' => $row('🔑 FTP Password', $this->data['ftp_password'] ?? null),
|
||||
'ftp_password_row' => $row('FTP Password', $this->data['ftp_password'] ?? null),
|
||||
'plesk_panel_url' => $this->data['plesk_panel_url'] ?? 'N/A',
|
||||
'plesk_login' => $this->data['plesk_login'] ?? 'N/A',
|
||||
'plesk_password_row' => $row('🔑 Password', $this->data['plesk_password'] ?? null),
|
||||
'plesk_password_row' => $row('Plesk Password', $this->data['plesk_password'] ?? null),
|
||||
'company_name' => $this->data['company_name'] ?? 'Portal Admin',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
@@ -30,14 +31,14 @@ class ManualDepositPendingNotification extends TemplateMailable
|
||||
'customer_name' => $this->data['customer_name'] ?? 'N/A',
|
||||
'customer_email' => $this->data['customer_email'] ?? 'N/A',
|
||||
'customer_phone_row' => !empty($this->data['customer_phone'])
|
||||
? '<tr><td>Số điện thoại</td><td>' . e($this->data['customer_phone']) . '</td></tr>'
|
||||
? EmailTemplateShell::row('Số điện thoại', e($this->data['customer_phone']))
|
||||
: '',
|
||||
'client_ip_row' => !empty($this->data['client_ip'])
|
||||
? '<tr><td>IP gửi yêu cầu</td><td>' . e($this->data['client_ip']) . '</td></tr>'
|
||||
? EmailTemplateShell::row('IP gửi yêu cầu', e($this->data['client_ip']))
|
||||
: '',
|
||||
'amount_formatted' => number_format((float) ($this->data['amount'] ?? 0), 0, ',', '.') . ' VNĐ',
|
||||
'proof_row' => !empty($this->data['proof_image_url'])
|
||||
? '<tr><td>Chứng từ</td><td><a href="' . e($this->data['proof_image_url']) . '">Xem chứng từ chuyển khoản</a></td></tr>'
|
||||
? EmailTemplateShell::row('Chứng từ', '<a style="color: #f16723;" href="' . e($this->data['proof_image_url']) . '" target="_blank" rel="noopener">Xem chứng từ chuyển khoản</a>')
|
||||
: '',
|
||||
'company_name' => $this->data['company_name'] ?? 'Portal Admin',
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
@@ -33,7 +34,7 @@ class TrialRegistrationRequest extends TemplateMailable
|
||||
'ssd' => $this->data['ssd'] ?? '-',
|
||||
'os' => $this->data['os'] ?? '-',
|
||||
'other_row' => !empty($this->data['other'])
|
||||
? '<tr><td>Yêu cầu khác</td><td>' . e($this->data['other']) . '</td></tr>'
|
||||
? EmailTemplateShell::row('Yêu cầu khác', e($this->data['other']))
|
||||
: '',
|
||||
'company_name' => $this->data['company_name'] ?? 'Portal Admin',
|
||||
'created_at' => $this->data['created_at'] ?? now()->format('d/m/Y H:i:s'),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail\Customer;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use App\Support\MailGlobals;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
@@ -28,17 +29,17 @@ class HostingAccountCreated extends TemplateMailable
|
||||
$data = [...MailGlobals::values(), ...$this->data];
|
||||
|
||||
$row = fn (string $label, ?string $value) => $value
|
||||
? "<tr><td style=\"padding: 7px 11px; border-right: 1px solid #e8e8e8;\">{$label}</td><td style=\"padding: 7px 11px; border-right: 1px solid #e8e8e8;\"><strong>{$value}</strong></td></tr>"
|
||||
? EmailTemplateShell::row($label, e($value))
|
||||
: '';
|
||||
|
||||
$pleskBlock = '';
|
||||
if (!empty($data['plesk_login'])) {
|
||||
$pleskBlock = '<p style="color: #f16723;"><strong>TÀI KHOẢN ĐĂNG NHẬP PLESK PANEL</strong></p>'
|
||||
. '<table cellpadding="0" cellspacing="0" border="0" style="width: 100%; table-layout: fixed; border: 1px solid #e8e8e8; border-right: 0;"><tbody>'
|
||||
. '<tr><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8; width: 200px;">Địa chỉ Panel:</td><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;"><a style="color: #f16723; text-decoration: none;" href="' . ($data['plesk_panel_url'] ?? '') . '" target="_blank" rel="noopener">' . ($data['plesk_panel_url'] ?? '') . '</a></td></tr>'
|
||||
. '<tr bgcolor="#FFF1E6"><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Tên đăng nhập:</td><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;"><strong>' . ($data['plesk_login'] ?? '') . '</strong></td></tr>'
|
||||
. (!empty($data['plesk_password']) ? '<tr><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Mật khẩu:</td><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;"><strong>' . $data['plesk_password'] . '</strong></td></tr>' : '')
|
||||
. '</tbody></table>';
|
||||
$pleskBlock = EmailTemplateShell::paragraph('<strong style="color: #f16723;">TÀI KHOẢN ĐĂNG NHẬP PLESK PANEL</strong>')
|
||||
. EmailTemplateShell::infoTable(array_filter([
|
||||
'Địa chỉ Panel' => '<a style="color: #f16723; text-decoration: none;" href="' . e($data['plesk_panel_url'] ?? '') . '" target="_blank" rel="noopener">' . e($data['plesk_panel_url'] ?? '') . '</a>',
|
||||
'Tên đăng nhập' => e($data['plesk_login']),
|
||||
'Mật khẩu' => !empty($data['plesk_password']) ? e($data['plesk_password']) : null,
|
||||
]));
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -46,9 +47,9 @@ class HostingAccountCreated extends TemplateMailable
|
||||
'full_name' => $data['customer_name'] ?? 'Quý khách',
|
||||
'domain_name' => $data['domain_name'] ?? '',
|
||||
'hosting_plan' => $data['hosting_plan'] ?? '',
|
||||
'server_host_row' => $row('Máy chủ / FTP Host:', $data['server_host'] ?? null),
|
||||
'ftp_username_row' => $row('Tài khoản FTP:', $data['ftp_username'] ?? null),
|
||||
'ftp_password_row' => $row('Mật khẩu FTP:', $data['ftp_password'] ?? null),
|
||||
'server_host_row' => $row('Máy chủ / FTP Host', $data['server_host'] ?? null),
|
||||
'ftp_username_row' => $row('Tài khoản FTP', $data['ftp_username'] ?? null),
|
||||
'ftp_password_row' => $row('Mật khẩu FTP', $data['ftp_password'] ?? null),
|
||||
'ftp_password_notice' => !empty($data['ftp_password'])
|
||||
? '<p style="text-align: justify; color: #b45309; font-size: 13px;">Lưu ý: Vì lý do bảo mật, mật khẩu FTP chỉ được gửi một lần duy nhất qua email và không được lưu trữ trên hệ thống. Vui lòng lưu lại ở nơi an toàn.</p>'
|
||||
: '',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail\Customer;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use App\Support\MailGlobals;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
@@ -38,7 +39,7 @@ class ManualDepositStatus extends TemplateMailable
|
||||
'status_message' => $data['status_message'] ?? ($data['message'] ?? ''),
|
||||
'deposit_date' => $data['deposit_date'] ?? now()->format('d/m/Y H:i:s'),
|
||||
'processed_date_row' => !empty($data['processed_date'])
|
||||
? '<tr><td style="padding:12px 16px;background-color:#fafafa;color:#78909c;width:42%">Thời gian xử lý</td><td style="padding:12px 16px;color:#263238">' . $data['processed_date'] . '</td></tr>'
|
||||
? EmailTemplateShell::row('Thời gian xử lý', e($data['processed_date']))
|
||||
: '',
|
||||
'portal_url' => $data['portal_url'] ?? MailGlobals::frontendUrl() . '/nap-tien',
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail\Customer;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use App\Support\MailGlobals;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailables\Attachment;
|
||||
@@ -44,21 +45,21 @@ class VmCredentials extends TemplateMailable
|
||||
$data = [...MailGlobals::values(), ...$this->data];
|
||||
|
||||
$row = fn (string $label, ?string $value) => $value
|
||||
? "<tr bgcolor=\"#FFF1E6\"><td style=\"padding: 7px 11px; border-right: 1px solid #e8e8e8; width: 200px;\">{$label}</td><td style=\"padding: 7px 11px; border-right: 1px solid #e8e8e8;\"><strong>{$value}</strong></td></tr>"
|
||||
? EmailTemplateShell::row($label, e($value))
|
||||
: '';
|
||||
|
||||
return [
|
||||
'app_name' => $data['company_name'] ?? config('app.name', 'The Gioi So'),
|
||||
'full_name' => $data['client_name'] ?? 'Quý khách',
|
||||
'product_name' => $data['product_name'] ?? 'Cloud VPS',
|
||||
'product_billing_row' => $row('Chu kỳ thanh toán:', $data['product_billing'] ?? null),
|
||||
'product_price_row' => $row('Giá dịch vụ:', !empty($data['product_price']) ? $data['product_price'] . ' VNĐ' : null),
|
||||
'os_name_row' => $row('Hệ điều hành:', $data['os_name'] ?? null),
|
||||
'product_billing_row' => $row('Chu kỳ thanh toán', $data['product_billing'] ?? null),
|
||||
'product_price_row' => $row('Giá dịch vụ', !empty($data['product_price']) ? $data['product_price'] . ' VNĐ' : null),
|
||||
'os_name_row' => $row('Hệ điều hành', $data['os_name'] ?? null),
|
||||
'service_host' => $data['service_host'] ?? '',
|
||||
'service_username' => $data['service_username'] ?? '',
|
||||
'service_password' => $data['service_password'] ?? '',
|
||||
'service_port_row' => $row('Cổng kết nối:', $data['service_port'] ?? null),
|
||||
'service_due_exp_row' => $row('Hạn sử dụng:', $data['service_due_exp'] ?? null),
|
||||
'service_port_row' => $row('Cổng kết nối', $data['service_port'] ?? null),
|
||||
'service_due_exp_row' => $row('Hạn sử dụng', $data['service_due_exp'] ?? null),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail\Customer;
|
||||
|
||||
use App\Mail\TemplateMailable;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use App\Support\MailGlobals;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
@@ -33,7 +34,7 @@ class VmPasswordChanged extends TemplateMailable
|
||||
'full_name' => $data['fullname'] ?? 'Quý khách',
|
||||
'vm_name' => $data['vm_name'] ?? '',
|
||||
'new_password_row' => $newPassword
|
||||
? '<div>Mật khẩu mới: <strong style="font-family: \'Menlo\',\'Consolas\',monospace; color: #111827;">' . $newPassword . '</strong></div>'
|
||||
? EmailTemplateShell::row('Mật khẩu mới', '<span style="font-family: Menlo,Consolas,monospace; color: #111827;">' . e($newPassword) . '</span>')
|
||||
: '',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Mail\Concerns\BuildsTemplateEnvelope;
|
||||
use App\Support\AutoEmailGate;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
@@ -22,6 +23,7 @@ class GenericTemplateMail extends Mailable
|
||||
public readonly string $renderedSubject,
|
||||
public readonly string $renderedHtml,
|
||||
public readonly array $mailConfig = [],
|
||||
public readonly ?string $scenario = null,
|
||||
) {}
|
||||
|
||||
public function envelope(): Envelope
|
||||
@@ -33,4 +35,9 @@ class GenericTemplateMail extends Mailable
|
||||
{
|
||||
return new Content(htmlString: $this->renderedHtml);
|
||||
}
|
||||
|
||||
public function buildViewData()
|
||||
{
|
||||
return [...parent::buildViewData(), AutoEmailGate::SCENARIO_DATA_KEY => $this->scenario];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Mail;
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\InvoicePaymentService;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Mail\Mailables\Attachment;
|
||||
@@ -113,10 +114,8 @@ class InvoiceNotificationMail extends TemplateMailable
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<tr><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Đã thanh toán:</td><td style="padding: 7px 11px;"><strong style="color: #059669;">'
|
||||
. number_format($paid, 0, ',', '.') . ' VNĐ</strong></td></tr>'
|
||||
. '<tr bgcolor="#FFF1E6"><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Còn thiếu:</td><td style="padding: 7px 11px;"><strong style="color: #dc2626;">'
|
||||
. number_format($outstanding, 0, ',', '.') . ' VNĐ</strong></td></tr>';
|
||||
return EmailTemplateShell::row('Đã thanh toán', '<span style="color: #059669;">' . number_format($paid, 0, ',', '.') . ' VNĐ</span>')
|
||||
. EmailTemplateShell::row('Còn thiếu', '<span style="color: #dc2626;">' . number_format($outstanding, 0, ',', '.') . ' VNĐ</span>');
|
||||
}
|
||||
|
||||
private function payButton(): string
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Mail;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Service;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
@@ -36,7 +37,7 @@ class ServiceRenewalReminderMail extends TemplateMailable
|
||||
? \Carbon\Carbon::parse($this->service->expires_at)->format('d/m/Y')
|
||||
: 'N/A',
|
||||
'price_row' => $price
|
||||
? '<tr><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Số tiền gia hạn:</td><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;"><strong>' . $price . ' VNĐ</strong></td></tr>'
|
||||
? EmailTemplateShell::row('Số tiền gia hạn', $price . ' VNĐ')
|
||||
: '',
|
||||
'login_url' => rtrim(config('app.frontend_url', ''), '/') . '/dang-nhap',
|
||||
'support_email' => 'support@tgs.com.vn',
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Mail\Concerns\BuildsTemplateEnvelope;
|
||||
use App\Models\Template;
|
||||
use App\Services\Email\EmailTemplateRenderer;
|
||||
use App\Services\Email\EmailTemplateService;
|
||||
use App\Support\AutoEmailGate;
|
||||
use App\Support\MailGlobals;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
@@ -34,6 +35,11 @@ abstract class TemplateMailable extends Mailable
|
||||
return new Content(htmlString: $this->rendered()['html']);
|
||||
}
|
||||
|
||||
public function buildViewData()
|
||||
{
|
||||
return [...parent::buildViewData(), AutoEmailGate::SCENARIO_DATA_KEY => $this->scenario()];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{subject: string, html: string}
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
@@ -34,7 +35,7 @@ class TicketCreated extends TemplateMailable implements ShouldQueue
|
||||
'ticket_num' => $this->ticket->ticket_num,
|
||||
'subject' => $this->ticket->subject,
|
||||
'room_row' => $room
|
||||
? '<tr><td style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; color: #666666; padding: 4px 0;" valign="top">Phòng ban:</td><td style="font-family: Arial, Helvetica, sans-serif; font-size: 13px; color: #000000; padding: 4px 0;" valign="top">' . $room . '</td></tr>'
|
||||
? EmailTemplateShell::row('Phòng ban', e($room))
|
||||
: '',
|
||||
'ticket_url' => rtrim(config('app.frontend_url', ''), '/') . '/tickets/' . $this->ticket->ticket_num,
|
||||
'app_name' => config('app.name', 'Thế Giới Số'),
|
||||
|
||||
@@ -109,6 +109,11 @@ class Service extends Model
|
||||
return $this->belongsTo(ModuleVmware::class, 'vm_id');
|
||||
}
|
||||
|
||||
public function resolveVmware(): ?ModuleVmware
|
||||
{
|
||||
return $this->vmware ?? $this->orderItem?->order?->vmWare?->first();
|
||||
}
|
||||
|
||||
/** The Plesk hosting account associated with this service, if type = hosting. */
|
||||
public function hostingAccount()
|
||||
{
|
||||
|
||||
@@ -79,12 +79,16 @@ class CustomerServiceProvisioningService
|
||||
$service = Service::where('customer_id', $customer->id)->where('id', $serviceId)->first();
|
||||
abort_if(!$service, 404, 'Không tìm thấy dịch vụ');
|
||||
|
||||
return DB::transaction(function () use ($service, $data) {
|
||||
$vmFields = ['os_id', 'vm_cpu', 'vm_ram_mb', 'vm_disk_gb', 'vm_ip', 'vm_dns', 'vm_user', 'vm_password', 'vm_port'];
|
||||
return DB::transaction(function () use ($customer, $service, $data) {
|
||||
$vmFields = ['virtual_machine_id', 'os_id', 'vm_cpu', 'vm_ram_mb', 'vm_disk_gb', 'vm_ip', 'vm_dns', 'vm_user', 'vm_password', 'vm_port'];
|
||||
|
||||
$service->fill(collect($data)->except(['price', ...$vmFields])->all());
|
||||
$service->save();
|
||||
|
||||
if (!empty($data['virtual_machine_id'])) {
|
||||
$this->assignVirtualMachine($service, $customer, (int) $data['virtual_machine_id']);
|
||||
}
|
||||
|
||||
if (array_key_exists('price', $data) && $data['price'] !== null && $service->orderItem) {
|
||||
$service->orderItem->update(['price' => $data['price']]);
|
||||
}
|
||||
@@ -117,6 +121,49 @@ class CustomerServiceProvisioningService
|
||||
});
|
||||
}
|
||||
|
||||
private function assignVirtualMachine(Service $service, Customer $customer, int $virtualMachineId): void
|
||||
{
|
||||
$vm = VirtualMachines::with('datacenter')->findOrFail($virtualMachineId);
|
||||
$current = $service->vmware;
|
||||
|
||||
if ($current && ($current->virtual_machine_id === $vm->id || $current->vmware_id === $vm->vm_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$takenBy = DB::table('module_vmware as mv')
|
||||
->join('services as s', 's.vm_id', '=', 'mv.id')
|
||||
->where(fn($q) => $q->where('mv.virtual_machine_id', $vm->id)->orWhere('mv.vmware_id', $vm->vm_id))
|
||||
->where('s.id', '!=', $service->id)
|
||||
->value('s.id');
|
||||
abort_if($takenBy, 422, "VM {$vm->name} đang gán cho dịch vụ #{$takenBy}.");
|
||||
|
||||
if ($current) {
|
||||
$current->update([
|
||||
'virtual_machine_id' => $vm->id,
|
||||
'vmware_id' => $vm->vm_id,
|
||||
'name' => $vm->name,
|
||||
'vcenter_id' => $vm->datacenter->vcenter_id ?? null,
|
||||
'cpu' => $vm->cpu,
|
||||
'ram_mb' => $vm->ram_mb,
|
||||
'ip' => $vm->ip,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$price = (int) ($service->orderItem->price ?? 0);
|
||||
$vmId = $this->createModuleVmwareIfNeeded(
|
||||
['virtual_machine_id' => $vm->id, 'server_name' => $service->server_name],
|
||||
$customer,
|
||||
$service->orderItem?->order,
|
||||
$price,
|
||||
$service->expires_at ?? now(),
|
||||
);
|
||||
|
||||
ModuleVmware::where('id', $vmId)->update(['service_id' => $service->id]);
|
||||
$service->update(['vm_id' => $vmId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gửi email nhắc gia hạn dịch vụ cho khách hàng.
|
||||
*/
|
||||
@@ -285,7 +332,7 @@ class CustomerServiceProvisioningService
|
||||
});
|
||||
}
|
||||
|
||||
private function createModuleVmwareIfNeeded(array $data, Customer $customer, Order $order, int $basePrice, string|Carbon $expiresAt): ?int
|
||||
private function createModuleVmwareIfNeeded(array $data, Customer $customer, ?Order $order, int $basePrice, string|Carbon $expiresAt): ?int
|
||||
{
|
||||
if (empty($data['virtual_machine_id'])) {
|
||||
return null;
|
||||
@@ -298,7 +345,7 @@ class CustomerServiceProvisioningService
|
||||
['virtual_machine_id' => $vm->id],
|
||||
[
|
||||
'customer_id' => $customer->id,
|
||||
'order_id' => $order->id,
|
||||
'order_id' => $order?->id,
|
||||
'name' => $vm->name,
|
||||
'vmware_id' => $vm->vm_id,
|
||||
'vcenter_id' => $vcenterId,
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace App\Services;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\Email\EmailTemplateService;
|
||||
use App\Support\AutoEmailGate;
|
||||
use App\Support\EmailTemplateShell;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
@@ -79,6 +81,10 @@ class InvoiceReminderService
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $force && AutoEmailGate::blocks(self::SCENARIO)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer = $invoice->customer;
|
||||
if (! $customer?->email) {
|
||||
return false;
|
||||
@@ -108,8 +114,7 @@ class InvoiceReminderService
|
||||
? 'Hoá đơn đã quá hạn ' . $daysLate . ' ngày.'
|
||||
: ($daysLate === 0 ? 'Hôm nay là hạn thanh toán của hoá đơn này.' : 'Hoá đơn sẽ đến hạn sau ' . abs($daysLate) . ' ngày.'),
|
||||
'paid_row' => (int) ($invoice->amount_paid ?? 0) > 0
|
||||
? '<tr><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Đã thanh toán:</td><td style="padding: 7px 11px;"><strong>'
|
||||
. number_format((int) $invoice->amount_paid, 0, ',', '.') . ' VNĐ</strong></td></tr>'
|
||||
? EmailTemplateShell::row('Đã thanh toán', number_format((int) $invoice->amount_paid, 0, ',', '.') . ' VNĐ')
|
||||
: '',
|
||||
'login_url' => rtrim((string) config('app.frontend_url', ''), '/') . '/dang-nhap',
|
||||
'support_email' => 'support@tgs.com.vn',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Context;
|
||||
|
||||
final class AutoEmailGate
|
||||
{
|
||||
public const SCENARIO_DATA_KEY = '__mail_scenario';
|
||||
|
||||
private const MANUAL_CONTEXT_KEY = 'mail.manual_send';
|
||||
|
||||
public static function markManual(): void
|
||||
{
|
||||
Context::addHidden(self::MANUAL_CONTEXT_KEY, true);
|
||||
}
|
||||
|
||||
public static function blocks(?string $scenario): bool
|
||||
{
|
||||
if ($scenario === null || Context::hasHidden(self::MANUAL_CONTEXT_KEY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($scenario, self::disabledScenarios(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function disabledScenarios(): array
|
||||
{
|
||||
$value = Setting::where('key', 'email_settings')->value('value');
|
||||
|
||||
return (array) (json_decode($value ?? '', true)['auto_disabled'] ?? []);
|
||||
}
|
||||
}
|
||||
@@ -2,75 +2,158 @@
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Khung 700px dùng chung cho template email trong tbl.template: header (banner) + footer
|
||||
* (menu dịch vụ + thông tin công ty). Mỗi scenario chỉ cần viết phần <tr class="NoiDungKM">.
|
||||
*
|
||||
* Thông tin công ty hardcode ở footer vì EmailTemplateRenderer chỉ thay placeholder dạng
|
||||
* {{key}} đã khai báo trong metadata.variables — mọi scenario đều phải khai lại nếu muốn
|
||||
* footer động, không đáng.
|
||||
*/
|
||||
class EmailTemplateShell
|
||||
{
|
||||
/** Bọc phần nội dung vào khung chuẩn. */
|
||||
private const BRAND = '#f16723';
|
||||
|
||||
private const SERVICE_LINKS = [
|
||||
'Premium Hosting' => 'https://tgs.com.vn/premium-hosting/',
|
||||
'Cloud Server' => 'https://tgs.com.vn/cloud-server/',
|
||||
'Thuê Server' => 'https://tgs.com.vn/may-chu-rieng',
|
||||
'Thiết kế website' => 'https://tgs.com.vn/thiet-ke-website/',
|
||||
];
|
||||
|
||||
public static function wrap(string $bodyHtml): string
|
||||
{
|
||||
return self::header() . $bodyHtml . self::footer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mở khối NoiDungKM với style chữ chuẩn, để scenario chỉ viết nội dung bên trong.
|
||||
*/
|
||||
public static function page(string $innerHtml): string
|
||||
{
|
||||
return self::wrap(self::body($innerHtml));
|
||||
}
|
||||
|
||||
public static function body(string $innerHtml): string
|
||||
{
|
||||
return '<tr class="NoiDungKM"><td style="width: 700px; padding: 32px 48px; background-color: #ffffff; font-family: Montserrat,-apple-system,sans-serif; font-size: 15px; line-height: 24px; color: #626262;" valign="top">'
|
||||
return '<tr class="NoiDungKM"><td style="box-sizing: border-box; width: 700px; padding: 32px 48px; background-color: #ffffff; font-family: Montserrat,-apple-system,sans-serif; font-size: 15px; line-height: 24px; color: #626262;" valign="top" bgcolor="#ffffff" width="700">'
|
||||
. $innerHtml
|
||||
. '</td></tr>';
|
||||
}
|
||||
|
||||
/** Khối "Lưu ý: email tự động" dùng ở cuối hầu hết scenario. */
|
||||
public static function title(string $html): string
|
||||
{
|
||||
return '<p style="box-sizing: border-box; margin: 0 0 6px 0; font-size: 21px; font-weight: bold; text-align: center; color: ' . self::BRAND . ';">' . $html . '</p>';
|
||||
}
|
||||
|
||||
public static function subtitle(string $html): string
|
||||
{
|
||||
return '<p style="box-sizing: border-box; margin: 0 0 8px 0; font-size: 14px; text-align: center; color: #6b7280;">' . $html . '</p>';
|
||||
}
|
||||
|
||||
public static function highlight(string $html): string
|
||||
{
|
||||
return '<table style="box-sizing: border-box; margin: 24px auto;" cellspacing="0" cellpadding="0"><tbody><tr>'
|
||||
. '<td style="box-sizing: border-box; background-color: #fff7f2; border: 2px dashed ' . self::BRAND . '; border-radius: 8px; padding: 14px 36px;" align="center" bgcolor="#fff7f2">'
|
||||
. '<span style="box-sizing: border-box; font-family: \'Courier New\', monospace; font-size: 30px; font-weight: 700; letter-spacing: 6px; color: ' . self::BRAND . ';">' . $html . '</span>'
|
||||
. '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
public static function heading(string $html): string
|
||||
{
|
||||
return '<p style="box-sizing: border-box; margin: 0 0 8px 0; font-weight: bold; color: #374151;">' . $html . '</p>';
|
||||
}
|
||||
|
||||
public static function greeting(string $nameHtml): string
|
||||
{
|
||||
return '<p style="box-sizing: border-box; margin: 24px 0 16px 0;">Kính chào <strong style="box-sizing: border-box; color: ' . self::BRAND . ';">' . $nameHtml . '</strong>,</p>';
|
||||
}
|
||||
|
||||
public static function paragraph(string $html): string
|
||||
{
|
||||
return '<p style="box-sizing: border-box; margin: 0 0 20px 0; text-align: justify;">' . $html . '</p>';
|
||||
}
|
||||
|
||||
public static function button(string $url, string $label): string
|
||||
{
|
||||
return '<table style="box-sizing: border-box; margin: 24px auto;" cellspacing="0" cellpadding="0"><tbody><tr>'
|
||||
. '<td style="box-sizing: border-box; background-color: ' . self::BRAND . '; padding: 12px 24px; border-radius: 4px;" align="center" bgcolor="' . self::BRAND . '">'
|
||||
. '<a style="box-sizing: border-box; text-decoration: none; font-size: 14px; font-weight: 600; color: #ffffff;" href="' . $url . '" target="_blank" rel="noopener">' . $label . '</a>'
|
||||
. '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
public static function note(string $html, string $label = 'Lưu ý:'): string
|
||||
{
|
||||
return '<table style="box-sizing: border-box; width: 100%; border-collapse: collapse; margin: 0 0 20px 0;" role="presentation" width="100%" cellspacing="0" cellpadding="0"><tbody><tr>'
|
||||
. '<td style="box-sizing: border-box; padding: 14px 16px; border-left: 3px solid ' . self::BRAND . '; background-color: #f8fafc; font-size: 13px; line-height: 20px; color: #626262;" bgcolor="#f8fafc">'
|
||||
. '<strong style="box-sizing: border-box; color: #374151;">' . $label . '</strong> ' . $html
|
||||
. '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
public static function autoNotice(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<table style="width: 100%; border-collapse: collapse;" role="presentation" cellspacing="0" cellpadding="0"><tbody><tr>
|
||||
<td style="padding: 14px 16px; border-left: 3px solid #f16723; background-color: #f8fafc; font-size: 13px; line-height: 20px; color: #626262;"><strong style="color: #374151;">Lưu ý:</strong> Đây là email được gửi tự động. Vui lòng không trả lời email này.</td>
|
||||
</tr></tbody></table>
|
||||
HTML;
|
||||
return self::note('Đây là email được gửi tự động. Vui lòng không trả lời email này.');
|
||||
}
|
||||
|
||||
public static function supportLine(): string
|
||||
{
|
||||
return self::paragraph('Mọi thắc mắc xin liên hệ <strong>support@tgs.com.vn</strong> hoặc <strong>(028) 7309. 7379</strong>.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $rows label => value HTML (đã escape)
|
||||
*/
|
||||
public static function infoTable(array $rows, string $extraRowsHtml = ''): string
|
||||
{
|
||||
$html = '';
|
||||
foreach ($rows as $label => $value) {
|
||||
$html .= self::row($label, $value);
|
||||
}
|
||||
|
||||
return '<table style="box-sizing: border-box; width: 100%; border-collapse: collapse; table-layout: fixed; margin: 0 0 20px 0; border: 1px solid #e8e8e8;" role="presentation" width="100%" cellspacing="0" cellpadding="0"><tbody>'
|
||||
. $html . $extraRowsHtml
|
||||
. '</tbody></table>';
|
||||
}
|
||||
|
||||
public static function row(string $label, string $valueHtml): string
|
||||
{
|
||||
return '<tr><td style="box-sizing: border-box; width: 38%; padding: 8px 12px; border-bottom: 1px solid #e8e8e8; background-color: #fafafa; color: #626262;" bgcolor="#fafafa">' . $label . '</td>'
|
||||
. '<td style="box-sizing: border-box; padding: 8px 12px; border-bottom: 1px solid #e8e8e8; color: #374151;"><strong>' . $valueHtml . '</strong></td></tr>';
|
||||
}
|
||||
|
||||
public static function header(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<table style="border-collapse: collapse; width: 100%; background-color: #efefef;" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="padding: 20px;" align="center" valign="top"><table style="padding-top: 20px; padding-bottom: 20px; border-collapse: collapse; background-color: #ffffff; width: 700px;" border="0" cellspacing="0" cellpadding="0"><tbody>
|
||||
<tr class="Header"><td style="width: 700px;" valign="top"><table align="center" width="700" bgcolor="#ffffff" style="border-collapse: collapse; width: 700px; max-width: 100%; text-align: center; background-color: #ffffff; border-top: 5px solid #f16723;" border="0" cellspacing="0" cellpadding="0"><tbody><tr><td style="padding: 0;"><a href="https://tgs.com.vn" style="display: block;" target="_blank" rel="noopener"><img src="https://res.cloudinary.com/izpast9i/image/upload/v1786499308/email_header.png" alt="Thư gửi từ CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ" style="width: 100%; display: block; border: none;" width="700"></a></td></tr></tbody></table></td></tr>
|
||||
<table style="box-sizing: border-box; border-collapse: collapse; width: 100%; background-color: #efefef;" border="0" width="100%" cellspacing="0" cellpadding="0" bgcolor="#efefef"><tbody><tr><td style="box-sizing: border-box; padding: 20px;" align="center" valign="top"><table style="box-sizing: border-box; padding-top: 20px; padding-bottom: 20px; border-collapse: collapse; background-color: #ffffff; width: 700px;" border="0" width="700" cellspacing="0" cellpadding="0" bgcolor="#ffffff"><tbody>
|
||||
<tr class="Header"><td style="box-sizing: border-box; width: 700px;" valign="top" width="700"><table style="box-sizing: border-box; border-collapse: collapse; width: 700px; max-width: 100%; text-align: center; background-color: #ffffff; border-top: 5px solid #f16723;" border="0" width="700" cellspacing="0" cellpadding="0" align="center" bgcolor="#ffffff"><tbody><tr><td style="box-sizing: border-box; padding: 0;"><a style="box-sizing: border-box; display: block;" href="https://tgs.com.vn" target="_blank" rel="noopener"><img style="box-sizing: border-box; width: 100%; display: block; border: none;" src="https://res.cloudinary.com/izpast9i/image/upload/v1786499308/email_header.png" alt="Thư gửi từ CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ" width="700"></a></td></tr></tbody></table></td></tr>
|
||||
HTML;
|
||||
}
|
||||
|
||||
public static function footer(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<tr class="Footer"><td align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="700" align="center" background="https://res.cloudinary.com/izpast9i/image/upload/v1786500298/email_footer.png" bgcolor="#F16723" style="border-collapse: collapse; width: 700px; background-image: url("https://res.cloudinary.com/izpast9i/image/upload/v1786500298/email_footer.png"); background-size: cover; background-position: center; background-repeat: no-repeat;"><tbody>
|
||||
<tr><td style="padding: 40px 30px 20px 30px; text-align: center;"><table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: separate; border-spacing: 8px 0; width: 100%;"><tbody><tr>
|
||||
<td width="25%" style="border: 1px solid rgba(255,255,255,0.35); border-radius: 8px; padding: 6px 4px; font-size: 12px; line-height: 16px; color: #ffffff; text-align: center; background-color: rgba(255,255,255,0.18);"><a href="https://tgs.com.vn/premium-hosting/" target="_blank" rel="noopener" style="color: #ffffff; text-decoration: none;">Premium Hosting</a></td>
|
||||
<td width="25%" style="border: 1px solid rgba(255,255,255,0.35); border-radius: 8px; padding: 6px 4px; font-size: 12px; line-height: 16px; color: #ffffff; text-align: center; background-color: rgba(255,255,255,0.18);"><a href="https://tgs.com.vn/cloud-server/" target="_blank" rel="noopener" style="color: #ffffff; text-decoration: none;">Cloud Server</a></td>
|
||||
<td width="25%" style="border: 1px solid rgba(255,255,255,0.35); border-radius: 8px; padding: 6px 4px; font-size: 12px; line-height: 16px; color: #ffffff; text-align: center; background-color: rgba(255,255,255,0.18);"><a href="https://cloudemail.vn/" target="_blank" rel="noopener" style="color: #ffffff; text-decoration: none;">Email doanh nghiệp</a></td>
|
||||
<td width="25%" style="border: 1px solid rgba(255,255,255,0.35); border-radius: 8px; padding: 6px 4px; font-size: 12px; line-height: 16px; color: #ffffff; text-align: center; background-color: rgba(255,255,255,0.18);"><a href="https://tgs.com.vn/thiet-ke-website/" target="_blank" rel="noopener" style="color: #ffffff; text-decoration: none;">Thiết kế website</a></td>
|
||||
</tr></tbody></table></td></tr>
|
||||
<tr><td style="padding: 6px 30px 0 30px; text-align: center;"><strong style="font-size: 14px; line-height: 22px; color: #ffffff;">CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ</strong></td></tr>
|
||||
<tr><td style="padding: 6px 30px 0 30px; text-align: center;"><span style="font-size: 12px; line-height: 18px; color: #ffffff;">Địa chỉ: 128/38 Tân Hương, Phường Phú Thọ Hòa, Thành Phố Hồ Chí Minh</span></td></tr>
|
||||
<tr><td style="padding: 6px 30px 0 30px; text-align: center;"><span style="font-size: 12px; line-height: 18px; color: #ffffff;">Tel: (028) 7309. 7379 - MST: 0318443116</span></td></tr>
|
||||
<tr><td style="padding: 6px 30px 0 30px; text-align: center;"><span style="font-size: 12px; line-height: 18px; color: #ffffff;">Email: <a href="mailto:support@tgs.com.vn" style="color: #ffffff; text-decoration: none;">support@tgs.com.vn</a></span></td></tr>
|
||||
<tr><td style="padding: 6px 30px 0 30px; text-align: center;"><span style="font-size: 12px; line-height: 18px; color: #ffffff;"><a href="https://www.tgs.com.vn" style="color: #ffffff; text-decoration: underline;">www.TGS.com.vn</a> & <a href="https://www.thegioiso.cloud" style="color: #ffffff; text-decoration: underline;">www.thegioiso.CLOUD</a> & <a href="https://www.thegioiserver.com" style="color: #ffffff; text-decoration: underline;">www.thegioiSERVER.com</a> & <a href="https://www.cloudemail.vn" style="color: #ffffff; text-decoration: underline;">www.CloudEmail.vn</a></span></td></tr>
|
||||
<tr><td style="padding: 6px 30px 16px 30px; text-align: center;"><span style="font-size: 12px; line-height: 20px; color: #ffffff; font-weight: bold;">Thế Giới Số - Premium HOSTING • CLOUD • SERVER & WEBSITE | 15+ năm Phụng Sự</span></td></tr>
|
||||
</tbody></table></td></tr>
|
||||
</tbody></table></td></tr></tbody></table>
|
||||
HTML;
|
||||
return '<tr class="Footer"><td style="box-sizing: border-box;" align="center" valign="top">'
|
||||
. '<table style="box-sizing: border-box; border-collapse: collapse; width: 700px; font-family: Montserrat,-apple-system,sans-serif; background-image: url(\'https://res.cloudinary.com/izpast9i/image/upload/v1786500298/email_footer.png\'); background-size: cover; background-position: center; background-repeat: no-repeat;" border="0" width="700" cellspacing="0" cellpadding="0" align="center" bgcolor="#F16723"><tbody>'
|
||||
. self::serviceBanner()
|
||||
. self::companyInfo()
|
||||
. '</tbody></table></td></tr>'
|
||||
. '</tbody></table></td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
private static function serviceBanner(): string
|
||||
{
|
||||
$cells = '';
|
||||
foreach (self::SERVICE_LINKS as $label => $url) {
|
||||
$cells .= '<td style="box-sizing: border-box; border: 1px solid rgba(255,255,255,0.35); border-radius: 8px; padding: 6px 4px; font-size: 12px; line-height: 16px; color: #ffffff; text-align: center; background-color: rgba(255,255,255,0.18);" align="center" width="25%">'
|
||||
. '<a style="box-sizing: border-box; color: #ffffff; text-decoration: none;" href="' . $url . '" target="_blank" rel="noopener">' . $label . '</a></td>';
|
||||
}
|
||||
|
||||
return '<tr><td style="box-sizing: border-box; padding: 40px 30px 20px 30px; text-align: center;" align="center">'
|
||||
. '<table style="box-sizing: border-box; border-collapse: separate; border-spacing: 8px 0; width: 100%;" border="0" width="100%" cellspacing="0" cellpadding="0"><tbody><tr>'
|
||||
. $cells
|
||||
. '</tr></tbody></table></td></tr>';
|
||||
}
|
||||
|
||||
private static function companyInfo(): string
|
||||
{
|
||||
$line = fn (string $html, string $padding = '6px 30px 0 30px') => '<tr><td style="box-sizing: border-box; padding: ' . $padding . '; text-align: center;" align="center">' . $html . '</td></tr>';
|
||||
$text = 'box-sizing: border-box; font-size: 12px; line-height: 18px; color: #ffffff;';
|
||||
|
||||
return $line('<strong style="box-sizing: border-box; font-size: 14px; line-height: 22px; color: #ffffff;">CÔNG TY CỔ PHẦN GIẢI PHÁP CÔNG NGHỆ THẾ GIỚI SỐ</strong>')
|
||||
. $line('<span style="' . $text . '">Địa chỉ: 128/38 Tân Hương, Phường Phú Thọ Hòa, Thành Phố Hồ Chí Minh</span>')
|
||||
. $line('<span style="' . $text . '">Tel: (028) 7309. 7379 - MST: 0318443116</span>')
|
||||
. $line('<span style="' . $text . '">Email: <a style="box-sizing: border-box; color: #ffffff; text-decoration: none;" href="mailto:support@tgs.com.vn">support@tgs.com.vn</a></span>')
|
||||
. $line('<span style="box-sizing: border-box; font-size: 12px; line-height: 20px; color: #ffffff; font-weight: bold;">🥇 Thế Giới Số 🥇 - Premium HOSTING • CLOUD • SERVER & WEBSITE | 15+ năm Phụng Sự🚀</span>', '6px 30px 16px 30px');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bọc một đoạn HTML thành email hoàn chỉnh (doctype + head). Thay cho blade
|
||||
* emails.customer.template-envelope — để không còn mail nào đi qua Blade.
|
||||
*/
|
||||
public static function document(?string $title, string $bodyHtml): string
|
||||
{
|
||||
$safeTitle = htmlspecialchars((string) $title, ENT_QUOTES, 'UTF-8');
|
||||
|
||||
Reference in New Issue
Block a user