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",
|
"ward_code": "32248",
|
||||||
"is_default": false
|
"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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Thông tin cá nhân - EuroTile Worker</title>
|
<title>Thông tin cá nhân - EuroTile Worker</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com/3.4.1"></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" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body class="text-gray-800">
|
||||||
|
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="header flex items-center justify-between p-4 border-b border-gray-200">
|
||||||
<a href="account.html" class="back-button">
|
<a href="account.html" class="text-gray-600">
|
||||||
<i class="fas fa-arrow-left"></i>
|
<i class="fas fa-arrow-left text-xl"></i>
|
||||||
</a>
|
</a>
|
||||||
<h1 class="header-title">Thông tin cá nhân</h1>
|
<h1 class="text-lg font-bold" style="margin-right: 97px;">Thông tin cá nhân</h1>
|
||||||
<div style="width: 32px;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container p-4 pb-24">
|
||||||
<div class="form-container">
|
<form id="profileForm" onsubmit="handleSubmit(event)">
|
||||||
<div class="card">
|
|
||||||
<!-- Profile Picture -->
|
<!-- Avatar Section -->
|
||||||
<div class="profile-avatar-section">
|
<div class="bg-white rounded-xl shadow-sm p-6 flex flex-col items-center">
|
||||||
<div class="profile-avatar">
|
<div class="relative">
|
||||||
<img src="https://placehold.co/100x100/005B9A/FFFFFF/png?text=HMH" alt="Avatar" id="avatarImage">
|
<img src="https://ui-avatars.com/api/?name=Nguyen+Van+A&background=3b82f6&color=fff&size=128"
|
||||||
<button class="avatar-edit-btn" onclick="changeAvatar()">
|
alt="Avatar"
|
||||||
<i class="fas fa-camera"></i>
|
id="avatarImage"
|
||||||
</button>
|
class="w-24 h-24 rounded-full border-4 border-white shadow-lg">
|
||||||
</div>
|
<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">
|
||||||
<input type="file" id="avatarInput" style="display: none;" accept="image/*">
|
<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>
|
</div>
|
||||||
|
|
||||||
<form id="profileForm">
|
<div class="info-note bg-blue-50 border-l-4 border-blue-400 text-blue-700 p-4 rounded-md mb-4 text-sm">
|
||||||
<!-- Full Name -->
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
<div class="form-group">
|
<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.
|
||||||
<label class="form-label">Họ và tên *</label>
|
</div>
|
||||||
<input type="text" class="form-input" value="Hoàng Minh Hiệp" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Phone -->
|
<div class="space-y-4">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Số điện thoại *</label>
|
<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>
|
||||||
<input type="tel" class="form-input" value="0347302911" required>
|
<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>
|
||||||
|
|
||||||
<!-- Email -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Email</label>
|
<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>
|
||||||
<input type="email" class="form-input" value="hoanghiep@example.com">
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Birth Date -->
|
<div class="grid grid-cols-2 gap-3 mt-6" id="verificationSubmitBtn" style="display: none;">
|
||||||
<div class="form-group">
|
<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>
|
||||||
<label class="form-label">Ngày sinh</label>
|
<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>
|
||||||
<input type="date" class="form-input" value="1985-03-15">
|
</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>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- Gender -->
|
<label class="font-semibold text-sm mb-1 block">Số điện thoại</label>
|
||||||
<div class="form-group">
|
<input type="tel" class="form-input readonly-input w-full p-2.5 rounded-lg" value="0983 441 099" readonly>
|
||||||
<label class="form-label">Giới tính</label>
|
</div>
|
||||||
<select class="form-select">
|
<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="">Chọn giới tính</option>
|
||||||
<option value="male" selected>Nam</option>
|
<option value="male" selected>Nam</option>
|
||||||
<option value="female">Nữ</option>
|
<option value="female">Nữ</option>
|
||||||
<option value="other">Khác</option>
|
<option value="other">Khác</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<!-- ID Number -->
|
<label class="font-semibold text-sm mb-1 block">Tên công ty/Cửa hàng</label>
|
||||||
<div class="form-group">
|
<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)">
|
||||||
<label class="form-label">Số CMND/CCCD</label>
|
</div>
|
||||||
<input type="text" class="form-input" value="123456789012">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- ID MST -->
|
<!-- Read-only Fields -->
|
||||||
<div class="form-group">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<label class="form-label">Mã số thuế</label>
|
<div class="bg-blue-50 border-l-4 border-blue-400 text-blue-700 p-3 rounded text-xs">
|
||||||
<input type="text" class="form-input" value="0359837618">
|
<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>
|
||||||
|
</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 -->
|
<!-- Action Buttons -->
|
||||||
<div class="form-actions">
|
<div class="mt-6">
|
||||||
<button type="button" class="btn btn-secondary" onclick="history.back()">
|
<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">
|
||||||
Hủy bỏ
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary" onclick="saveProfile()">
|
|
||||||
<i class="fas fa-save"></i>
|
|
||||||
Lưu thay đổi
|
Lưu thay đổi
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function changeAvatar() {
|
let accountStatus = 'chua_xac_thuc';
|
||||||
document.getElementById('avatarInput').click();
|
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 (verificationData.certificateFile) {
|
||||||
if (e.target.files && e.target.files[0]) {
|
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>`;
|
||||||
const reader = new FileReader();
|
certCard.classList.add('has-file', 'readonly');
|
||||||
reader.onload = function(e) {
|
certCard.onclick = null;
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -448,20 +448,20 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.circleCheck,
|
FontAwesomeIcons.circleCheck,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
const Text('Đã xóa địa chỉ'),
|
Text('Đã xóa địa chỉ'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xFF10B981),
|
backgroundColor: Color(0xFF10B981),
|
||||||
duration: const Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.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';
|
||||||
@@ -19,12 +20,13 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
|||||||
///
|
///
|
||||||
/// Shows all products that the user has marked as favorites.
|
/// Shows all products that the user has marked as favorites.
|
||||||
/// Features:
|
/// Features:
|
||||||
|
/// - Search bar for filtering favorites
|
||||||
/// - Grid layout of favorite products
|
/// - Grid layout of favorite products
|
||||||
/// - Pull-to-refresh
|
/// - Pull-to-refresh
|
||||||
/// - Empty state when no favorites
|
/// - Empty state when no favorites
|
||||||
/// - Error state with retry
|
/// - Error state with retry
|
||||||
/// - Clear all functionality
|
/// - Clear all functionality
|
||||||
class FavoritesPage extends ConsumerWidget {
|
class FavoritesPage extends HookConsumerWidget {
|
||||||
const FavoritesPage({super.key});
|
const FavoritesPage({super.key});
|
||||||
|
|
||||||
/// Show confirmation dialog before clearing all favorites
|
/// Show confirmation dialog before clearing all favorites
|
||||||
@@ -76,18 +78,40 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// Search controller
|
||||||
|
final searchController = useTextEditingController();
|
||||||
|
final searchQuery = useState('');
|
||||||
|
|
||||||
// Watch favorites and products
|
// Watch favorites and products
|
||||||
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
|
||||||
final favoriteCount = ref.watch(favoriteCountProvider);
|
final favoriteCount = ref.watch(favoriteCountProvider);
|
||||||
|
|
||||||
// Track if we've loaded data at least once to prevent empty state flash
|
// 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(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
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(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text('Yêu thích', style: TextStyle(color: Colors.black)),
|
title: const Text('Yêu thích', style: TextStyle(color: Colors.black)),
|
||||||
@@ -115,7 +139,11 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
// Clear all button
|
// Clear all button
|
||||||
if (favoriteCount > 0)
|
if (favoriteCount > 0)
|
||||||
IconButton(
|
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ả',
|
tooltip: 'Xóa tất cả',
|
||||||
onPressed: () => _showClearAllDialog(context, ref, favoriteCount),
|
onPressed: () => _showClearAllDialog(context, ref, favoriteCount),
|
||||||
),
|
),
|
||||||
@@ -125,6 +153,9 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: favoriteProductsAsync.when(
|
child: favoriteProductsAsync.when(
|
||||||
data: (products) {
|
data: (products) {
|
||||||
|
// Filter products based on search
|
||||||
|
final filteredProducts = filterProducts(products);
|
||||||
|
|
||||||
// IMPORTANT: Only show empty state after we've confirmed data loaded
|
// IMPORTANT: Only show empty state after we've confirmed data loaded
|
||||||
// This prevents empty state flash during initial load
|
// This prevents empty state flash during initial load
|
||||||
if (products.isEmpty && hasLoadedOnce) {
|
if (products.isEmpty && hasLoadedOnce) {
|
||||||
@@ -136,12 +167,119 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
return const _LoadingState();
|
return const _LoadingState();
|
||||||
}
|
}
|
||||||
|
|
||||||
return RefreshIndicator(
|
return Column(
|
||||||
onRefresh: () async {
|
children: [
|
||||||
// Use the new refresh method from AsyncNotifier
|
// Search Bar
|
||||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
Padding(
|
||||||
},
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
child: _FavoritesGrid(products: products),
|
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: () {
|
loading: () {
|
||||||
@@ -156,7 +294,9 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
RefreshIndicator(
|
RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
await ref
|
||||||
|
.read(favoriteProductsProvider.notifier)
|
||||||
|
.refresh();
|
||||||
},
|
},
|
||||||
child: _FavoritesGrid(products: previousValue),
|
child: _FavoritesGrid(products: previousValue),
|
||||||
),
|
),
|
||||||
@@ -177,7 +317,9 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text('Đang tải...'),
|
Text('Đang tải...'),
|
||||||
@@ -212,7 +354,9 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
RefreshIndicator(
|
RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
await ref
|
||||||
|
.read(favoriteProductsProvider.notifier)
|
||||||
|
.refresh();
|
||||||
},
|
},
|
||||||
child: _FavoritesGrid(products: previousValue),
|
child: _FavoritesGrid(products: previousValue),
|
||||||
),
|
),
|
||||||
@@ -241,7 +385,9 @@ class FavoritesPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await ref.read(favoriteProductsProvider.notifier).refresh();
|
await ref
|
||||||
|
.read(favoriteProductsProvider.notifier)
|
||||||
|
.refresh();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Thử lại',
|
'Thử lại',
|
||||||
|
|||||||
Reference in New Issue
Block a user