tài sản mượn

This commit is contained in:
2026-05-07 09:44:39 +07:00
parent 395b1f6e85
commit f49d5a427c
2 changed files with 288 additions and 2 deletions

View File

@@ -26,6 +26,8 @@ class AccountManager {
this.assetPageSize = 10;
this.assetBorrowPage = 1;
this.assetBorrowPageSize = 10;
this.myBorrowedAssetPage = 1;
this.myBorrowedAssetPageSize = 10;
this.apiBase = '/api';
this.currentPage = 'dashboard';
this.accountSearchTerm = '';
@@ -37,6 +39,7 @@ class AccountManager {
this.assetStatusFilter = '';
this.assetBorrows = [];
this.assetBorrowSearchTerm = '';
this.myBorrowedAssetSearchTerm = '';
this.assetBorrowProductSearchTimer = undefined;
this.assetBorrowProductItems = [];
this.assetBorrowProductQuery = '';
@@ -234,6 +237,9 @@ class AccountManager {
mainContent.innerHTML = this.getAssetBorrowsContent();
this.setupAssetBorrowListeners();
this.setupAddButtonListeners();
} else if (page === 'my-borrowed-assets') {
mainContent.innerHTML = this.getMyBorrowedAssetsContent();
this.setupMyBorrowedAssetsListeners();
} else if (page === 'asset-departments') {
mainContent.innerHTML = this.getAssetDepartmentsContent();
this.setupAssetDepartmentListeners();
@@ -1081,6 +1087,7 @@ class AccountManager {
const appSearch = document.getElementById('appSearch');
const assetSearch = document.getElementById('assetSearch');
const assetBorrowSearch = document.getElementById('assetBorrowSearch');
const myBorrowedAssetSearch = document.getElementById('myBorrowedAssetSearch');
const assetDepartmentSearch = document.getElementById('assetDepartmentSearch');
const assetProjectSearch = document.getElementById('assetProjectSearch');
@@ -1108,6 +1115,12 @@ class AccountManager {
assetBorrowSearch.setSelectionRange(pos, pos);
}
if (myBorrowedAssetSearch && myBorrowedAssetSearch.dataset.focused === 'true') {
const pos = myBorrowedAssetSearch.selectionStart || myBorrowedAssetSearch.value.length;
myBorrowedAssetSearch.focus();
myBorrowedAssetSearch.setSelectionRange(pos, pos);
}
if (assetDepartmentSearch && assetDepartmentSearch.dataset.focused === 'true') {
const pos = assetDepartmentSearch.selectionStart || assetDepartmentSearch.value.length;
assetDepartmentSearch.focus();
@@ -1358,6 +1371,104 @@ class AccountManager {
});
}
normalizeNameForMatching(value) {
const normalized = String(value || '').trim().toLowerCase();
if (!normalized) {
return '';
}
return normalized
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/\s+/g, ' ');
}
getCurrentUserBorrowerNameKeys() {
const candidates = [
this.getCurrentUserDisplayName(),
this.currentUser?.FullName,
this.currentUser?.fullname,
this.currentUser?.user?.FullName,
this.currentUser?.user?.fullname,
this.currentUser?.Username,
this.currentUser?.username,
this.currentUser?.user?.Username,
this.currentUser?.user?.username
];
const keys = new Set();
candidates.forEach(item => {
const key = this.normalizeNameForMatching(item);
if (key) {
keys.add(key);
}
});
return [...keys];
}
getCurrentUserBorrowedAssets() {
const borrowerKeys = new Set(this.getCurrentUserBorrowerNameKeys());
if (!borrowerKeys.size) {
return [];
}
return this.assets
.map(asset => {
const borrowerEntries = this.parseBorrowerEntries(asset?.Borrower);
const matchedEntries = borrowerEntries.filter(entry => {
const key = this.normalizeNameForMatching(entry?.name);
return key && borrowerKeys.has(key);
});
const borrowedQuantity = matchedEntries.reduce((sum, entry) => (
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
), 0);
if (borrowedQuantity <= 0) {
return null;
}
return {
...asset,
BorrowedQuantityByCurrentUser: borrowedQuantity,
BorrowedNamesByCurrentUser: matchedEntries
.map(entry => this.formatBorrowerDisplay(entry?.name, entry?.quantity))
.filter(Boolean)
.join('; ')
};
})
.filter(Boolean);
}
getFilteredMyBorrowedAssets() {
const search = String(this.myBorrowedAssetSearchTerm || '').toLowerCase();
const rows = this.getCurrentUserBorrowedAssets();
if (!search) {
return rows;
}
return rows.filter(item => {
const haystack = [
item.AssetCode,
item.AssetName,
item.Model,
item.SerialNumber,
item.Project,
item.Department,
item.Location,
item.Unit,
item.Status,
item.BorrowedQuantityByCurrentUser,
item.BorrowedNamesByCurrentUser,
item.Notes
].map(value => String(value || '').toLowerCase());
return haystack.some(value => value.includes(search));
});
}
getFilteredAssetBorrows() {
const search = String(this.assetBorrowSearchTerm || '').toLowerCase();
const rows = Array.isArray(this.assetBorrows) ? this.assetBorrows : [];
@@ -2895,6 +3006,177 @@ class AccountManager {
`;
}
renderMyBorrowedAssetsPager(pageInfo) {
const pager = document.getElementById('myBorrowedAssetsPager');
if (!pager) {
return;
}
pager.innerHTML = `
<span>Hiển thị ${pageInfo.start}-${pageInfo.end} / ${pageInfo.total}</span>
<div class="flex items-center gap-2">
<button class="my-borrowed-asset-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === 1 ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current - 1}" ${pageInfo.current === 1 ? 'disabled' : ''}>Trước</button>
<span class="text-[11px]">Trang ${pageInfo.current} / ${pageInfo.totalPages}</span>
<button class="my-borrowed-asset-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === pageInfo.totalPages ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current + 1}" ${pageInfo.current === pageInfo.totalPages ? 'disabled' : ''}>Tiếp</button>
</div>
`;
}
getMyBorrowedAssetsContent() {
const filteredAssets = this.getFilteredMyBorrowedAssets();
const pageInfo = this.getPaged(filteredAssets, this.myBorrowedAssetPage, this.myBorrowedAssetPageSize);
this.myBorrowedAssetPage = pageInfo.current;
return `
<div class="my-borrowed-assets-page flex flex-col p-4 md:p-6 overflow-hidden h-full">
<div class="page-header flex items-center justify-between gap-4 mb-5 shrink-0">
<div>
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Tài sản đang mượn</h1>
<p class="text-sm text-on-surface-variant">Danh sách tài sản bạn đang mượn theo tài khoản đăng nhập.</p>
</div>
</div>
<div class="page-filters flex items-center gap-3 mb-4 shrink-0">
<div class="flex items-center gap-1.5 flex-1">
<span class="text-[10px] font-bold uppercase text-on-surface-variant">Tìm kiếm</span>
<input
id="myBorrowedAssetSearch"
value="${this.escapeHtml(this.myBorrowedAssetSearchTerm)}"
class="flex-1 bg-surface-container-low border-slate-200 rounded-md text-[11px] py-1 px-2 focus:ring-1 focus:ring-primary shadow-sm"
placeholder="Mã, tên tài sản, dự án, vị trí..."
>
</div>
</div>
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse">
<thead class="sticky top-0 z-10 bg-slate-50 border-b border-slate-200">
<tr>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">STT</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Mã tài sản</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tên tài sản</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Số lượng đang mượn</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Đơn vị</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vị trí</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Trạng thái</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 my-borrowed-assets-table-body">
${pageInfo.data.length > 0 ? pageInfo.data.map((asset, index) => {
const statusMeta = this.getAssetStatusMeta(asset.Status);
return `
<tr class="hover:bg-slate-50/80 transition-colors">
<td class="px-4 py-3 text-sm text-slate-600">${pageInfo.start + index}</td>
<td class="px-4 py-3 text-sm font-semibold text-slate-700">${this.escapeHtml(asset.AssetCode || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-700">${this.escapeHtml(asset.AssetName || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-700 font-bold">${Number(asset.BorrowedQuantityByCurrentUser) || 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(asset.Unit || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(asset.Project || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(asset.Location || '-')}</td>
<td class="px-4 py-3 text-sm">
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}">${statusMeta.label}</span>
</td>
<td class="px-4 py-3 text-sm text-slate-600 max-w-xs truncate" title="${this.escapeHtml(asset.Notes || '')}">${this.escapeHtml(asset.Notes || '-')}</td>
</tr>
`;
}).join('') : `
<tr>
<td colspan="9" class="px-4 py-8 text-sm text-center text-slate-500">Hiện tại bạn chưa mượn tài sản nào.</td>
</tr>
`}
</tbody>
</table>
</div>
<div class="page-pager flex items-center justify-between px-4 py-2 border-t border-slate-200 bg-slate-50 text-xs text-slate-600" id="myBorrowedAssetsPager">
<span>Hiển thị ${pageInfo.start}-${pageInfo.end} / ${pageInfo.total}</span>
<div class="flex items-center gap-2">
<button class="my-borrowed-asset-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === 1 ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current - 1}" ${pageInfo.current === 1 ? 'disabled' : ''}>Trước</button>
<span class="text-[11px]">Trang ${pageInfo.current} / ${pageInfo.totalPages}</span>
<button class="my-borrowed-asset-page-btn px-2 py-1 border border-slate-200 rounded text-xs ${pageInfo.current === pageInfo.totalPages ? 'opacity-50 cursor-not-allowed' : ''}" data-page="${pageInfo.current + 1}" ${pageInfo.current === pageInfo.totalPages ? 'disabled' : ''}>Tiếp</button>
</div>
</div>
</div>
</div>
`;
}
renderMyBorrowedAssetsTableBody() {
const tbody = document.querySelector('.my-borrowed-assets-table-body');
if (!tbody) {
return;
}
const pageInfo = this.getPaged(this.getFilteredMyBorrowedAssets(), this.myBorrowedAssetPage, this.myBorrowedAssetPageSize);
this.myBorrowedAssetPage = pageInfo.current;
if (!pageInfo.data.length) {
tbody.innerHTML = `
<tr>
<td colspan="9" class="px-4 py-8 text-sm text-center text-slate-500">Hiện tại bạn chưa mượn tài sản nào.</td>
</tr>
`;
} else {
tbody.innerHTML = pageInfo.data.map((asset, index) => {
const statusMeta = this.getAssetStatusMeta(asset.Status);
return `
<tr class="hover:bg-slate-50/80 transition-colors">
<td class="px-4 py-3 text-sm text-slate-600">${pageInfo.start + index}</td>
<td class="px-4 py-3 text-sm font-semibold text-slate-700">${this.escapeHtml(asset.AssetCode || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-700">${this.escapeHtml(asset.AssetName || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-700 font-bold">${Number(asset.BorrowedQuantityByCurrentUser) || 0}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(asset.Unit || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(asset.Project || '-')}</td>
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(asset.Location || '-')}</td>
<td class="px-4 py-3 text-sm">
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}">${statusMeta.label}</span>
</td>
<td class="px-4 py-3 text-sm text-slate-600 max-w-xs truncate" title="${this.escapeHtml(asset.Notes || '')}">${this.escapeHtml(asset.Notes || '-')}</td>
</tr>
`;
}).join('');
}
this.renderMyBorrowedAssetsPager(pageInfo);
this.setupMyBorrowedAssetsPagerListeners();
}
setupMyBorrowedAssetsPagerListeners() {
document.querySelectorAll('.my-borrowed-asset-page-btn').forEach(btn => {
btn.addEventListener('click', () => {
const targetPage = Number(btn.dataset.page);
if (!targetPage || targetPage < 1) {
return;
}
this.myBorrowedAssetPage = targetPage;
this.renderMyBorrowedAssetsTableBody();
});
});
}
setupMyBorrowedAssetsListeners() {
const searchInput = document.getElementById('myBorrowedAssetSearch');
if (searchInput && searchInput.dataset.boundInput !== 'true') {
searchInput.addEventListener('input', event => {
this.myBorrowedAssetSearchTerm = String(event.target.value || '').trim();
this.myBorrowedAssetPage = 1;
this.renderMyBorrowedAssetsTableBody();
});
searchInput.addEventListener('focus', () => {
searchInput.dataset.focused = 'true';
});
searchInput.addEventListener('blur', () => {
searchInput.dataset.focused = 'false';
});
searchInput.dataset.boundInput = 'true';
}
this.setupMyBorrowedAssetsPagerListeners();
}
getAssetBorrowsContent() {
const canManageAssets = this.canCurrentUserManageAssets();
const filteredBorrows = this.getFilteredAssetBorrows();
@@ -4592,8 +4874,8 @@ class AccountManager {
await this.fetchAssets();
await this.fetchAssetDepartments();
await this.fetchAssetProjects();
if (this.currentPage === 'assets') {
this.renderView('assets');
if (this.currentPage === 'assets' || this.currentPage === 'my-borrowed-assets') {
this.renderView(this.currentPage);
}
}

View File

@@ -244,6 +244,10 @@
<span class="material-symbols-outlined">assignment_returned</span>
<span>Mượn/Trả tài sản</span>
</a>
<a href="#my-borrowed-assets" data-nav="my-borrowed-assets" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">inventory</span>
<span>Tài sản đang mượn</span>
</a>
<a href="#asset-departments" data-nav="asset-departments" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">apartment</span>
<span> Phòng Ban</span>