add search fav
This commit is contained in:
@@ -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"
|
||||
}'
|
||||
@@ -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 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>
|
||||
<input type="file" id="avatarInput" style="display: none;" accept="image/*">
|
||||
</div>
|
||||
|
||||
<form id="profileForm">
|
||||
<!-- Full Name -->
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="space-y-4">
|
||||
<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>
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
|
||||
<!-- Phone -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Số điện thoại *</label>
|
||||
<input type="tel" class="form-input" value="0347302911" required>
|
||||
<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>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-input" value="hoanghiep@example.com">
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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="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">
|
||||
<!-- Action Buttons -->
|
||||
<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>
|
||||
</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>
|
||||
Lưu thay đổi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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';
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
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 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();
|
||||
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>
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
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 = '';
|
||||
},
|
||||
child: _FavoritesGrid(products: products),
|
||||
)
|
||||
: 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',
|
||||
|
||||
Reference in New Issue
Block a user