update order detail

This commit is contained in:
Phuoc Nguyen
2025-11-25 11:57:56 +07:00
parent c3b5653420
commit 039dfb9fb5
22 changed files with 1587 additions and 288 deletions

View File

@@ -16,14 +16,16 @@
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Chi tiết đơn hàng</h1>
<div class="header-actions">
<div style="width: 32px;"></div>
<!--<div class="header-actions">
<button class="header-action-btn" onclick="shareOrder()">
<i class="fas fa-share"></i>
</button>
<button class="header-action-btn" onclick="printOrder()">
<i class="fas fa-print"></i>
</button>
</div>
</div>-->
</div>
<div class="order-detail-content" style="padding-bottom: 0px;">
@@ -46,22 +48,33 @@
</div>
</div>
<div class="timeline-item completed">
<div class="timeline-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="timeline-content">
<div class="timeline-title">Xác nhận đơn hàng</div>
<div class="timeline-date">03/08/2023 - 10:15</div>
</div>
</div>
<div class="timeline-item active">
<div class="timeline-icon">
<i class="fas fa-cog fa-spin"></i>
</div>
<div class="timeline-content">
<div class="timeline-title">Đã xác nhận đơn hàng</div>
<div class="timeline-date">03/08/2023 - 10:15 (Đang xử lý)</div>
<div class="timeline-title">Xử lý</div>
<div class="timeline-date">Chuẩn bị hàng và vận chuyển</div>
</div>
</div>
<div class="timeline-item pending">
<div class="timeline-icon">
<i class="fas fa-check-circle"></i>
<i class="fas fa-box-open"></i>
</div>
<div class="timeline-content">
<div class="timeline-title">Đã hoàn thành</div>
<div class="timeline-title">Hoàn thành</div>
<div class="timeline-date">Dự kiến: 07/08/2023</div>
</div>
</div>
@@ -69,11 +82,11 @@
</div>
<!-- Delivery Information Card -->
<div class="delivery-info-card">
<!--<div class="delivery-info-card">
<h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
<div class="delivery-details">
<!--<div class="delivery-method">
<div class="delivery-method">
<div class="delivery-method-icon">
<i class="fas fa-truck"></i>
</div>
@@ -81,16 +94,16 @@
<div class="method-name">Giao hàng tiêu chuẩn</div>
<div class="method-description">Giao trong 3-5 ngày làm việc</div>
</div>
</div>-->
</div>
<div class="delivery-dates">
<!--<div class="date-item">
<div class="date-item">
<div class="date-label">
<i class="fas fa-calendar-alt"></i>
Ngày xuất kho
</div>
<div class="date-value confirmed">05/08/2023</div>
</div>-->
</div>
<div class="date-item">
<div class="date-label">
@@ -120,55 +133,179 @@
</div>
</div>
</div>
</div>
</div>-->
<!-- Customer Information -->
<div class="customer-info-card">
<h3><i class="fas fa-user-circle"></i> Thông tin khách hàng</h3>
<div class="customer-details">
<div class="customer-row">
<span class="customer-label">Tên khách hàng:</span>
<span class="customer-value">Nguyễn Văn A</span>
<!--<div class="customer-info-card">
<h3><i class="fas fa-user-circle"></i> Thông tin khách hàng</h3>-->
<div class="delivery-info-card">
<h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
<!-- Address Section -->
<!--<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
Địa chỉ nhận hàng
</label>
<a href="addresses.html" class="block border border-gray-200 rounded-lg p-3 hover:border-blue-500 hover:bg-blue-50 transition group">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="font-semibold text-gray-900 mb-1">Hoàng Minh Hiệp</div>
<div class="text-sm text-gray-600 mb-1">0347302911</div>
<div class="text-sm text-gray-600">
123 Đường Võ Văn Ngân, Phường Linh Chiểu,
Thành phố Thủ Đức, TP.HCM
</div>
</div>
</div>
</a>
</div>-->
<!-- Address Section -->
<div class="mb-4">
<!-- Label + Button -->
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-gray-700">
Địa chỉ nhận hàng
</label>
<a href="addresses.html"
class="text-blue-600 text-sm font-medium hover:underline px-3 py-1 border rounded-lg hover:bg-blue-50">
Cập nhật
</a>
</div>
<!-- Address Box -->
<a href="addresses.html"
class="block border border-gray-200 rounded-lg p-3 hover:border-blue-500 hover:bg-blue-50 transition group">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="font-semibold text-gray-900 mb-1">Hoàng Minh Hiệp</div>
<div class="text-sm text-gray-600 mb-1">0347302911</div>
<div class="text-sm text-gray-600">
123 Đường Võ Văn Ngân, Phường Linh Chiểu,
Thành phố Thủ Đức, TP.HCM
</div>
</div>
</div>
</a>
</div>
<!--<div class="customer-row">
<span class="customer-label">Ngày lấy hàng:</span>
<span class="customer-value">07/08/2025</span>
</div>
<div class="customer-row">
<span class="customer-label">Số điện thoại:</span>
<span class="customer-value">0901234567</span>
</div>
<div class="customer-row">
<span class="customer-label">Email:</span>
<span class="customer-value">nguyenvana@email.com</span>
<span class="customer-label">Ghi chú:</span>
<span class="customer-value">Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng.</span>
</div>
<div class="customer-row">
<span class="customer-label">Loại khách hàng:</span>
<span class="customer-badge vip">DIAMOND</span>
</div>-->
<!-- Pickup Date -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
Ngày lấy hàng
</label>
<div class="font-semibold text-gray-900 mb-1" style="font-weight: 450;">07/08/2025</div>
</div>
<!-- Pickup Date -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">
Ghi chú
</label>
<div class="font-semibold text-gray-900 mb-1" style="font-weight: 450;">Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng</div>
</div>
</div>
</div>
<!-- Invoice Information -->
<div class="customer-info-card">
<h3><i class="fas fa-file-invoice"></i> Thông tin hóa đơn</h3>
<div class="customer-details">
<!-- Title + Update Button -->
<div class="flex items-center justify-between mb-3">
<h3 class="flex items-center gap-2" style=" margin-bottom: 0px;">
<i class="fas fa-file-invoice"></i>
Thông tin hóa đơn
</h3>
<a href="addresses.html"
class="text-blue-600 text-sm font-medium hover:underline px-3 py-1 border rounded-lg hover:bg-blue-50">
Cập nhật
</a>
</div>
<div class="border-t border-gray-200 pt-4">
<a href="addresses.html" class="block border border-gray-200 rounded-lg p-3 hover:border-blue-500 hover:bg-blue-50 transition group">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="font-semibold text-gray-900 mb-1">Công ty TNHH Xây dựng Minh Long</div>
<div class="text-sm text-gray-600 mb-0.5">Mã số thuế: 0134000687</div>
<div class="text-sm text-gray-600 mb-0.5">Số điện thoại: 0339797979</div>
<div class="text-sm text-gray-600 mb-0.5">Email: minhlong.org@gmail.com</div>
<div class="text-sm text-gray-600">
Địa chỉ: 11 Đường Hoàng Hữu Nam, Phường Linh Chiểu,
Thành phố Thủ Đức, TP.HCM
</div>
</div>
</div>
</a>
</div>
<!--<div class="customer-details">
<div class="customer-row">
<span class="customer-label">Tên công ty:</span>
<span class="customer-value">Công ty TNHH Xây dựng ABC</span>
<span class="customer-value">Công ty TNHH Xây dựng Minh Long</span>
</div>
<div class="customer-row">
<span class="customer-label">Mã số thuế:</span>
<span class="customer-value">0123456789</span>
<span class="customer-value">0134000687</span>
</div>
<div class="customer-row">
<span class="customer-label">Địa chỉ công ty:</span>
<span class="customer-value">123 Nguyễn Trãi, Quận 1, TP.HCM</span>
<span class="customer-label">Địa chỉ:</span>
<span class="customer-value">11 Đường Hoàng Hữu Nam, Phường Linh Chiểu, Thành phố Thủ Đức, TP.HCM</span>
</div>
<div class="customer-row">
<span class="customer-label">Email nhận hóa đơn:</span>
<span class="customer-value">ketoan@abc.com</span>
<span class="customer-value">minhlong.org@gmail.com</span>
</div>
<div class="customer-row">
<span class="customer-label">Số điện thoại:</span>
<span class="customer-value">0339797979</span>
</div>
<div class="customer-row">
<span class="customer-label">Loại hóa đơn:</span>
<span class="customer-badge" style="background: #d1ecf1; color: #0c5460;">Hóa đơn VAT</span>
</div>
</div>-->
</div>
<!-- Invoices List Block (NEW) -->
<div class="delivery-info-card">
<h3><i class="fas fa-file-invoice-dollar text-blue-600"></i> Hóa đơn đã xuất</h3>
<div class="invoices-list">
<!-- Invoice Card 1 -->
<div class="invoice-item" onclick="window.location.href='invoice-detail.html?id=INV20240001'">
<div class="invoice-item-icon">
<i class="fas fa-file-invoice"></i>
</div>
<div class="invoice-item-content">
<div class="invoice-item-title">#INV20240001</div>
<div class="invoice-item-subtitle">Ngày xuất: 03/08/2024 - 10:00</div>
</div>
<div class="invoice-item-amount">12.771.000đ</div>
<i class="fas fa-chevron-right invoice-item-arrow"></i>
</div>
</div>
</div>
@@ -249,10 +386,11 @@
<i class="fas fa-credit-card"></i>
Phương thức thanh toán:
</div>
<div class="payment-value">Chuyển khoản ngân hàng</div>
<div class="payment-value">Thanh toán một phần</div>
</div>
<div class="order-notes">
<!--<div class="order-notes">
<div class="notes-label">
<i class="fas fa-sticky-note"></i>
Ghi chú đơn hàng:
@@ -260,6 +398,68 @@
<div class="notes-content">
Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng.
</div>
</div>-->
</div>
<!-- Payment History -->
<div class="detail-container">
<div class="detail-card">
<h3 class="section-title">
<i class="fas fa-history" style="color: #2563eb;"></i>
Lịch sử thanh toán
</h3>
<div class="payment-history" style ="margin-bottom: 0px;" id="payment-history">
<!-- Payment Card 1 (Clickable for modal) -->
<div class="history-item" onclick="openPaymentModal('PAY20240001', '6.385.500đ', 'Chuyển khoản', '03/08/2024 - 14:30', 'TK20241020001', 'https://placehold.co/600x400/E8F4FD/005B9A/png?text=Bi%C3%AAn+lai+thanh+to%C3%A1n')">
<div class="history-icon">
<i class="fas fa-check"></i>
</div>
<div class="history-content">
<div class="history-title">#PAY20240001</div>
<!--<div class="history-details">Chuyển khoản | Ref: TK20241020001</div>-->
<div class="history-date">03/08/2024 - 14:30</div>
</div>
<div class="history-amount">6.385.500đ</div>
<i class="fas fa-chevron-right" style="color: #9ca3af; margin-left: 8px;"></i>
</div>
<!-- Payment Card 2 -->
<div class="history-item" onclick="openPaymentModal('PAY20240002', '6.385.500đ', 'Tiền mặt', '05/08/2024 - 09:15', 'CASH-20240805-001', '')">
<div class="history-icon">
<i class="fas fa-check"></i>
</div>
<div class="history-content">
<div class="history-title">#PAY20240002</div>
<!--<div class="history-details">Tiền mặt | Ref: CASH-20240805-001</div>-->
<div class="history-date">05/08/2024 - 09:15</div>
</div>
<div class="history-amount">6.385.500đ</div>
<i class="fas fa-chevron-right" style="color: #9ca3af; margin-left: 8px;"></i>
</div>
<!-- Payment Summary -->
<div class="summary-row">
<span>Còn lại:</span>
<span class="remaining-amount" id="remaining-amount">10.000.000đ</span>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-primary" onclick="makePayment()" id="pay-button">
<i class="fas fa-credit-card"></i>
Thanh toán
</button>
<button class="btn btn-secondary" onclick="contactSupport()">
<i class="fas fa-comments"></i>
Liên hệ hỗ trợ
</button>
</div>
</div>
</div>
@@ -276,11 +476,11 @@
</button>
</div>-->
<!-- Floating Action Button -->
<a href="chat-list.html" class="fab-link">
<!--<a href="chat-list.html" class="fab-link">
<button class="fab">
<i class="fas fa-comments"></i>
</button>
</a>
</a>-->
<!--<a href="chat-list.html" class="fab">-->
<!--<button class="fab">-->
<!--<i class="fas fa-comments"></i>-->
@@ -683,6 +883,14 @@
font-size: 14px;
}
.detail-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
/* Action Buttons */
.order-actions {
position: fixed;
@@ -731,6 +939,200 @@
}
/* Mobile Responsiveness */
/* Invoices List Styles */
.invoices-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.invoice-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.invoice-item:hover {
border-color: var(--primary-blue);
background: #F0F7FF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
}
.invoice-item-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.invoice-item-content {
flex: 1;
}
.invoice-item-title {
font-weight: 600;
color: var(--text-dark);
font-size: 14px;
margin-bottom: 2px;
}
.invoice-item-subtitle {
font-size: 12px;
color: var(--text-light);
}
.invoice-item-amount {
font-weight: 700;
color: #dc2626;
font-size: 14px;
text-align: right;
}
.invoice-item-arrow {
color: var(--text-light);
font-size: 14px;
}
.history-item {
cursor: pointer;
transition: all 0.3s ease;
}
.history-item:hover {
background: #f8fafc;
border-radius: 8px;
transform: translateX(4px);
}
/* Payment Modal Styles */
.payment-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
}
.payment-modal.active {
display: flex;
}
.payment-modal-content {
background: white;
border-radius: 16px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.payment-modal-header {
padding: 20px;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.payment-modal-header h3 {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.payment-modal-close {
width: 32px;
height: 32px;
border-radius: 50%;
background: #f3f4f6;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.payment-modal-close:hover {
background: #e5e7eb;
}
.payment-modal-body {
padding: 20px;
}
.payment-detail-row {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
}
.payment-detail-row:last-child {
border-bottom: none;
}
.payment-detail-label {
color: #6b7280;
font-size: 14px;
}
.payment-detail-value {
color: #1f2937;
font-weight: 600;
font-size: 14px;
text-align: right;
}
.payment-detail-value.amount {
color: #065f46;
font-size: 18px;
font-weight: 700;
}
.payment-receipt-image {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #e5e7eb;
}
.payment-receipt-image img {
width: 100%;
height: auto;
display: block;
}
@media (max-width: 480px) {
.status-timeline-card,
.delivery-info-card,
@@ -763,7 +1165,7 @@
.date-item,
.customer-row,
.summary-row {
flex-direction: column;
/*flex-direction: column;*/
align-items: flex-start;
gap: 4px;
}
@@ -775,10 +1177,393 @@
text-align: left;
font-size: 14px;
}
.payment-modal-content {
margin: 20px;
max-height: calc(100vh - 40px);
}
.invoice-item-amount {
font-size: 13px;
}
}
</style>
<style>
.detail-container {
max-width: 480px;
margin: 0 auto;
padding: 20px;
background: #f8fafc;
}
.detail-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
.invoice-header {
text-align: center;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #e5e7eb;
}
.invoice-id {
font-size: 24px;
font-weight: 700;
color: #1f2937;
margin-bottom: 8px;
}
.invoice-date {
color: #6b7280;
font-size: 14px;
margin-bottom: 12px;
}
.status-badge {
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
display: inline-block;
}
.status-overdue {
background: #fee2e2;
color: #dc2626;
}
.status-unpaid {
background: #fef3c7;
color: #d97706;
}
.status-paid {
background: #d1fae5;
color: #065f46;
}
.status-partial {
background: #e0e7ff;
color: #3730a3;
}
.payment-summary {
background: #f8fafc;
border-radius: 8px;
padding: 20px;
margin-bottom: 24px;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 16px;
}
.summary-row:last-child {
margin-bottom: 0;
padding-top: 12px;
border-top: 2px solid #e5e7eb;
font-weight: 700;
font-size: 18px;
}
.remaining-amount {
color: #dc2626;
font-weight: 700;
}
.section-title {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.product-item {
display: flex;
align-items: flex-start;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 12px;
}
.product-image {
width: 60px;
height: 60px;
background: #f3f4f6;
border-radius: 8px;
margin-right: 16px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
color: #9ca3af;
}
.product-info {
flex: 1;
}
.product-name {
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
font-size: 14px;
}
.product-sku {
font-size: 12px;
color: #6b7280;
margin-bottom: 8px;
}
.product-details {
display: flex;
justify-content: space-between;
font-size: 12px;
}
.product-quantity {
color: #6b7280;
}
.product-price {
font-weight: 600;
color: #1f2937;
}
.payment-history {
margin-bottom: 24px;
}
.history-item {
display: flex;
align-items: center;
padding: 16px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 12px;
}
.history-icon {
width: 40px;
height: 40px;
background: #d1fae5;
color: #065f46;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
font-size: 16px;
}
.history-content {
flex: 1;
}
.history-title {
font-weight: 600;
color: #1f2937;
margin-bottom: 4px;
}
.history-details {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.history-date {
font-size: 12px;
color: #9ca3af;
}
.history-amount {
text-align: right;
font-weight: 600;
color: #065f46;
font-size: 16px;
}
.action-buttons {
display: flex;
gap: 12px;
}
.btn {
flex: 1;
padding: 14px 20px;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
}
.btn-secondary {
background: white;
color: #374151;
border: 2px solid #e5e7eb;
}
.btn-secondary:hover {
border-color: #2563eb;
color: #2563eb;
}
.btn-download {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-download:hover {
background: #e5e7eb;
}
.download-section {
text-align: center;
}
.empty-history {
text-align: center;
padding: 40px 20px;
color: #9ca3af;
}
.empty-history i {
font-size: 32px;
margin-bottom: 12px;
color: #d1d5db;
}
@media (max-width: 768px) {
.detail-container {
padding: 15px;
}
.detail-card {
padding: 15px;
}
.action-buttons {
flex-direction: column;
}
.product-details {
flex-direction: column;
gap: 4px;
}
.summary-row {
font-size: 14px;
}
.summary-row:last-child {
font-size: 16px;
}
}
</style>
<!-- Payment Detail Modal -->
<div id="paymentModal" class="payment-modal">
<div class="payment-modal-content">
<div class="payment-modal-header">
<h3>Chi tiết thanh toán</h3>
<button class="payment-modal-close" onclick="closePaymentModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="payment-modal-body">
<div class="payment-detail-row">
<span class="payment-detail-label">Mã giao dịch:</span>
<span class="payment-detail-value" id="modal-transaction-id"></span>
</div>
<div class="payment-detail-row">
<span class="payment-detail-label">Thời gian:</span>
<span class="payment-detail-value" id="modal-datetime"></span>
</div>
<div class="payment-detail-row">
<span class="payment-detail-label">Phương thức:</span>
<span class="payment-detail-value" id="modal-method"></span>
</div>
<div class="payment-detail-row">
<span class="payment-detail-label">Mã tham chiếu:</span>
<span class="payment-detail-value" id="modal-reference"></span>
</div>
<div class="payment-detail-row">
<span class="payment-detail-label">Số tiền:</span>
<span class="payment-detail-value amount" id="modal-amount"></span>
</div>
<div id="modal-receipt-container" class="payment-receipt-image" style="display: none;">
<img id="modal-receipt-image" src="" alt="Biên lai thanh toán">
</div>
</div>
</div>
</div>
<script>
function openPaymentModal(transactionId, amount, method, datetime, reference, receiptImage) {
document.getElementById('modal-transaction-id').textContent = transactionId;
document.getElementById('modal-amount').textContent = amount;
document.getElementById('modal-method').textContent = method;
document.getElementById('modal-datetime').textContent = datetime;
document.getElementById('modal-reference').textContent = reference;
const receiptContainer = document.getElementById('modal-receipt-container');
const receiptImg = document.getElementById('modal-receipt-image');
if (receiptImage && receiptImage !== '') {
receiptImg.src = receiptImage;
receiptContainer.style.display = 'block';
} else {
receiptContainer.style.display = 'none';
}
document.getElementById('paymentModal').classList.add('active');
document.body.style.overflow = 'hidden';
}
function closePaymentModal() {
document.getElementById('paymentModal').classList.remove('active');
document.body.style.overflow = 'auto';
}
// Close modal when clicking outside
document.getElementById('paymentModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closePaymentModal();
}
});
function shareOrder() {
if (navigator.share) {
navigator.share({

View File

@@ -221,14 +221,14 @@ class AppTheme {
// ==================== Switch Theme ====================
switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.grey500;
}),
trackColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.lightBlue;
}
return AppColors.grey100;
@@ -237,20 +237,20 @@ class AppTheme {
// ==================== Checkbox Theme ====================
checkboxTheme: CheckboxThemeData(
fillColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.white;
}),
checkColor: MaterialStateProperty.all(AppColors.white),
checkColor: WidgetStateProperty.all(AppColors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
),
// ==================== Radio Theme ====================
radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith((states) {
if (states.contains(MaterialState.selected)) {
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.grey500;

View File

@@ -7,6 +7,8 @@ library;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
// ============================================================================
// String Extensions
@@ -422,26 +424,26 @@ extension BuildContextExtensions on BuildContext {
}
/// Navigate to route
Future<T?> push<T>(Widget page) {
return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
}
// Future<T?> push<T>(Widget page) {
// return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
// }
/// Navigate and replace current route
Future<T?> pushReplacement<T>(Widget page) {
return Navigator.of(
this,
).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
}
// Future<T?> pushReplacement<T>(Widget page) {
// return Navigator.of(
// this,
// ).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
// }
/// Pop current route
void pop<T>([T? result]) {
Navigator.of(this).pop(result);
}
// void pop<T>([T? result]) {
// GoRouter.of(this).pop(result);
// }
/// Pop until first route
void popUntilFirst() {
Navigator.of(this).popUntil((route) => route.isFirst);
}
// void popUntilFirst() {
// Navigator.of(this).popUntil((route) => route.isFirst);
// }
}
// ============================================================================
@@ -466,4 +468,26 @@ extension NumExtensions on num {
final mod = math.pow(10.0, places);
return ((this * mod).round().toDouble() / mod);
}
/// Format as Vietnamese currency (đồng)
/// Returns formatted string like "1.153.434đ"
String get toVNCurrency {
final formatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return formatter.format(this);
}
/// Format as Vietnamese currency with custom symbol
/// Returns formatted string with custom symbol
String toCurrency({String symbol = 'đ', int decimalDigits = 0}) {
final formatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: symbol,
decimalDigits: decimalDigits,
);
return formatter.format(this);
}
}

View File

@@ -657,7 +657,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: items.containsKey(value) ? value : null,
initialValue: items.containsKey(value) ? value : null,
isExpanded: true,
validator: validator,
decoration: InputDecoration(
@@ -796,7 +796,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: items.containsKey(value) ? value : null,
initialValue: items.containsKey(value) ? value : null,
isExpanded: true,
validator: validator,
decoration: InputDecoration(

View File

@@ -453,7 +453,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
@@ -759,7 +759,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
return customerGroupsAsync.when(
data: (groups) {
return DropdownButtonFormField<CustomerGroup>(
value: _selectedRole,
initialValue: _selectedRole,
decoration: _buildInputDecoration(
hintText: 'Chọn vai trò',
prefixIcon: FontAwesomeIcons.briefcase,
@@ -831,7 +831,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
return citiesAsync.when(
data: (cities) {
return DropdownButtonFormField<City>(
value: _selectedCity,
initialValue: _selectedCity,
decoration: _buildInputDecoration(
hintText: 'Chọn tỉnh/thành phố',
prefixIcon: Icons.location_city,

View File

@@ -55,7 +55,7 @@ class RoleDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: value,
initialValue: value,
decoration: InputDecoration(
hintText: 'Chọn vai trò của bạn',
hintStyle: const TextStyle(

View File

@@ -167,7 +167,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Price
Text(
'${currencyFormatter.format(widget.item.product.basePrice)}/${widget.item.product.unit ?? ''}',
'${currencyFormatter.format(widget.item.product.basePrice)}/',
style: AppTypography.titleMedium.copyWith(
color: AppColors.primaryBlue,
fontWeight: FontWeight.bold,
@@ -252,7 +252,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Unit label
Text(
widget.item.product.unit ?? '',
'',
style: AppTypography.bodySmall.copyWith(
color: AppColors.grey500,
),
@@ -273,7 +273,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
const TextSpan(text: '(Quy đổi: '),
TextSpan(
text:
'${widget.item.quantityConverted.toStringAsFixed(2)} ${widget.item.product.unit ?? ''}',
'${widget.item.quantityConverted.toStringAsFixed(2)} ',
style: const TextStyle(fontWeight: FontWeight.bold),
),
const TextSpan(text: ' = '),

View File

@@ -31,7 +31,7 @@ class MemberCardWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
@@ -64,7 +64,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
memberCard.memberType.displayName,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.8,
@@ -79,7 +79,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
'Valid through',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
color: Colors.white.withValues(alpha: 0.8),
fontSize: 11,
),
),
@@ -123,7 +123,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
'CLASS: ${memberCard.tier.displayName}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),
@@ -132,7 +132,7 @@ class MemberCardWidget extends StatelessWidget {
Text(
'Points: ${_formatPoints(memberCard.points)}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.w600,
),

View File

@@ -24,8 +24,8 @@ class OrderDetailModel {
final List<OrderItemDetailModel> items;
final PaymentTermsInfoModel paymentTerms;
final List<TimelineItemModel> timeline;
final List<dynamic> payments;
final List<dynamic> invoices;
final List<PaymentInfoModel> payments;
final List<InvoiceInfoModel> invoices;
/// Create from JSON
factory OrderDetailModel.fromJson(Map<String, dynamic> json) {
@@ -50,8 +50,14 @@ class OrderDetailModel {
.map((item) =>
TimelineItemModel.fromJson(item as Map<String, dynamic>))
.toList(),
payments: json['payments'] as List<dynamic>? ?? [],
invoices: json['invoices'] as List<dynamic>? ?? [],
payments: (json['payments'] as List<dynamic>? ?? [])
.map((item) =>
PaymentInfoModel.fromJson(item as Map<String, dynamic>))
.toList(),
invoices: (json['invoices'] as List<dynamic>? ?? [])
.map((item) =>
InvoiceInfoModel.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
@@ -64,8 +70,8 @@ class OrderDetailModel {
'items': items.map((item) => item.toJson()).toList(),
'payment_terms': paymentTerms.toJson(),
'timeline': timeline.map((item) => item.toJson()).toList(),
'payments': payments,
'invoices': invoices,
'payments': payments.map((item) => item.toJson()).toList(),
'invoices': invoices.map((item) => item.toJson()).toList(),
};
}
@@ -78,8 +84,8 @@ class OrderDetailModel {
items: items.map((item) => item.toEntity()).toList(),
paymentTerms: paymentTerms.toEntity(),
timeline: timeline.map((item) => item.toEntity()).toList(),
payments: payments,
invoices: invoices,
payments: payments.map((item) => item.toEntity()).toList(),
invoices: invoices.map((item) => item.toEntity()).toList(),
);
}
@@ -96,8 +102,12 @@ class OrderDetailModel {
timeline: entity.timeline
.map((item) => TimelineItemModel.fromEntity(item))
.toList(),
payments: entity.payments,
invoices: entity.invoices,
payments: entity.payments
.map((item) => PaymentInfoModel.fromEntity(item))
.toList(),
invoices: entity.invoices
.map((item) => InvoiceInfoModel.fromEntity(item))
.toList(),
);
}
}
@@ -500,3 +510,93 @@ class TimelineItemModel {
);
}
}
/// Payment Info Model
class PaymentInfoModel {
const PaymentInfoModel({
required this.name,
required this.creationDate,
required this.amount,
});
final String name;
final String creationDate;
final double amount;
factory PaymentInfoModel.fromJson(Map<String, dynamic> json) {
return PaymentInfoModel(
name: json['name'] as String,
creationDate: json['creation_date'] as String,
amount: (json['amount'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'creation_date': creationDate,
'amount': amount,
};
}
PaymentInfo toEntity() {
return PaymentInfo(
name: name,
creationDate: creationDate,
amount: amount,
);
}
factory PaymentInfoModel.fromEntity(PaymentInfo entity) {
return PaymentInfoModel(
name: entity.name,
creationDate: entity.creationDate,
amount: entity.amount,
);
}
}
/// Invoice Info Model
class InvoiceInfoModel {
const InvoiceInfoModel({
required this.name,
required this.postingDate,
required this.grandTotal,
});
final String name;
final String postingDate;
final double grandTotal;
factory InvoiceInfoModel.fromJson(Map<String, dynamic> json) {
return InvoiceInfoModel(
name: json['name'] as String,
postingDate: json['posting_date'] as String,
grandTotal: (json['grand_total'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'posting_date': postingDate,
'grand_total': grandTotal,
};
}
InvoiceInfo toEntity() {
return InvoiceInfo(
name: name,
postingDate: postingDate,
grandTotal: grandTotal,
);
}
factory InvoiceInfoModel.fromEntity(InvoiceInfo entity) {
return InvoiceInfoModel(
name: entity.name,
postingDate: entity.postingDate,
grandTotal: entity.grandTotal,
);
}
}

View File

@@ -24,8 +24,8 @@ class OrderDetail extends Equatable {
final List<OrderItemDetail> items;
final PaymentTermsInfo paymentTerms;
final List<TimelineItem> timeline;
final List<dynamic> payments; // Payment entities can be added later
final List<dynamic> invoices; // Invoice entities can be added later
final List<PaymentInfo> payments;
final List<InvoiceInfo> invoices;
@override
List<Object?> get props => [
@@ -219,3 +219,35 @@ class TimelineItem extends Equatable {
@override
List<Object?> get props => [label, value, status];
}
/// Payment Info
class PaymentInfo extends Equatable {
const PaymentInfo({
required this.name,
required this.creationDate,
required this.amount,
});
final String name;
final String creationDate;
final double amount;
@override
List<Object?> get props => [name, creationDate, amount];
}
/// Invoice Info
class InvoiceInfo extends Equatable {
const InvoiceInfo({
required this.name,
required this.postingDate,
required this.grandTotal,
});
final String name;
final String postingDate;
final double grandTotal;
@override
List<Object?> get props => [name, postingDate, grandTotal];
}

View File

@@ -12,6 +12,7 @@ import 'package:intl/intl.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/enums/status_color.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/orders/domain/entities/order_detail.dart';
import 'package:worker/features/orders/presentation/providers/orders_provider.dart';
@@ -82,16 +83,24 @@ class OrderDetailPage extends ConsumerWidget {
_buildStatusTimelineCard(orderDetail),
// Delivery/Address Information Card
_buildAddressInfoCard(orderDetail),
_buildAddressInfoCard(context, orderDetail),
// Customer Information Card
_buildCustomerInfoCard(orderDetail),
// Invoice Information Card
_buildInvoiceInfoCard(context, orderDetail),
// Invoices List Card
_buildInvoicesListCard(context, orderDetail),
// Products List Card
_buildProductsListCard(orderDetail),
// Order Summary Card
_buildOrderSummaryCard(orderDetail),
// Payment History Card
_buildPaymentHistoryCard(context, orderDetail),
const SizedBox(height: 16),
],
),
),
@@ -327,7 +336,7 @@ class OrderDetailPage extends ConsumerWidget {
}
/// Build Address Info Card
Widget _buildAddressInfoCard(OrderDetail orderDetail) {
Widget _buildAddressInfoCard(BuildContext context, OrderDetail orderDetail) {
final order = orderDetail.order;
final shippingAddress = orderDetail.shippingAddress;
final dateFormatter = DateFormat('dd/MM/yyyy');
@@ -341,15 +350,15 @@ class OrderDetailPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
const Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.truck,
color: AppColors.primaryBlue,
size: 18,
),
const SizedBox(width: 8),
const Text(
SizedBox(width: 8),
Text(
'Thông tin giao hàng',
style: TextStyle(
fontSize: 16,
@@ -362,86 +371,139 @@ class OrderDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
// Delivery Date
_buildInfoRow(
icon: FontAwesomeIcons.calendar,
label: 'Ngày giao hàng',
value: dateFormatter.format(DateTime.parse(order.deliveryDate)),
// Address Section with Label + Button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Địa chỉ nhận hàng',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.grey500,
),
),
TextButton(
onPressed: () {
// TODO: Navigate to address update
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng đang phát triển')),
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: const BorderSide(color: AppColors.grey100),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Cập nhật',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryBlue,
),
const SizedBox(height: 12),
_buildInfoRow(
icon: FontAwesomeIcons.locationDot,
label: 'Địa chỉ giao hàng',
value:
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
),
const SizedBox(height: 12),
_buildInfoRow(
icon: FontAwesomeIcons.user,
label: 'Người nhận',
value: '${shippingAddress.addressTitle} - ${shippingAddress.phone}',
),
],
),
),
);
}
/// Build Info Row
Widget _buildInfoRow({
required IconData icon,
required String label,
required String value,
Color? valueColor,
}) {
return Row(
const SizedBox(height: 8),
// Address Box
Container(
padding: const EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Row(
children: [
FaIcon(icon, size: 14, color: AppColors.grey500),
const SizedBox(width: 6),
Expanded(
child: Text(
label,
Text(
shippingAddress.addressTitle,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 4),
Text(
shippingAddress.phone,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
flex: 2,
child: Text(
value,
textAlign: TextAlign.right,
const SizedBox(height: 16),
// Pickup Date
const Text(
'Ngày lấy hàng',
style: TextStyle(
fontSize: 14,
fontWeight: valueColor != null
? FontWeight.w600
: FontWeight.w500,
color: valueColor ?? AppColors.grey900,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
dateFormatter.format(DateTime.parse(order.deliveryDate)),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
),
if (order.description.isNotEmpty) ...[ const SizedBox(height: 16),
// Notes
const Text(
'Ghi chú',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
order.description,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
),
],
],
),
),
);
}
/// Build Customer Info Card
Widget _buildCustomerInfoCard(OrderDetail orderDetail) {
final order = orderDetail.order;
/// Build Invoice Info Card
Widget _buildInvoiceInfoCard(BuildContext context, OrderDetail orderDetail) {
final billingAddress = orderDetail.billingAddress;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 1,
@@ -451,16 +513,146 @@ class OrderDetailPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title + Update Button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const FaIcon(
FontAwesomeIcons.user,
const Row(
children: [
FaIcon(
FontAwesomeIcons.fileInvoice,
color: AppColors.primaryBlue,
size: 18,
),
const SizedBox(width: 8),
const Text(
'Thông tin khách hàng',
SizedBox(width: 8),
Text(
'Thông tin hóa đơn',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
],
),
TextButton(
onPressed: () {
// TODO: Navigate to invoice update
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng đang phát triển')),
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: AppColors.grey100),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Cập nhật',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryBlue,
),
),
),
],
),
const SizedBox(height: 12),
// Invoice Address Box
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
billingAddress.addressTitle,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
if (billingAddress.taxCode.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
'Mã số thuế: ${billingAddress.taxCode}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
const SizedBox(height: 2),
Text(
'Số điện thoại: ${billingAddress.phone}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 2),
Text(
'Email: ${billingAddress.email}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
const SizedBox(height: 2),
Text(
'Địa chỉ: ${billingAddress.addressLine1}, ${billingAddress.wardName}, ${billingAddress.cityName}',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
),
],
),
),
);
}
/// Build Invoices List Card
Widget _buildInvoicesListCard(BuildContext context, OrderDetail orderDetail) {
final invoices = orderDetail.invoices;
if (invoices.isEmpty) {
return const SizedBox.shrink();
}
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
FaIcon(
FontAwesomeIcons.fileInvoiceDollar,
color: AppColors.primaryBlue,
size: 18,
),
SizedBox(width: 8),
Text(
'Hóa đơn đã xuất',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -472,54 +664,109 @@ class OrderDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
_buildCustomerRow('Tên khách hàng:', order.customer),
const SizedBox(height: 12),
_buildCustomerRow('Số điện thoại:', billingAddress.phone),
const SizedBox(height: 12),
_buildCustomerRow('Email:', billingAddress.email),
const SizedBox(height: 12),
if (billingAddress.taxCode.isNotEmpty) ...[
_buildCustomerRow('Mã số thuế:', billingAddress.taxCode),
const SizedBox(height: 12),
],
// Invoice Items (Mock data for now)
...invoices.map((e) => _buildInvoiceItem(
invoiceId: e.name,
date: e.postingDate,
amount: e.grandTotal.toVNCurrency,
onTap: () {
// TODO: Navigate to invoice detail
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chức năng đang phát triển')),
);
},
),),
],
),
),
);
}
/// Build Customer Row
Widget _buildCustomerRow(String label, String value) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
/// Build Invoice Item
Widget _buildInvoiceItem({
required String invoiceId,
required String date,
required String amount,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.primaryBlue, Color(0xFF1d4ed8)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: FaIcon(
FontAwesomeIcons.fileInvoice,
color: Colors.white,
size: 18,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
),
Text(
value,
invoiceId,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 2),
Text(
'Ngày xuất: $date',
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
const SizedBox(width: 4,),
Text(
amount,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.danger,
),
),
const SizedBox(width: 8),
const FaIcon(
FontAwesomeIcons.chevronRight,
size: 14,
color: AppColors.grey500,
),
],
),
),
);
}
/// Build Products List Card
Widget _buildProductsListCard(OrderDetail orderDetail) {
final items = orderDetail.items;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -530,15 +777,15 @@ class OrderDetailPage extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
const Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.box,
color: AppColors.primaryBlue,
size: 18,
),
const SizedBox(width: 8),
const Text(
SizedBox(width: 8),
Text(
'Sản phẩm đặt hàng',
style: TextStyle(
fontSize: 16,
@@ -657,14 +904,14 @@ class OrderDetailPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${currencyFormatter.format(item.price)}/m²',
'${item.price.toVNCurrency}/m²',
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
Text(
currencyFormatter.format(item.totalAmount),
item.totalAmount.toVNCurrency,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -692,11 +939,6 @@ class OrderDetailPage extends ConsumerWidget {
Widget _buildOrderSummaryCard(OrderDetail orderDetail) {
final order = orderDetail.order;
final paymentTerms = orderDetail.paymentTerms;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -728,13 +970,13 @@ class OrderDetailPage extends ConsumerWidget {
const SizedBox(height: 16),
_buildSummaryRow('Tổng tiền hàng:', currencyFormatter.format(order.total)),
_buildSummaryRow('Tổng tiền hàng:', order.total.toVNCurrency),
const SizedBox(height: 8),
if (order.totalRemaining > 0) ...[
_buildSummaryRow(
'Còn lại:',
currencyFormatter.format(order.totalRemaining),
order.totalRemaining.toVNCurrency,
valueColor: AppColors.warning,
),
const SizedBox(height: 8),
@@ -744,7 +986,7 @@ class OrderDetailPage extends ConsumerWidget {
_buildSummaryRow(
'Tổng cộng:',
currencyFormatter.format(order.grandTotal),
order.grandTotal.toVNCurrency,
isTotal: true,
),
@@ -784,31 +1026,169 @@ class OrderDetailPage extends ConsumerWidget {
),
),
if (order.description.isNotEmpty) ...[
const Divider(height: 24),
],
),
),
);
}
// Order Notes
Row(
/// Build Payment History Card
Widget _buildPaymentHistoryCard(BuildContext context, OrderDetail orderDetail) {
final order = orderDetail.order;
final payments = orderDetail.payments;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16,
children: [
const FaIcon(FontAwesomeIcons.noteSticky, size: 14, color: AppColors.grey500),
const SizedBox(width: 6),
const Text(
'Ghi chú đơn hàng:',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
const Row(
children: [
FaIcon(
FontAwesomeIcons.clockRotateLeft,
color: AppColors.primaryBlue,
size: 18,
),
SizedBox(width: 8),
Text(
'Lịch sử thanh toán',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
],
),
const SizedBox(height: 4),
Text(
order.description,
style: const TextStyle(
...payments.map((e) => _buildPaymentItem(
paymentId: e.name,
date: e.creationDate,
amount: e.amount.toVNCurrency,
onTap: () {
// TODO: Show payment detail modal
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Chi tiết thanh toán')),
);
},
)),
// Payment Summary
Container(
padding: const EdgeInsets.only(top: 12),
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: AppColors.grey100, width: 1),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Còn lại:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
height: 1.4,
color: AppColors.grey500,
),
),
Text(
order.totalRemaining.toVNCurrency,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.danger,
),
),
],
),
),
],
),
),
);
}
/// Build Payment Item
Widget _buildPaymentItem({
required String paymentId,
required String date,
required String amount,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey100),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: Color(0xFFD1FAE5),
shape: BoxShape.circle,
),
child: const Center(
child: FaIcon(
FontAwesomeIcons.check,
color: Color(0xFF065F46),
size: 16,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
paymentId,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 2),
Text(
date,
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
],
),
),
const SizedBox(width: 4),
Text(
amount,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF065F46),
),
),
const SizedBox(width: 8),
const FaIcon(
FontAwesomeIcons.chevronRight,
size: 14,
color: AppColors.grey500,
),
],
),
),
@@ -848,8 +1228,6 @@ class OrderDetailPage extends ConsumerWidget {
/// Build Action Buttons
Widget _buildActionButtons(BuildContext context, OrderDetail orderDetail) {
final shippingAddress = orderDetail.shippingAddress;
return Container(
decoration: BoxDecoration(
color: AppColors.white,
@@ -866,23 +1244,22 @@ class OrderDetailPage extends ConsumerWidget {
spacing: 12,
children: [
Expanded(
child: OutlinedButton.icon(
child: ElevatedButton.icon(
onPressed: () {
// TODO: Navigate to payment page
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gọi ${shippingAddress.phone}...'),
const SnackBar(
content: Text('Chức năng thanh toán đang phát triển'),
),
);
},
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
label: const Text('Liên hệ'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
side: const BorderSide(
color: AppColors.grey100,
width: 2,
),
foregroundColor: AppColors.grey900,
icon: const FaIcon(FontAwesomeIcons.creditCard, size: 18),
label: const Text('Thanh toán'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
@@ -890,21 +1267,24 @@ class OrderDetailPage extends ConsumerWidget {
),
),
Expanded(
child: ElevatedButton.icon(
child: OutlinedButton.icon(
onPressed: () {
// TODO: Navigate to chat/support
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Chức năng đang phát triển...'),
content: Text('Liên hệ hỗ trợ...'),
),
);
},
icon: const FaIcon(FontAwesomeIcons.penToSquare, size: 18),
label: const Text('Cập nhật'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
elevation: 0,
icon: const FaIcon(FontAwesomeIcons.comments, size: 18),
label: const Text('Liên hệ hỗ trợ'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: BorderSide(
color: AppColors.grey100,
width: 2,
),
foregroundColor: AppColors.grey900,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),

View File

@@ -23,7 +23,7 @@ class DocumentCard extends StatelessWidget {
border: Border.all(color: AppColors.grey100),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),

View File

@@ -28,7 +28,6 @@ class ProductModel extends HiveObject {
this.specifications,
this.itemGroupName,
this.brand,
this.unit,
this.conversionOfSm,
this.introAttributes,
required this.isActive,
@@ -83,10 +82,6 @@ class ProductModel extends HiveObject {
@HiveField(10)
final String? brand;
/// Unit of measurement (m2, box, piece, etc.)
@HiveField(11)
final String? unit;
/// Conversion factor for Square Meter UOM (tiles per m²)
/// Used to calculate: Số viên = Số lượng × conversionOfSm
@HiveField(17)
@@ -204,7 +199,6 @@ class ProductModel extends HiveObject {
: null,
itemGroupName: json['item_group_name'] as String?,
brand: json['brand'] as String?,
unit: json['currency'] as String?, // Use currency as unit for now
conversionOfSm: json['conversion_of_sm'] != null
? (json['conversion_of_sm'] as num).toDouble()
: null,
@@ -260,7 +254,6 @@ class ProductModel extends HiveObject {
specifications: null,
itemGroupName: json['item_group_name'] as String?,
brand: null, // Not provided by wishlist API
unit: json['currency'] as String? ?? '',
conversionOfSm: json['conversion_of_sm'] != null
? (json['conversion_of_sm'] as num).toDouble()
: null,
@@ -291,7 +284,6 @@ class ProductModel extends HiveObject {
: null,
'item_group_name': itemGroupName,
'brand': brand,
'unit': unit,
'conversion_of_sm': conversionOfSm,
'intro_attributes': introAttributes != null
? jsonDecode(introAttributes!)
@@ -406,7 +398,6 @@ class ProductModel extends HiveObject {
String? specifications,
String? itemGroupName,
String? brand,
String? unit,
double? conversionOfSm,
String? introAttributes,
bool? isActive,
@@ -427,7 +418,6 @@ class ProductModel extends HiveObject {
specifications: specifications ?? this.specifications,
itemGroupName: itemGroupName ?? this.itemGroupName,
brand: brand ?? this.brand,
unit: unit ?? this.unit,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
introAttributes: introAttributes ?? this.introAttributes,
isActive: isActive ?? this.isActive,
@@ -471,7 +461,6 @@ class ProductModel extends HiveObject {
specifications: specificationsMap ?? {},
itemGroupName: itemGroupName,
brand: brand,
unit: unit,
conversionOfSm: conversionOfSm,
introAttributes: introAttributesList,
isActive: isActive,

View File

@@ -28,7 +28,6 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
specifications: fields[8] as String?,
itemGroupName: fields[9] as String?,
brand: fields[10] as String?,
unit: fields[11] as String?,
conversionOfSm: (fields[17] as num?)?.toDouble(),
introAttributes: fields[18] as String?,
isActive: fields[12] as bool,
@@ -42,7 +41,7 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
@override
void write(BinaryWriter writer, ProductModel obj) {
writer
..writeByte(19)
..writeByte(18)
..writeByte(0)
..write(obj.productId)
..writeByte(1)
@@ -65,8 +64,6 @@ class ProductModelAdapter extends TypeAdapter<ProductModel> {
..write(obj.itemGroupName)
..writeByte(10)
..write(obj.brand)
..writeByte(11)
..write(obj.unit)
..writeByte(12)
..write(obj.isActive)
..writeByte(13)

View File

@@ -22,7 +22,6 @@ class Product {
required this.specifications,
this.itemGroupName,
this.brand,
this.unit,
this.conversionOfSm,
this.introAttributes,
required this.isActive,
@@ -64,9 +63,6 @@ class Product {
/// Brand name
final String? brand;
/// Unit of measurement (e.g., "m²", "viên", "hộp")
final String? unit;
/// Conversion factor for Square Meter UOM (tiles per m²)
/// Used to calculate: Số viên = Số lượng × conversionOfSm
final double? conversionOfSm;
@@ -154,7 +150,6 @@ class Product {
Map<String, dynamic>? specifications,
String? itemGroupName,
String? brand,
String? unit,
double? conversionOfSm,
List<Map<String, String>>? introAttributes,
bool? isActive,
@@ -175,7 +170,6 @@ class Product {
specifications: specifications ?? this.specifications,
itemGroupName: itemGroupName ?? this.itemGroupName,
brand: brand ?? this.brand,
unit: unit ?? this.unit,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
introAttributes: introAttributes ?? this.introAttributes,
isActive: isActive ?? this.isActive,
@@ -203,7 +197,6 @@ class Product {
other.basePrice == basePrice &&
other.itemGroupName == itemGroupName &&
other.brand == brand &&
other.unit == unit &&
other.isActive == isActive &&
other.isFeatured == isFeatured &&
other.erpnextItemCode == erpnextItemCode;
@@ -218,7 +211,6 @@ class Product {
basePrice,
itemGroupName,
brand,
unit,
isActive,
isFeatured,
erpnextItemCode,

View File

@@ -156,7 +156,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Đã thêm $_quantity ${product.unit} ${product.name} vào giỏ hàng!',
'Đã thêm $_quantity ${product.name} vào giỏ hàng!',
),
duration: const Duration(seconds: 2),
action: SnackBarAction(
@@ -246,7 +246,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
right: 0,
child: StickyActionBar(
quantity: _quantity,
unit: product.unit ?? '',
unit: '',
conversionOfSm: product.conversionOfSm,
uomFromIntroAttributes: product.getIntroAttribute('UOM'),
onIncrease: _increaseQuantity,

View File

@@ -220,7 +220,7 @@ class ProductCard extends ConsumerWidget {
// Price
Text(
'${_formatPrice(product.effectivePrice)}/${product.unit}',
'${_formatPrice(product.effectivePrice)}/',
style: const TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.bold,

View File

@@ -61,7 +61,7 @@ class ProductInfoSection extends StatelessWidget {
children: [
// Current Price
Text(
'${_formatPrice(product.basePrice)}/${product.unit ?? ''}',
'${_formatPrice(product.basePrice)}/',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w700,

View File

@@ -271,7 +271,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedStyle.value.isEmpty
initialValue: selectedStyle.value.isEmpty
? null
: selectedStyle.value,
decoration: InputDecoration(
@@ -365,7 +365,7 @@ class DesignRequestCreatePage extends HookConsumerWidget {
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: selectedBudget.value.isEmpty
initialValue: selectedBudget.value.isEmpty
? null
: selectedBudget.value,
decoration: InputDecoration(

View File

@@ -124,7 +124,7 @@ class SearchAppBar extends StatelessWidget implements PreferredSizeWidget {
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)),
hintStyle: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
border: InputBorder.none,
suffixIcon: controller?.text.isNotEmpty ?? false
? IconButton(

View File

@@ -45,7 +45,7 @@ class GradientCard extends StatelessWidget {
shadows ??
[
BoxShadow(
color: Colors.black.withOpacity(0.1 * (elevation / 4)),
color: Colors.black.withValues(alpha: 0.1 * (elevation / 4)),
blurRadius: elevation,
offset: Offset(0, elevation / 2),
),
@@ -243,9 +243,9 @@ class _ShimmerGradientCardState extends State<ShimmerGradientCard>
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white.withOpacity(0.1),
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1),
Colors.white.withValues(alpha: 0.1),
Colors.white.withValues(alpha: 0.3),
Colors.white.withValues(alpha: 0.1),
],
stops: [
_controller.value - 0.3,

View File

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+10
version: 1.0.1+12
environment:
sdk: ^3.10.0