case hỏng
This commit is contained in:
@@ -692,6 +692,19 @@ function normalizeImportToken(value) {
|
||||
.replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
function normalizeAssetDamageType(value) {
|
||||
const normalized = normalizeImportToken(value);
|
||||
if (['disposed', 'dispose', 'disposal', 'thanhly', 'liquidated', 'liquidation'].includes(normalized)) {
|
||||
return 'disposed';
|
||||
}
|
||||
|
||||
return 'damaged';
|
||||
}
|
||||
|
||||
function getAssetDamageTypeLabel(value) {
|
||||
return normalizeAssetDamageType(value) === 'disposed' ? 'Thanh lý' : 'Hỏng';
|
||||
}
|
||||
|
||||
function isHeaderLikeAssetImportRow(row = {}) {
|
||||
const headerTokens = new Set([
|
||||
'stt',
|
||||
@@ -1841,6 +1854,41 @@ async function createTables() {
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
|
||||
)
|
||||
END`,
|
||||
|
||||
// Asset Damage/Disposal History Table
|
||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDamageDisposalHistory')
|
||||
BEGIN
|
||||
CREATE TABLE AssetDamageDisposalHistory (
|
||||
DamageHistoryId INT PRIMARY KEY IDENTITY(1,1),
|
||||
AssetId INT NOT NULL,
|
||||
AssetCode NVARCHAR(100) NOT NULL,
|
||||
AssetName NVARCHAR(255) NOT NULL,
|
||||
ActionType NVARCHAR(20) NOT NULL,
|
||||
ActionLabel NVARCHAR(50) NOT NULL,
|
||||
ActionQuantity INT NOT NULL DEFAULT 1,
|
||||
Unit NVARCHAR(50) NULL,
|
||||
PreviousQuantity INT NOT NULL DEFAULT 0,
|
||||
NextQuantity INT NOT NULL DEFAULT 0,
|
||||
PreviousImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
NextImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
PreviousExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
NextExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
PreviousEndingBalance INT NOT NULL DEFAULT 0,
|
||||
NextEndingBalance INT NOT NULL DEFAULT 0,
|
||||
PreviousNewQuantity INT NOT NULL DEFAULT 0,
|
||||
NextNewQuantity INT NOT NULL DEFAULT 0,
|
||||
PreviousUsedQuantity INT NOT NULL DEFAULT 0,
|
||||
NextUsedQuantity INT NOT NULL DEFAULT 0,
|
||||
ActionNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
CreatedByName NVARCHAR(100) NULL,
|
||||
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
)
|
||||
END`,
|
||||
|
||||
// AuditLog Table
|
||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||
@@ -1911,6 +1959,15 @@ async function createTables() {
|
||||
console.error('AssetExportHistory index creation error:', err.message);
|
||||
}
|
||||
|
||||
// Ensure AssetDamageDisposalHistory indexes exist
|
||||
try {
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_AssetId') CREATE INDEX IX_AssetDamageDisposalHistory_AssetId ON AssetDamageDisposalHistory(AssetId);`);
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionDate') CREATE INDEX IX_AssetDamageDisposalHistory_ActionDate ON AssetDamageDisposalHistory(ActionDate DESC);`);
|
||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionType') CREATE INDEX IX_AssetDamageDisposalHistory_ActionType ON AssetDamageDisposalHistory(ActionType);`);
|
||||
} catch (err) {
|
||||
console.error('AssetDamageDisposalHistory index creation error:', err.message);
|
||||
}
|
||||
|
||||
// Ensure new columns exist on Applications for migrations
|
||||
try {
|
||||
await pool.request().query(`IF EXISTS (
|
||||
@@ -4979,6 +5036,291 @@ app.get('/api/asset-export-history', requireAssetOrAdmin, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/asset-damage-disposal-history', requireAssetOrAdmin, async (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(parsePositiveInteger(req.query.limit, 300), 2000);
|
||||
const result = await pool.request()
|
||||
.input('limit', sql.Int, limit)
|
||||
.query(`
|
||||
SELECT TOP (@limit)
|
||||
DamageHistoryId,
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
ActionType,
|
||||
ActionLabel,
|
||||
ActionQuantity,
|
||||
Unit,
|
||||
PreviousQuantity,
|
||||
NextQuantity,
|
||||
PreviousImportInPeriod,
|
||||
NextImportInPeriod,
|
||||
PreviousExportInPeriod,
|
||||
NextExportInPeriod,
|
||||
PreviousEndingBalance,
|
||||
NextEndingBalance,
|
||||
PreviousNewQuantity,
|
||||
NextNewQuantity,
|
||||
PreviousUsedQuantity,
|
||||
NextUsedQuantity,
|
||||
ActionNote,
|
||||
CreatedBy,
|
||||
CreatedByName,
|
||||
ActionDate,
|
||||
CreatedDate,
|
||||
UpdatedDate
|
||||
FROM AssetDamageDisposalHistory
|
||||
ORDER BY ActionDate DESC, DamageHistoryId DESC
|
||||
`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: Array.isArray(result.recordset) ? result.recordset : []
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/assets/:id/damage-disposal', requireAssetOrAdmin, async (req, res) => {
|
||||
let transaction;
|
||||
|
||||
try {
|
||||
const assetId = Number(req.params.id);
|
||||
const actionType = normalizeAssetDamageType(req.body?.actionType || req.body?.reason);
|
||||
const actionLabel = getAssetDamageTypeLabel(actionType);
|
||||
const actionQuantity = parseNonNegativeInteger(req.body?.quantity, 0);
|
||||
const actionNote = String(req.body?.note || '').trim() || null;
|
||||
const createdBy = getUserIdFromRequest(req);
|
||||
const createdByName = await getUserDisplayNameById(createdBy);
|
||||
const actionDate = new Date();
|
||||
|
||||
if (!Number.isInteger(assetId) || assetId <= 0) {
|
||||
return res.status(400).json({ success: false, message: 'Asset id is invalid' });
|
||||
}
|
||||
|
||||
if (actionQuantity <= 0) {
|
||||
return res.status(400).json({ success: false, message: 'Số lượng phải lớn hơn 0' });
|
||||
}
|
||||
|
||||
transaction = new sql.Transaction(pool);
|
||||
await transaction.begin();
|
||||
|
||||
const assetResult = await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.query(`
|
||||
SELECT TOP 1
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
Quantity,
|
||||
ImportInPeriod,
|
||||
ExportInPeriod,
|
||||
EndingBalance,
|
||||
NewQuantity,
|
||||
UsedQuantity,
|
||||
Unit
|
||||
FROM AssetInventory WITH (UPDLOCK, ROWLOCK)
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
const asset = assetResult.recordset?.[0];
|
||||
if (!asset) {
|
||||
await transaction.rollback();
|
||||
return res.status(404).json({ success: false, message: 'Asset not found' });
|
||||
}
|
||||
|
||||
const previousQuantity = parseNonNegativeInteger(asset.Quantity, 0);
|
||||
const previousImportInPeriod = parseNonNegativeInteger(asset.ImportInPeriod, 0);
|
||||
const previousExportInPeriod = parseNonNegativeInteger(asset.ExportInPeriod, 0);
|
||||
const storedEndingBalance = parseOptionalNonNegativeInteger(asset.EndingBalance);
|
||||
const previousEndingBalance = storedEndingBalance !== null
|
||||
? storedEndingBalance
|
||||
: Math.max(previousQuantity + previousImportInPeriod - previousExportInPeriod, 0);
|
||||
const previousStockBuckets = normalizeAssetStockBuckets(
|
||||
previousEndingBalance,
|
||||
parseOptionalNonNegativeInteger(asset.NewQuantity) ?? previousEndingBalance,
|
||||
parseOptionalNonNegativeInteger(asset.UsedQuantity) ?? 0
|
||||
);
|
||||
|
||||
if (previousEndingBalance <= 0) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({ success: false, message: 'Tài sản đã hết tồn cuối kỳ' });
|
||||
}
|
||||
|
||||
if (actionQuantity > previousEndingBalance) {
|
||||
await transaction.rollback();
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Số lượng ${actionLabel.toLowerCase()} (${actionQuantity}) vượt quá tồn cuối kỳ (${previousEndingBalance})`
|
||||
});
|
||||
}
|
||||
|
||||
let remainingSourceReduction = actionQuantity;
|
||||
const reduceFromQuantity = Math.min(previousQuantity, remainingSourceReduction);
|
||||
const nextQuantity = Math.max(previousQuantity - reduceFromQuantity, 0);
|
||||
remainingSourceReduction -= reduceFromQuantity;
|
||||
const reduceFromImport = Math.min(previousImportInPeriod, remainingSourceReduction);
|
||||
const nextImportInPeriod = Math.max(previousImportInPeriod - reduceFromImport, 0);
|
||||
|
||||
const nextExportInPeriod = previousExportInPeriod;
|
||||
const nextEndingBalance = Math.max(nextQuantity + nextImportInPeriod - nextExportInPeriod, 0);
|
||||
|
||||
let nextUsedQuantity = previousStockBuckets.usedQuantity;
|
||||
let nextNewQuantity = previousStockBuckets.newQuantity;
|
||||
let remainingStockReduction = actionQuantity;
|
||||
const reduceFromUsed = Math.min(nextUsedQuantity, remainingStockReduction);
|
||||
nextUsedQuantity -= reduceFromUsed;
|
||||
remainingStockReduction -= reduceFromUsed;
|
||||
const reduceFromNew = Math.min(nextNewQuantity, remainingStockReduction);
|
||||
nextNewQuantity -= reduceFromNew;
|
||||
|
||||
const nextStockBuckets = normalizeAssetStockBuckets(nextEndingBalance, nextNewQuantity, nextUsedQuantity);
|
||||
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextExportInPeriod);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.input('quantity', sql.Int, nextQuantity)
|
||||
.input('importInPeriod', sql.Int, nextImportInPeriod)
|
||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||
.input('newQuantity', sql.Int, nextStockBuckets.newQuantity)
|
||||
.input('usedQuantity', sql.Int, nextStockBuckets.usedQuantity)
|
||||
.input('status', sql.NVarChar, nextStatus)
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Quantity = @quantity,
|
||||
ImportInPeriod = @importInPeriod,
|
||||
EndingBalance = @endingBalance,
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
UpdatedDate = GETDATE()
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
const historyResult = await new sql.Request(transaction)
|
||||
.input('assetId', sql.Int, assetId)
|
||||
.input('assetCode', sql.NVarChar, String(asset.AssetCode || '').trim())
|
||||
.input('assetName', sql.NVarChar, String(asset.AssetName || '').trim())
|
||||
.input('actionType', sql.NVarChar, actionType)
|
||||
.input('actionLabel', sql.NVarChar, actionLabel)
|
||||
.input('actionQuantity', sql.Int, actionQuantity)
|
||||
.input('unit', sql.NVarChar, String(asset.Unit || '').trim() || null)
|
||||
.input('previousQuantity', sql.Int, previousQuantity)
|
||||
.input('nextQuantity', sql.Int, nextQuantity)
|
||||
.input('previousImportInPeriod', sql.Int, previousImportInPeriod)
|
||||
.input('nextImportInPeriod', sql.Int, nextImportInPeriod)
|
||||
.input('previousExportInPeriod', sql.Int, previousExportInPeriod)
|
||||
.input('nextExportInPeriod', sql.Int, nextExportInPeriod)
|
||||
.input('previousEndingBalance', sql.Int, previousEndingBalance)
|
||||
.input('nextEndingBalance', sql.Int, nextEndingBalance)
|
||||
.input('previousNewQuantity', sql.Int, previousStockBuckets.newQuantity)
|
||||
.input('nextNewQuantity', sql.Int, nextStockBuckets.newQuantity)
|
||||
.input('previousUsedQuantity', sql.Int, previousStockBuckets.usedQuantity)
|
||||
.input('nextUsedQuantity', sql.Int, nextStockBuckets.usedQuantity)
|
||||
.input('actionNote', sql.NVarChar, actionNote)
|
||||
.input('createdBy', sql.Int, createdBy)
|
||||
.input('createdByName', sql.NVarChar, createdByName)
|
||||
.input('actionDate', sql.DateTime, actionDate)
|
||||
.query(`
|
||||
INSERT INTO AssetDamageDisposalHistory (
|
||||
AssetId,
|
||||
AssetCode,
|
||||
AssetName,
|
||||
ActionType,
|
||||
ActionLabel,
|
||||
ActionQuantity,
|
||||
Unit,
|
||||
PreviousQuantity,
|
||||
NextQuantity,
|
||||
PreviousImportInPeriod,
|
||||
NextImportInPeriod,
|
||||
PreviousExportInPeriod,
|
||||
NextExportInPeriod,
|
||||
PreviousEndingBalance,
|
||||
NextEndingBalance,
|
||||
PreviousNewQuantity,
|
||||
NextNewQuantity,
|
||||
PreviousUsedQuantity,
|
||||
NextUsedQuantity,
|
||||
ActionNote,
|
||||
CreatedBy,
|
||||
CreatedByName,
|
||||
ActionDate
|
||||
)
|
||||
OUTPUT
|
||||
INSERTED.DamageHistoryId,
|
||||
INSERTED.AssetId,
|
||||
INSERTED.AssetCode,
|
||||
INSERTED.AssetName,
|
||||
INSERTED.ActionType,
|
||||
INSERTED.ActionLabel,
|
||||
INSERTED.ActionQuantity,
|
||||
INSERTED.Unit,
|
||||
INSERTED.PreviousQuantity,
|
||||
INSERTED.NextQuantity,
|
||||
INSERTED.PreviousImportInPeriod,
|
||||
INSERTED.NextImportInPeriod,
|
||||
INSERTED.PreviousExportInPeriod,
|
||||
INSERTED.NextExportInPeriod,
|
||||
INSERTED.PreviousEndingBalance,
|
||||
INSERTED.NextEndingBalance,
|
||||
INSERTED.PreviousNewQuantity,
|
||||
INSERTED.NextNewQuantity,
|
||||
INSERTED.PreviousUsedQuantity,
|
||||
INSERTED.NextUsedQuantity,
|
||||
INSERTED.ActionNote,
|
||||
INSERTED.CreatedBy,
|
||||
INSERTED.CreatedByName,
|
||||
INSERTED.ActionDate,
|
||||
INSERTED.CreatedDate,
|
||||
INSERTED.UpdatedDate
|
||||
VALUES (
|
||||
@assetId,
|
||||
@assetCode,
|
||||
@assetName,
|
||||
@actionType,
|
||||
@actionLabel,
|
||||
@actionQuantity,
|
||||
@unit,
|
||||
@previousQuantity,
|
||||
@nextQuantity,
|
||||
@previousImportInPeriod,
|
||||
@nextImportInPeriod,
|
||||
@previousExportInPeriod,
|
||||
@nextExportInPeriod,
|
||||
@previousEndingBalance,
|
||||
@nextEndingBalance,
|
||||
@previousNewQuantity,
|
||||
@nextNewQuantity,
|
||||
@previousUsedQuantity,
|
||||
@nextUsedQuantity,
|
||||
@actionNote,
|
||||
@createdBy,
|
||||
@createdByName,
|
||||
@actionDate
|
||||
)
|
||||
`);
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Đã ghi nhận tài sản ${actionLabel.toLowerCase()}`,
|
||||
data: historyResult.recordset?.[0] || null
|
||||
});
|
||||
} catch (err) {
|
||||
if (transaction) {
|
||||
try {
|
||||
await transaction.rollback();
|
||||
} catch (_rollbackErr) {
|
||||
// Ignore rollback error, respond original error below.
|
||||
}
|
||||
}
|
||||
res.status(500).json({ success: false, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
let transaction;
|
||||
try {
|
||||
|
||||
@@ -484,7 +484,45 @@ BEGIN
|
||||
END
|
||||
|
||||
-- ===========================================
|
||||
-- 9. CREATE AUDIT LOG TABLE
|
||||
-- 9. CREATE ASSET DAMAGE/DISPOSAL HISTORY TABLE
|
||||
-- ===========================================
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDamageDisposalHistory')
|
||||
BEGIN
|
||||
CREATE TABLE AssetDamageDisposalHistory (
|
||||
DamageHistoryId INT PRIMARY KEY IDENTITY(1,1),
|
||||
AssetId INT NOT NULL,
|
||||
AssetCode NVARCHAR(100) NOT NULL,
|
||||
AssetName NVARCHAR(255) NOT NULL,
|
||||
ActionType NVARCHAR(20) NOT NULL,
|
||||
ActionLabel NVARCHAR(50) NOT NULL,
|
||||
ActionQuantity INT NOT NULL DEFAULT 1,
|
||||
Unit NVARCHAR(50) NULL,
|
||||
PreviousQuantity INT NOT NULL DEFAULT 0,
|
||||
NextQuantity INT NOT NULL DEFAULT 0,
|
||||
PreviousImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
NextImportInPeriod INT NOT NULL DEFAULT 0,
|
||||
PreviousExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
NextExportInPeriod INT NOT NULL DEFAULT 0,
|
||||
PreviousEndingBalance INT NOT NULL DEFAULT 0,
|
||||
NextEndingBalance INT NOT NULL DEFAULT 0,
|
||||
PreviousNewQuantity INT NOT NULL DEFAULT 0,
|
||||
NextNewQuantity INT NOT NULL DEFAULT 0,
|
||||
PreviousUsedQuantity INT NOT NULL DEFAULT 0,
|
||||
NextUsedQuantity INT NOT NULL DEFAULT 0,
|
||||
ActionNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
CreatedByName NVARCHAR(100) NULL,
|
||||
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
);
|
||||
PRINT 'Table AssetDamageDisposalHistory created successfully.';
|
||||
END
|
||||
|
||||
-- ===========================================
|
||||
-- 10. CREATE AUDIT LOG TABLE
|
||||
-- ===========================================
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||
BEGIN
|
||||
@@ -503,7 +541,7 @@ BEGIN
|
||||
END
|
||||
|
||||
-- ===========================================
|
||||
-- 10. CREATE INDEXES
|
||||
-- 11. CREATE INDEXES
|
||||
-- ===========================================
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
||||
BEGIN
|
||||
@@ -590,10 +628,25 @@ BEGIN
|
||||
CREATE INDEX IX_AssetExportHistory_ExportedDate ON AssetExportHistory(ExportedDate DESC);
|
||||
END
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_AssetId')
|
||||
BEGIN
|
||||
CREATE INDEX IX_AssetDamageDisposalHistory_AssetId ON AssetDamageDisposalHistory(AssetId);
|
||||
END
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionDate')
|
||||
BEGIN
|
||||
CREATE INDEX IX_AssetDamageDisposalHistory_ActionDate ON AssetDamageDisposalHistory(ActionDate DESC);
|
||||
END
|
||||
|
||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionType')
|
||||
BEGIN
|
||||
CREATE INDEX IX_AssetDamageDisposalHistory_ActionType ON AssetDamageDisposalHistory(ActionType);
|
||||
END
|
||||
|
||||
PRINT 'Indexes created successfully.';
|
||||
|
||||
-- ===========================================
|
||||
-- 11. INSERT INITIAL DATA
|
||||
-- 12. INSERT INITIAL DATA
|
||||
-- ===========================================
|
||||
|
||||
-- Check if admin user exists
|
||||
|
||||
319
public/js/app.js
319
public/js/app.js
@@ -53,6 +53,7 @@ class AccountManager {
|
||||
this.assetProjects = [];
|
||||
this.assetProjectSearchTerm = '';
|
||||
this.assetExportHistories = [];
|
||||
this.assetDamageHistories = [];
|
||||
this.selectedAssetIds = new Set();
|
||||
this.mobileBreakpoint = 900;
|
||||
this.boundResizeHandler = null;
|
||||
@@ -71,6 +72,7 @@ class AccountManager {
|
||||
this.assetBorrowAutoRefreshTimer = undefined;
|
||||
this.pendingAssetRequestDeleteConfirmResolver = undefined;
|
||||
this.pendingBulkAssetDeleteConfirmResolver = undefined;
|
||||
this.pendingAssetDamageId = undefined;
|
||||
}
|
||||
|
||||
configureNotifications() {
|
||||
@@ -1072,6 +1074,114 @@ class AccountManager {
|
||||
this.renderAssetExportHistoryModal();
|
||||
}
|
||||
|
||||
normalizeAssetDamageType(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'disposed' || normalized === 'thanh_ly' || normalized === 'thanh ly'
|
||||
? 'disposed'
|
||||
: 'damaged';
|
||||
}
|
||||
|
||||
getAssetDamageTypeMeta(value) {
|
||||
const type = this.normalizeAssetDamageType(value);
|
||||
if (type === 'disposed') {
|
||||
return {
|
||||
value: 'disposed',
|
||||
label: 'Thanh lý',
|
||||
className: 'bg-slate-100 text-slate-700 border border-slate-200'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: 'damaged',
|
||||
label: 'Hỏng',
|
||||
className: 'bg-red-100 text-red-700 border border-red-200'
|
||||
};
|
||||
}
|
||||
|
||||
async fetchAssetDamageHistories(limit = 300) {
|
||||
try {
|
||||
const safeLimit = Number.isFinite(Number(limit)) ? Math.max(1, Math.min(Number(limit), 2000)) : 300;
|
||||
const res = await fetch(`${this.apiBase}/asset-damage-disposal-history?limit=${safeLimit}`, {
|
||||
headers: this.getAuthHeaders(false)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
this.assetDamageHistories = Array.isArray(data.data) ? data.data : [];
|
||||
} else {
|
||||
console.error('Load asset damage/disposal history failed:', data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch asset damage/disposal history error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
buildAssetDamageHistoryRowsHtml(rows = []) {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
return `
|
||||
<tr>
|
||||
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu tài sản hỏng/thanh lý.</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
return rows.map(item => {
|
||||
const typeMeta = this.getAssetDamageTypeMeta(item?.ActionType);
|
||||
const assetLabel = [String(item?.AssetCode || '').trim(), String(item?.AssetName || '').trim()]
|
||||
.filter(Boolean)
|
||||
.join(' - ') || '-';
|
||||
const unit = String(item?.Unit || '').trim();
|
||||
const quantityLabel = `${Number(item?.ActionQuantity) || 0}${unit ? ` ${unit}` : ''}`;
|
||||
return `
|
||||
<tr class="hover:bg-slate-50/80 transition-colors">
|
||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateTime(item?.ActionDate || item?.CreatedDate)}</td>
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-semibold ${typeMeta.className}">${typeMeta.label}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">${this.escapeHtml(assetLabel)}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700 font-semibold">${this.escapeHtml(quantityLabel)}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousQuantity) || 0} -> ${Number(item?.NextQuantity) || 0}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousEndingBalance) || 0} -> ${Number(item?.NextEndingBalance) || 0}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousNewQuantity) || 0} -> ${Number(item?.NextNewQuantity) || 0}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousUsedQuantity) || 0} -> ${Number(item?.NextUsedQuantity) || 0}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(item?.CreatedByName || '-')}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-pre-line">${this.escapeHtml(item?.ActionNote || '-')}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
renderAssetDamageHistoryModal() {
|
||||
const tbody = document.getElementById('assetDamageHistoryTableBody');
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = this.buildAssetDamageHistoryRowsHtml(this.assetDamageHistories);
|
||||
}
|
||||
|
||||
async openAssetDamageHistoryModal() {
|
||||
if (!this.ensureAssetManagePermission('xem danh sach tai san hong/thanh ly')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('assetDamageHistoryModal');
|
||||
const tbody = document.getElementById('assetDamageHistoryTableBody');
|
||||
if (!modal || !tbody) {
|
||||
this.notifyFailure('Không tìm thấy bảng tài sản hỏng/thanh lý.');
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Đang tải dữ liệu tài sản hỏng/thanh lý...</td>
|
||||
</tr>
|
||||
`;
|
||||
modal.classList.add('open');
|
||||
|
||||
await this.fetchAssetDamageHistories();
|
||||
this.renderAssetDamageHistoryModal();
|
||||
}
|
||||
|
||||
async fetchRoles() {
|
||||
try {
|
||||
const res = await fetch(`${this.apiBase}/roles`);
|
||||
@@ -1244,6 +1354,14 @@ class AccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
const assetDamageForm = document.getElementById('assetDamageForm');
|
||||
if (assetDamageForm) {
|
||||
if (!assetDamageForm.dataset.boundSubmit) {
|
||||
assetDamageForm.addEventListener('submit', (e) => this.handleAssetDamageSubmit(e));
|
||||
assetDamageForm.dataset.boundSubmit = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
const assetBorrowRequestForm = document.getElementById('assetBorrowRequestForm');
|
||||
if (assetBorrowRequestForm) {
|
||||
if (!assetBorrowRequestForm.dataset.boundSubmit) {
|
||||
@@ -4278,14 +4396,22 @@ class AccountManager {
|
||||
<span class="material-symbols-outlined text-base">history</span>
|
||||
Lịch sử xuất
|
||||
</button>
|
||||
<button id="openAssetDamageHistoryBtn" class="border border-red-200 text-red-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-red-50' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
||||
<span class="material-symbols-outlined text-base">inventory_2</span>
|
||||
DS hỏng/TL
|
||||
</button>
|
||||
<button id="addAssetBtn" 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 ${canManageAssets ? 'hover:bg-primary-dim' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
||||
<span class="material-symbols-outlined text-base">add_box</span>
|
||||
Thêm tài sản
|
||||
</button>
|
||||
<button id="borrowAssetBtn" class="border border-primary text-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-primary/5' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
||||
<button id="borrowAssetBtn" class="border border-primary text-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${(canManageAssets && selectedCount === 1) ? 'hover:bg-primary/5' : 'opacity-50 cursor-not-allowed'}" ${(canManageAssets && selectedCount === 1) ? '' : 'disabled'}>
|
||||
<span class="material-symbols-outlined text-base">handshake</span>
|
||||
Xuất tài sản
|
||||
</button>
|
||||
<button id="damageAssetBtn" class="border border-red-300 text-red-700 px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${(canManageAssets && selectedCount === 1) ? 'hover:bg-red-50' : 'opacity-50 cursor-not-allowed'}" ${(canManageAssets && selectedCount === 1) ? '' : 'disabled'}>
|
||||
<span class="material-symbols-outlined text-base">broken_image</span>
|
||||
Hỏng/Thanh lý
|
||||
</button>
|
||||
<input id="assetImportInput" type="file" accept=".xlsx,.xls" class="hidden" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -4609,6 +4735,16 @@ class AccountManager {
|
||||
borrowAssetBtn.disabled = disabled;
|
||||
borrowAssetBtn.classList.toggle('opacity-50', disabled);
|
||||
borrowAssetBtn.classList.toggle('cursor-not-allowed', disabled);
|
||||
borrowAssetBtn.classList.toggle('hover:bg-primary/5', !disabled);
|
||||
}
|
||||
|
||||
const damageAssetBtn = document.getElementById('damageAssetBtn');
|
||||
if (damageAssetBtn) {
|
||||
const disabled = !canManageAssets || selectedCount !== 1;
|
||||
damageAssetBtn.disabled = disabled;
|
||||
damageAssetBtn.classList.toggle('opacity-50', disabled);
|
||||
damageAssetBtn.classList.toggle('cursor-not-allowed', disabled);
|
||||
damageAssetBtn.classList.toggle('hover:bg-red-50', !disabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5051,6 +5187,161 @@ class AccountManager {
|
||||
return asset;
|
||||
}
|
||||
|
||||
getSingleSelectedAssetForDamage(showWarning = true) {
|
||||
const selectedIds = [...this.selectedAssetIds];
|
||||
if (!selectedIds.length) {
|
||||
if (showWarning) {
|
||||
this.notifyWarning('Vui lòng chọn 1 tài sản để ghi nhận hỏng/thanh lý.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (selectedIds.length > 1) {
|
||||
if (showWarning) {
|
||||
this.notifyWarning('Chỉ chọn đúng 1 tài sản cho mỗi lần ghi nhận hỏng/thanh lý.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetId = Number(selectedIds[0]);
|
||||
const asset = this.assets.find(item => Number(item?.AssetId) === assetId) || null;
|
||||
if (!asset && showWarning) {
|
||||
this.notifyFailure('Không tìm thấy tài sản đã chọn.');
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
openAssetDamageModal() {
|
||||
if (!this.ensureAssetManagePermission('ghi nhan tai san hong/thanh ly')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = this.getSingleSelectedAssetForDamage(true);
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metrics = this.buildAssetQuantityMetrics(asset);
|
||||
if (metrics.endingBalance <= 0) {
|
||||
this.notifyWarning('Tài sản đã hết tồn cuối kỳ, không thể ghi nhận hỏng/thanh lý thêm.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingAssetDamageId = Number(asset.AssetId);
|
||||
|
||||
const modal = document.getElementById('assetDamageModal');
|
||||
const assetIdInput = document.getElementById('assetDamageAssetIdInput');
|
||||
const assetNameInput = document.getElementById('assetDamageAssetNameInput');
|
||||
const typeInput = document.getElementById('assetDamageTypeInput');
|
||||
const quantityInput = document.getElementById('assetDamageQuantityInput');
|
||||
const currentQuantityInput = document.getElementById('assetDamageCurrentQuantityInput');
|
||||
const currentEndingInput = document.getElementById('assetDamageCurrentEndingInput');
|
||||
const currentNewInput = document.getElementById('assetDamageCurrentNewInput');
|
||||
const currentUsedInput = document.getElementById('assetDamageCurrentUsedInput');
|
||||
const actorInput = document.getElementById('assetDamageActorInput');
|
||||
const noteInput = document.getElementById('assetDamageNoteInput');
|
||||
|
||||
if (!modal || !assetNameInput || !quantityInput || !currentQuantityInput || !currentEndingInput) {
|
||||
this.notifyFailure('Không tìm thấy biểu mẫu hỏng/thanh lý tài sản.');
|
||||
return;
|
||||
}
|
||||
|
||||
const stockSplit = this.normalizeAssetStockSplit(
|
||||
metrics.endingBalance,
|
||||
asset?.NewQuantity ?? metrics.endingBalance,
|
||||
asset?.UsedQuantity ?? 0
|
||||
);
|
||||
|
||||
if (assetIdInput) {
|
||||
assetIdInput.value = String(asset.AssetId || '');
|
||||
}
|
||||
assetNameInput.value = `${asset.AssetCode || ''} - ${asset.AssetName || ''}`.trim();
|
||||
currentQuantityInput.value = String(metrics.quantity);
|
||||
currentEndingInput.value = String(metrics.endingBalance);
|
||||
if (currentNewInput) currentNewInput.value = String(stockSplit.newQuantity);
|
||||
if (currentUsedInput) currentUsedInput.value = String(stockSplit.usedQuantity);
|
||||
if (typeInput) typeInput.value = 'damaged';
|
||||
quantityInput.value = '1';
|
||||
quantityInput.min = '1';
|
||||
quantityInput.max = String(metrics.endingBalance);
|
||||
if (actorInput) actorInput.value = this.getCurrentUserDisplayName();
|
||||
if (noteInput) noteInput.value = '';
|
||||
|
||||
modal.classList.add('open');
|
||||
}
|
||||
|
||||
async handleAssetDamageSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.ensureAssetManagePermission('ghi nhan tai san hong/thanh ly')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetIdInput = document.getElementById('assetDamageAssetIdInput');
|
||||
const typeInput = document.getElementById('assetDamageTypeInput');
|
||||
const quantityInput = document.getElementById('assetDamageQuantityInput');
|
||||
const noteInput = document.getElementById('assetDamageNoteInput');
|
||||
|
||||
const selectedAssetId = Number(assetIdInput?.value || this.pendingAssetDamageId);
|
||||
if (!Number.isFinite(selectedAssetId) || selectedAssetId <= 0) {
|
||||
this.notifyFailure('Không xác định được tài sản cần ghi nhận.');
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = this.assets.find(item => Number(item?.AssetId) === selectedAssetId);
|
||||
if (!asset) {
|
||||
this.notifyFailure('Không tìm thấy tài sản cần ghi nhận.');
|
||||
return;
|
||||
}
|
||||
|
||||
const actionType = this.normalizeAssetDamageType(typeInput?.value || 'damaged');
|
||||
const actionMeta = this.getAssetDamageTypeMeta(actionType);
|
||||
const actionQuantity = this.parseNonNegativeInteger(quantityInput?.value ?? 0, 0);
|
||||
const currentMetrics = this.buildAssetQuantityMetrics(asset);
|
||||
|
||||
if (actionQuantity <= 0) {
|
||||
this.notifyWarning('Số lượng phải lớn hơn 0.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionQuantity > currentMetrics.endingBalance) {
|
||||
this.notifyWarning(`Số lượng ${actionMeta.label.toLowerCase()} (${actionQuantity}) vượt quá tồn cuối kỳ (${currentMetrics.endingBalance}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/assets/${selectedAssetId}/damage-disposal`, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeaders(true),
|
||||
body: JSON.stringify({
|
||||
actionType,
|
||||
quantity: actionQuantity,
|
||||
note: String(noteInput?.value || '').trim()
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
this.notifyFailure(data.message || 'Ghi nhận hỏng/thanh lý thất bại');
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingAssetDamageId = undefined;
|
||||
document.getElementById('assetDamageModal')?.classList.remove('open');
|
||||
this.notifySuccess(data.message || `Đã ghi nhận tài sản ${actionMeta.label.toLowerCase()}`);
|
||||
await this.refreshAssetsUI();
|
||||
|
||||
const historyModal = document.getElementById('assetDamageHistoryModal');
|
||||
if (historyModal?.classList.contains('open')) {
|
||||
await this.fetchAssetDamageHistories();
|
||||
this.renderAssetDamageHistoryModal();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.notifyFailure('Ghi nhận hỏng/thanh lý thất bại');
|
||||
}
|
||||
}
|
||||
|
||||
async openBorrowAssetModal() {
|
||||
if (!this.ensureAssetManagePermission('xuat tai san')) {
|
||||
return;
|
||||
@@ -6616,10 +6907,17 @@ class AccountManager {
|
||||
borrowAssetBtn.dataset.boundClick = 'true';
|
||||
}
|
||||
|
||||
const damageAssetBtn = document.getElementById('damageAssetBtn');
|
||||
if (damageAssetBtn && !damageAssetBtn.dataset.boundClick) {
|
||||
damageAssetBtn.addEventListener('click', () => this.openAssetDamageModal());
|
||||
damageAssetBtn.dataset.boundClick = 'true';
|
||||
}
|
||||
|
||||
const importAssetBtn = document.getElementById('importAssetBtn');
|
||||
const assetImportInput = document.getElementById('assetImportInput');
|
||||
const exportAssetBtn = document.getElementById('exportAssetBtn');
|
||||
const openAssetExportHistoryBtn = document.getElementById('openAssetExportHistoryBtn');
|
||||
const openAssetDamageHistoryBtn = document.getElementById('openAssetDamageHistoryBtn');
|
||||
|
||||
if (importAssetBtn && assetImportInput && !importAssetBtn.dataset.boundClick) {
|
||||
importAssetBtn.addEventListener('click', () => {
|
||||
@@ -6645,6 +6943,11 @@ class AccountManager {
|
||||
openAssetExportHistoryBtn.addEventListener('click', () => this.openAssetExportHistoryModal());
|
||||
openAssetExportHistoryBtn.dataset.boundClick = 'true';
|
||||
}
|
||||
|
||||
if (openAssetDamageHistoryBtn && !openAssetDamageHistoryBtn.dataset.boundClick) {
|
||||
openAssetDamageHistoryBtn.addEventListener('click', () => this.openAssetDamageHistoryModal());
|
||||
openAssetDamageHistoryBtn.dataset.boundClick = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
setupFilters() {
|
||||
@@ -7856,6 +8159,13 @@ function closeBorrowAssetModal() {
|
||||
}
|
||||
}
|
||||
|
||||
function closeAssetDamageModal() {
|
||||
const modal = document.getElementById('assetDamageModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAssetExportHistoryModal() {
|
||||
const modal = document.getElementById('assetExportHistoryModal');
|
||||
if (modal) {
|
||||
@@ -7863,6 +8173,13 @@ function closeAssetExportHistoryModal() {
|
||||
}
|
||||
}
|
||||
|
||||
function closeAssetDamageHistoryModal() {
|
||||
const modal = document.getElementById('assetDamageHistoryModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAssetBorrowRequestModal() {
|
||||
const modal = document.getElementById('assetBorrowRequestModal');
|
||||
const dropdown = document.getElementById('assetBorrowProductDropdown');
|
||||
|
||||
@@ -357,6 +357,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Damage/Disposal Modal -->
|
||||
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDamageModal">
|
||||
<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="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||||
<h3 class="text-base font-extrabold text-slate-900">Hỏng / Thanh lý tài sản</h3>
|
||||
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetDamageModal()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="assetDamageForm" class="p-6 space-y-4">
|
||||
<input type="hidden" id="assetDamageAssetIdInput">
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tài sản</label>
|
||||
<input type="text" id="assetDamageAssetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Lý do</label>
|
||||
<select id="assetDamageTypeInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
|
||||
<option value="damaged">Hỏng</option>
|
||||
<option value="disposed">Thanh lý</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng</label>
|
||||
<input type="number" id="assetDamageQuantityInput" min="1" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng hiện tại</label>
|
||||
<input type="number" id="assetDamageCurrentQuantityInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kỳ hiện tại</label>
|
||||
<input type="number" id="assetDamageCurrentEndingInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">SL hàng mới</label>
|
||||
<input type="number" id="assetDamageCurrentNewInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">SL đã qua sử dụng</label>
|
||||
<input type="number" id="assetDamageCurrentUsedInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người thao tác</label>
|
||||
<input type="text" id="assetDamageActorInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú</label>
|
||||
<textarea id="assetDamageNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập tình trạng, biên bản hoặc lý do chi tiết"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAssetDamageModal()">Hủy</button>
|
||||
<button type="submit" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xác nhận</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Export History Modal -->
|
||||
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetExportHistoryModal" style="z-index: 125;">
|
||||
<div class="modal-content w-full max-w-6xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
|
||||
@@ -393,6 +453,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Damage/Disposal History Modal -->
|
||||
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDamageHistoryModal" style="z-index: 126;">
|
||||
<div class="modal-content w-full max-w-6xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
|
||||
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||||
<h3 class="text-base font-extrabold text-slate-900">Tài sản hỏng / thanh lý</h3>
|
||||
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetDamageHistoryModal()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 pt-4 overflow-auto">
|
||||
<div class="rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse" style="min-width: 1280px;">
|
||||
<thead class="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">Ngày giờ</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">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</th>
|
||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tồn đầu kỳ</th>
|
||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tồn cuối kỳ</th>
|
||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL hàng mới</th>
|
||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL đã sử dụng</th>
|
||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người thao tác</th>
|
||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="assetDamageHistoryTableBody" class="divide-y divide-slate-100">
|
||||
<tr>
|
||||
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu tài sản hỏng/thanh lý.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Borrow Request Modal -->
|
||||
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetBorrowRequestModal">
|
||||
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-visible m-4">
|
||||
|
||||
Reference in New Issue
Block a user