fix mượn trả

This commit is contained in:
2026-04-25 09:24:35 +07:00
parent bc7a484a01
commit 4fb7f412bf
5 changed files with 197 additions and 26 deletions

View File

@@ -3230,42 +3230,67 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
} }
}); });
app.delete('/api/asset-borrows/:id', requireAssetOrAdmin, async (req, res) => { app.delete('/api/asset-borrows/:id', async (req, res) => {
try { try {
const requesterRole = normalizeRole(req.headers['x-user-role'] || req.query.userRole);
const requesterId = getUserIdFromRequest(req);
const canManageRequests = requesterRole === 'admin' || requesterRole === 'asset';
const borrowId = Number(req.params.id); const borrowId = Number(req.params.id);
if (!Number.isInteger(borrowId) || borrowId <= 0) { if (!Number.isInteger(borrowId) || borrowId <= 0) {
return res.status(400).json({ success: false, message: 'Mã đơn không hp l' }); return res.status(400).json({ success: false, message: 'Ma don khong hop le' });
}
if (!canManageRequests && (!Number.isInteger(requesterId) || requesterId <= 0)) {
return res.status(401).json({ success: false, message: 'Yeu cau xac thuc nguoi dung' });
} }
const deleteResult = await pool.request() const deleteResult = await pool.request()
.input('borrowId', sql.Int, borrowId) .input('borrowId', sql.Int, borrowId)
.input('requesterId', sql.Int, requesterId || -1)
.query(` .query(`
DELETE FROM AssetBorrowRequests DELETE FROM AssetBorrowRequests
OUTPUT DELETED.BorrowId OUTPUT DELETED.BorrowId
WHERE BorrowId = @borrowId WHERE BorrowId = @borrowId
AND LOWER(LTRIM(RTRIM(ISNULL(RequestStatus, '')))) = 'pending' AND LOWER(LTRIM(RTRIM(ISNULL(RequestStatus, '')))) = 'pending'
${canManageRequests ? '' : 'AND CreatedBy = @requesterId'}
`); `);
if (Array.isArray(deleteResult.recordset) && deleteResult.recordset.length > 0) { if (Array.isArray(deleteResult.recordset) && deleteResult.recordset.length > 0) {
return res.json({ success: true, message: 'Đã xóa đơn ch' }); return res.json({ success: true, message: 'Da huy don cho' });
} }
const existed = await pool.request() const existed = await pool.request()
.input('borrowId', sql.Int, borrowId) .input('borrowId', sql.Int, borrowId)
.query(` .query(`
SELECT TOP 1 BorrowId, RequestStatus SELECT TOP 1 BorrowId, RequestStatus, CreatedBy
FROM AssetBorrowRequests FROM AssetBorrowRequests
WHERE BorrowId = @borrowId WHERE BorrowId = @borrowId
`); `);
const row = existed.recordset?.[0]; const row = existed.recordset?.[0];
if (!row) { if (!row) {
return res.status(404).json({ success: false, message: 'Không tìm thy đơn cn xóa' }); return res.status(404).json({ success: false, message: 'Khong tim thay don can xoa' });
}
if (!canManageRequests && Number(row.CreatedBy) !== requesterId) {
return res.status(403).json({
success: false,
message: 'Ban chi duoc huy don do chinh minh tao'
});
}
const currentStatus = normalizeAssetRequestStatus(row.RequestStatus);
if (currentStatus !== 'pending') {
return res.status(400).json({
success: false,
message: 'Chi duoc huy don o trang thai cho xu ly'
});
} }
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Chỉ được xóa đơn ở trạng thái chờ xử lý' message: 'Khong the huy don vao luc nay'
}); });
} catch (err) { } catch (err) {
return res.status(500).json({ success: false, message: err.message }); return res.status(500).json({ success: false, message: err.message });

File diff suppressed because one or more lines are too long

View File

@@ -59,6 +59,7 @@ class AccountManager {
this.assetBorrowRequestType = 'borrow'; this.assetBorrowRequestType = 'borrow';
this.pendingAssetRequestRejectId = undefined; this.pendingAssetRequestRejectId = undefined;
this.assetBorrowAutoRefreshTimer = undefined; this.assetBorrowAutoRefreshTimer = undefined;
this.pendingAssetRequestDeleteConfirmResolver = undefined;
} }
configureNotifications() { configureNotifications() {
@@ -1008,6 +1009,21 @@ class AccountManager {
} }
} }
const confirmAssetRequestDeleteBtn = document.getElementById('confirmAssetRequestDeleteBtn');
if (confirmAssetRequestDeleteBtn && confirmAssetRequestDeleteBtn.dataset.boundClick !== 'true') {
confirmAssetRequestDeleteBtn.addEventListener('click', () => this.resolveAssetRequestDeleteConfirm(true));
confirmAssetRequestDeleteBtn.dataset.boundClick = 'true';
}
document.querySelectorAll('.cancel-asset-request-delete-confirm').forEach(btn => {
if (btn.dataset.boundClick === 'true') {
return;
}
btn.addEventListener('click', () => this.resolveAssetRequestDeleteConfirm(false));
btn.dataset.boundClick = 'true';
});
this.setupAssetBorrowRequestModalListeners(); this.setupAssetBorrowRequestModalListeners();
const assetDepartmentForm = document.getElementById('assetDepartmentForm'); const assetDepartmentForm = document.getElementById('assetDepartmentForm');
@@ -2151,6 +2167,11 @@ class AccountManager {
const statusMeta = this.getAssetRequestStatusMeta(item.RequestStatus); const statusMeta = this.getAssetRequestStatusMeta(item.RequestStatus);
const note = String(item?.RequestNote || '').trim(); const note = String(item?.RequestNote || '').trim();
const rejectReason = String(item?.RejectReason || '').trim(); const rejectReason = String(item?.RejectReason || '').trim();
const canCancel = this.canCurrentUserCancelAssetRequest(item);
const requestId = Number(item?.BorrowId) || 0;
const cancelActionHtml = canCancel
? `<button class="asset-borrow-cancel-btn px-3 py-1.5 rounded-md bg-red-600 hover:bg-red-700 text-white text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Hủy đơn</button>`
: `<span class="text-xs text-slate-400">-</span>`;
return ` return `
<tr class="hover:bg-slate-50/80 transition-colors"> <tr class="hover:bg-slate-50/80 transition-colors">
@@ -2171,14 +2192,33 @@ class AccountManager {
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(item.BorrowDate)}</td> <td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(item.BorrowDate)}</td>
<td class="px-4 py-3 text-sm text-slate-600 max-w-xs">${this.escapeHtml(note || '-')}</td> <td class="px-4 py-3 text-sm text-slate-600 max-w-xs">${this.escapeHtml(note || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600 max-w-xs">${this.escapeHtml(rejectReason || '-')}</td> <td class="px-4 py-3 text-sm text-slate-600 max-w-xs">${this.escapeHtml(rejectReason || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600">${cancelActionHtml}</td>
</tr> </tr>
`; `;
} }
canCurrentUserCancelAssetRequest(item) {
const status = this.normalizeAssetRequestStatus(item?.RequestStatus);
if (status !== 'pending') {
return false;
}
if (this.canCurrentUserManageAssets()) {
return true;
}
const currentUserId = Number(this.getUserId());
const createdBy = Number(item?.CreatedBy);
return Number.isFinite(currentUserId)
&& currentUserId > 0
&& Number.isFinite(createdBy)
&& createdBy === currentUserId;
}
buildAssetBorrowEmptyRowHtml() { buildAssetBorrowEmptyRowHtml() {
return ` return `
<tr> <tr>
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có đơn mượn/trả tài sản nào.</td> <td colspan="11" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có đơn mượn/trả tài sản nào.</td>
</tr> </tr>
`; `;
} }
@@ -2235,7 +2275,7 @@ class AccountManager {
> >
<span class="material-symbols-outlined text-base">notifications_active</span> <span class="material-symbols-outlined text-base">notifications_active</span>
Đơn chờ Đơn chờ
<span id="pendingAssetBorrowsCountBadge" class="${pendingCount > 0 ? '' : 'hidden'} absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-full bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">${pendingCount > 99 ? '99+' : pendingCount}</span> <span id="pendingAssetBorrowsCountBadge" class="${pendingCount > 0 ? '' : 'hidden'} absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-[999px] bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">${pendingCount > 99 ? '99+' : pendingCount}</span>
</button> </button>
` : ''} ` : ''}
</div> </div>
@@ -2268,6 +2308,7 @@ class AccountManager {
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày</th> <th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th> <th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Lý do</th> <th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Lý do</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Hành động</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-100 asset-borrows-table-body"> <tbody class="divide-y divide-slate-100 asset-borrows-table-body">
@@ -2339,6 +2380,30 @@ class AccountManager {
searchInput.dataset.boundInput = 'true'; searchInput.dataset.boundInput = 'true';
} }
const tableBody = document.querySelector('.asset-borrows-table-body');
if (tableBody && tableBody.dataset.boundActions !== 'true') {
tableBody.addEventListener('click', (event) => {
const cancelButton = event.target.closest('.asset-borrow-cancel-btn');
if (!cancelButton) {
return;
}
const requestId = Number(cancelButton.dataset.requestId);
if (!Number.isFinite(requestId) || requestId <= 0) {
return;
}
this.deletePendingAssetBorrowRequest(requestId, {
confirmMessage: `Bạn có chắc muốn hủy đơn #${requestId}?`,
confirmButtonText: 'Hủy đơn',
successMessage: 'Đã hủy đơn thành công',
failureMessage: 'Hủy đơn thất bại'
});
});
tableBody.dataset.boundActions = 'true';
}
this.setupAssetBorrowPagerListeners(); this.setupAssetBorrowPagerListeners();
} }
@@ -2489,6 +2554,7 @@ class AccountManager {
this.notifyFailure('Tạo đơn thất bại'); this.notifyFailure('Tạo đơn thất bại');
} }
} }
buildPendingAssetRequestCardHtml(item) { buildPendingAssetRequestCardHtml(item) {
const typeMeta = this.getAssetRequestTypeMeta(item?.RequestType); const typeMeta = this.getAssetRequestTypeMeta(item?.RequestType);
const dateLabel = typeMeta.value === 'return' ? 'Ngày trả' : 'Ngày mượn'; const dateLabel = typeMeta.value === 'return' ? 'Ngày trả' : 'Ngày mượn';
@@ -2508,10 +2574,10 @@ buildPendingAssetRequestCardHtml(item) {
<div><span class="font-bold">Ghi chú:</span> ${this.escapeHtml(note || '-')}</div> <div><span class="font-bold">Ghi chú:</span> ${this.escapeHtml(note || '-')}</div>
<div><span class="font-bold">${dateLabel}:</span> ${this.formatDateOnly(item?.BorrowDate)}</div> <div><span class="font-bold">${dateLabel}:</span> ${this.formatDateOnly(item?.BorrowDate)}</div>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2 pt-1"> <div class="flex items-center gap-2 pt-1">
<button class="asset-request-approve-btn w-full px-3 py-1.5 rounded-md bg-primary hover:bg-primary-dim text-on-primary text-xs font-bold" data-request-id="${requestId}">Chấp nhận</button> <button class="asset-request-approve-btn flex-1 min-w-0 px-3 py-1.5 rounded-md bg-primary hover:bg-primary-dim text-on-primary text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Chấp nhận</button>
<button class="asset-request-reject-btn w-full px-3 py-1.5 rounded-md bg-red-600 hover:bg-red-700 text-white text-xs font-bold" data-request-id="${requestId}">Từ chối</button> <button class="asset-request-reject-btn flex-1 min-w-0 px-3 py-1.5 rounded-md bg-red-600 hover:bg-red-700 text-white text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Từ chối</button>
<button class="asset-request-delete-btn w-full px-3 py-1.5 rounded-md bg-slate-700 hover:bg-slate-800 text-white text-xs font-bold" data-request-id="${requestId}">Xóa đơn</button> <button class="asset-request-delete-btn flex-1 min-w-0 px-3 py-1.5 rounded-md bg-slate-700 hover:bg-slate-800 text-white text-xs font-bold whitespace-nowrap" data-request-id="${requestId}">Xóa đơn</button>
</div> </div>
</div> </div>
`; `;
@@ -2639,6 +2705,43 @@ buildPendingAssetRequestCardHtml(item) {
reasonInput.focus(); reasonInput.focus();
} }
resolveAssetRequestDeleteConfirm(confirmed) {
const modal = document.getElementById('assetRequestDeleteConfirmModal');
if (modal) {
modal.classList.remove('open');
}
const resolver = this.pendingAssetRequestDeleteConfirmResolver;
this.pendingAssetRequestDeleteConfirmResolver = undefined;
if (typeof resolver === 'function') {
resolver(Boolean(confirmed));
}
}
async confirmAssetRequestDelete(message, confirmButtonText = 'Xóa đơn') {
const modal = document.getElementById('assetRequestDeleteConfirmModal');
const messageNode = document.getElementById('assetRequestDeleteConfirmMessage');
const confirmButton = document.getElementById('confirmAssetRequestDeleteBtn');
if (!modal || !messageNode || !confirmButton) {
return window.confirm(message);
}
messageNode.textContent = String(message || 'Bạn có chắc muốn thực hiện thao tác này?');
confirmButton.textContent = String(confirmButtonText || 'Xóa đơn');
if (this.pendingAssetRequestDeleteConfirmResolver) {
this.resolveAssetRequestDeleteConfirm(false);
}
modal.style.zIndex = '140';
modal.classList.add('open');
return new Promise(resolve => {
this.pendingAssetRequestDeleteConfirmResolver = resolve;
});
}
async handleAssetRequestRejectSubmit(event) { async handleAssetRequestRejectSubmit(event) {
event.preventDefault(); event.preventDefault();
@@ -2686,7 +2789,10 @@ buildPendingAssetRequestCardHtml(item) {
&& failureMessage.toLowerCase().includes('xóa đơn chờ'); && failureMessage.toLowerCase().includes('xóa đơn chờ');
if (canAutoSuggestDelete) { if (canAutoSuggestDelete) {
const shouldDelete = window.confirm(`Đơn #${requestId} không còn hợp lệ. Bạn có muốn xóa đơn chờ này không?`); const shouldDelete = await this.confirmAssetRequestDelete(
`Đơn #${requestId} không còn hợp lệ. Bạn có muốn xóa đơn chờ này không?`,
'Xóa đơn'
);
if (shouldDelete) { if (shouldDelete) {
await this.deletePendingAssetBorrowRequest(requestId); await this.deletePendingAssetBorrowRequest(requestId);
} }
@@ -2722,19 +2828,18 @@ buildPendingAssetRequestCardHtml(item) {
} }
} }
async deletePendingAssetBorrowRequest(requestId) { async deletePendingAssetBorrowRequest(requestId, options = {}) {
if (!this.canCurrentUserManageAssets()) {
this.notifyWarning('Chỉ role Asset/Admin mới được xóa đơn chờ.');
return;
}
const targetId = Number(requestId); const targetId = Number(requestId);
if (!Number.isFinite(targetId) || targetId <= 0) { if (!Number.isFinite(targetId) || targetId <= 0) {
this.notifyWarning('Không xác định được đơn cần xóa.'); this.notifyWarning('Không xác định được đơn cần xóa.');
return; return;
} }
const confirmed = window.confirm(`Bạn có chắc muốn xóa đơn chờ #${targetId}?`); const confirmMessage = String(options?.confirmMessage || `Bạn có chắc muốn xóa đơn chờ #${targetId}?`);
const confirmButtonText = String(options?.confirmButtonText || 'Xóa đơn');
const successMessage = String(options?.successMessage || 'Đã xóa đơn chờ');
const failureMessage = String(options?.failureMessage || 'Xóa đơn chờ thất bại');
const confirmed = await this.confirmAssetRequestDelete(confirmMessage, confirmButtonText);
if (!confirmed) { if (!confirmed) {
return; return;
} }
@@ -2747,11 +2852,16 @@ buildPendingAssetRequestCardHtml(item) {
const data = await response.json(); const data = await response.json();
if (!response.ok || !data.success) { if (!response.ok || !data.success) {
this.notifyFailure(data.message || 'Xóa đơn chờ thất bại'); const resolvedFailureMessage = data.message || failureMessage;
if (response.status === 403) {
this.notifyWarning(resolvedFailureMessage);
} else {
this.notifyFailure(resolvedFailureMessage);
}
return; return;
} }
this.notifySuccess(data.message || 'Đã xóa đơn chờ'); this.notifySuccess(data.message || successMessage);
await this.fetchAssetBorrows(); await this.fetchAssetBorrows();
if (this.currentPage === 'asset-borrows') { if (this.currentPage === 'asset-borrows') {
@@ -2766,7 +2876,7 @@ buildPendingAssetRequestCardHtml(item) {
this.updatePendingAssetRequestsBadge(); this.updatePendingAssetRequestsBadge();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
this.notifyFailure('Xóa đơn chờ thất bại'); this.notifyFailure(failureMessage);
} }
} }
getAssetsContent() { getAssetsContent() {
@@ -5279,6 +5389,9 @@ buildPendingAssetRequestCardHtml(item) {
} }
closeModals() { closeModals() {
if (this.pendingAssetRequestDeleteConfirmResolver) {
this.resolveAssetRequestDeleteConfirm(false);
}
document.querySelectorAll('.modal-backdrop').forEach(modal => { document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open'); modal.classList.remove('open');
}); });
@@ -6186,6 +6299,9 @@ buildPendingAssetRequestCardHtml(item) {
// Global modal close functions // Global modal close functions
function closeAllModals() { function closeAllModals() {
if (app?.pendingAssetRequestDeleteConfirmResolver) {
app.resolveAssetRequestDeleteConfirm(false);
}
document.querySelectorAll('.modal-backdrop').forEach(modal => { document.querySelectorAll('.modal-backdrop').forEach(modal => {
modal.classList.remove('open'); modal.classList.remove('open');
}); });
@@ -6250,6 +6366,7 @@ function closeAssetPendingRequestsModal() {
if (modal) { if (modal) {
modal.classList.remove('open'); modal.classList.remove('open');
} }
closeAssetRequestDeleteConfirmModal();
const rejectModal = document.getElementById('assetRequestRejectModal'); const rejectModal = document.getElementById('assetRequestRejectModal');
if (rejectModal) { if (rejectModal) {
rejectModal.classList.remove('open'); rejectModal.classList.remove('open');
@@ -6263,6 +6380,18 @@ function closeAssetRequestRejectModal() {
} }
} }
function closeAssetRequestDeleteConfirmModal() {
if (app?.pendingAssetRequestDeleteConfirmResolver) {
app.resolveAssetRequestDeleteConfirm(false);
return;
}
const modal = document.getElementById('assetRequestDeleteConfirmModal');
if (modal) {
modal.classList.remove('open');
}
}
function closeAssetDepartmentModal() { function closeAssetDepartmentModal() {
const modal = document.getElementById('assetDepartmentModal'); const modal = document.getElementById('assetDepartmentModal');
if (modal) { if (modal) {

View File

@@ -454,6 +454,23 @@
</div> </div>
</div> </div>
<!-- Confirm Delete/Cancel Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[120] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestDeleteConfirmModal" style="z-index: 140;">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Xác nhận thao tác</h3>
</div>
<div class="p-6">
<p id="assetRequestDeleteConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn thực hiện thao tác này?</p>
<div class="flex gap-3">
<button type="button" class="cancel-asset-request-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg">Đóng</button>
<button type="button" id="confirmAssetRequestDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa đơn</button>
</div>
</div>
</div>
</div>
<!-- Reject Asset Request Modal --> <!-- Reject Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestRejectModal" style="z-index: 130;"> <div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestRejectModal" style="z-index: 130;">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4"> <div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">

View File

@@ -280,7 +280,7 @@
<button id="pendingAssetRequestsBtn" type="button" class="hidden relative flex items-center gap-2 px-3 py-2 rounded-lg border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-800 transition-colors" title="Đơn chờ xử lý"> <button id="pendingAssetRequestsBtn" type="button" class="hidden relative flex items-center gap-2 px-3 py-2 rounded-lg border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-800 transition-colors" title="Đơn chờ xử lý">
<span class="material-symbols-outlined text-base">notifications_active</span> <span class="material-symbols-outlined text-base">notifications_active</span>
<span class="text-xs font-bold">Đơn chờ</span> <span class="text-xs font-bold">Đơn chờ</span>
<span id="pendingAssetRequestsBadge" class="hidden absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-full bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">0</span> <span id="pendingAssetRequestsBadge" class="hidden absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-[999px] bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">0</span>
</button> </button>
<button id="profileBtn" type="button" class="profile-btn flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Sửa hồ sơ"> <button id="profileBtn" type="button" class="profile-btn flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Sửa hồ sơ">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span> <span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>