Compare commits
4 Commits
8ff7b3b505
...
597c6a0e57
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597c6a0e57 | ||
|
|
e0a9b3b9f4 | ||
|
|
b9b6d91a87 | ||
|
|
d4de557662 |
@@ -8,78 +8,6 @@
|
|||||||
<link rel="stylesheet" href="assets/css/style.css">
|
<link rel="stylesheet" href="assets/css/style.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
</head>
|
</head>
|
||||||
<style>
|
|
||||||
.modal-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
animation: slideUp 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from { transform: translateY(20px); opacity: 0; }
|
|
||||||
to { transform: translateY(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
padding: 20px;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #6b7280;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.document-card {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-item.active {
|
|
||||||
background: var(--primary-blue);
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<body>
|
<body>
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -88,15 +16,12 @@
|
|||||||
<i class="fas fa-arrow-left"></i>
|
<i class="fas fa-arrow-left"></i>
|
||||||
</a>
|
</a>
|
||||||
<h1 class="header-title">Lịch sử điểm</h1>
|
<h1 class="header-title">Lịch sử điểm</h1>
|
||||||
<!--<div style="width: 32px;"></div>-->
|
<div style="width: 32px;"></div>
|
||||||
<button class="back-button" onclick="openInfoModal()">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- Filter Section -->
|
<!-- Filter Section -->
|
||||||
<!--<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="d-flex justify-between align-center">
|
<div class="d-flex justify-between align-center">
|
||||||
<h3 class="card-title">Bộ lọc</h3>
|
<h3 class="card-title">Bộ lọc</h3>
|
||||||
<i class="fas fa-filter" style="color: var(--primary-blue);"></i>
|
<i class="fas fa-filter" style="color: var(--primary-blue);"></i>
|
||||||
@@ -104,184 +29,221 @@
|
|||||||
<p class="text-muted" style="font-size: 12px; margin-top: 8px;">
|
<p class="text-muted" style="font-size: 12px; margin-top: 8px;">
|
||||||
Thời gian hiệu lực: 01/01/2023 - 31/12/2023
|
Thời gian hiệu lực: 01/01/2023 - 31/12/2023
|
||||||
</p>
|
</p>
|
||||||
</div>-->
|
</div>
|
||||||
|
|
||||||
<!-- Points History List -->
|
<!-- Points History List -->
|
||||||
<div class="points-history-list">
|
<div class="points-history-list">
|
||||||
<!-- Transaction Item 1 -->
|
<!-- Transaction Card 1 -->
|
||||||
<div class="card mb-3">
|
<div class="transaction-card">
|
||||||
<div class="d-flex justify-between align-start mb-2">
|
<div class="card-header">
|
||||||
<div style="flex: 1;">
|
<span class="transaction-code">GD-00083</span>
|
||||||
<h4 style="color: var(--primary-blue); font-weight: 500; margin-bottom: 4px;">
|
<span class="transaction-date">28/09/2023</span>
|
||||||
Giao dịch mua hàng 00083
|
</div>
|
||||||
</h4>
|
<div class="card-content">
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
<div class="transaction-desc">
|
||||||
Thời gian: 28/09/2023 17:23:18
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">Giao dịch mua hàng</div>
|
||||||
</p>
|
<div style="font-size: 13px; color: #999;">Mã tham chiếu: #HD-2023-00083</div>
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
</div>
|
||||||
Giao dịch: 100.000.000 VND
|
<div class="transaction-points positive">
|
||||||
</p>
|
+3 điểm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="balance-after">Số dư sau: <strong>604 điểm</strong></span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-complaint" onclick="openComplaint(this)">
|
|
||||||
Khiếu nại
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-end align-center" style="margin-top: 12px;">
|
<!-- Transaction Card 2 -->
|
||||||
<div style="text-align: right;">
|
<div class="transaction-card">
|
||||||
<div style="color: var(--success-color); font-weight: 500;">+3</div>
|
<div class="card-header">
|
||||||
<div style="color: var(--primary-blue); font-size: 12px;">Điểm mới: 604</div>
|
<span class="transaction-code">GD-00081</span>
|
||||||
|
<span class="transaction-date">27/09/2023</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="transaction-desc">
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">Giao dịch mua hàng</div>
|
||||||
|
<div style="font-size: 13px; color: #999;">Mã tham chiếu: #HD-2023-00081</div>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-points neutral">
|
||||||
|
0 điểm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="balance-after">Số dư sau: <strong>601 điểm</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Card 3 -->
|
||||||
|
<div class="transaction-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="transaction-code">EXP-2023-001</span>
|
||||||
|
<span class="transaction-date">20/09/2023</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="transaction-desc">
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">Điểm thưởng hết hạn</div>
|
||||||
|
<div style="font-size: 13px; color: #999;">Hết hạn sử dụng</div>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-points negative">
|
||||||
|
-5 điểm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="balance-after">Số dư sau: <strong>601 điểm</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Card 4 -->
|
||||||
|
<div class="transaction-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="transaction-code">RDM-2023-042</span>
|
||||||
|
<span class="transaction-date">19/09/2023</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="transaction-desc">
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">Đổi Voucher giảm giá</div>
|
||||||
|
<div style="font-size: 13px; color: #999;">Voucher 5.000.000đ - HSG</div>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-points negative">
|
||||||
|
-500 điểm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="balance-after">Số dư sau: <strong>606 điểm</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Card 5 -->
|
||||||
|
<div class="transaction-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="transaction-code">REF-2023-128</span>
|
||||||
|
<span class="transaction-date">10/09/2023</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="transaction-desc">
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">Giới thiệu thành công</div>
|
||||||
|
<div style="font-size: 13px; color: #999;">Giới thiệu: Nguyễn Văn A</div>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-points positive">
|
||||||
|
+5 điểm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="balance-after">Số dư sau: <strong>1.106 điểm</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Card 6 -->
|
||||||
|
<div class="transaction-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="transaction-code">RDM-2023-038</span>
|
||||||
|
<span class="transaction-date">05/09/2023</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="transaction-desc">
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">Đổi quà tặng</div>
|
||||||
|
<div style="font-size: 13px; color: #999;">Tai nghe Bluetooth Sony WH-1000XM4</div>
|
||||||
|
</div>
|
||||||
|
<div class="transaction-points negative">
|
||||||
|
-200 điểm
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="balance-after">Số dư sau: <strong>1.101 điểm</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transaction Item 2 -->
|
<style>
|
||||||
<div class="card mb-3">
|
/* Modern Transaction Card Styles */
|
||||||
<div class="d-flex justify-between align-start mb-2">
|
.transaction-card {
|
||||||
<div style="flex: 1;">
|
background: white;
|
||||||
<h4 style="color: var(--primary-blue); font-weight: 500; margin-bottom: 4px;">
|
border-radius: 12px;
|
||||||
Giao dịch mua hàng 00081
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
</h4>
|
margin-bottom: 12px;
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
overflow: hidden;
|
||||||
Thời gian: 27/09/2023 17:23:18
|
transition: all 0.2s;
|
||||||
</p>
|
}
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
|
||||||
Giao dịch: 200.000.000 VND
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn-complaint" onclick="openComplaint(this)">
|
|
||||||
Khiếu nại
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-end align-center" style="margin-top: 12px;">
|
.transaction-card:hover {
|
||||||
<div style="text-align: right;">
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
<div style="color: var(--text-dark); font-weight: 500;">0</div>
|
transform: translateY(-2px);
|
||||||
<div style="color: var(--primary-blue); font-size: 12px;">Điểm mới: 604</div>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Transaction Item 3 -->
|
.transaction-card .card-header {
|
||||||
<div class="card mb-3">
|
display: flex;
|
||||||
<div class="d-flex justify-between align-start mb-2">
|
justify-content: space-between;
|
||||||
<div style="flex: 1;">
|
align-items: center;
|
||||||
<h4 style="color: var(--primary-blue); font-weight: 500; margin-bottom: 4px;">
|
padding: 12px 16px;
|
||||||
Điểm thưởng hết hạn
|
background: #f9fafb;
|
||||||
</h4>
|
border-bottom: 1px solid #e5e7eb;
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
}
|
||||||
Thời gian: 20/09/2023 17:23:18
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn-complaint" onclick="openComplaint(this)">
|
|
||||||
Khiếu nại
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-end align-center" style="margin-top: 12px;">
|
.transaction-code {
|
||||||
<div style="text-align: right;">
|
font-weight: 700;
|
||||||
<div style="color: var(--danger-color); font-weight: 500;">-5</div>
|
font-size: 14px;
|
||||||
<div style="color: var(--primary-blue); font-size: 12px;">Điểm mới: 604</div>
|
color: #005B9A;
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Transaction Item 4 -->
|
.transaction-date {
|
||||||
<div class="card mb-3">
|
font-size: 12px;
|
||||||
<div class="d-flex justify-between align-start mb-2">
|
color: #9ca3af;
|
||||||
<div style="flex: 1;">
|
}
|
||||||
<h4 style="color: var(--primary-blue); font-weight: 500; margin-bottom: 4px;">
|
|
||||||
Đổi Voucher HSG
|
|
||||||
</h4>
|
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
|
||||||
Thời gian: 19/09/2023 17:23:18
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn-complaint" onclick="openComplaint(this)">
|
|
||||||
Khiếu nại
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-end align-center" style="margin-top: 12px;">
|
.transaction-card .card-content {
|
||||||
<div style="text-align: right;">
|
display: flex;
|
||||||
<div style="color: var(--danger-color); font-weight: 500;">-500</div>
|
justify-content: space-between;
|
||||||
<div style="color: var(--primary-blue); font-size: 12px;">Điểm mới: 604</div>
|
align-items: center;
|
||||||
</div>
|
padding: 16px;
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Transaction Item 5 -->
|
.transaction-desc {
|
||||||
<div class="card mb-3">
|
flex: 1;
|
||||||
<div class="d-flex justify-between align-start mb-2">
|
}
|
||||||
<div style="flex: 1;">
|
|
||||||
<h4 style="color: var(--primary-blue); font-weight: 500; margin-bottom: 4px;">
|
|
||||||
Giới thiệu người dùng
|
|
||||||
</h4>
|
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
|
||||||
Thời gian: 10/09/2023 17:23:18
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn-complaint" onclick="openComplaint(this)">
|
|
||||||
Khiếu nại
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-end align-center" style="margin-top: 12px;">
|
.transaction-points {
|
||||||
<div style="text-align: right;">
|
font-size: 24px;
|
||||||
<div style="color: var(--success-color); font-weight: 500;">+5</div>
|
font-weight: 700;
|
||||||
<div style="color: var(--primary-blue); font-size: 12px;">Điểm mới: 604</div>
|
text-align: right;
|
||||||
</div>
|
margin-left: 16px;
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Transaction Item 6 -->
|
.transaction-points.positive {
|
||||||
<div class="card mb-3">
|
color: #059669;
|
||||||
<div class="d-flex justify-between align-start mb-2">
|
}
|
||||||
<div style="flex: 1;">
|
|
||||||
<h4 style="color: var(--primary-blue); font-weight: 500; margin-bottom: 4px;">
|
|
||||||
Đổi quà
|
|
||||||
</h4>
|
|
||||||
<p class="text-muted" style="font-size: 12px;">
|
|
||||||
Thời gian: 19/09/2023 17:23:18
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button class="btn-complaint" onclick="openComplaint(this)">
|
|
||||||
Khiếu nại
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-end align-center" style="margin-top: 12px;">
|
.transaction-points.negative {
|
||||||
<div style="text-align: right;">
|
color: #dc2626;
|
||||||
<div style="color: var(--danger-color); font-weight: 500;">-200</div>
|
}
|
||||||
<div style="color: var(--primary-blue); font-size: 12px;">Điểm mới: 604</div>
|
|
||||||
</div>
|
.transaction-points.neutral {
|
||||||
</div>
|
color: #6b7280;
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
</div>
|
.transaction-card .card-footer {
|
||||||
<!-- Info Modal -->
|
padding: 10px 16px;
|
||||||
<div id="infoModal" class="modal-overlay" style="display: none;">
|
background: #fafafa;
|
||||||
<div class="modal-content info-modal">
|
border-top: 1px solid #f0f0f0;
|
||||||
<div class="modal-header">
|
}
|
||||||
<h3 class="modal-title" style="font-weight: bold;">Hướng dẫn sử dụng</h3>
|
|
||||||
<button class="modal-close" onclick="closeInfoModal()">
|
.balance-after {
|
||||||
<i class="fas fa-times"></i>
|
font-size: 13px;
|
||||||
</button>
|
color: #666;
|
||||||
</div>
|
}
|
||||||
<div class="modal-body">
|
|
||||||
<p>Đây là nội dung hướng dẫn sử dụng cho tính năng Lịch sử điểm:</p>
|
.balance-after strong {
|
||||||
<ul class="list-disc ml-6 mt-3">
|
color: #005B9A;
|
||||||
<li>Đây là sao kê chi tiết tất cả các giao dịch cộng/trừ điểm của bạn.</li>
|
font-weight: 600;
|
||||||
<li>Bạn có thể kiểm tra điểm được cộng từ đơn hàng, từ việc đăng ký công trình, hoặc điểm bị trừ khi đổi quà.</li>
|
}
|
||||||
<li>Nếu phát hiện giao dịch bị sai sót, hãy bấm nút "Khiếu nại" trên dòng giao dịch đó để gửi yêu cầu hỗ trợ.</li>
|
|
||||||
</ul>
|
@media (max-width: 480px) {
|
||||||
</div>
|
.transaction-points {
|
||||||
<div class="modal-footer">
|
font-size: 20px;
|
||||||
<button class="btn btn-primary" onclick="closeInfoModal()">Đóng</button>
|
}
|
||||||
</div>
|
}
|
||||||
</div>
|
</style>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function openComplaint(buttonElement) {
|
function openComplaint(buttonElement) {
|
||||||
@@ -302,21 +264,6 @@
|
|||||||
|
|
||||||
window.location.href = `point-complaint.html?${params.toString()}`;
|
window.location.href = `point-complaint.html?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openInfoModal() {
|
|
||||||
document.getElementById('infoModal').style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeInfoModal() {
|
|
||||||
document.getElementById('infoModal').style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close modal when clicking outside
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (e.target.classList.contains('modal-overlay')) {
|
|
||||||
e.target.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -319,7 +319,124 @@
|
|||||||
color: #0369a1;
|
color: #0369a1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-row {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px dashed #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-row-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-product {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-product:hover {
|
||||||
|
background: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-input-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-input-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-label.required::after {
|
||||||
|
content: " *";
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-subtotal {
|
||||||
|
background: #eff6ff;
|
||||||
|
border: 2px dashed #005B9A;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-subtotal-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-subtotal-value {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #005B9A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-product {
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
border: 2px dashed var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-product:hover {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
background: linear-gradient(135deg, #eff6ff, #dbeafe);
|
||||||
|
border: 2px solid #005B9A;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
|
.product-input-group {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
.content {
|
.content {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -403,44 +520,9 @@
|
|||||||
placeholder="Nhập số hóa đơn (nếu có)"
|
placeholder="Nhập số hóa đơn (nếu có)"
|
||||||
onchange="validateForm()">
|
onchange="validateForm()">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Total Amount -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label required">Tổng giá trị đơn hàng (VNĐ)</label>
|
|
||||||
<input type="number"
|
|
||||||
class="form-input"
|
|
||||||
id="totalAmount"
|
|
||||||
placeholder="0"
|
|
||||||
min="0"
|
|
||||||
step="1000"
|
|
||||||
required
|
|
||||||
oninput="calculatePoints(); validateForm()">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Points Estimate -->
|
|
||||||
<!--<div class="points-estimate" id="pointsEstimate">
|
|
||||||
<div class="estimate-title">Điểm dự kiến nhận được</div>
|
|
||||||
<div class="estimate-text" id="estimateText">0 điểm</div>
|
|
||||||
</div>-->
|
|
||||||
|
|
||||||
<!-- Products Purchased -->
|
|
||||||
<!--<div class="form-group">
|
|
||||||
<label class="form-label">Sản phẩm đã mua</label>
|
|
||||||
<textarea class="form-input form-textarea"
|
|
||||||
id="products"
|
|
||||||
placeholder="Mô tả các sản phẩm đã mua (tùy chọn)"
|
|
||||||
rows="3"></textarea>
|
|
||||||
</div>-->
|
|
||||||
|
|
||||||
<!-- Points Estimate -->
|
|
||||||
<div class="points-estimate" id="pointsEstimate">
|
|
||||||
<div class="estimate-title">Điểm dự kiến nhận được</div>
|
|
||||||
<div class="estimate-text" id="estimateText">0 điểm</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Company Information -->
|
<!-- Company Information -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Tên công ty</label>
|
<label class="form-label">Tên đơn vị mua hàng</label>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
id="companyName"
|
id="companyName"
|
||||||
@@ -454,26 +536,41 @@
|
|||||||
id="taxCode"
|
id="taxCode"
|
||||||
placeholder="Nhập mã số thuế (nếu có)">
|
placeholder="Nhập mã số thuế (nếu có)">
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Dynamic Product List -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Số lượng (m²) đã mua</label>
|
<label class="form-label required">Danh sách sản phẩm</label>
|
||||||
<input type="number"
|
<div id="productsList">
|
||||||
class="form-input"
|
<!-- Product rows will be added here dynamically -->
|
||||||
id="squareMeters"
|
</div>
|
||||||
placeholder="0"
|
<button type="button" onclick="addProductRow()" class="btn-add-product">
|
||||||
min="0"
|
<i class="fas fa-plus-circle"></i>
|
||||||
step="0.01">
|
Thêm sản phẩm
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Products Purchased -->
|
<!-- Order Summary (Auto-calculated) -->
|
||||||
<div class="form-group">
|
<div class="summary-card">
|
||||||
<label class="form-label">Sản phẩm đã mua</label>
|
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">
|
||||||
<textarea class="form-input form-textarea"
|
<i class="fas fa-calculator" style="color: #005B9A; margin-right: 8px;"></i>
|
||||||
id="products"
|
Tổng kết đơn hàng
|
||||||
placeholder="Mô tả các sản phẩm đã mua (tùy chọn)"
|
</h3>
|
||||||
rows="3"></textarea>
|
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; padding-bottom: 12px; border-bottom: 1px dashed #e5e7eb;">
|
||||||
|
<span style="color: #666; font-size: 15px;">Tổng số lượng:</span>
|
||||||
|
<span id="totalSquareMeters" style="color: #333; font-weight: 600; font-size: 15px;">0 m²</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span style="color: #666; font-size: 15px;">Tổng giá trị đơn hàng:</span>
|
||||||
|
<span id="totalAmount" style="color: #005B9A; font-weight: 700; font-size: 18px;">0 đ</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Points Estimate -->
|
||||||
|
<div class="points-estimate" id="pointsEstimate">
|
||||||
|
<div class="estimate-title">Điểm dự kiến nhận được</div>
|
||||||
|
<div class="estimate-text" id="estimateText">0 điểm</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Invoice Images -->
|
<!-- Invoice Images -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -504,13 +601,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Additional Notes -->
|
<!-- Additional Notes -->
|
||||||
<div class="form-group">
|
<!--<div class="form-group">
|
||||||
<label class="form-label">Ghi chú thêm</label>
|
<label class="form-label">Ghi chú thêm</label>
|
||||||
<textarea class="form-input form-textarea"
|
<textarea class="form-input form-textarea"
|
||||||
id="notes"
|
id="notes"
|
||||||
placeholder="Ghi chú thêm về đơn hàng (tùy chọn)"
|
placeholder="Ghi chú thêm về đơn hàng (tùy chọn)"
|
||||||
rows="3"></textarea>
|
rows="3"></textarea>
|
||||||
</div>
|
</div>-->
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Submit Button -->
|
||||||
<button type="submit" class="submit-button" id="submitButton" disabled>
|
<button type="submit" class="submit-button" id="submitButton" disabled>
|
||||||
@@ -523,26 +620,153 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
let selectedFiles = [];
|
let selectedFiles = [];
|
||||||
|
let productCounter = 0;
|
||||||
|
let products = [];
|
||||||
|
|
||||||
// Set max date to today
|
// Set max date to today
|
||||||
document.getElementById('purchaseDate').max = new Date().toISOString().split('T')[0];
|
document.getElementById('purchaseDate').max = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Add first product row on page load
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
addProductRow();
|
||||||
|
});
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addProductRow() {
|
||||||
|
productCounter++;
|
||||||
|
const productsList = document.getElementById('productsList');
|
||||||
|
|
||||||
|
const productRow = document.createElement('div');
|
||||||
|
productRow.className = 'product-row';
|
||||||
|
productRow.id = `product-${productCounter}`;
|
||||||
|
productRow.innerHTML = `
|
||||||
|
<div class="product-row-header">
|
||||||
|
<span class="product-row-title">
|
||||||
|
<i class="fas fa-box"></i> Sản phẩm #${productCounter}
|
||||||
|
</span>
|
||||||
|
<button type="button" onclick="removeProductRow(${productCounter})" class="btn-remove-product">
|
||||||
|
<i class="fas fa-trash"></i> Xóa
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="product-input-full">
|
||||||
|
<label class="product-label required">Tên sản phẩm</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-input"
|
||||||
|
id="productName-${productCounter}"
|
||||||
|
placeholder="Ví dụ: Gạch Granite 60x60"
|
||||||
|
required
|
||||||
|
onchange="updateCalculations()">
|
||||||
|
</div>
|
||||||
|
<div class="product-input-group">
|
||||||
|
<div>
|
||||||
|
<label class="product-label required">Số lượng (m²)</label>
|
||||||
|
<input type="number"
|
||||||
|
class="form-input"
|
||||||
|
id="productQty-${productCounter}"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
required
|
||||||
|
oninput="updateCalculations()">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="product-label required">Đơn giá (VNĐ/m²)</label>
|
||||||
|
<input type="number"
|
||||||
|
class="form-input"
|
||||||
|
id="productPrice-${productCounter}"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
step="1000"
|
||||||
|
required
|
||||||
|
oninput="updateCalculations()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="product-subtotal">
|
||||||
|
<div class="product-subtotal-label">Thành tiền</div>
|
||||||
|
<div class="product-subtotal-value" id="productSubtotal-${productCounter}">0 đ</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
productsList.appendChild(productRow);
|
||||||
|
products.push(productCounter);
|
||||||
|
updateCalculations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProductRow(id) {
|
||||||
|
if (products.length <= 1) {
|
||||||
|
alert('Cần ít nhất một sản phẩm!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = document.getElementById(`product-${id}`);
|
||||||
|
if (row) {
|
||||||
|
row.remove();
|
||||||
|
products = products.filter(p => p !== id);
|
||||||
|
updateCalculations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCalculations() {
|
||||||
|
let totalQty = 0;
|
||||||
|
let totalValue = 0;
|
||||||
|
|
||||||
|
products.forEach(id => {
|
||||||
|
const qtyInput = document.getElementById(`productQty-${id}`);
|
||||||
|
const priceInput = document.getElementById(`productPrice-${id}`);
|
||||||
|
const subtotalEl = document.getElementById(`productSubtotal-${id}`);
|
||||||
|
|
||||||
|
if (qtyInput && priceInput && subtotalEl) {
|
||||||
|
const qty = parseFloat(qtyInput.value) || 0;
|
||||||
|
const price = parseFloat(priceInput.value) || 0;
|
||||||
|
const subtotal = qty * price;
|
||||||
|
|
||||||
|
totalQty += qty;
|
||||||
|
totalValue += subtotal;
|
||||||
|
|
||||||
|
subtotalEl.textContent = formatCurrency(subtotal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update summary
|
||||||
|
document.getElementById('totalSquareMeters').textContent = totalQty.toFixed(2) + ' m²';
|
||||||
|
document.getElementById('totalAmount').textContent = formatCurrency(totalValue);
|
||||||
|
|
||||||
|
// Calculate points
|
||||||
|
calculatePoints(totalValue);
|
||||||
|
validateForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCurrency(value) {
|
||||||
|
return new Intl.NumberFormat('vi-VN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'VND',
|
||||||
|
minimumFractionDigits: 0
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
function validateForm() {
|
function validateForm() {
|
||||||
const purchaseDate = document.getElementById('purchaseDate').value;
|
const purchaseDate = document.getElementById('purchaseDate').value;
|
||||||
const storeLocation = document.getElementById('storeLocation').value;
|
|
||||||
const totalAmount = document.getElementById('totalAmount').value;
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
|
||||||
const isValid = purchaseDate && storeLocation && totalAmount && hasFiles;
|
// Check if at least one product is filled
|
||||||
|
let hasValidProduct = false;
|
||||||
|
products.forEach(id => {
|
||||||
|
const name = document.getElementById(`productName-${id}`).value;
|
||||||
|
const qty = document.getElementById(`productQty-${id}`).value;
|
||||||
|
const price = document.getElementById(`productPrice-${id}`).value;
|
||||||
|
if (name && qty && price) {
|
||||||
|
hasValidProduct = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isValid = purchaseDate && hasValidProduct && hasFiles;
|
||||||
document.getElementById('submitButton').disabled = !isValid;
|
document.getElementById('submitButton').disabled = !isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePoints() {
|
function calculatePoints(totalAmount) {
|
||||||
const totalAmount = document.getElementById('totalAmount').value;
|
|
||||||
const pointsEstimate = document.getElementById('pointsEstimate');
|
const pointsEstimate = document.getElementById('pointsEstimate');
|
||||||
const estimateText = document.getElementById('estimateText');
|
const estimateText = document.getElementById('estimateText');
|
||||||
|
|
||||||
|
|||||||
@@ -632,6 +632,7 @@
|
|||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "DBIZ Partner";
|
INFOPLIST_KEY_CFBundleDisplayName = "DBIZ Partner";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -934,6 +935,7 @@
|
|||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "DBIZ Partner";
|
INFOPLIST_KEY_CFBundleDisplayName = "DBIZ Partner";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -959,6 +961,7 @@
|
|||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "DBIZ Partner";
|
INFOPLIST_KEY_CFBundleDisplayName = "DBIZ Partner";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
|
|||||||
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
|
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
|
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
|
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
|
||||||
|
import 'package:worker/features/loyalty/presentation/pages/points_record_create_page.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/pages/points_records_page.dart';
|
import 'package:worker/features/loyalty/presentation/pages/points_records_page.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
|
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
|
||||||
import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
|
import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
|
||||||
@@ -323,6 +324,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
MaterialPage(key: state.pageKey, child: const PointsRecordsPage()),
|
MaterialPage(key: state.pageKey, child: const PointsRecordsPage()),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Points Record Create Route
|
||||||
|
GoRoute(
|
||||||
|
path: RouteNames.pointsRecordCreate,
|
||||||
|
name: 'loyalty_points_record_create',
|
||||||
|
pageBuilder: (context, state) =>
|
||||||
|
MaterialPage(key: state.pageKey, child: const PointsRecordCreatePage()),
|
||||||
|
),
|
||||||
|
|
||||||
// Orders Route
|
// Orders Route
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.orders,
|
path: RouteNames.orders,
|
||||||
@@ -651,6 +660,7 @@ class RouteNames {
|
|||||||
static const String rewards = '$loyalty/rewards';
|
static const String rewards = '$loyalty/rewards';
|
||||||
static const String pointsHistory = '$loyalty/points-history';
|
static const String pointsHistory = '$loyalty/points-history';
|
||||||
static const String pointsRecords = '$loyalty/points-records';
|
static const String pointsRecords = '$loyalty/points-records';
|
||||||
|
static const String pointsRecordCreate = '$loyalty/points-records/create';
|
||||||
static const String myGifts = '$loyalty/gifts';
|
static const String myGifts = '$loyalty/gifts';
|
||||||
static const String referral = '$loyalty/referral';
|
static const String referral = '$loyalty/referral';
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ class _HomePageState extends ConsumerState<HomePage> {
|
|||||||
promotions: promotions,
|
promotions: promotions,
|
||||||
onPromotionTap: (promotion) {
|
onPromotionTap: (promotion) {
|
||||||
// Navigate to promotion details
|
// Navigate to promotion details
|
||||||
context.push('/promotions/${promotion.id}');
|
context.push('/news/${promotion.id}');
|
||||||
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class BuyerInfoModel {
|
|||||||
final String? wardCode;
|
final String? wardCode;
|
||||||
final String? cityName;
|
final String? cityName;
|
||||||
final String? wardName;
|
final String? wardName;
|
||||||
|
final String? customerName;
|
||||||
|
|
||||||
const BuyerInfoModel({
|
const BuyerInfoModel({
|
||||||
this.name,
|
this.name,
|
||||||
@@ -100,6 +101,7 @@ class BuyerInfoModel {
|
|||||||
this.wardCode,
|
this.wardCode,
|
||||||
this.cityName,
|
this.cityName,
|
||||||
this.wardName,
|
this.wardName,
|
||||||
|
this.customerName,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory BuyerInfoModel.fromJson(Map<String, dynamic> json) {
|
factory BuyerInfoModel.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -115,6 +117,7 @@ class BuyerInfoModel {
|
|||||||
wardCode: json['ward_code'] as String?,
|
wardCode: json['ward_code'] as String?,
|
||||||
cityName: json['city_name'] as String?,
|
cityName: json['city_name'] as String?,
|
||||||
wardName: json['ward_name'] as String?,
|
wardName: json['ward_name'] as String?,
|
||||||
|
customerName: json['customer_name'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +133,7 @@ class BuyerInfoModel {
|
|||||||
'ward_code': wardCode,
|
'ward_code': wardCode,
|
||||||
'city_name': cityName,
|
'city_name': cityName,
|
||||||
'ward_name': wardName,
|
'ward_name': wardName,
|
||||||
|
'customer_name': customerName,
|
||||||
};
|
};
|
||||||
|
|
||||||
BuyerInfo toEntity() => BuyerInfo(
|
BuyerInfo toEntity() => BuyerInfo(
|
||||||
@@ -144,6 +148,7 @@ class BuyerInfoModel {
|
|||||||
wardCode: wardCode,
|
wardCode: wardCode,
|
||||||
cityName: cityName,
|
cityName: cityName,
|
||||||
wardName: wardName,
|
wardName: wardName,
|
||||||
|
customerName: customerName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class BuyerInfo extends Equatable {
|
|||||||
final String? wardCode;
|
final String? wardCode;
|
||||||
final String? cityName;
|
final String? cityName;
|
||||||
final String? wardName;
|
final String? wardName;
|
||||||
|
final String? customerName;
|
||||||
|
|
||||||
const BuyerInfo({
|
const BuyerInfo({
|
||||||
this.name,
|
this.name,
|
||||||
@@ -88,6 +89,7 @@ class BuyerInfo extends Equatable {
|
|||||||
this.wardCode,
|
this.wardCode,
|
||||||
this.cityName,
|
this.cityName,
|
||||||
this.wardName,
|
this.wardName,
|
||||||
|
this.customerName
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Get formatted full address
|
/// Get formatted full address
|
||||||
@@ -118,6 +120,7 @@ class BuyerInfo extends Equatable {
|
|||||||
wardCode,
|
wardCode,
|
||||||
cityName,
|
cityName,
|
||||||
wardName,
|
wardName,
|
||||||
|
customerName
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -294,10 +294,10 @@ class InvoiceDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
List<_InfoLine> _buildBuyerInfoLines(Invoice invoice) {
|
List<_InfoLine> _buildBuyerInfoLines(Invoice invoice) {
|
||||||
return [
|
return [
|
||||||
if (invoice.buyerInfo!.name != null)
|
if (invoice.buyerInfo!.customerName != null)
|
||||||
_InfoLine(label: 'Người mua hàng', value: invoice.buyerInfo!.name!),
|
_InfoLine(label: 'Người mua hàng', value: invoice.buyerInfo!.customerName!),
|
||||||
if (invoice.customerName != null)
|
if (invoice.buyerInfo!.addressTitle != null)
|
||||||
_InfoLine(label: 'Tên đơn vị', value: invoice.customerName!),
|
_InfoLine(label: 'Tên đơn vị', value: invoice.buyerInfo!.addressTitle!),
|
||||||
if (invoice.buyerInfo!.taxCode != null)
|
if (invoice.buyerInfo!.taxCode != null)
|
||||||
_InfoLine(label: 'Mã số thuế', value: invoice.buyerInfo!.taxCode!),
|
_InfoLine(label: 'Mã số thuế', value: invoice.buyerInfo!.taxCode!),
|
||||||
if (invoice.buyerInfo!.fullAddress.isNotEmpty)
|
if (invoice.buyerInfo!.fullAddress.isNotEmpty)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: now.add(const Duration(days: 365)),
|
expiryDate: now.add(const Duration(days: 365)),
|
||||||
timestamp: DateTime(2023, 9, 28, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 28, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_002',
|
entryId: 'entry_002',
|
||||||
@@ -108,7 +108,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: now.add(const Duration(days: 365)),
|
expiryDate: now.add(const Duration(days: 365)),
|
||||||
timestamp: DateTime(2023, 9, 27, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 27, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_003',
|
entryId: 'entry_003',
|
||||||
@@ -123,7 +123,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: null,
|
expiryDate: null,
|
||||||
timestamp: DateTime(2023, 9, 20, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 20, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_004',
|
entryId: 'entry_004',
|
||||||
@@ -138,7 +138,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: null,
|
expiryDate: null,
|
||||||
timestamp: DateTime(2023, 9, 19, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 19, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_005',
|
entryId: 'entry_005',
|
||||||
@@ -153,7 +153,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: now.add(const Duration(days: 365)),
|
expiryDate: now.add(const Duration(days: 365)),
|
||||||
timestamp: DateTime(2023, 9, 10, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 10, 17, 23, 18),
|
||||||
),
|
),
|
||||||
LoyaltyPointEntryModel(
|
LoyaltyPointEntryModel(
|
||||||
entryId: 'entry_006',
|
entryId: 'entry_006',
|
||||||
@@ -168,7 +168,7 @@ class PointsHistoryLocalDataSource {
|
|||||||
complaintStatus: ComplaintStatus.none,
|
complaintStatus: ComplaintStatus.none,
|
||||||
balanceAfter: 604,
|
balanceAfter: 604,
|
||||||
expiryDate: null,
|
expiryDate: null,
|
||||||
timestamp: DateTime(2023, 9, 5, 17, 23, 18),
|
timestamp: DateTime(2025, 9, 5, 17, 23, 18),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-VG-001',
|
invoiceNumber: 'INV-VG-001',
|
||||||
storeName: 'Công ty TNHH Vingroup',
|
storeName: 'Công ty TNHH Vingroup',
|
||||||
transactionDate: DateTime(2023, 11, 15),
|
transactionDate: DateTime(2025, 11, 15),
|
||||||
invoiceAmount: 2500000,
|
invoiceAmount: 2500000,
|
||||||
notes: 'Gạch granite cao cấp cho khu vực lobby và hành lang',
|
notes: 'Gạch granite cao cấp cho khu vực lobby và hành lang',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -39,8 +39,8 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
],
|
],
|
||||||
status: PointsStatus.approved,
|
status: PointsStatus.approved,
|
||||||
pointsEarned: 250,
|
pointsEarned: 250,
|
||||||
submittedAt: DateTime(2023, 11, 15, 10, 0),
|
submittedAt: DateTime(2025, 11, 15, 10, 0),
|
||||||
processedAt: DateTime(2023, 11, 20, 14, 30),
|
processedAt: DateTime(2025, 11, 20, 14, 30),
|
||||||
processedBy: 'admin001',
|
processedBy: 'admin001',
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
@@ -48,21 +48,21 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-BTX-002',
|
invoiceNumber: 'INV-BTX-002',
|
||||||
storeName: 'Tập đoàn Bitexco',
|
storeName: 'Tập đoàn Bitexco',
|
||||||
transactionDate: DateTime(2023, 11, 25),
|
transactionDate: DateTime(2025, 11, 25),
|
||||||
invoiceAmount: 1250000,
|
invoiceAmount: 1250000,
|
||||||
notes: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm',
|
notes: 'Gạch porcelain 80x80 cho sảnh chính và khu mua sắm',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
'https://example.com/invoice3.jpg',
|
'https://example.com/invoice3.jpg',
|
||||||
],
|
],
|
||||||
status: PointsStatus.pending,
|
status: PointsStatus.pending,
|
||||||
submittedAt: DateTime(2023, 11, 25, 9, 15),
|
submittedAt: DateTime(2025, 11, 25, 9, 15),
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
recordId: 'PRR003',
|
recordId: 'PRR003',
|
||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-ABC-003',
|
invoiceNumber: 'INV-ABC-003',
|
||||||
storeName: 'Công ty TNHH ABC Manufacturing',
|
storeName: 'Công ty TNHH ABC Manufacturing',
|
||||||
transactionDate: DateTime(2023, 11, 20),
|
transactionDate: DateTime(2025, 11, 20),
|
||||||
invoiceAmount: 4200000,
|
invoiceAmount: 4200000,
|
||||||
notes: 'Gạch chống trơn cho khu vực sản xuất và kho bãi',
|
notes: 'Gạch chống trơn cho khu vực sản xuất và kho bãi',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -71,8 +71,8 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
],
|
],
|
||||||
status: PointsStatus.rejected,
|
status: PointsStatus.rejected,
|
||||||
rejectReason: 'Hình ảnh minh chứng không hợp lệ',
|
rejectReason: 'Hình ảnh minh chứng không hợp lệ',
|
||||||
submittedAt: DateTime(2023, 11, 20, 11, 0),
|
submittedAt: DateTime(2025, 11, 20, 11, 0),
|
||||||
processedAt: DateTime(2023, 11, 28, 16, 45),
|
processedAt: DateTime(2025, 11, 28, 16, 45),
|
||||||
processedBy: 'admin002',
|
processedBy: 'admin002',
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
@@ -80,7 +80,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-ECO-004',
|
invoiceNumber: 'INV-ECO-004',
|
||||||
storeName: 'Ecopark Group',
|
storeName: 'Ecopark Group',
|
||||||
transactionDate: DateTime(2023, 10, 10),
|
transactionDate: DateTime(2025, 10, 10),
|
||||||
invoiceAmount: 3700000,
|
invoiceAmount: 3700000,
|
||||||
notes: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn',
|
notes: 'Gạch ceramic vân gỗ cho khu vực phòng khách và sân vườn',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -88,8 +88,8 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
],
|
],
|
||||||
status: PointsStatus.approved,
|
status: PointsStatus.approved,
|
||||||
pointsEarned: 370,
|
pointsEarned: 370,
|
||||||
submittedAt: DateTime(2023, 10, 10, 8, 30),
|
submittedAt: DateTime(2025, 10, 10, 8, 30),
|
||||||
processedAt: DateTime(2023, 10, 15, 10, 20),
|
processedAt: DateTime(2025, 10, 15, 10, 20),
|
||||||
processedBy: 'admin001',
|
processedBy: 'admin001',
|
||||||
),
|
),
|
||||||
PointsRecord(
|
PointsRecord(
|
||||||
@@ -97,7 +97,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
userId: 'user123',
|
userId: 'user123',
|
||||||
invoiceNumber: 'INV-DMD-005',
|
invoiceNumber: 'INV-DMD-005',
|
||||||
storeName: 'Diamond Hospitality Group',
|
storeName: 'Diamond Hospitality Group',
|
||||||
transactionDate: DateTime(2023, 12, 1),
|
transactionDate: DateTime(2025, 12, 1),
|
||||||
invoiceAmount: 8600000,
|
invoiceAmount: 8600000,
|
||||||
notes: 'Gạch marble tự nhiên cho lobby và phòng suite',
|
notes: 'Gạch marble tự nhiên cho lobby và phòng suite',
|
||||||
attachments: const [
|
attachments: const [
|
||||||
@@ -106,7 +106,7 @@ class PointsRecordRemoteDataSourceImpl
|
|||||||
'https://example.com/invoice9.jpg',
|
'https://example.com/invoice9.jpg',
|
||||||
],
|
],
|
||||||
status: PointsStatus.pending,
|
status: PointsStatus.pending,
|
||||||
submittedAt: DateTime(2023, 12, 1, 13, 0),
|
submittedAt: DateTime(2025, 12, 1, 13, 0),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,28 +8,29 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/database/models/enums.dart';
|
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/features/loyalty/data/datasources/points_history_local_datasource.dart';
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.dart';
|
import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/providers/points_history_provider.dart';
|
import 'package:worker/features/loyalty/presentation/providers/points_history_provider.dart';
|
||||||
|
|
||||||
/// Points History Page
|
/// Points History Page
|
||||||
///
|
///
|
||||||
/// Features:
|
/// Features:
|
||||||
/// - Filter section with date range
|
/// - Filter section with date range picker
|
||||||
/// - List of transaction cards
|
/// - List of transaction cards with new design
|
||||||
/// - Each card shows: description, date, amount, points change, new balance
|
/// - Each card shows: code, date, description, reference, points change, balance after
|
||||||
/// - Complaint button for each transaction
|
|
||||||
class PointsHistoryPage extends ConsumerWidget {
|
class PointsHistoryPage extends ConsumerWidget {
|
||||||
const PointsHistoryPage({super.key});
|
const PointsHistoryPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
// Use unfiltered data for now (mock data)
|
||||||
final historyAsync = ref.watch(pointsHistoryProvider);
|
final historyAsync = ref.watch(pointsHistoryProvider);
|
||||||
|
final filter = ref.watch(pointsHistoryFilterProvider);
|
||||||
|
//todo: implement filtering logic in provider later
|
||||||
|
//:note: final historyAsync = ref.watch(filteredPointsHistoryProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
@@ -53,26 +54,22 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: historyAsync.when(
|
child: historyAsync.when(
|
||||||
data: (entries) {
|
data: (entries) {
|
||||||
if (entries.isEmpty) {
|
return ListView(
|
||||||
return _buildEmptyState(colorScheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
// Filter Section
|
// Filter Section
|
||||||
_buildFilterSection(colorScheme),
|
_buildFilterSection(context, ref, colorScheme, filter),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Transaction List
|
// Empty state or Transaction List
|
||||||
|
if (entries.isEmpty)
|
||||||
|
_buildEmptyStateInline(colorScheme)
|
||||||
|
else
|
||||||
...entries.map(
|
...entries.map(
|
||||||
(entry) => _buildTransactionCard(context, ref, entry, colorScheme),
|
(entry) => _buildTransactionCard(context, entry, colorScheme),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const CustomLoadingIndicator(),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
@@ -82,14 +79,21 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build filter section
|
/// Build filter section with date pickers
|
||||||
Widget _buildFilterSection(ColorScheme colorScheme) {
|
Widget _buildFilterSection(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
PointsHistoryFilter filter,
|
||||||
|
) {
|
||||||
|
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -104,156 +108,130 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
FaIcon(FontAwesomeIcons.sliders, color: colorScheme.primary, size: 18),
|
FaIcon(FontAwesomeIcons.filter, color: colorScheme.primary, size: 18),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 16),
|
||||||
Text(
|
|
||||||
'Thời gian hiệu lực: 01/01/2023 - 31/12/2023',
|
|
||||||
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build transaction card
|
// Date range row
|
||||||
Widget _buildTransactionCard(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
LoyaltyPointEntryModel entry,
|
|
||||||
ColorScheme colorScheme,
|
|
||||||
) {
|
|
||||||
final dateFormatter = DateFormat('dd/MM/yyyy HH:mm:ss');
|
|
||||||
final currencyFormatter = NumberFormat.currency(
|
|
||||||
locale: 'vi_VN',
|
|
||||||
symbol: 'VND',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get transaction amount if it's a purchase
|
|
||||||
final datasource = ref.read(pointsHistoryLocalDataSourceProvider);
|
|
||||||
final transactionAmount = datasource.getTransactionAmount(
|
|
||||||
entry.description,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 1,
|
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Top row: Description and Complaint button
|
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
|
// Start date
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: _buildDateField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
context: context,
|
||||||
children: [
|
colorScheme: colorScheme,
|
||||||
// Description
|
label: 'Từ ngày',
|
||||||
Text(
|
value: filter.startDate,
|
||||||
entry.description,
|
dateFormatter: dateFormatter,
|
||||||
style: TextStyle(
|
onTap: () async {
|
||||||
fontSize: 15,
|
final date = await showDatePicker(
|
||||||
fontWeight: FontWeight.w500,
|
context: context,
|
||||||
color: colorScheme.primary,
|
initialDate: filter.startDate ?? DateTime.now(),
|
||||||
),
|
firstDate: DateTime(2020),
|
||||||
),
|
lastDate: filter.endDate ?? DateTime.now(),
|
||||||
const SizedBox(height: 4),
|
|
||||||
|
|
||||||
// Timestamp
|
|
||||||
Text(
|
|
||||||
'Thời gian: ${dateFormatter.format(entry.timestamp)}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Transaction amount (if purchase)
|
|
||||||
if (transactionAmount != null) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
'Giao dịch: ${currencyFormatter.format(transactionAmount)}',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
|
|
||||||
// Complaint button
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Chức năng khiếu nại đang phát triển'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (date != null) {
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setStartDate(date);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
side: BorderSide(color: colorScheme.onSurfaceVariant),
|
|
||||||
foregroundColor: colorScheme.onSurface,
|
|
||||||
textStyle: const TextStyle(fontSize: 12),
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text('Khiếu nại'),
|
const SizedBox(width: 12),
|
||||||
|
// End date
|
||||||
|
Expanded(
|
||||||
|
child: _buildDateField(
|
||||||
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Đến ngày',
|
||||||
|
value: filter.endDate,
|
||||||
|
dateFormatter: dateFormatter,
|
||||||
|
onTap: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: filter.endDate ?? DateTime.now(),
|
||||||
|
firstDate: filter.startDate ?? DateTime(2020),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setEndDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Bottom row: Points change and new balance
|
// Quick filter chips
|
||||||
Row(
|
SingleChildScrollView(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
_buildQuickFilterChip(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
context: context,
|
||||||
children: [
|
ref: ref,
|
||||||
// Points change
|
colorScheme: colorScheme,
|
||||||
Text(
|
label: 'Hôm nay',
|
||||||
entry.points > 0 ? '+${entry.points}' : '${entry.points}',
|
onTap: () {
|
||||||
style: TextStyle(
|
final now = DateTime.now();
|
||||||
fontSize: 16,
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
fontWeight: FontWeight.w500,
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(today, today);
|
||||||
color: entry.points > 0
|
},
|
||||||
? AppColors.success
|
|
||||||
: entry.points < 0
|
|
||||||
? AppColors.danger
|
|
||||||
: colorScheme.onSurface,
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: '7 ngày',
|
||||||
|
onTap: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final weekAgo = today.subtract(const Duration(days: 7));
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(weekAgo, today);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
// New balance
|
context: context,
|
||||||
Text(
|
ref: ref,
|
||||||
'Điểm mới: ${entry.balanceAfter}',
|
colorScheme: colorScheme,
|
||||||
style: TextStyle(
|
label: '30 ngày',
|
||||||
fontSize: 12,
|
onTap: () {
|
||||||
color: colorScheme.primary,
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final monthAgo = today.subtract(const Duration(days: 30));
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(monthAgo, today);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Năm nay',
|
||||||
|
onTap: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).setDateRange(
|
||||||
|
DateTime(now.year, 1, 1),
|
||||||
|
DateTime(now.year, 12, 31),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildQuickFilterChip(
|
||||||
|
context: context,
|
||||||
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
label: 'Tất cả',
|
||||||
|
onTap: () {
|
||||||
|
ref.read(pointsHistoryFilterProvider.notifier).clearFilter();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -261,30 +239,257 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build empty state
|
Widget _buildDateField({
|
||||||
Widget _buildEmptyState(ColorScheme colorScheme) {
|
required BuildContext context,
|
||||||
return Center(
|
required ColorScheme colorScheme,
|
||||||
|
required String label,
|
||||||
|
required DateTime? value,
|
||||||
|
required DateFormat dateFormatter,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value != null ? dateFormatter.format(value) : 'Chọn',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: value != null ? colorScheme.onSurface : colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FaIcon(
|
||||||
|
FontAwesomeIcons.calendar,
|
||||||
|
size: 14,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildQuickFilterChip({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetRef ref,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build transaction card with new design
|
||||||
|
Widget _buildTransactionCard(
|
||||||
|
BuildContext context,
|
||||||
|
LoyaltyPointEntryModel entry,
|
||||||
|
ColorScheme colorScheme,
|
||||||
|
) {
|
||||||
|
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||||
|
|
||||||
|
// Determine points color
|
||||||
|
Color pointsColor;
|
||||||
|
String pointsPrefix;
|
||||||
|
if (entry.points > 0) {
|
||||||
|
pointsColor = AppColors.success;
|
||||||
|
pointsPrefix = '+';
|
||||||
|
} else if (entry.points < 0) {
|
||||||
|
pointsColor = AppColors.danger;
|
||||||
|
pointsPrefix = '';
|
||||||
|
} else {
|
||||||
|
pointsColor = colorScheme.onSurfaceVariant;
|
||||||
|
pointsPrefix = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 1,
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header: Code and Date
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
entry.entryId,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
dateFormatter.format(entry.timestamp),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Content: Description and Points
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Description
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
entry.description,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Mã tham chiếu: #${entry.entryId}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
// Points
|
||||||
|
Text(
|
||||||
|
'$pointsPrefix${entry.points} điểm',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: pointsColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Footer: Balance After
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerLowest,
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(
|
||||||
|
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Số dư sau: ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${entry.balanceAfter} điểm',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build inline empty state (inside scrollable list)
|
||||||
|
Widget _buildEmptyStateInline(ColorScheme colorScheme) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 60),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.clockRotateLeft,
|
FontAwesomeIcons.clockRotateLeft,
|
||||||
size: 80,
|
size: 64,
|
||||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Chưa có lịch sử điểm',
|
'Không có giao dịch',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Kéo xuống để làm mới',
|
'Không tìm thấy giao dịch trong khoảng thời gian này',
|
||||||
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -293,7 +498,6 @@ class PointsHistoryPage extends ConsumerWidget {
|
|||||||
|
|
||||||
/// Build error state
|
/// Build error state
|
||||||
Widget _buildErrorState(Object error, ColorScheme colorScheme) {
|
Widget _buildErrorState(Object error, ColorScheme colorScheme) {
|
||||||
print(error.toString());
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
import 'package:worker/features/loyalty/domain/entities/points_record.dart';
|
||||||
import 'package:worker/features/loyalty/presentation/providers/points_records_provider.dart';
|
import 'package:worker/features/loyalty/presentation/providers/points_records_provider.dart';
|
||||||
|
|
||||||
@@ -47,13 +48,12 @@ class PointsRecordsPage extends ConsumerWidget {
|
|||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
// TODO: Navigate to points record create page
|
final result = await context.push<bool>(RouteNames.pointsRecordCreate);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (result == true) {
|
||||||
const SnackBar(
|
// Refresh list after successful creation
|
||||||
content: Text('Tính năng tạo ghi nhận điểm sẽ được cập nhật'),
|
ref.invalidate(allPointsRecordsProvider);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
|
|||||||
@@ -9,6 +9,59 @@ import 'package:worker/features/loyalty/data/models/loyalty_point_entry_model.da
|
|||||||
|
|
||||||
part 'points_history_provider.g.dart';
|
part 'points_history_provider.g.dart';
|
||||||
|
|
||||||
|
/// Points History Filter State
|
||||||
|
class PointsHistoryFilter {
|
||||||
|
const PointsHistoryFilter({
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime? startDate;
|
||||||
|
final DateTime? endDate;
|
||||||
|
|
||||||
|
PointsHistoryFilter copyWith({
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
bool clearStartDate = false,
|
||||||
|
bool clearEndDate = false,
|
||||||
|
}) {
|
||||||
|
return PointsHistoryFilter(
|
||||||
|
startDate: clearStartDate ? null : (startDate ?? this.startDate),
|
||||||
|
endDate: clearEndDate ? null : (endDate ?? this.endDate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Points History Filter Provider
|
||||||
|
@riverpod
|
||||||
|
class PointsHistoryFilterNotifier extends _$PointsHistoryFilterNotifier {
|
||||||
|
@override
|
||||||
|
PointsHistoryFilter build() {
|
||||||
|
// Default: current year
|
||||||
|
final now = DateTime.now();
|
||||||
|
return PointsHistoryFilter(
|
||||||
|
startDate: DateTime(now.year, 1, 1),
|
||||||
|
endDate: DateTime(now.year, 12, 31),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDateRange(DateTime? start, DateTime? end) {
|
||||||
|
state = PointsHistoryFilter(startDate: start, endDate: end);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStartDate(DateTime? date) {
|
||||||
|
state = state.copyWith(startDate: date);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEndDate(DateTime? date) {
|
||||||
|
state = state.copyWith(endDate: date);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearFilter() {
|
||||||
|
state = const PointsHistoryFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Points History Local Data Source Provider
|
/// Points History Local Data Source Provider
|
||||||
@riverpod
|
@riverpod
|
||||||
PointsHistoryLocalDataSource pointsHistoryLocalDataSource(Ref ref) {
|
PointsHistoryLocalDataSource pointsHistoryLocalDataSource(Ref ref) {
|
||||||
@@ -44,3 +97,41 @@ class PointsHistory extends _$PointsHistory {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
@riverpod
|
||||||
|
Future<List<LoyaltyPointEntryModel>> filteredPointsHistory(Ref ref) async {
|
||||||
|
final entries = await ref.watch(pointsHistoryProvider.future);
|
||||||
|
final filter = ref.watch(pointsHistoryFilterProvider);
|
||||||
|
|
||||||
|
return entries.where((entry) {
|
||||||
|
// Filter by start date
|
||||||
|
if (filter.startDate != null) {
|
||||||
|
final startOfDay = DateTime(
|
||||||
|
filter.startDate!.year,
|
||||||
|
filter.startDate!.month,
|
||||||
|
filter.startDate!.day,
|
||||||
|
);
|
||||||
|
if (entry.timestamp.isBefore(startOfDay)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by end date
|
||||||
|
if (filter.endDate != null) {
|
||||||
|
final endOfDay = DateTime(
|
||||||
|
filter.endDate!.year,
|
||||||
|
filter.endDate!.month,
|
||||||
|
filter.endDate!.day,
|
||||||
|
23,
|
||||||
|
59,
|
||||||
|
59,
|
||||||
|
);
|
||||||
|
if (entry.timestamp.isAfter(endOfDay)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,68 @@ part of 'points_history_provider.dart';
|
|||||||
|
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Points History Filter Provider
|
||||||
|
|
||||||
|
@ProviderFor(PointsHistoryFilterNotifier)
|
||||||
|
const pointsHistoryFilterProvider = PointsHistoryFilterNotifierProvider._();
|
||||||
|
|
||||||
|
/// Points History Filter Provider
|
||||||
|
final class PointsHistoryFilterNotifierProvider
|
||||||
|
extends
|
||||||
|
$NotifierProvider<PointsHistoryFilterNotifier, PointsHistoryFilter> {
|
||||||
|
/// Points History Filter Provider
|
||||||
|
const PointsHistoryFilterNotifierProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'pointsHistoryFilterProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$pointsHistoryFilterNotifierHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
PointsHistoryFilterNotifier create() => PointsHistoryFilterNotifier();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(PointsHistoryFilter value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<PointsHistoryFilter>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$pointsHistoryFilterNotifierHash() =>
|
||||||
|
r'ef2587f4461c9488d9b15ed033e1d362042795f8';
|
||||||
|
|
||||||
|
/// Points History Filter Provider
|
||||||
|
|
||||||
|
abstract class _$PointsHistoryFilterNotifier
|
||||||
|
extends $Notifier<PointsHistoryFilter> {
|
||||||
|
PointsHistoryFilter build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final created = build();
|
||||||
|
final ref = this.ref as $Ref<PointsHistoryFilter, PointsHistoryFilter>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<PointsHistoryFilter, PointsHistoryFilter>,
|
||||||
|
PointsHistoryFilter,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleValue(ref, created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Points History Local Data Source Provider
|
/// Points History Local Data Source Provider
|
||||||
|
|
||||||
@ProviderFor(pointsHistoryLocalDataSource)
|
@ProviderFor(pointsHistoryLocalDataSource)
|
||||||
@@ -130,3 +192,50 @@ abstract class _$PointsHistory
|
|||||||
element.handleValue(ref, created);
|
element.handleValue(ref, created);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
|
||||||
|
@ProviderFor(filteredPointsHistory)
|
||||||
|
const filteredPointsHistoryProvider = FilteredPointsHistoryProvider._();
|
||||||
|
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
|
||||||
|
final class FilteredPointsHistoryProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<List<LoyaltyPointEntryModel>>,
|
||||||
|
List<LoyaltyPointEntryModel>,
|
||||||
|
FutureOr<List<LoyaltyPointEntryModel>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<List<LoyaltyPointEntryModel>>,
|
||||||
|
$FutureProvider<List<LoyaltyPointEntryModel>> {
|
||||||
|
/// Filtered Points History Provider
|
||||||
|
const FilteredPointsHistoryProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'filteredPointsHistoryProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$filteredPointsHistoryHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$FutureProviderElement<List<LoyaltyPointEntryModel>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $FutureProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<List<LoyaltyPointEntryModel>> create(Ref ref) {
|
||||||
|
return filteredPointsHistory(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$filteredPointsHistoryHash() =>
|
||||||
|
r'989e2bf824eeb161b44b67d9ee81b713444a6e87';
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/features/products/domain/entities/product.dart';
|
import 'package:worker/features/products/domain/entities/product.dart';
|
||||||
@@ -273,17 +274,14 @@ class ProductCard extends ConsumerWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 36.0,
|
height: 36.0,
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
// TODO: Open 360 view in browser
|
final url = Uri.parse(product.customLink360!);
|
||||||
// For now, show a message
|
if (await canLaunchUrl(url)) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await launchUrl(
|
||||||
const SnackBar(
|
url,
|
||||||
content: Text(
|
mode: LaunchMode.inAppWebView,
|
||||||
'Đang phát triển tính năng xem 360°',
|
|
||||||
),
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: colorScheme.primary,
|
foregroundColor: colorScheme.primary,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/features/products/domain/entities/product.dart';
|
import 'package:worker/features/products/domain/entities/product.dart';
|
||||||
|
|
||||||
@@ -18,7 +19,6 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
|||||||
/// - Image indicators (dots)
|
/// - Image indicators (dots)
|
||||||
/// - Thumbnail gallery row (horizontal scroll)
|
/// - Thumbnail gallery row (horizontal scroll)
|
||||||
class ImageGallerySection extends StatefulWidget {
|
class ImageGallerySection extends StatefulWidget {
|
||||||
|
|
||||||
const ImageGallerySection({super.key, required this.product});
|
const ImageGallerySection({super.key, required this.product});
|
||||||
final Product product;
|
final Product product;
|
||||||
|
|
||||||
@@ -40,22 +40,15 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_currentImageIndex = index;
|
_currentImageIndex = index;
|
||||||
});
|
});
|
||||||
_pageController.animateToPage(
|
_pageController.animateToPage(index, duration: AppDuration.medium, curve: Curves.easeInOut);
|
||||||
index,
|
|
||||||
duration: AppDuration.medium,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _open360View() {
|
void _open360View() async {
|
||||||
if (widget.product.customLink360 != null) {
|
if (widget.product.customLink360 != null) {
|
||||||
// TODO: Open in browser
|
final url = Uri.parse(widget.product.customLink360!);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (await canLaunchUrl(url)) {
|
||||||
const SnackBar(
|
await launchUrl(url, mode: LaunchMode.inAppWebView);
|
||||||
content: Text('Đang phát triển tính năng xem 360°'),
|
}
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,11 +101,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
),
|
),
|
||||||
errorWidget: (context, url, error) => Container(
|
errorWidget: (context, url, error) => Container(
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
child: Icon(
|
child: Icon(FontAwesomeIcons.image, size: 64, color: colorScheme.onSurfaceVariant),
|
||||||
FontAwesomeIcons.image,
|
|
||||||
size: 64,
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -131,10 +120,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
onTap: _open360View,
|
onTap: _open360View,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
horizontal: 12,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -144,11 +130,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
builder: (context, double value, child) {
|
builder: (context, double value, child) {
|
||||||
return Transform.rotate(
|
return Transform.rotate(
|
||||||
angle: value * 2 * 3.14159,
|
angle: value * 2 * 3.14159,
|
||||||
child: Icon(
|
child: Icon(FontAwesomeIcons.arrowsRotate, size: 10, color: colorScheme.surface),
|
||||||
FontAwesomeIcons.arrowsRotate,
|
|
||||||
size: 10,
|
|
||||||
color: colorScheme.surface,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onEnd: () {
|
onEnd: () {
|
||||||
@@ -159,11 +141,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'360°',
|
'360°',
|
||||||
style: TextStyle(
|
style: TextStyle(color: colorScheme.surface, fontSize: 12, fontWeight: FontWeight.w600),
|
||||||
color: colorScheme.surface,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -245,9 +223,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isActive
|
color: isActive ? colorScheme.primary : Colors.transparent,
|
||||||
? colorScheme.primary
|
|
||||||
: Colors.transparent,
|
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -256,15 +232,10 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, url) =>
|
placeholder: (context, url) => Container(color: colorScheme.surfaceContainerHighest),
|
||||||
Container(color: colorScheme.surfaceContainerHighest),
|
|
||||||
errorWidget: (context, url, error) => Container(
|
errorWidget: (context, url, error) => Container(
|
||||||
color: colorScheme.surfaceContainerHighest,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
child: Icon(
|
child: Icon(FontAwesomeIcons.image, size: 20, color: colorScheme.onSurfaceVariant),
|
||||||
FontAwesomeIcons.image,
|
|
||||||
size: 20,
|
|
||||||
color: colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -285,12 +256,7 @@ class _ImageGallerySectionState extends State<ImageGallerySection> {
|
|||||||
|
|
||||||
/// Image Lightbox for full-screen image viewing
|
/// Image Lightbox for full-screen image viewing
|
||||||
class _ImageLightbox extends StatefulWidget {
|
class _ImageLightbox extends StatefulWidget {
|
||||||
|
const _ImageLightbox({required this.images, required this.imageCaptions, required this.initialIndex});
|
||||||
const _ImageLightbox({
|
|
||||||
required this.images,
|
|
||||||
required this.imageCaptions,
|
|
||||||
required this.initialIndex,
|
|
||||||
});
|
|
||||||
final List<String> images;
|
final List<String> images;
|
||||||
final Map<String, String> imageCaptions;
|
final Map<String, String> imageCaptions;
|
||||||
final int initialIndex;
|
final int initialIndex;
|
||||||
@@ -318,19 +284,13 @@ class _ImageLightboxState extends State<_ImageLightbox> {
|
|||||||
|
|
||||||
void _nextImage() {
|
void _nextImage() {
|
||||||
if (_currentIndex < widget.images.length - 1) {
|
if (_currentIndex < widget.images.length - 1) {
|
||||||
_pageController.nextPage(
|
_pageController.nextPage(duration: AppDuration.medium, curve: Curves.easeInOut);
|
||||||
duration: AppDuration.medium,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _previousImage() {
|
void _previousImage() {
|
||||||
if (_currentIndex > 0) {
|
if (_currentIndex > 0) {
|
||||||
_pageController.previousPage(
|
_pageController.previousPage(duration: AppDuration.medium, curve: Curves.easeInOut);
|
||||||
duration: AppDuration.medium,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,11 +332,8 @@ class _ImageLightboxState extends State<_ImageLightbox> {
|
|||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: widget.images[index],
|
imageUrl: widget.images[index],
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorWidget: (context, url, error) => Icon(
|
errorWidget: (context, url, error) =>
|
||||||
FontAwesomeIcons.circleExclamation,
|
Icon(FontAwesomeIcons.circleExclamation, color: colorScheme.surface, size: 64),
|
||||||
color: colorScheme.surface,
|
|
||||||
size: 64,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -393,15 +350,9 @@ class _ImageLightboxState extends State<_ImageLightbox> {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(FontAwesomeIcons.chevronLeft, color: colorScheme.surface, size: 32),
|
||||||
FontAwesomeIcons.chevronLeft,
|
|
||||||
color: colorScheme.surface,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
onPressed: _previousImage,
|
onPressed: _previousImage,
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(backgroundColor: Colors.white.withAlpha(51)),
|
||||||
backgroundColor: Colors.white.withAlpha(51),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -414,15 +365,9 @@ class _ImageLightboxState extends State<_ImageLightbox> {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(FontAwesomeIcons.chevronRight, color: colorScheme.surface, size: 32),
|
||||||
FontAwesomeIcons.chevronRight,
|
|
||||||
color: colorScheme.surface,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
onPressed: _nextImage,
|
onPressed: _nextImage,
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(backgroundColor: Colors.white.withAlpha(51)),
|
||||||
backgroundColor: Colors.white.withAlpha(51),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -53,10 +53,14 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
bool _isSubmitting = false;
|
bool _isSubmitting = false;
|
||||||
bool _isLoadingDetail = false;
|
bool _isLoadingDetail = false;
|
||||||
String? _deletingFileId; // Track which file is being deleted
|
String? _deletingFileId; // Track which file is being deleted
|
||||||
|
bool _isAllowModify = true; // From API detail response
|
||||||
|
|
||||||
/// Whether we're editing an existing submission
|
/// Whether we're editing an existing submission
|
||||||
bool get isEditing => widget.submission != null;
|
bool get isEditing => widget.submission != null;
|
||||||
|
|
||||||
|
/// Whether the form is read-only (editing but not allowed to modify)
|
||||||
|
bool get isReadOnly => isEditing && !_isAllowModify;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -101,6 +105,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
|
|
||||||
// Set existing files from API
|
// Set existing files from API
|
||||||
_existingFiles = detail.filesList;
|
_existingFiles = detail.filesList;
|
||||||
|
|
||||||
|
// Set modify permission from API
|
||||||
|
_isAllowModify = detail.isAllowModify;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -146,7 +153,11 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
isEditing ? 'Chỉnh sửa Dự án' : 'Đăng ký Công trình',
|
isReadOnly
|
||||||
|
? 'Chi tiết Dự án'
|
||||||
|
: isEditing
|
||||||
|
? 'Chỉnh sửa Dự án'
|
||||||
|
: 'Đăng ký Công trình',
|
||||||
style: TextStyle(color: colorScheme.onSurface),
|
style: TextStyle(color: colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -197,10 +208,12 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
|
|
||||||
// File Upload
|
// File Upload
|
||||||
_buildFileUploadCard(colorScheme),
|
_buildFileUploadCard(colorScheme),
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Submit Button
|
// Submit Button - only show when not read-only
|
||||||
|
if (!isReadOnly) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
_buildSubmitButton(colorScheme),
|
_buildSubmitButton(colorScheme),
|
||||||
|
],
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -376,7 +389,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Upload Area
|
// Upload Area - only show when not read-only
|
||||||
|
if (!isReadOnly)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _pickFiles,
|
onTap: _pickFiles,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -488,7 +502,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
int maxLines = 1,
|
int maxLines = 1,
|
||||||
TextInputType? keyboardType,
|
TextInputType? keyboardType,
|
||||||
String? helperText,
|
String? helperText,
|
||||||
|
bool? readOnly,
|
||||||
}) {
|
}) {
|
||||||
|
final isFieldReadOnly = readOnly ?? isReadOnly;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -517,11 +533,12 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
|
readOnly: isFieldReadOnly,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: colorScheme.surface,
|
fillColor: isFieldReadOnly ? colorScheme.surfaceContainerHighest : colorScheme.surface,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest),
|
||||||
@@ -594,10 +611,10 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
progressListAsync.when(
|
progressListAsync.when(
|
||||||
data: (progressList) => DropdownButtonFormField<ProjectProgress>(
|
data: (progressList) => DropdownButtonFormField<ProjectProgress>(
|
||||||
initialValue: _selectedProgress,
|
value: _selectedProgress,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: colorScheme.surface,
|
fillColor: isReadOnly ? colorScheme.surfaceContainerHighest : colorScheme.surface,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest),
|
||||||
@@ -618,7 +635,9 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
child: Text(progress.status),
|
child: Text(progress.status),
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: isReadOnly
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedProgress = value;
|
_selectedProgress = value;
|
||||||
});
|
});
|
||||||
@@ -692,11 +711,11 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: _pickExpectedDate,
|
onTap: isReadOnly ? null : _pickExpectedDate,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: colorScheme.surface,
|
color: isReadOnly ? colorScheme.surfaceContainerHighest : colorScheme.surface,
|
||||||
border: Border.all(color: colorScheme.surfaceContainerHighest),
|
border: Border.all(color: colorScheme.surfaceContainerHighest),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
@@ -847,8 +866,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Only show remove button when not uploading
|
// Only show remove button when not uploading and not read-only
|
||||||
if (!_isSubmitting)
|
if (!_isSubmitting && !isReadOnly)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(
|
icon: const FaIcon(
|
||||||
FontAwesomeIcons.xmark,
|
FontAwesomeIcons.xmark,
|
||||||
@@ -961,8 +980,8 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Delete button or checkmark
|
// Delete button or checkmark - only show delete when not read-only
|
||||||
if (!_isSubmitting && !isDeleting)
|
if (!_isSubmitting && !isDeleting && !isReadOnly)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(
|
icon: const FaIcon(
|
||||||
FontAwesomeIcons.trash,
|
FontAwesomeIcons.trash,
|
||||||
@@ -1147,13 +1166,13 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
height: 24,
|
height: 24,
|
||||||
child: CustomLoadingIndicator(color: Colors.white, size: 20),
|
child: CustomLoadingIndicator(color: Colors.white, size: 20),
|
||||||
)
|
)
|
||||||
: const Row(
|
: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaIcon(FontAwesomeIcons.paperPlane, size: 16),
|
const FaIcon(FontAwesomeIcons.paperPlane, size: 16),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'Gửi đăng ký',
|
isEditing ? 'Cập nhật' : 'Gửi đăng ký',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -1166,10 +1185,19 @@ class _SubmissionCreatePageState extends ConsumerState<SubmissionCreatePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickExpectedDate() async {
|
Future<void> _pickExpectedDate() async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
|
||||||
|
// For editing mode with past date, allow selecting from that date
|
||||||
|
// Otherwise, start from today
|
||||||
|
final firstDate = (_expectedStartDate != null && _expectedStartDate!.isBefore(today))
|
||||||
|
? _expectedStartDate!
|
||||||
|
: today;
|
||||||
|
|
||||||
final date = await showDatePicker(
|
final date = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: _expectedStartDate ?? DateTime.now(),
|
initialDate: _expectedStartDate ?? today,
|
||||||
firstDate: DateTime.now(),
|
firstDate: firstDate,
|
||||||
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
|
lastDate: DateTime.now().add(const Duration(days: 365 * 3)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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.1+23
|
version: 1.0.1+26
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.0
|
sdk: ^3.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user