- Updated VpsServiceResource to return product UUID instead of ID. - Modified VirtualMachineService to utilize product data for CPU, RAM, and disk settings. - Replaced AssignIpToGuestJob with UpdateVmConfigJob in CreateVirtualMachineJob to streamline VM configuration after cloning. - Adjusted AssignIpToGuestJob to remove unnecessary UpdateVmConfigJob dispatch and focus on IP assignment. - Enhanced UpdateVmConfigJob to handle CPU/RAM configuration and power cycling more effectively. - Introduced ChangeIpVMJob for handling customer-initiated IP changes asynchronously. - Updated InvoiceRepository to set invoice due date based on maximum months instead of a fixed 7 days. - Improved web design quote PDF layout for better readability. - Added Times-Roman font metrics for PDF generation.
381 lines
13 KiB
PHP
381 lines
13 KiB
PHP
@php
|
|
$brandColor = $brandColor ?? '#1a56db';
|
|
$companyLogo = $companyLogo ?? setting('setting.logo') ?? '';
|
|
|
|
$frontendLabels = [
|
|
'nextjs' => 'Next.js',
|
|
'react' => 'React',
|
|
'vue' => 'Vue',
|
|
'nuxt' => 'Nuxt',
|
|
'astro' => 'Astro',
|
|
'angular' => 'Angular',
|
|
];
|
|
$frontendLabel = $quote->frontend_framework
|
|
? ($frontendLabels[$quote->frontend_framework] ?? $quote->frontend_framework)
|
|
: null;
|
|
|
|
$items = is_array($quote->items) ? $quote->items : [];
|
|
$referenceWebsites = is_array($quote->reference_websites) ? $quote->reference_websites : [];
|
|
|
|
$vatAmount = round(($quote->subtotal - $quote->discount) * $quote->vat_percent / 100, 2);
|
|
$fmt = fn ($n) => number_format((float) $n, 0, ',', '.');
|
|
@endphp
|
|
<!DOCTYPE html>
|
|
<html lang="vi">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<title>Báo giá {{ $quote->quote_number }}</title>
|
|
<style>
|
|
@page { size: A4; margin: 16mm 18mm; }
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
font-family: "DejaVu Serif", "Times New Roman", serif;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
color: #111;
|
|
font-size: 10px;
|
|
line-height: 1.45;
|
|
background: #fff;
|
|
padding: 20px;
|
|
}
|
|
|
|
.quote-page {
|
|
min-height: 277mm;
|
|
padding: 3mm;
|
|
border: 1px solid #efefe;
|
|
}
|
|
|
|
/* Header */
|
|
.top-table { width: 100%; border-collapse: collapse; margin-bottom: 8px; table-layout: fixed; }
|
|
.top-table td { vertical-align: top; }
|
|
.logo-cell { width: 32mm; }
|
|
.logo-cell img { width: 30mm; max-height: 16mm; }
|
|
.company-cell { padding-left: 8px; font-size: 10px; line-height: 1.5; }
|
|
.company-cell .company-name { font-size: 13px; font-weight: bold; text-transform: uppercase; color: {{ $brandColor }}; }
|
|
.meta-cell { width: 52mm; font-size: 10px; text-align: right; }
|
|
|
|
.doc-title { text-align: center; margin: 10px 0 2px; }
|
|
.doc-title h1 { font-size: 21px; font-weight: bold; text-transform: uppercase; letter-spacing: 0; color: #111; }
|
|
.doc-title .sub { font-size: 11px; font-style: italic; color: #6b7280; }
|
|
|
|
.divider { border: none; border-top: 1px solid #bbb; margin: 8px 0 10px; }
|
|
|
|
.seller-section {
|
|
position: relative;
|
|
min-height: 120px;
|
|
padding-right: 120px;
|
|
}
|
|
|
|
.seller-section .company-name {
|
|
margin: 0 0 3px;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.info-row {
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.bank-table {
|
|
width: 100%;
|
|
margin: 4px 0;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.bank-table td {
|
|
padding: 0;
|
|
vertical-align: top;
|
|
}
|
|
|
|
.bank-table-label {
|
|
width: 155px;
|
|
}
|
|
|
|
.qr-code {
|
|
position: absolute;
|
|
top: 8px;
|
|
right: 15px;
|
|
width: 90px;
|
|
height: 90px;
|
|
border: 1px solid #555;
|
|
text-align: center;
|
|
line-height: 90px;
|
|
}
|
|
|
|
/* Section */
|
|
.section { margin-bottom: 10px; }
|
|
.section-title { font-size: 12px; font-weight: bold; text-transform: uppercase; color: #111; border-bottom: 1px solid #e5e7eb; padding-bottom: 3px; margin-bottom: 6px; }
|
|
|
|
.info-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
.info-table td { padding: 2px 8px 2px 0; vertical-align: top; }
|
|
.info-table .lbl { width: 34mm; font-weight: bold; color: #374151; }
|
|
|
|
.two-col { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
.two-col > tbody > tr > td { width: 50%; vertical-align: top; padding-right: 10px; }
|
|
|
|
/* Items table */
|
|
.items-table-wrapper {
|
|
position: relative;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.items-table-watermark {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 50%;
|
|
width: 430px;
|
|
height: auto;
|
|
opacity: 0.3;
|
|
margin-left: -215px;
|
|
z-index: 0;
|
|
}
|
|
|
|
.items-table { width: 100%; border-collapse: collapse; margin-top: 4px; font-size: 10px; table-layout: auto; }
|
|
.items-table th, .items-table td { border: 1px solid #777; vertical-align: top; overflow-wrap: break-word; }
|
|
.items-table th { padding: 4px; height: 38px; text-align: center; vertical-align: middle; }
|
|
.items-table td { padding: 1px 2px; line-height: 1.2; }
|
|
.items-table thead th { background: transparent; color: #111; font-weight: bold; text-align: center; }
|
|
.items-table tr { page-break-inside: avoid; }
|
|
.items-table td.center { text-align: center; }
|
|
.items-table td.right { text-align: right; }
|
|
.items-table td.summary-label { text-align: right; font-weight: bold; }
|
|
.items-table td.summary-value { text-align: right; font-weight: bold; }
|
|
.items-table tr.total td { font-size: 12px; font-weight: bold; }
|
|
.items-table .item-desc { color: #6b7280; font-size: 9px; }
|
|
.items-table .item-desc p { margin: 2px 0; }
|
|
.items-table .item-desc ul,
|
|
.items-table .item-desc ol { margin: 2px 0; padding-left: 16px; }
|
|
.items-table { position: relative; z-index: 1; }
|
|
|
|
/* Summary */
|
|
.summary-table { width: 62mm; border-collapse: collapse; margin-top: 6px; margin-left: auto; font-size: 11px; }
|
|
.summary-table td { padding: 3px 6px; }
|
|
.summary-table .lbl { text-align: left; color: #374151; }
|
|
.summary-table .val { text-align: right; }
|
|
.summary-table tr.total td { font-weight: bold; font-size: 13px; color: #111; border-top: 1px solid #777; }
|
|
|
|
.note-box { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 3px; padding: 7px 9px; margin-top: 6px; }
|
|
|
|
.badge { display: inline-block; padding: 1px 6px; border: 1px solid {{ $brandColor }}; border-radius: 3px; color: {{ $brandColor }}; font-size: 9px; }
|
|
|
|
ul.ref-list { margin: 2px 0 0 14px; }
|
|
|
|
/* Footer / sign */
|
|
.sign-table { width: 100%; border-collapse: collapse; margin-top: 12mm; text-align: center; page-break-inside: avoid; }
|
|
.sign-table td { width: 50%; vertical-align: top; }
|
|
.sign-title { font-weight: bold; }
|
|
.sign-hint { font-size: 9px; font-style: italic; color: #6b7280; }
|
|
.footer-note { margin-top: 8mm; text-align: center; font-size: 9px; font-style: italic; color: #6b7280; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="quote-page">
|
|
<table class="top-table">
|
|
<tr>
|
|
<td class="logo-cell">
|
|
<img src="{{ $companyLogo ?: public_path('images/image.png') }}" alt="logo"/>
|
|
</td>
|
|
<td class="company-cell">
|
|
<h1>BÁO GIÁ WEBSITE</h1>
|
|
|
|
</td>
|
|
<td class="meta-cell">
|
|
<div><strong>Số báo giá:</strong> {{ $quote->quote_number }}</div>
|
|
<div><strong>Ngày lập:</strong> {{ $quote->created_at?->format('d/m/Y') }}</div>
|
|
@if ($quote->valid_until)
|
|
<div><strong>Hiệu lực đến:</strong> {{ $quote->valid_until->format('d/m/Y') }}</div>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<hr class="divider"/>
|
|
|
|
<div class="seller-section">
|
|
<h3 class="company-name">{{ $companyName }}</h3>
|
|
|
|
@if ($companyTaxCode)
|
|
<div class="info-row">Mã số thuế <span style="font-style: italic">(Tax code)</span>: {{ $companyTaxCode }}</div>
|
|
@endif
|
|
|
|
@if ($companyAddress)
|
|
<div class="info-row">Địa chỉ <span style="font-style: italic">(Address)</span>: {{ $companyAddress }}</div>
|
|
@endif
|
|
|
|
@if ($companyPhone)
|
|
<div class="info-row">Điện thoại <span style="font-style: italic">(Tel)</span>: {{ $companyPhone }}</div>
|
|
@endif
|
|
|
|
@if ($companyEmail)
|
|
<div class="info-row">Email: {{ $companyEmail }}</div>
|
|
@endif
|
|
|
|
<table class="bank-table">
|
|
<tr>
|
|
<td class="bank-table-label">Số tài khoản <span style="font-style: italic">(Bank account)</span>:</td>
|
|
<td>7379868 Ngân hàng TMCP Á Châu (ACB) Chi nhánh TP. Hồ Chí Minh</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="bank-table-label"></td>
|
|
<td>797379868 Ngân hàng TMCP Quân Đội (MB) Chi nhánh Phú Nhuận</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div class="qr-code">
|
|
<img src="{{ public_path('images/qr-code-tgs.jpg') }}" alt="QR Code" style="width: 100%; height: auto;">
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider"/>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Thông tin khách hàng</div>
|
|
<table class="two-col">
|
|
<tr>
|
|
<td>
|
|
<table class="info-table">
|
|
<tr><td class="lbl">Khách hàng:</td><td>{{ $quote->customer_name }}</td></tr>
|
|
@if ($quote->customer_company)
|
|
<tr><td class="lbl">Công ty:</td><td>{{ $quote->customer_company }}</td></tr>
|
|
@endif
|
|
</table>
|
|
</td>
|
|
<td>
|
|
<table class="info-table">
|
|
@if ($quote->customer_phone)
|
|
<tr><td class="lbl">Điện thoại:</td><td>{{ $quote->customer_phone }}</td></tr>
|
|
@endif
|
|
@if ($quote->customer_email)
|
|
<tr><td class="lbl">Email:</td><td>{{ $quote->customer_email }}</td></tr>
|
|
@endif
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Thông tin dự án website</div>
|
|
<table class="info-table">
|
|
<tr><td class="lbl">Website:</td><td>{{ $quote->website_name }}</td></tr>
|
|
<tr><td class="lbl">Thể loại:</td><td>{{ $quote->website_category }}</td></tr>
|
|
<tr>
|
|
<td class="lbl">Nền tảng:</td>
|
|
<td>
|
|
{{ $platformLabel }}
|
|
@if ($frontendLabel) — Frontend: {{ $frontendLabel }} @endif
|
|
@if ($quote->custom_admin) <span class="badge">Custom Admin</span> @endif
|
|
</td>
|
|
</tr>
|
|
@if ($licenseFeeUsd > 0)
|
|
<tr><td class="lbl">Bản quyền:</td><td>Elementor Pro — {{ $licenseFeeUsd }}$/năm</td></tr>
|
|
@endif
|
|
@if ($quote->description)
|
|
<tr><td class="lbl">Mô tả:</td><td>{{ $quote->description }}</td></tr>
|
|
@endif
|
|
@if ($quote->requirements)
|
|
<tr><td class="lbl">Yêu cầu:</td><td>{{ $quote->requirements }}</td></tr>
|
|
@endif
|
|
@if (count($referenceWebsites) > 0)
|
|
<tr>
|
|
<td class="lbl">Website mẫu:</td>
|
|
<td>
|
|
@foreach ($referenceWebsites as $ref)
|
|
{{ $ref }}<br/>
|
|
@endforeach
|
|
</td>
|
|
</tr>
|
|
@endif
|
|
</table>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Chi tiết báo giá</div>
|
|
<div class="items-table-wrapper">
|
|
<img src="{{ public_path('images/image.png') }}" alt="Watermark" class="items-table-watermark">
|
|
<table class="items-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 8mm;">STT<br><span style="font-style: italic">(No)</span></th>
|
|
<th>Hạng mục</th>
|
|
<th style="width: 12mm;">SL</th>
|
|
<th style="width: 26mm;">Đơn giá ({{ $quote->currency }})</th>
|
|
<th style="width: 28mm;">Thành tiền ({{ $quote->currency }})</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach ($items as $i => $item)
|
|
<tr>
|
|
<td class="center">{{ $i + 1 }}</td>
|
|
<td>
|
|
{{ $item['name'] ?? '' }}
|
|
@if (!empty($item['description']))
|
|
<div class="item-desc">{!! $item['description'] !!}</div>
|
|
@endif
|
|
</td>
|
|
<td class="center">{{ $item['quantity'] ?? 1 }}</td>
|
|
<td class="right">{{ $fmt($item['unit_price'] ?? 0) }}</td>
|
|
<td class="right">{{ $fmt(($item['quantity'] ?? 1) * ($item['unit_price'] ?? 0)) }}</td>
|
|
</tr>
|
|
@endforeach
|
|
<tr>
|
|
<td colspan="4" class="summary-label">Tạm tính:</td>
|
|
<td class="summary-value">{{ $fmt($quote->subtotal) }}</td>
|
|
</tr>
|
|
@if ($quote->discount > 0)
|
|
<tr>
|
|
<td colspan="4" class="summary-label">Giảm giá:</td>
|
|
<td class="summary-value">-{{ $fmt($quote->discount) }}</td>
|
|
</tr>
|
|
@endif
|
|
@if ($quote->vat_percent > 0)
|
|
<tr>
|
|
<td colspan="4" class="summary-label">
|
|
VAT ({{ rtrim(rtrim(number_format($quote->vat_percent, 2, '.', ''), '0'), '.') }}%):
|
|
</td>
|
|
<td class="summary-value">{{ $fmt($vatAmount) }}</td>
|
|
</tr>
|
|
@endif
|
|
<tr class="total">
|
|
<td colspan="4" class="summary-label">Tổng cộng:</td>
|
|
<td class="summary-value">{{ $fmt($quote->total) }} {{ $quote->currency }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
@if ($quote->note)
|
|
<div class="note-box">
|
|
<strong>Ghi chú:</strong> {{ $quote->note }}
|
|
</div>
|
|
@endif
|
|
|
|
<table class="sign-table">
|
|
<tr>
|
|
<td>
|
|
<div class="sign-title">Đại diện {{ $companyName }}</div>
|
|
@if ($quote->creator)
|
|
<div style="margin-top: 16mm;">{{ $quote->creator->name }}</div>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div class="footer-note">
|
|
Báo giá có hiệu lực {{ $quote->valid_until ? 'đến ngày ' . $quote->valid_until->format('d/m/Y') : 'trong 30 ngày kể từ ngày lập' }}.
|
|
Cảm ơn Quý khách đã quan tâm đến dịch vụ thiết kế website của {{ $companyName }}.
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|