Compare commits

..

2 Commits

Author SHA1 Message Date
Phuoc Nguyen
039dfb9fb5 update order detail 2025-11-25 11:57:56 +07:00
Phuoc Nguyen
c3b5653420 order success 2025-11-24 17:28:17 +07:00
31 changed files with 1642 additions and 322 deletions

View File

@@ -16,14 +16,16 @@
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
</a> </a>
<h1 class="header-title">Chi tiết đơn hàng</h1> <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()"> <button class="header-action-btn" onclick="shareOrder()">
<i class="fas fa-share"></i> <i class="fas fa-share"></i>
</button> </button>
<button class="header-action-btn" onclick="printOrder()"> <button class="header-action-btn" onclick="printOrder()">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</button> </button>
</div> </div>-->
</div> </div>
<div class="order-detail-content" style="padding-bottom: 0px;"> <div class="order-detail-content" style="padding-bottom: 0px;">
@@ -46,22 +48,33 @@
</div> </div>
</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-item active">
<div class="timeline-icon"> <div class="timeline-icon">
<i class="fas fa-cog fa-spin"></i> <i class="fas fa-cog fa-spin"></i>
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">Đã xác nhận đơn hàng</div> <div class="timeline-title">Xử lý</div>
<div class="timeline-date">03/08/2023 - 10:15 (Đang xử lý)</div> <div class="timeline-date">Chuẩn bị hàng và vận chuyển</div>
</div> </div>
</div> </div>
<div class="timeline-item pending"> <div class="timeline-item pending">
<div class="timeline-icon"> <div class="timeline-icon">
<i class="fas fa-check-circle"></i> <i class="fas fa-box-open"></i>
</div> </div>
<div class="timeline-content"> <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 class="timeline-date">Dự kiến: 07/08/2023</div>
</div> </div>
</div> </div>
@@ -69,11 +82,11 @@
</div> </div>
<!-- Delivery Information Card --> <!-- 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> <h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
<div class="delivery-details"> <div class="delivery-details">
<!--<div class="delivery-method"> <div class="delivery-method">
<div class="delivery-method-icon"> <div class="delivery-method-icon">
<i class="fas fa-truck"></i> <i class="fas fa-truck"></i>
</div> </div>
@@ -81,16 +94,16 @@
<div class="method-name">Giao hàng tiêu chuẩn</div> <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 class="method-description">Giao trong 3-5 ngày làm việc</div>
</div> </div>
</div>--> </div>
<div class="delivery-dates"> <div class="delivery-dates">
<!--<div class="date-item"> <div class="date-item">
<div class="date-label"> <div class="date-label">
<i class="fas fa-calendar-alt"></i> <i class="fas fa-calendar-alt"></i>
Ngày xuất kho Ngày xuất kho
</div> </div>
<div class="date-value confirmed">05/08/2023</div> <div class="date-value confirmed">05/08/2023</div>
</div>--> </div>
<div class="date-item"> <div class="date-item">
<div class="date-label"> <div class="date-label">
@@ -120,55 +133,179 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>-->
<!-- Customer Information --> <!-- Customer Information -->
<div class="customer-info-card"> <!--<div class="customer-info-card">
<h3><i class="fas fa-user-circle"></i> Thông tin khách hàng</h3> <h3><i class="fas fa-user-circle"></i> Thông tin khách hàng</h3>-->
<div class="customer-details"> <div class="delivery-info-card">
<div class="customer-row"> <h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
<span class="customer-label">Tên khách hàng:</span> <!-- Address Section -->
<span class="customer-value">Nguyễn Văn A</span> <!--<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>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Số điện thoại:</span> <span class="customer-label">Ghi chú:</span>
<span class="customer-value">0901234567</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">Email:</span>
<span class="customer-value">nguyenvana@email.com</span>
</div> </div>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Loại khách hàng:</span> <span class="customer-label">Loại khách hàng:</span>
<span class="customer-badge vip">DIAMOND</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> </div>
</div> </div>
<!-- Invoice Information --> <!-- Invoice Information -->
<div class="customer-info-card"> <div class="customer-info-card">
<h3><i class="fas fa-file-invoice"></i> Thông tin hóa đơn</h3> <!-- Title + Update Button -->
<div class="customer-details"> <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"> <div class="customer-row">
<span class="customer-label">Tên công ty:</span> <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>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Mã số thuế:</span> <span class="customer-label">Mã số thuế:</span>
<span class="customer-value">0123456789</span> <span class="customer-value">0134000687</span>
</div> </div>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Địa chỉ công ty:</span> <span class="customer-label">Địa chỉ:</span>
<span class="customer-value">123 Nguyễn Trãi, Quận 1, TP.HCM</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>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Email nhận hóa đơn:</span> <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>
<div class="customer-row"> <div class="customer-row">
<span class="customer-label">Loại hóa đơn:</span> <span class="customer-label">Loại hóa đơn:</span>
<span class="customer-badge" style="background: #d1ecf1; color: #0c5460;">Hóa đơn VAT</span> <span class="customer-badge" style="background: #d1ecf1; color: #0c5460;">Hóa đơn VAT</span>
</div> </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>
</div> </div>
@@ -249,10 +386,11 @@
<i class="fas fa-credit-card"></i> <i class="fas fa-credit-card"></i>
Phương thức thanh toán: Phương thức thanh toán:
</div> </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>
<div class="order-notes">
<!--<div class="order-notes">
<div class="notes-label"> <div class="notes-label">
<i class="fas fa-sticky-note"></i> <i class="fas fa-sticky-note"></i>
Ghi chú đơn hàng: Ghi chú đơn hàng:
@@ -260,6 +398,68 @@
<div class="notes-content"> <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. 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>
<!-- 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> </div>
</div> </div>
@@ -276,11 +476,11 @@
</button> </button>
</div>--> </div>-->
<!-- Floating Action Button --> <!-- Floating Action Button -->
<a href="chat-list.html" class="fab-link"> <!--<a href="chat-list.html" class="fab-link">
<button class="fab"> <button class="fab">
<i class="fas fa-comments"></i> <i class="fas fa-comments"></i>
</button> </button>
</a> </a>-->
<!--<a href="chat-list.html" class="fab">--> <!--<a href="chat-list.html" class="fab">-->
<!--<button class="fab">--> <!--<button class="fab">-->
<!--<i class="fas fa-comments"></i>--> <!--<i class="fas fa-comments"></i>-->
@@ -683,6 +883,14 @@
font-size: 14px; 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 */ /* Action Buttons */
.order-actions { .order-actions {
position: fixed; position: fixed;
@@ -731,6 +939,200 @@
} }
/* Mobile Responsiveness */ /* 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) { @media (max-width: 480px) {
.status-timeline-card, .status-timeline-card,
.delivery-info-card, .delivery-info-card,
@@ -763,7 +1165,7 @@
.date-item, .date-item,
.customer-row, .customer-row,
.summary-row { .summary-row {
flex-direction: column; /*flex-direction: column;*/
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
} }
@@ -775,10 +1177,393 @@
text-align: left; text-align: left;
font-size: 14px; 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> </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> <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() { function shareOrder() {
if (navigator.share) { if (navigator.share) {
navigator.share({ navigator.share({

View File

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

View File

@@ -7,6 +7,8 @@ library;
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
// ============================================================================ // ============================================================================
// String Extensions // String Extensions
@@ -422,26 +424,26 @@ extension BuildContextExtensions on BuildContext {
} }
/// Navigate to route /// Navigate to route
Future<T?> push<T>(Widget page) { // Future<T?> push<T>(Widget page) {
return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page)); // return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
} // }
/// Navigate and replace current route /// Navigate and replace current route
Future<T?> pushReplacement<T>(Widget page) { // Future<T?> pushReplacement<T>(Widget page) {
return Navigator.of( // return Navigator.of(
this, // this,
).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page)); // ).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
} // }
/// Pop current route /// Pop current route
void pop<T>([T? result]) { // void pop<T>([T? result]) {
Navigator.of(this).pop(result); // GoRouter.of(this).pop(result);
} // }
/// Pop until first route /// Pop until first route
void popUntilFirst() { // void popUntilFirst() {
Navigator.of(this).popUntil((route) => route.isFirst); // Navigator.of(this).popUntil((route) => route.isFirst);
} // }
} }
// ============================================================================ // ============================================================================
@@ -466,4 +468,26 @@ extension NumExtensions on num {
final mod = math.pow(10.0, places); final mod = math.pow(10.0, places);
return ((this * mod).round().toDouble() / mod); 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), const SizedBox(height: 8),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: items.containsKey(value) ? value : null, initialValue: items.containsKey(value) ? value : null,
isExpanded: true, isExpanded: true,
validator: validator, validator: validator,
decoration: InputDecoration( decoration: InputDecoration(
@@ -796,7 +796,7 @@ class AddressFormPage extends HookConsumerWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: items.containsKey(value) ? value : null, initialValue: items.containsKey(value) ? value : null,
isExpanded: true, isExpanded: true,
validator: validator, validator: validator,
decoration: InputDecoration( decoration: InputDecoration(

View File

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

View File

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

View File

@@ -56,10 +56,11 @@ class CheckoutPage extends HookConsumerWidget {
// Payment method (will be set to first payment term name from API) // Payment method (will be set to first payment term name from API)
final paymentMethod = useState<String>(''); final paymentMethod = useState<String>('');
// Price negotiation // Price negotiation (ignore_pricing_rule in API)
final needsNegotiation = useState<bool>(false); final ignorePricingRule = useState<bool>(false);
final needsContract = useState(false); // Contract request (contract_request in API)
final contractRequest = useState<bool>(false);
// Watch API provider for payment terms // Watch API provider for payment terms
final paymentTermsListAsync = ref.watch(paymentTermsListProvider); final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
@@ -145,8 +146,8 @@ class CheckoutPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Payment Method Section (hidden if negotiation is checked) // Payment Method Section (hidden if price negotiation is checked)
if (!needsNegotiation.value) if (!ignorePricingRule.value)
paymentTermsListAsync.when( paymentTermsListAsync.when(
data: (paymentTerms) { data: (paymentTerms) {
// Set default payment method to first term if not set // Set default payment method to first term if not set
@@ -220,7 +221,7 @@ class CheckoutPage extends HookConsumerWidget {
), ),
), ),
if (!needsNegotiation.value) if (!ignorePricingRule.value)
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Discount Code Section // Discount Code Section
@@ -240,7 +241,7 @@ class CheckoutPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Price Negotiation Section // Price Negotiation Section
PriceNegotiationSection(needsNegotiation: needsNegotiation), PriceNegotiationSection(ignorePricingRule: ignorePricingRule),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
@@ -256,9 +257,9 @@ class CheckoutPage extends HookConsumerWidget {
child: Row( child: Row(
children: [ children: [
Checkbox( Checkbox(
value: needsContract.value, value: contractRequest.value,
onChanged: (value) { onChanged: (value) {
needsContract.value = value ?? false; contractRequest.value = value ?? false;
}, },
activeColor: AppColors.warning, activeColor: AppColors.warning,
), ),
@@ -308,7 +309,8 @@ class CheckoutPage extends HookConsumerWidget {
// Place Order Button // Place Order Button
CheckoutSubmitButton( CheckoutSubmitButton(
formKey: formKey, formKey: formKey,
needsNegotiation: needsNegotiation.value, ignorePricingRule: ignorePricingRule.value,
contractRequest: contractRequest.value,
needsInvoice: needsInvoice.value, needsInvoice: needsInvoice.value,
selectedAddress: selectedAddress.value, selectedAddress: selectedAddress.value,
paymentMethod: paymentMethod.value, paymentMethod: paymentMethod.value,

View File

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

View File

@@ -20,7 +20,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
const CheckoutSubmitButton({ const CheckoutSubmitButton({
super.key, super.key,
required this.formKey, required this.formKey,
required this.needsNegotiation, required this.ignorePricingRule,
required this.contractRequest,
required this.needsInvoice, required this.needsInvoice,
required this.selectedAddress, required this.selectedAddress,
required this.paymentMethod, required this.paymentMethod,
@@ -30,7 +31,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
}); });
final GlobalKey<FormState> formKey; final GlobalKey<FormState> formKey;
final bool needsNegotiation; final bool ignorePricingRule;
final bool contractRequest;
final bool needsInvoice; final bool needsInvoice;
final Address? selectedAddress; final Address? selectedAddress;
final String paymentMethod; final String paymentMethod;
@@ -62,7 +64,7 @@ class CheckoutSubmitButton extends HookConsumerWidget {
} }
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: needsNegotiation backgroundColor: ignorePricingRule
? AppColors.warning ? AppColors.warning
: AppColors.primaryBlue, : AppColors.primaryBlue,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@@ -73,7 +75,7 @@ class CheckoutSubmitButton extends HookConsumerWidget {
), ),
), ),
child: Text( child: Text(
needsNegotiation ? 'Gửi yêu cầu đàm phán' : 'Đặt hàng', ignorePricingRule ? 'Gửi yêu cầu đàm phán' : 'Đặt hàng',
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -122,7 +124,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
deliveryAddress: deliveryAddressData, deliveryAddress: deliveryAddressData,
paymentMethod: paymentMethod, paymentMethod: paymentMethod,
needsInvoice: needsInvoice, needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation, ignorePricingRule: ignorePricingRule,
contractRequest: contractRequest,
notes: notes, notes: notes,
).future); ).future);
@@ -131,7 +134,7 @@ class CheckoutSubmitButton extends HookConsumerWidget {
result['orderId'] as String? ?? result['orderId'] as String? ??
'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; 'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
if (needsNegotiation) { if (ignorePricingRule) {
// Navigate to order success page with negotiation flag // Navigate to order success page with negotiation flag
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();

View File

@@ -13,8 +13,8 @@ import 'package:worker/core/theme/colors.dart';
/// Allows user to request price negotiation instead of direct order. /// Allows user to request price negotiation instead of direct order.
class PriceNegotiationSection extends HookWidget { class PriceNegotiationSection extends HookWidget {
const PriceNegotiationSection({super.key, required this.needsNegotiation}); const PriceNegotiationSection({super.key, required this.ignorePricingRule});
final ValueNotifier<bool> needsNegotiation; final ValueNotifier<bool> ignorePricingRule;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -29,9 +29,9 @@ class PriceNegotiationSection extends HookWidget {
child: Row( child: Row(
children: [ children: [
Checkbox( Checkbox(
value: needsNegotiation.value, value: ignorePricingRule.value,
onChanged: (value) { onChanged: (value) {
needsNegotiation.value = value ?? false; ignorePricingRule.value = value ?? false;
}, },
activeColor: AppColors.warning, activeColor: AppColors.warning,
), ),

View File

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

View File

@@ -92,6 +92,8 @@ class OrderRemoteDataSource {
/// "customer_address": "...", /// "customer_address": "...",
/// "description": "...", /// "description": "...",
/// "payment_terms": "...", /// "payment_terms": "...",
/// "ignore_pricing_rule": true,
/// "contract_request": true,
/// "items": [{"item_id": "...", "qty_entered": 0, "primary_qty": 0, "price_entered": 0}] /// "items": [{"item_id": "...", "qty_entered": 0, "primary_qty": 0, "price_entered": 0}]
/// } /// }
/// Returns: { "message": { "name": "SAL-ORD-2025-00001", ... } } /// Returns: { "message": { "name": "SAL-ORD-2025-00001", ... } }
@@ -100,7 +102,8 @@ class OrderRemoteDataSource {
required Map<String, dynamic> deliveryAddress, required Map<String, dynamic> deliveryAddress,
required String paymentMethod, required String paymentMethod,
bool needsInvoice = false, bool needsInvoice = false,
bool needsNegotiation = false, bool ignorePricingRule = false,
bool contractRequest = false,
String? notes, String? notes,
}) async { }) async {
try { try {
@@ -126,6 +129,8 @@ class OrderRemoteDataSource {
'customer_address': deliveryAddress['name'] ?? '', 'customer_address': deliveryAddress['name'] ?? '',
'description': notes ?? 'Order from mobile app', 'description': notes ?? 'Order from mobile app',
'payment_terms': paymentMethod, 'payment_terms': paymentMethod,
'ignore_pricing_rule': ignorePricingRule,
'contract_request': contractRequest,
'items': formattedItems, 'items': formattedItems,
}; };

View File

@@ -24,8 +24,8 @@ class OrderDetailModel {
final List<OrderItemDetailModel> items; final List<OrderItemDetailModel> items;
final PaymentTermsInfoModel paymentTerms; final PaymentTermsInfoModel paymentTerms;
final List<TimelineItemModel> timeline; final List<TimelineItemModel> timeline;
final List<dynamic> payments; final List<PaymentInfoModel> payments;
final List<dynamic> invoices; final List<InvoiceInfoModel> invoices;
/// Create from JSON /// Create from JSON
factory OrderDetailModel.fromJson(Map<String, dynamic> json) { factory OrderDetailModel.fromJson(Map<String, dynamic> json) {
@@ -50,8 +50,14 @@ class OrderDetailModel {
.map((item) => .map((item) =>
TimelineItemModel.fromJson(item as Map<String, dynamic>)) TimelineItemModel.fromJson(item as Map<String, dynamic>))
.toList(), .toList(),
payments: json['payments'] as List<dynamic>? ?? [], payments: (json['payments'] as List<dynamic>? ?? [])
invoices: json['invoices'] 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(), 'items': items.map((item) => item.toJson()).toList(),
'payment_terms': paymentTerms.toJson(), 'payment_terms': paymentTerms.toJson(),
'timeline': timeline.map((item) => item.toJson()).toList(), 'timeline': timeline.map((item) => item.toJson()).toList(),
'payments': payments, 'payments': payments.map((item) => item.toJson()).toList(),
'invoices': invoices, 'invoices': invoices.map((item) => item.toJson()).toList(),
}; };
} }
@@ -78,8 +84,8 @@ class OrderDetailModel {
items: items.map((item) => item.toEntity()).toList(), items: items.map((item) => item.toEntity()).toList(),
paymentTerms: paymentTerms.toEntity(), paymentTerms: paymentTerms.toEntity(),
timeline: timeline.map((item) => item.toEntity()).toList(), timeline: timeline.map((item) => item.toEntity()).toList(),
payments: payments, payments: payments.map((item) => item.toEntity()).toList(),
invoices: invoices, invoices: invoices.map((item) => item.toEntity()).toList(),
); );
} }
@@ -96,8 +102,12 @@ class OrderDetailModel {
timeline: entity.timeline timeline: entity.timeline
.map((item) => TimelineItemModel.fromEntity(item)) .map((item) => TimelineItemModel.fromEntity(item))
.toList(), .toList(),
payments: entity.payments, payments: entity.payments
invoices: entity.invoices, .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

@@ -98,7 +98,8 @@ class OrderRepositoryImpl implements OrderRepository {
required Map<String, dynamic> deliveryAddress, required Map<String, dynamic> deliveryAddress,
required String paymentMethod, required String paymentMethod,
bool needsInvoice = false, bool needsInvoice = false,
bool needsNegotiation = false, bool ignorePricingRule = false,
bool contractRequest = false,
String? notes, String? notes,
}) async { }) async {
try { try {
@@ -107,7 +108,8 @@ class OrderRepositoryImpl implements OrderRepository {
deliveryAddress: deliveryAddress, deliveryAddress: deliveryAddress,
paymentMethod: paymentMethod, paymentMethod: paymentMethod,
needsInvoice: needsInvoice, needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation, ignorePricingRule: ignorePricingRule,
contractRequest: contractRequest,
notes: notes, notes: notes,
); );
} catch (e) { } catch (e) {

View File

@@ -24,8 +24,8 @@ class OrderDetail extends Equatable {
final List<OrderItemDetail> items; final List<OrderItemDetail> items;
final PaymentTermsInfo paymentTerms; final PaymentTermsInfo paymentTerms;
final List<TimelineItem> timeline; final List<TimelineItem> timeline;
final List<dynamic> payments; // Payment entities can be added later final List<PaymentInfo> payments;
final List<dynamic> invoices; // Invoice entities can be added later final List<InvoiceInfo> invoices;
@override @override
List<Object?> get props => [ List<Object?> get props => [
@@ -219,3 +219,35 @@ class TimelineItem extends Equatable {
@override @override
List<Object?> get props => [label, value, status]; 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

@@ -31,7 +31,8 @@ abstract class OrderRepository {
required Map<String, dynamic> deliveryAddress, required Map<String, dynamic> deliveryAddress,
required String paymentMethod, required String paymentMethod,
bool needsInvoice = false, bool needsInvoice = false,
bool needsNegotiation = false, bool ignorePricingRule = false,
bool contractRequest = false,
String? notes, String? notes,
}); });

View File

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

View File

@@ -183,7 +183,7 @@ class OrderSuccessPage extends StatelessWidget {
// Navigate to order details page // Navigate to order details page
context.pushReplacementNamed( context.pushReplacementNamed(
RouteNames.orderDetail, RouteNames.orderDetail,
pathParameters: {'orderId': orderNumber}, pathParameters: {'id': orderNumber},
); );
}, },
icon: const FaIcon(FontAwesomeIcons.eye, size: 18), icon: const FaIcon(FontAwesomeIcons.eye, size: 18),

View File

@@ -31,7 +31,8 @@ Future<Map<String, dynamic>> createOrder(
required Map<String, dynamic> deliveryAddress, required Map<String, dynamic> deliveryAddress,
required String paymentMethod, required String paymentMethod,
bool needsInvoice = false, bool needsInvoice = false,
bool needsNegotiation = false, bool ignorePricingRule = false,
bool contractRequest = false,
String? notes, String? notes,
}) async { }) async {
final repository = await ref.watch(orderRepositoryProvider.future); final repository = await ref.watch(orderRepositoryProvider.future);
@@ -40,7 +41,8 @@ Future<Map<String, dynamic>> createOrder(
deliveryAddress: deliveryAddress, deliveryAddress: deliveryAddress,
paymentMethod: paymentMethod, paymentMethod: paymentMethod,
needsInvoice: needsInvoice, needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation, ignorePricingRule: ignorePricingRule,
contractRequest: contractRequest,
notes: notes, notes: notes,
); );
} }

View File

@@ -83,7 +83,8 @@ final class CreateOrderProvider
Map<String, dynamic> deliveryAddress, Map<String, dynamic> deliveryAddress,
String paymentMethod, String paymentMethod,
bool needsInvoice, bool needsInvoice,
bool needsNegotiation, bool ignorePricingRule,
bool contractRequest,
String? notes, String? notes,
}) })
super.argument, super.argument,
@@ -120,7 +121,8 @@ final class CreateOrderProvider
Map<String, dynamic> deliveryAddress, Map<String, dynamic> deliveryAddress,
String paymentMethod, String paymentMethod,
bool needsInvoice, bool needsInvoice,
bool needsNegotiation, bool ignorePricingRule,
bool contractRequest,
String? notes, String? notes,
}); });
return createOrder( return createOrder(
@@ -129,7 +131,8 @@ final class CreateOrderProvider
deliveryAddress: argument.deliveryAddress, deliveryAddress: argument.deliveryAddress,
paymentMethod: argument.paymentMethod, paymentMethod: argument.paymentMethod,
needsInvoice: argument.needsInvoice, needsInvoice: argument.needsInvoice,
needsNegotiation: argument.needsNegotiation, ignorePricingRule: argument.ignorePricingRule,
contractRequest: argument.contractRequest,
notes: argument.notes, notes: argument.notes,
); );
} }
@@ -145,7 +148,7 @@ final class CreateOrderProvider
} }
} }
String _$createOrderHash() => r'2d13526815e19a2bbef2f2974dad991d8ffcb594'; String _$createOrderHash() => r'622a1d98d53a6696a302cde85842d449a8164fe7';
/// Create Order Provider /// Create Order Provider
/// ///
@@ -160,7 +163,8 @@ final class CreateOrderFamily extends $Family
Map<String, dynamic> deliveryAddress, Map<String, dynamic> deliveryAddress,
String paymentMethod, String paymentMethod,
bool needsInvoice, bool needsInvoice,
bool needsNegotiation, bool ignorePricingRule,
bool contractRequest,
String? notes, String? notes,
}) })
> { > {
@@ -182,7 +186,8 @@ final class CreateOrderFamily extends $Family
required Map<String, dynamic> deliveryAddress, required Map<String, dynamic> deliveryAddress,
required String paymentMethod, required String paymentMethod,
bool needsInvoice = false, bool needsInvoice = false,
bool needsNegotiation = false, bool ignorePricingRule = false,
bool contractRequest = false,
String? notes, String? notes,
}) => CreateOrderProvider._( }) => CreateOrderProvider._(
argument: ( argument: (
@@ -190,7 +195,8 @@ final class CreateOrderFamily extends $Family
deliveryAddress: deliveryAddress, deliveryAddress: deliveryAddress,
paymentMethod: paymentMethod, paymentMethod: paymentMethod,
needsInvoice: needsInvoice, needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation, ignorePricingRule: ignorePricingRule,
contractRequest: contractRequest,
notes: notes, notes: notes,
), ),
from: this, from: this,

View File

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

View File

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

View File

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

View File

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

View File

@@ -156,7 +156,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( 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), duration: const Duration(seconds: 2),
action: SnackBarAction( action: SnackBarAction(
@@ -246,7 +246,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
right: 0, right: 0,
child: StickyActionBar( child: StickyActionBar(
quantity: _quantity, quantity: _quantity,
unit: product.unit ?? '', unit: '',
conversionOfSm: product.conversionOfSm, conversionOfSm: product.conversionOfSm,
uomFromIntroAttributes: product.getIntroAttribute('UOM'), uomFromIntroAttributes: product.getIntroAttribute('UOM'),
onIncrease: _increaseQuantity, onIncrease: _increaseQuantity,

View File

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

View File

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

View File

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

View File

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

View File

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