add search fav

This commit is contained in:
Phuoc Nguyen
2025-11-19 10:50:21 +07:00
parent fc4711a18e
commit 03a7b7940a
4 changed files with 530 additions and 136 deletions

View File

@@ -26,3 +26,12 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
"ward_code": "32248",
"is_default": false
}'
#delete address
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.address.delete' \
--header 'Cookie: sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; full_name=phuoc; sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
--header 'X-Frappe-Csrf-Token: a22fa53eeaa923f71f2fd879d2863a0985a6f2107f5f7f66d34cd62d' \
--header 'Content-Type: application/json' \
--data '{
"name": "Công ty Tiến Nguyễn-Billing"
}'

View File

@@ -4,155 +4,394 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Thông tin cá nhân - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<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">
<script src="https://cdn.tailwindcss.com/3.4.1"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<style>
body {
background-color: #f7fafc;
}
.form-input, .form-select {
border: 1px solid #e2e8f0;
transition: border-color 0.2s ease-in-out;
}
.form-input:focus, .form-select:focus {
border-color: #3b82f6;
outline: none;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.readonly-input {
background-color: #f1f5f9;
cursor: not-allowed;
color: #64748b;
}
.nav-item.active {
color: #3b82f6;
}
.header {
position: sticky;
top: 0;
z-index: 50;
background-color: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
@keyframes slideDown {
from { opacity: 0; transform: translate(-50%, -20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes slideUp {
from { opacity: 1; transform: translate(-50%, 0); }
to { opacity: 0; transform: translate(-50%, -20px); }
}
.upload-card.has-file {
border-color: #22c55e;
background-color: #f0fdf4;
}
.upload-card.readonly {
cursor: not-allowed;
opacity: 0.7;
}
</style>
</head>
<body>
<body class="text-gray-800">
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="account.html" class="back-button">
<i class="fas fa-arrow-left"></i>
<div class="header flex items-center justify-between p-4 border-b border-gray-200">
<a href="account.html" class="text-gray-600">
<i class="fas fa-arrow-left text-xl"></i>
</a>
<h1 class="header-title">Thông tin cá nhân</h1>
<div style="width: 32px;"></div>
<h1 class="text-lg font-bold" style="margin-right: 97px;">Thông tin cá nhân</h1>
</div>
<div class="container">
<div class="form-container">
<div class="card">
<!-- Profile Picture -->
<div class="profile-avatar-section">
<div class="profile-avatar">
<img src="https://placehold.co/100x100/005B9A/FFFFFF/png?text=HMH" alt="Avatar" id="avatarImage">
<button class="avatar-edit-btn" onclick="changeAvatar()">
<i class="fas fa-camera"></i>
</button>
</div>
<input type="file" id="avatarInput" style="display: none;" accept="image/*">
<div class="container p-4 pb-24">
<form id="profileForm" onsubmit="handleSubmit(event)">
<!-- Avatar Section -->
<div class="bg-white rounded-xl shadow-sm p-6 flex flex-col items-center">
<div class="relative">
<img src="https://ui-avatars.com/api/?name=Nguyen+Van+A&background=3b82f6&color=fff&size=128"
alt="Avatar"
id="avatarImage"
class="w-24 h-24 rounded-full border-4 border-white shadow-lg">
<label for="avatarInput" class="absolute -bottom-2 -right-2 w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white cursor-pointer shadow">
<i class="fas fa-camera text-sm"></i>
</label>
<input type="file" id="avatarInput" accept="image/*" class="hidden" onchange="handleAvatarChange(this)">
</div>
<h2 id="fullNameDisplay" class="text-xl font-bold mt-4">Nguyễn Văn A</h2>
<p class="text-gray-500">Thầu thợ</p>
<div id="accountStatusCard" class="mt-4">
<!-- Dynamic content based on status -->
</div>
</div>
<!-- Verification Form (Hidden by default) -->
<div id="verificationFormContainer" class="bg-white rounded-xl shadow-sm mt-4 p-5" style="display: none;">
<div class="flex items-center gap-3 border-b pb-3 mb-4">
<i class="fas fa-file-check text-blue-500"></i>
<h3 class="font-bold text-base">Thông tin xác thực</h3>
</div>
<form id="profileForm">
<!-- Full Name -->
<div class="form-group">
<label class="form-label">Họ và tên *</label>
<input type="text" class="form-input" value="Hoàng Minh Hiệp" required>
</div>
<div class="info-note bg-blue-50 border-l-4 border-blue-400 text-blue-700 p-4 rounded-md mb-4 text-sm">
<i class="fas fa-info-circle mr-2"></i>
<strong>Lưu ý:</strong> Vui lòng cung cấp ảnh chụp rõ ràng các giấy tờ xác thực để được phê duyệt nhanh chóng.
</div>
<!-- Phone -->
<div class="space-y-4">
<div class="form-group">
<label class="form-label">Số điện thoại *</label>
<input type="tel" class="form-input" value="0347302911" required>
<label class="form-label font-semibold text-sm mb-2 block">Ảnh mặt trước CCCD/CMND <span class="text-red-500">*</span></label>
<div id="idCardUploadCard" class="upload-card border-2 border-dashed rounded-lg p-6 text-center cursor-pointer bg-gray-50 hover:bg-gray-100" onclick="handleUploadClick('idCardInput')">
<div id="idCardPreview" class="upload-content text-gray-500">
<i class="fas fa-camera text-2xl"></i>
<span class="mt-2 text-sm font-semibold">Chụp ảnh hoặc chọn file</span>
<span class="text-xs">JPG, PNG tối đa 5MB</span>
</div>
</div>
<input type="file" id="idCardInput" accept="image/*" class="hidden" onchange="handleVerificationFileUpload(this, 'idCardPreview')">
</div>
<!-- Email -->
<div class="form-group">
<label class="form-label">Email</label>
<input type="email" class="form-input" value="hoanghiep@example.com">
<label class="form-label font-semibold text-sm mb-2 block">Ảnh chứng chỉ hành nghề hoặc GPKD <span class="text-red-500">*</span></label>
<div id="certificateUploadCard" class="upload-card border-2 border-dashed rounded-lg p-6 text-center cursor-pointer bg-gray-50 hover:bg-gray-100" onclick="handleUploadClick('certificateInput')">
<div id="certificatePreview" class="upload-content text-gray-500">
<i class="fas fa-file-certificate text-2xl"></i>
<span class="mt-2 text-sm font-semibold">Chụp ảnh hoặc chọn file</span>
<span class="text-xs">JPG, PNG tối đa 5MB</span>
</div>
</div>
<input type="file" id="certificateInput" accept="image/*" class="hidden" onchange="handleVerificationFileUpload(this, 'certificatePreview')">
</div>
</div>
<!-- Birth Date -->
<div class="form-group">
<label class="form-label">Ngày sinh</label>
<input type="date" class="form-input" value="1985-03-15">
<div class="grid grid-cols-2 gap-3 mt-6" id="verificationSubmitBtn" style="display: none;">
<button type="button" class="w-full bg-gray-200 text-gray-700 font-bold py-2.5 px-4 rounded-lg" onclick="cancelVerification()">Hủy</button>
<button type="button" class="w-full bg-blue-500 text-white font-bold py-2.5 px-4 rounded-lg" onclick="submitVerification()">Gửi xác thực</button>
</div>
</div>
<!-- Combined Personal Information Section -->
<div class="bg-white rounded-xl shadow-sm mt-4 p-5">
<div class="flex items-center gap-3 border-b pb-3 mb-4">
<i class="fas fa-user-circle text-blue-500"></i>
<h3 class="font-bold text-base">Thông tin cá nhân</h3>
</div>
<div class="space-y-4">
<div>
<label class="font-semibold text-sm mb-1 block">Họ và tên <span class="text-red-500">*</span></label>
<input type="text" id="fullName" class="form-input w-full p-2.5 rounded-lg" value="Nguyễn Văn A" placeholder="Nhập họ và tên" required onkeyup="document.getElementById('fullNameDisplay').textContent = this.value">
</div>
<!-- Gender -->
<div class="form-group">
<label class="form-label">Giới tính</label>
<select class="form-select">
<div>
<label class="font-semibold text-sm mb-1 block">Số điện thoại</label>
<input type="tel" class="form-input readonly-input w-full p-2.5 rounded-lg" value="0983 441 099" readonly>
</div>
<div>
<label class="font-semibold text-sm mb-1 block">Email</label>
<input type="email" class="form-input readonly-input w-full p-2.5 rounded-lg" value="nguyenvana@email.com" readonly>
</div>
<!--<div>
<label class="font-semibold text-sm mb-1 block">Vai trò</label>
<input class="form-input readonly-input w-full p-2.5 rounded-lg" value="Thầu thợ" disabled>
</div>-->
<div>
<label class="font-semibold text-sm mb-1 block">Ngày sinh</label>
<input type="date" id="birthDate" class="form-input w-full p-2.5 rounded-lg" value="1990-05-15">
</div>
<div>
<label class="font-semibold text-sm mb-1 block">Giới tính</label>
<select id="gender" class="form-select w-full p-2.5 rounded-lg bg-white">
<option value="">Chọn giới tính</option>
<option value="male" selected>Nam</option>
<option value="female">Nữ</option>
<option value="other">Khác</option>
</select>
</div>
<!-- ID Number -->
<div class="form-group">
<label class="form-label">Số CMND/CCCD</label>
<input type="text" class="form-input" value="123456789012">
<div>
<label class="font-semibold text-sm mb-1 block">Tên công ty/Cửa hàng</label>
<input type="text" id="companyName" class="form-input w-full p-2.5 rounded-lg" value="Gạch ốp lát Phương Nam" placeholder="Nhập tên (không bắt buộc)">
</div>
<div>
<label class="font-semibold text-sm mb-1 block">Mã số thuế</label>
<input type="text" id="taxCode" class="form-input w-full p-2.5 rounded-lg" value="0312345678" placeholder="Nhập mã số thuế (không bắt buộc)">
</div>
</div>
</div>
<!-- ID MST -->
<div class="form-group">
<label class="form-label">Mã số thuế</label>
<input type="text" class="form-input" value="0359837618">
<!-- Read-only Fields -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-blue-50 border-l-4 border-blue-400 text-blue-700 p-3 rounded text-xs">
<i class="fas fa-info-circle mr-2"></i>
Để thay đổi số điện thoại, email hoặc vai trò, vui lòng liên hệ bộ phận hỗ trợ.
</div>
</div>
<!-- Company -->
<div class="form-group">
<label class="form-label">Công ty</label>
<input type="text" class="form-input" value="Công ty TNHH Xây dựng ABC">
</div>
<!-- Address -->
<div class="form-group">
<label class="form-label">Địa chỉ</label>
<input type="text" class="form-input" value="123 Man Thiện, Thủ Đức, Hồ Chí Minh">
</div>
<!-- Position -->
<div class="form-group">
<label class="form-label">Chức vụ</label>
<select class="form-select">
<option value="">Chọn chức vụ</option>
<option value="contractor" selected>Thầu thợ</option>
<option value="architect">Kiến trúc sư</option>
<option value="dealer">Đại lý phân phối</option>
<option value="broker">Môi giới</option>
<option value="other">Khác</option>
</select>
</div>
<!-- Experience -->
<div class="form-group">
<label class="form-label">Kinh nghiệm (năm)</label>
<input type="number" class="form-input" value="10" min="0">
</div>
</form>
</div>
<!-- Action Buttons -->
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="history.back()">
Hủy bỏ
</button>
<button type="submit" class="btn btn-primary" onclick="saveProfile()">
<i class="fas fa-save"></i>
<div class="mt-6">
<button id="submit-btn" type="submit" class="w-full bg-blue-500 text-white font-bold py-3 px-4 rounded-lg shadow-md hover:bg-blue-600 transition-colors">
Lưu thay đổi
</button>
</div>
</div>
</form>
</div>
</div>
<script>
function changeAvatar() {
document.getElementById('avatarInput').click();
let accountStatus = 'chua_xac_thuc';
let verificationData = {
idNumber: '079123456789',
taxCode: '0312345678',
idCardFile: null,
certificateFile: null
};
function initializeAccountStatus() {
const statusContainer = document.getElementById('accountStatusCard');
const verificationFormContainer = document.getElementById('verificationFormContainer');
statusContainer.innerHTML = '';
if (accountStatus === 'chua_xac_thuc') {
const unverifiedBadge = document.createElement('div');
unverifiedBadge.className = 'flex items-center gap-4';
unverifiedBadge.innerHTML = `
<div class="inline-flex items-center gap-1.5 bg-red-100 text-red-700 text-xs font-semibold px-2.5 py-1 rounded-full">
<i class="fas fa-exclamation-circle"></i>
<span>Chưa xác thực</span>
</div>
<button class="text-blue-500 font-semibold text-sm" onclick="showVerificationForm()">
Xác thực ngay <i class="fas fa-arrow-right text-xs"></i>
</button>
`;
statusContainer.appendChild(unverifiedBadge);
verificationFormContainer.style.display = 'none';
} else if (accountStatus === 'cho_xac_thuc') {
statusContainer.innerHTML = `
<div class="inline-flex items-center gap-1.5 bg-yellow-100 text-yellow-800 text-xs font-semibold px-2.5 py-1 rounded-full cursor-pointer" onclick="viewVerificationInfo()">
<i class="fas fa-clock"></i>
<span>Đang chờ xác thực</span>
</div>`;
verificationFormContainer.style.display = 'none';
} else if (accountStatus === 'da_xac_thuc') {
statusContainer.innerHTML = `
<div class="inline-flex items-center gap-1.5 bg-green-100 text-green-700 text-xs font-semibold px-2.5 py-1 rounded-full cursor-pointer" onclick="viewVerificationInfo()">
<i class="fas fa-check-circle"></i>
<span>Đã xác thực</span>
</div>`;
verificationFormContainer.style.display = 'none';
}
}
function showVerificationForm() {
const verificationFormContainer = document.getElementById('verificationFormContainer');
const verificationSubmitBtn = document.getElementById('verificationSubmitBtn');
// Clear upload previews
document.getElementById('idCardPreview').innerHTML = `<i class="fas fa-camera text-2xl"></i><span class="mt-2 text-sm font-semibold">Chụp ảnh hoặc chọn file</span><span class="text-xs">JPG, PNG tối đa 5MB</span>`;
document.getElementById('certificatePreview').innerHTML = `<i class="fas fa-file-certificate text-2xl"></i><span class="mt-2 text-sm font-semibold">Chụp ảnh hoặc chọn file</span><span class="text-xs">JPG, PNG tối đa 5MB</span>`;
const idCard = document.getElementById('idCardUploadCard');
const certCard = document.getElementById('certificateUploadCard');
idCard.classList.remove('has-file', 'readonly');
certCard.classList.remove('has-file', 'readonly');
idCard.onclick = () => handleUploadClick('idCardInput');
certCard.onclick = () => handleUploadClick('certificateInput');
verificationFormContainer.style.display = 'block';
verificationSubmitBtn.style.display = 'grid';
setTimeout(() => verificationFormContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100);
}
function viewVerificationInfo() {
const verificationFormContainer = document.getElementById('verificationFormContainer');
const verificationSubmitBtn = document.getElementById('verificationSubmitBtn');
const idCard = document.getElementById('idCardUploadCard');
const certCard = document.getElementById('certificateUploadCard');
if (verificationData.idCardFile) {
document.getElementById('idCardPreview').innerHTML = `<i class="fas fa-check-circle text-3xl text-green-500"></i><span class="mt-2 text-sm font-semibold text-green-600">CCCD_front.jpg</span>`;
idCard.classList.add('has-file', 'readonly');
idCard.onclick = null;
}
document.getElementById('avatarInput').addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('avatarImage').src = e.target.result;
};
reader.readAsDataURL(e.target.files[0]);
}
});
function saveProfile() {
const form = document.getElementById('profileForm');
if (form.checkValidity()) {
alert('Thông tin đã được cập nhật thành công!');
window.location.href = 'account.html';
} else {
form.reportValidity();
}
if (verificationData.certificateFile) {
document.getElementById('certificatePreview').innerHTML = `<i class="fas fa-check-circle text-3xl text-green-500"></i><span class="mt-2 text-sm font-semibold text-green-600">certificate.jpg</span>`;
certCard.classList.add('has-file', 'readonly');
certCard.onclick = null;
}
verificationFormContainer.style.display = 'block';
verificationSubmitBtn.style.display = 'none';
setTimeout(() => verificationFormContainer.scrollIntoView({ behavior: 'smooth', block: 'center' }), 100);
}
function cancelVerification() {
document.getElementById('verificationFormContainer').style.display = 'none';
}
function submitVerification() {
if (!document.getElementById('idCardInput').files.length) return showToast('Vui lòng upload ảnh CCCD/CMND', 'error');
if (!document.getElementById('certificateInput').files.length) return showToast('Vui lòng upload ảnh chứng chỉ', 'error');
verificationData.idCardFile = document.getElementById('idCardInput').files[0];
verificationData.certificateFile = document.getElementById('certificateInput').files[0];
showToast('Đang gửi thông tin xác thực...', 'info');
setTimeout(() => {
accountStatus = 'cho_xac_thuc';
initializeAccountStatus();
document.getElementById('verificationFormContainer').style.display = 'none';
showToast('Đã gửi thông tin thành công! Vui lòng chờ duyệt.', 'success');
window.scrollTo({ top: 0, behavior: 'smooth' });
}, 1500);
}
function handleUploadClick(inputId) {
// Add readonly check
const uploadCard = document.getElementById(inputId).parentElement;
if (uploadCard.classList.contains('readonly')) return;
document.getElementById(inputId).click();
}
function handleAvatarChange(input) {
const file = input.files[0];
if (file) {
if (!file.type.startsWith('image/')) return showToast('Vui lòng chọn file hình ảnh (JPG, PNG)', 'error');
if (file.size > 5 * 1024 * 1024) return showToast('File không được vượt quá 5MB', 'error');
const reader = new FileReader();
reader.onload = e => document.getElementById('avatarImage').src = e.target.result;
reader.readAsDataURL(file);
showToast('Đã chọn ảnh đại diện mới', 'success');
}
}
function handleVerificationFileUpload(input, previewId) {
const file = input.files[0];
const previewContainer = document.getElementById(previewId);
const uploadCard = previewContainer.parentElement;
if (file) {
if (!file.type.startsWith('image/')) return showToast('Vui lòng chọn file hình ảnh (JPG, PNG)', 'error');
if (file.size > 5 * 1024 * 1024) return showToast('File không được vượt quá 5MB', 'error');
const reader = new FileReader();
reader.onload = e => {
previewContainer.innerHTML = `
<img src="${e.target.result}" alt="Preview" class="w-full h-24 object-contain rounded-md mb-2">
<div class="text-sm font-semibold text-green-600 truncate">${file.name}</div>
<div class="text-xs text-gray-500">Nhấn để thay đổi</div>
`;
uploadCard.classList.add('has-file');
};
reader.readAsDataURL(file);
showToast('Đã upload file thành công', 'success');
}
}
function handleSubmit(event) {
event.preventDefault();
if (!document.getElementById('fullName').value) return showToast('Vui lòng nhập họ và tên', 'error');
saveProfile();
}
function saveProfile() {
const submitBtn = document.getElementById('submit-btn');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Đang lưu...';
submitBtn.disabled = true;
}
function showToast(message, type = 'success') {
const colors = { success: 'bg-green-500', error: 'bg-red-500', info: 'bg-blue-500' };
const icons = { success: 'fa-check-circle', error: 'fa-exclamation-circle', info: 'fa-info-circle' };
const toast = document.createElement('div');
toast.className = `fixed top-5 left-1/2 -translate-x-1/2 ${colors[type]} text-white py-2 px-5 rounded-lg shadow-lg flex items-center gap-2 text-sm z-[100]`;
toast.innerHTML = `<i class="fas ${icons[type]}"></i><span>${message}</span>`;
toast.style.animation = 'slideDown 0.3s ease';
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideUp 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
document.addEventListener('DOMContentLoaded', function() {
initializeAccountStatus();
// FOR TESTING:
// accountStatus = 'chua_xac_thuc';
// accountStatus = 'cho_xac_thuc';
// accountStatus = 'da_xac_thuc';
// initializeAccountStatus();
});
</script>
</body>
</html>

View File

@@ -448,20 +448,20 @@ class AddressesPage extends HookConsumerWidget {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
const SnackBar(
content: Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.circleCheck,
color: Colors.white,
size: 18,
),
const SizedBox(width: 12),
const Text('Đã xóa địa chỉ'),
SizedBox(width: 12),
Text('Đã xóa địa chỉ'),
],
),
backgroundColor: const Color(0xFF10B981),
duration: const Duration(seconds: 2),
backgroundColor: Color(0xFF10B981),
duration: Duration(seconds: 2),
),
);
}

