tài sản mượn
This commit is contained in:
286
public/js/app.js
286
public/js/app.js
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user