reponsive moblie

This commit is contained in:
2026-07-17 09:54:41 +07:00
parent c232fe698f
commit 5c818583dc
3 changed files with 241 additions and 9 deletions

File diff suppressed because one or more lines are too long

View File

@@ -5125,6 +5125,81 @@ class AccountManager {
}).join(''); }).join('');
} }
getConsumableMobileCardHtml(item, rowNumber, canManageAssets) {
const statusMeta = this.getConsumableStockStatusMeta(item);
const safe = value => this.escapeHtml(String(value ?? '').trim() || '-');
const number = value => Number.isFinite(Number(value)) ? Number(value) : 0;
const unit = String(item?.Unit || '').trim();
const exportedSummary = String(item?.ExportedSummary || '').trim();
const recipientSummary = String(item?.RecipientSummary || '').trim();
const projectSummary = String(item?.ProjectSummary || '').trim();
const reason = String(item?.ExportReason || '').trim();
return `
<article class="consumable-mobile-card rounded-xl border border-slate-200 bg-white p-3 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-[10px] font-bold uppercase tracking-wider text-slate-400">#${rowNumber} · ${safe(item?.ConsumableCode)}</div>
<h3 class="mt-0.5 text-sm font-extrabold text-slate-800">${safe(item?.ConsumableName)}</h3>
<div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-slate-500">
<span>Tháng: <strong class="text-slate-700">${safe(item?.RequestMonth)}</strong></span>
<span>Model: <strong class="text-slate-700">${safe(item?.Model)}</strong></span>
<span>ĐVT: <strong class="text-slate-700">${safe(unit)}</strong></span>
</div>
</div>
<span class="shrink-0 inline-block px-2 py-1 rounded text-[10px] font-semibold ${statusMeta.className}">${statusMeta.label}</span>
</div>
<div class="mt-3 grid grid-cols-4 gap-1.5 border-y border-slate-100 py-3 text-center">
<div class="rounded-lg bg-slate-50 px-1 py-2">
<span class="block text-[9px] font-bold uppercase text-slate-400">Tồn đầu</span>
<strong class="text-sm text-slate-700">${number(item?.OpeningBalance)}</strong>
</div>
<div class="rounded-lg bg-emerald-50 px-1 py-2">
<span class="block text-[9px] font-bold uppercase text-emerald-600">Nhập</span>
<strong class="text-sm text-emerald-700">${number(item?.ImportInPeriod)}</strong>
</div>
<div class="rounded-lg bg-amber-50 px-1 py-2">
<span class="block text-[9px] font-bold uppercase text-amber-600">Xuất</span>
<strong class="text-sm text-amber-700">${number(item?.ExportInPeriod)}</strong>
</div>
<div class="rounded-lg bg-blue-50 px-1 py-2">
<span class="block text-[9px] font-bold uppercase text-blue-600">Tồn cuối</span>
<strong class="text-sm text-blue-700">${number(item?.EndingBalance)}</strong>
</div>
</div>
${(exportedSummary || recipientSummary || projectSummary) ? `
<div class="mt-3 grid grid-cols-1 gap-2 rounded-lg bg-slate-50 px-3 py-2 text-xs text-slate-600">
${exportedSummary ? `<div><span class="font-bold text-slate-500">Đã xuất:</span> <span class="whitespace-pre-line">${safe(exportedSummary)}</span></div>` : ''}
${recipientSummary ? `<div><span class="font-bold text-slate-500">Người đang nhận:</span> <span class="whitespace-pre-line">${safe(recipientSummary)}</span></div>` : ''}
${projectSummary ? `<div><span class="font-bold text-slate-500">Dự án nhận:</span> <span class="whitespace-pre-line">${safe(projectSummary)}</span></div>` : ''}
</div>
` : ''}
<div class="mt-3 flex items-end justify-between gap-3 text-xs">
<div class="min-w-0 text-slate-600">
${reason ? `<div><span class="font-bold text-slate-500">Lý do:</span> ${safe(reason)}</div>` : ''}
<div class="mt-0.5 text-[10px] text-slate-400">Cập nhật: ${this.formatDateOnly(item?.UpdatedDate || item?.CreatedDate)}</div>
</div>
<div class="flex shrink-0 items-center gap-1.5">
<button class="export-consumable inline-flex items-center gap-1 rounded-md border border-emerald-200 px-2.5 py-2 text-xs font-bold text-emerald-700 ${canManageAssets ? '' : 'opacity-40 cursor-not-allowed'}" data-consumable-id="${item?.ConsumableId}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">outbox</span>
Xuất
</button>
<button class="edit-consumable inline-flex items-center gap-1 rounded-md border border-primary/30 px-2.5 py-2 text-xs font-bold text-primary ${canManageAssets ? '' : 'opacity-40 cursor-not-allowed'}" data-consumable-id="${item?.ConsumableId}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">edit</span>
Sửa
</button>
<button class="delete-consumable inline-flex items-center justify-center rounded-md border border-red-200 p-2 text-red-600 ${canManageAssets ? '' : 'opacity-40 cursor-not-allowed'}" data-consumable-id="${item?.ConsumableId}" title="Xóa" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">delete</span>
</button>
</div>
</div>
</article>
`;
}
getConsumablesContent() { getConsumablesContent() {
const canManageAssets = this.canCurrentUserManageAssets(); const canManageAssets = this.canCurrentUserManageAssets();
const filteredConsumables = this.getFilteredConsumables(); const filteredConsumables = this.getFilteredConsumables();
@@ -5149,7 +5224,7 @@ class AccountManager {
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Vật tư tiêu hao</h1> <h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Vật tư tiêu hao</h1>
<p class="text-sm text-on-surface-variant">Quản lý xuất nhập tồn vật tư tiêu hao theo tháng.</p> <p class="text-sm text-on-surface-variant">Quản lý xuất nhập tồn vật tư tiêu hao theo tháng.</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="consumable-header-actions flex flex-wrap items-center gap-2">
<button id="createConsumableBorrowRequestBtn" class="bg-primary text-on-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 hover:bg-primary-dim"> <button id="createConsumableBorrowRequestBtn" class="bg-primary text-on-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 hover:bg-primary-dim">
<span class="material-symbols-outlined text-base">assignment_add</span> <span class="material-symbols-outlined text-base">assignment_add</span>
Tạo đơn mượn Tạo đơn mượn
@@ -5214,7 +5289,7 @@ class AccountManager {
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0"> <div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
${pageInfo.data.length > 0 ? ` ${pageInfo.data.length > 0 ? `
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1"> <div class="consumables-desktop-table table-wrap overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse" style="min-width: 2120px; border-collapse: separate; border-spacing: 0;"> <table class="w-full text-left border-collapse" style="min-width: 2120px; border-collapse: separate; border-spacing: 0;">
<thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200"> <thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200">
<tr> <tr>
@@ -5242,6 +5317,9 @@ class AccountManager {
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="consumables-mobile-list flex-1 flex-col gap-3 overflow-y-auto bg-slate-50/70 p-3">
${pageInfo.data.map((item, index) => this.getConsumableMobileCardHtml(item, pageInfo.start + index, canManageAssets)).join('')}
</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="consumablesPager"> <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="consumablesPager">
<span>Hiển thị ${pageInfo.start}-${pageInfo.end} / ${pageInfo.total}</span> <span>Hiển thị ${pageInfo.start}-${pageInfo.end} / ${pageInfo.total}</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -5279,6 +5357,14 @@ class AccountManager {
this.consumablePage = pageInfo.current; this.consumablePage = pageInfo.current;
tbody.innerHTML = this.buildConsumableTableRows(pageInfo); tbody.innerHTML = this.buildConsumableTableRows(pageInfo);
const mobileList = document.querySelector('.consumables-mobile-list');
if (mobileList) {
const canManageAssets = this.canCurrentUserManageAssets();
mobileList.innerHTML = pageInfo.data
.map((item, index) => this.getConsumableMobileCardHtml(item, pageInfo.start + index, canManageAssets))
.join('');
}
const pager = document.getElementById('consumablesPager'); const pager = document.getElementById('consumablesPager');
if (pager) { if (pager) {
pager.innerHTML = ` pager.innerHTML = `
@@ -5628,6 +5714,65 @@ class AccountManager {
this.setupConsumableExportHistoryPagerListeners(); this.setupConsumableExportHistoryPagerListeners();
} }
getAssetMobileCardHtml(asset, rowNumber, canManageAssets) {
const statusMeta = this.getAssetStatusMeta(asset?.Status);
const assetId = Number(asset?.AssetId);
const isSelected = Number.isFinite(assetId) && this.selectedAssetIds.has(assetId);
const unit = String(asset?.Unit || '').trim();
const endingBalance = Number(asset?.EndingBalance) || 0;
const borrowedQuantity = Number(asset?.ExportInPeriod) || 0;
const borrower = this.formatBorrowerSummaryText(asset?.Borrower);
const safe = value => this.escapeHtml(String(value ?? '').trim() || '-');
return `
<article class="asset-mobile-card rounded-xl border border-slate-200 bg-white p-3 shadow-sm">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 items-start gap-2">
<input type="checkbox" class="asset-row-checkbox mt-1 w-4 h-4 shrink-0" data-asset-id="${asset?.AssetId}" ${isSelected ? 'checked' : ''} ${canManageAssets ? '' : 'disabled'} aria-label="Chọn tài sản ${safe(asset?.AssetCode)}">
<div class="min-w-0">
<div class="text-[10px] font-bold uppercase tracking-wider text-slate-400">#${rowNumber} · ${safe(asset?.AssetCode)}</div>
<h3 class="mt-0.5 text-sm font-extrabold text-slate-800">${safe(asset?.AssetName)}</h3>
</div>
</div>
<span class="shrink-0 inline-block px-2 py-1 rounded text-[10px] font-semibold ${statusMeta.className}">${statusMeta.label}</span>
</div>
<div class="mt-3 grid grid-cols-2 gap-x-3 gap-y-2 border-t border-slate-100 pt-3 text-xs">
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Model</span><span class="text-slate-700">${safe(asset?.Model)}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Serial</span><span class="text-slate-700">${safe(asset?.SerialNumber)}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Tồn cuối kỳ</span><span class="font-bold text-slate-800">${endingBalance}${unit ? ` ${safe(unit)}` : ''}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Đang mượn</span><span class="font-bold text-slate-800">${borrowedQuantity}${unit ? ` ${safe(unit)}` : ''}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Phòng ban</span><span class="text-slate-700">${safe(asset?.Department)}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Dự án</span><span class="text-slate-700">${safe(asset?.Project)}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Vị trí</span><span class="text-slate-700">${safe(asset?.Location)}</span></div>
<div><span class="block text-[10px] font-bold uppercase text-slate-400">Phụ trách</span><span class="text-slate-700">${safe(asset?.Custodian)}</span></div>
</div>
${borrower ? `
<div class="mt-3 rounded-lg bg-slate-50 px-3 py-2 text-xs text-slate-600">
<span class="block text-[10px] font-bold uppercase text-slate-400">Người mượn</span>
<span class="whitespace-pre-line">${safe(borrower)}</span>
</div>
` : ''}
<div class="mt-3 flex items-center justify-end gap-2 border-t border-slate-100 pt-3">
<button class="view-asset inline-flex items-center gap-1 rounded-md border border-slate-200 px-3 py-2 text-xs font-bold text-slate-600" data-asset-id="${asset?.AssetId}">
<span class="material-symbols-outlined text-base">info</span>
Chi tiết
</button>
<button class="edit-asset inline-flex items-center gap-1 rounded-md border border-primary/30 px-3 py-2 text-xs font-bold text-primary ${canManageAssets ? '' : 'opacity-40 cursor-not-allowed'}" data-asset-id="${asset?.AssetId}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">edit</span>
Sửa
</button>
<button class="delete-asset inline-flex items-center gap-1 rounded-md border border-red-200 px-3 py-2 text-xs font-bold text-red-600 ${canManageAssets ? '' : 'opacity-40 cursor-not-allowed'}" data-asset-id="${asset?.AssetId}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">delete</span>
Xóa
</button>
</div>
</article>
`;
}
getAssetsContent() { getAssetsContent() {
this.syncSelectedAssetIds(); this.syncSelectedAssetIds();
const canManageAssets = this.canCurrentUserManageAssets(); const canManageAssets = this.canCurrentUserManageAssets();
@@ -5648,7 +5793,7 @@ class AccountManager {
<h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Quản lý tài sản</h1> <h1 class="text-2xl font-extrabold text-on-surface tracking-tight">Quản lý tài sản</h1>
<p class="text-sm text-on-surface-variant">Theo dõi tài sản, kho và trạng thái sử dụng.</p> <p class="text-sm text-on-surface-variant">Theo dõi tài sản, kho và trạng thái sử dụng.</p>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="asset-header-actions flex flex-wrap items-center gap-2">
<button id="importAssetBtn" class="border border-slate-300 text-slate-700 px-3 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-slate-100' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}> <button id="importAssetBtn" class="border border-slate-300 text-slate-700 px-3 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-slate-100' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
<span class="material-symbols-outlined text-base">upload_file</span> <span class="material-symbols-outlined text-base">upload_file</span>
Nhập Excel Nhập Excel
@@ -5703,7 +5848,7 @@ class AccountManager {
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0"> <div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
${pageInfo.data.length > 0 ? ` ${pageInfo.data.length > 0 ? `
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1"> <div class="assets-desktop-table table-wrap overflow-y-auto overflow-x-auto flex-1">
<table class="w-full text-left border-collapse" style="min-width: 2720px; border-collapse: separate; border-spacing: 0;"> <table class="w-full text-left border-collapse" style="min-width: 2720px; border-collapse: separate; border-spacing: 0;">
<thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200"> <thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200">
<tr> <tr>
@@ -5791,6 +5936,9 @@ class AccountManager {
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="assets-mobile-list flex-1 flex-col gap-3 overflow-y-auto bg-slate-50/70 p-3">
${pageInfo.data.map((asset, index) => this.getAssetMobileCardHtml(asset, pageInfo.start + index, canManageAssets)).join('')}
</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="assetsPager"> <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="assetsPager">
<span>Hiển thị ${pageInfo.start}-${pageInfo.end} / ${pageInfo.total}</span> <span>Hiển thị ${pageInfo.start}-${pageInfo.end} / ${pageInfo.total}</span>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -5876,6 +6024,13 @@ class AccountManager {
`; `;
}).join(''); }).join('');
const mobileList = document.querySelector('.assets-mobile-list');
if (mobileList) {
mobileList.innerHTML = pageInfo.data
.map((asset, index) => this.getAssetMobileCardHtml(asset, pageInfo.start + index, canManageAssets))
.join('');
}
const pager = document.getElementById('assetsPager'); const pager = document.getElementById('assetsPager');
if (pager) { if (pager) {
pager.innerHTML = ` pager.innerHTML = `
@@ -5916,6 +6071,10 @@ class AccountManager {
return; return;
} }
document.querySelectorAll(`.asset-row-checkbox[data-asset-id="${assetId}"]`).forEach(peer => {
peer.checked = checkbox.checked;
});
if (checkbox.checked) { if (checkbox.checked) {
this.selectedAssetIds.add(assetId); this.selectedAssetIds.add(assetId);
} else { } else {
@@ -5963,7 +6122,15 @@ class AccountManager {
updateAssetBulkActionState() { updateAssetBulkActionState() {
const canManageAssets = this.canCurrentUserManageAssets(); const canManageAssets = this.canCurrentUserManageAssets();
const rowCheckboxes = Array.from(document.querySelectorAll('.asset-row-checkbox')); const rowCheckboxes = Array.from(document.querySelectorAll('.asset-row-checkbox'));
const selectedOnPage = rowCheckboxes.filter(checkbox => checkbox.checked).length; const assetSelectionById = new Map();
rowCheckboxes.forEach(checkbox => {
const assetId = Number(checkbox.dataset.assetId);
if (!Number.isFinite(assetId)) {
return;
}
assetSelectionById.set(assetId, (assetSelectionById.get(assetId) || false) || checkbox.checked);
});
const selectedOnPage = Array.from(assetSelectionById.values()).filter(Boolean).length;
if (!canManageAssets) { if (!canManageAssets) {
this.selectedAssetIds.clear(); this.selectedAssetIds.clear();
@@ -5974,9 +6141,10 @@ class AccountManager {
const selectAllCheckbox = document.getElementById('selectAllAssetsCheckbox'); const selectAllCheckbox = document.getElementById('selectAllAssetsCheckbox');
if (selectAllCheckbox) { if (selectAllCheckbox) {
const hasRows = rowCheckboxes.length > 0; const assetCountOnPage = assetSelectionById.size;
selectAllCheckbox.checked = canManageAssets && hasRows && selectedOnPage === rowCheckboxes.length; const hasRows = assetCountOnPage > 0;
selectAllCheckbox.indeterminate = canManageAssets && selectedOnPage > 0 && selectedOnPage < rowCheckboxes.length; selectAllCheckbox.checked = canManageAssets && hasRows && selectedOnPage === assetCountOnPage;
selectAllCheckbox.indeterminate = canManageAssets && selectedOnPage > 0 && selectedOnPage < assetCountOnPage;
selectAllCheckbox.disabled = !canManageAssets; selectAllCheckbox.disabled = !canManageAssets;
} }

View File

@@ -73,6 +73,11 @@
display: none; display: none;
} }
.assets-mobile-list,
.consumables-mobile-list {
display: none;
}
@media (max-width: 900px) { @media (max-width: 900px) {
body.app-shell { body.app-shell {
width: 100%; width: 100%;
@@ -177,6 +182,54 @@
min-width: 700px; min-width: 700px;
} }
.assets-page,
.consumables-page {
padding: 0.75rem;
}
.assets-page .page-header,
.consumables-page .page-header {
margin-bottom: 0.75rem;
}
.assets-page .asset-header-actions,
.consumables-page .consumable-header-actions {
width: 100%;
display: flex;
flex-flow: row nowrap;
overflow-x: auto;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
padding-bottom: 0.25rem;
}
.assets-page .asset-header-actions > button,
.consumables-page .consumable-header-actions > button {
width: auto;
flex: 0 0 auto;
}
.assets-page .page-filters,
.consumables-page .page-filters {
margin-bottom: 0.75rem;
}
.assets-page .assets-desktop-table,
.consumables-page .consumables-desktop-table {
display: none;
}
.assets-page .assets-mobile-list,
.consumables-page .consumables-mobile-list {
display: flex;
min-height: 0;
}
.assets-page .asset-mobile-card,
.consumables-page .consumable-mobile-card {
overflow-wrap: anywhere;
}
.page-pager { .page-pager {
gap: 0.5rem; gap: 0.5rem;
flex-direction: column; flex-direction: column;
@@ -201,7 +254,18 @@
.apps-stats { .apps-stats {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.assets-page .page-header h1,
.consumables-page .page-header h1 {
font-size: 1.25rem;
} }
.assets-page .page-header p,
.consumables-page .page-header p {
font-size: 0.75rem;
}
}
</style> </style>
</head> </head>
<body class="app-shell bg-background text-on-surface antialiased flex h-screen w-screen"> <body class="app-shell bg-background text-on-surface antialiased flex h-screen w-screen">