View File

@@ -5,9 +5,10 @@
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shimmer/shimmer.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
@@ -19,12 +20,13 @@ import 'package:worker/features/products/domain/entities/product.dart';
///
/// Shows all products that the user has marked as favorites.
/// Features:
/// - Search bar for filtering favorites
/// - Grid layout of favorite products
/// - Pull-to-refresh
/// - Empty state when no favorites
/// - Error state with retry
/// - Clear all functionality
class FavoritesPage extends ConsumerWidget {
class FavoritesPage extends HookConsumerWidget {
const FavoritesPage({super.key});
/// Show confirmation dialog before clearing all favorites
@@ -76,18 +78,40 @@ class FavoritesPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Search controller
final searchController = useTextEditingController();
final searchQuery = useState('');
// Watch favorites and products
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
final favoriteCount = ref.watch(favoriteCountProvider);
// Track if we've loaded data at least once to prevent empty state flash
final hasLoadedOnce = favoriteProductsAsync.hasValue || favoriteProductsAsync.hasError;
final hasLoadedOnce =
favoriteProductsAsync.hasValue || favoriteProductsAsync.hasError;
// Filter products based on search query
List<Product> filterProducts(List<Product> products) {
if (searchQuery.value.isEmpty) return products;
final query = searchQuery.value.toLowerCase().trim();
return products.where((product) {
final matchesName = product.name.toLowerCase().contains(query);
final matchesSku =
product.erpnextItemCode?.toLowerCase().contains(query) ?? false;
return matchesName || matchesSku;
}).toList();
}
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text('Yêu thích', style: TextStyle(color: Colors.black)),
@@ -115,7 +139,11 @@ class FavoritesPage extends ConsumerWidget {
// Clear all button
if (favoriteCount > 0)
IconButton(
icon: const FaIcon(FontAwesomeIcons.trashCan, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.trashCan,
color: Colors.black,
size: 20,
),
tooltip: 'Xóa tất cả',
onPressed: () => _showClearAllDialog(context, ref, favoriteCount),
),
@@ -125,6 +153,9 @@ class FavoritesPage extends ConsumerWidget {
body: SafeArea(
child: favoriteProductsAsync.when(
data: (products) {
// Filter products based on search
final filteredProducts = filterProducts(products);
// IMPORTANT: Only show empty state after we've confirmed data loaded
// This prevents empty state flash during initial load
if (products.isEmpty && hasLoadedOnce) {
@@ -136,12 +167,119 @@ class FavoritesPage extends ConsumerWidget {
return const _LoadingState();
}
return RefreshIndicator(
onRefresh: () async {
// Use the new refresh method from AsyncNotifier
await ref.read(favoriteProductsProvider.notifier).refresh();
},
child: _FavoritesGrid(products: products),
return Column(
children: [
// Search Bar
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: TextField(
controller: searchController,
onChanged: (value) => searchQuery.value = value,
decoration: InputDecoration(
hintText: 'Tìm kiếm sản phẩm...',
hintStyle: const TextStyle(color: AppColors.grey500),
prefixIcon: const Icon(
Icons.search,
color: AppColors.grey500,
),
suffixIcon: searchQuery.value.isNotEmpty
? IconButton(
icon: const Icon(
Icons.clear,
color: AppColors.grey500,
),
onPressed: () {
searchController.clear();
searchQuery.value = '';
},
)
: null,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(color: AppColors.grey100),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(color: AppColors.grey100),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
vertical: AppSpacing.sm,
),
),
),
),
// Results count when searching
if (searchQuery.value.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md,
),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Tìm thấy ${filteredProducts.length} sản phẩm',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
),
),
// Products Grid
Expanded(
child:
filteredProducts.isEmpty && searchQuery.value.isNotEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FaIcon(
FontAwesomeIcons.magnifyingGlass,
size: 64,
color: AppColors.grey500,
),
const SizedBox(height: AppSpacing.md),
Text(
'Không tìm thấy "${searchQuery.value}"',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: AppSpacing.sm),
const Text(
'Thử tìm kiếm với từ khóa khác',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
)
: RefreshIndicator(
onRefresh: () async {
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: _FavoritesGrid(products: filteredProducts),
),
),
],
);
},
loading: () {
@@ -156,7 +294,9 @@ class FavoritesPage extends ConsumerWidget {
children: [
RefreshIndicator(
onRefresh: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: _FavoritesGrid(products: previousValue),
),
@@ -177,7 +317,9 @@ class FavoritesPage extends ConsumerWidget {
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
SizedBox(width: 8),
Text('Đang tải...'),
@@ -212,7 +354,9 @@ class FavoritesPage extends ConsumerWidget {
children: [
RefreshIndicator(
onRefresh: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: _FavoritesGrid(products: previousValue),
),
@@ -241,7 +385,9 @@ class FavoritesPage extends ConsumerWidget {
),
TextButton(
onPressed: () async {
await ref.read(favoriteProductsProvider.notifier).refresh();
await ref
.read(favoriteProductsProvider.notifier)
.refresh();
},
child: const Text(
'Thử lại',