add dự án
This commit is contained in:
@@ -263,6 +263,10 @@ function normalizeDepartmentName(value) {
|
|||||||
return String(value || '').trim();
|
return String(value || '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProjectName(value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
async function syncAssetDepartmentsFromInventory() {
|
async function syncAssetDepartmentsFromInventory() {
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
return;
|
return;
|
||||||
@@ -1547,6 +1551,17 @@ async function createTables() {
|
|||||||
)
|
)
|
||||||
END`,
|
END`,
|
||||||
|
|
||||||
|
// Asset Projects Table
|
||||||
|
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE AssetProjects (
|
||||||
|
ProjectId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
|
ProjectName NVARCHAR(150) NOT NULL,
|
||||||
|
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||||
|
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||||
|
)
|
||||||
|
END`,
|
||||||
|
|
||||||
// Asset Borrow Requests Table
|
// Asset Borrow Requests Table
|
||||||
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
|
`IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -1614,6 +1629,13 @@ async function createTables() {
|
|||||||
console.error('AssetDepartments index creation error:', err.message);
|
console.error('AssetDepartments index creation error:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure AssetProjects indexes exist
|
||||||
|
try {
|
||||||
|
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetProjects_ProjectName') CREATE UNIQUE INDEX UX_AssetProjects_ProjectName ON AssetProjects(ProjectName);`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('AssetProjects index creation error:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure AssetBorrowRequests indexes exist
|
// Ensure AssetBorrowRequests indexes exist
|
||||||
try {
|
try {
|
||||||
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId') CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);`);
|
await pool.request().query(`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId') CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);`);
|
||||||
@@ -3058,6 +3080,216 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) =
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// API ROUTES - Asset Projects
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
app.get('/api/asset-projects', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await pool.request().query(`
|
||||||
|
SELECT
|
||||||
|
p.ProjectId,
|
||||||
|
p.ProjectName,
|
||||||
|
p.CreatedDate,
|
||||||
|
p.UpdatedDate,
|
||||||
|
COUNT(ai.AssetId) AS AssetCount
|
||||||
|
FROM AssetProjects p
|
||||||
|
LEFT JOIN AssetInventory ai
|
||||||
|
ON LOWER(LTRIM(RTRIM(ai.Project))) = LOWER(LTRIM(RTRIM(p.ProjectName)))
|
||||||
|
GROUP BY p.ProjectId, p.ProjectName, p.CreatedDate, p.UpdatedDate
|
||||||
|
ORDER BY p.ProjectName ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.json({ success: true, data: result.recordset });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ success: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/asset-projects', requireAssetOrAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const projectName = normalizeProjectName(req.body?.projectName);
|
||||||
|
if (!projectName) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existed = await pool.request()
|
||||||
|
.input('projectName', sql.NVarChar, projectName)
|
||||||
|
.query(`
|
||||||
|
SELECT TOP 1 ProjectId
|
||||||
|
FROM AssetProjects
|
||||||
|
WHERE LOWER(LTRIM(RTRIM(ProjectName))) = LOWER(@projectName)
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (existed.recordset.length > 0) {
|
||||||
|
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const inserted = await pool.request()
|
||||||
|
.input('projectName', sql.NVarChar, projectName)
|
||||||
|
.query(`
|
||||||
|
INSERT INTO AssetProjects (ProjectName)
|
||||||
|
VALUES (@projectName);
|
||||||
|
SELECT SCOPE_IDENTITY() AS ProjectId;
|
||||||
|
`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Da them du an',
|
||||||
|
projectId: inserted.recordset[0]?.ProjectId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (String(err.message || '').includes('UX_AssetProjects_ProjectName')) {
|
||||||
|
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({ success: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const projectId = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(projectId) || projectId <= 0) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Ma du an khong hop le' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectName = normalizeProjectName(req.body?.projectName);
|
||||||
|
if (!projectName) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Ten du an la bat buoc' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentResult = await pool.request()
|
||||||
|
.input('projectId', sql.Int, projectId)
|
||||||
|
.query(`
|
||||||
|
SELECT ProjectId, ProjectName
|
||||||
|
FROM AssetProjects
|
||||||
|
WHERE ProjectId = @projectId
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (currentResult.recordset.length === 0) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Khong tim thay du an' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentProject = currentResult.recordset[0];
|
||||||
|
const currentName = String(currentProject.ProjectName || '').trim();
|
||||||
|
|
||||||
|
if (currentName.toLowerCase() === projectName.toLowerCase()) {
|
||||||
|
return res.json({ success: true, message: 'Da cap nhat du an' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicated = await pool.request()
|
||||||
|
.input('projectName', sql.NVarChar, projectName)
|
||||||
|
.input('projectId', sql.Int, projectId)
|
||||||
|
.query(`
|
||||||
|
SELECT TOP 1 ProjectId
|
||||||
|
FROM AssetProjects
|
||||||
|
WHERE ProjectId <> @projectId
|
||||||
|
AND LOWER(LTRIM(RTRIM(ProjectName))) = LOWER(@projectName)
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (duplicated.recordset.length > 0) {
|
||||||
|
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = new sql.Transaction(pool);
|
||||||
|
await transaction.begin();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new sql.Request(transaction)
|
||||||
|
.input('projectId', sql.Int, projectId)
|
||||||
|
.input('projectName', sql.NVarChar, projectName)
|
||||||
|
.query(`
|
||||||
|
UPDATE AssetProjects
|
||||||
|
SET ProjectName = @projectName,
|
||||||
|
UpdatedDate = GETDATE()
|
||||||
|
WHERE ProjectId = @projectId
|
||||||
|
`);
|
||||||
|
|
||||||
|
await new sql.Request(transaction)
|
||||||
|
.input('oldProjectName', sql.NVarChar, currentName)
|
||||||
|
.input('newProjectName', sql.NVarChar, projectName)
|
||||||
|
.query(`
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET Project = @newProjectName,
|
||||||
|
UpdatedDate = GETDATE()
|
||||||
|
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@oldProjectName)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.json({ success: true, message: 'Da cap nhat du an' });
|
||||||
|
} catch (transactionErr) {
|
||||||
|
try {
|
||||||
|
await transaction.rollback();
|
||||||
|
} catch (rollbackErr) {
|
||||||
|
// Ignore rollback errors if transaction already ended.
|
||||||
|
}
|
||||||
|
throw transactionErr;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (String(err.message || '').includes('UX_AssetProjects_ProjectName')) {
|
||||||
|
return res.status(409).json({ success: false, message: 'Du an da ton tai' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({ success: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const projectId = Number(req.params.id);
|
||||||
|
if (!Number.isInteger(projectId) || projectId <= 0) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Ma du an khong hop le' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentResult = await pool.request()
|
||||||
|
.input('projectId', sql.Int, projectId)
|
||||||
|
.query(`
|
||||||
|
SELECT ProjectId, ProjectName
|
||||||
|
FROM AssetProjects
|
||||||
|
WHERE ProjectId = @projectId
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (currentResult.recordset.length === 0) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Khong tim thay du an' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectName = String(currentResult.recordset[0].ProjectName || '').trim();
|
||||||
|
const transaction = new sql.Transaction(pool);
|
||||||
|
await transaction.begin();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new sql.Request(transaction)
|
||||||
|
.input('projectName', sql.NVarChar, projectName)
|
||||||
|
.query(`
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET Project = NULL,
|
||||||
|
UpdatedDate = GETDATE()
|
||||||
|
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@projectName)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await new sql.Request(transaction)
|
||||||
|
.input('projectId', sql.Int, projectId)
|
||||||
|
.query(`
|
||||||
|
DELETE FROM AssetProjects
|
||||||
|
WHERE ProjectId = @projectId
|
||||||
|
`);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
res.json({ success: true, message: 'Da xoa du an' });
|
||||||
|
} catch (transactionErr) {
|
||||||
|
try {
|
||||||
|
await transaction.rollback();
|
||||||
|
} catch (rollbackErr) {
|
||||||
|
// Ignore rollback errors if transaction already ended.
|
||||||
|
}
|
||||||
|
throw transactionErr;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ success: false, message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// API ROUTES - Asset Inventory
|
// API ROUTES - Asset Inventory
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|||||||
@@ -154,7 +154,21 @@ WHERE NOT EXISTS (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 6. CREATE ASSET BORROW REQUESTS TABLE
|
-- 6. CREATE ASSET PROJECTS TABLE
|
||||||
|
-- ===========================================
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE AssetProjects (
|
||||||
|
ProjectId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
|
ProjectName NVARCHAR(150) NOT NULL,
|
||||||
|
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||||
|
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||||
|
);
|
||||||
|
PRINT 'Table AssetProjects created successfully.';
|
||||||
|
END
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 7. CREATE ASSET BORROW REQUESTS TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -246,7 +260,7 @@ UPDATE AssetBorrowRequests
|
|||||||
SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');
|
SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 7. CREATE AUDIT LOG TABLE
|
-- 8. CREATE AUDIT LOG TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -265,7 +279,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 8. CREATE INDEXES
|
-- 9. CREATE INDEXES
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -302,6 +316,11 @@ BEGIN
|
|||||||
CREATE UNIQUE INDEX UX_AssetDepartments_DepartmentName ON AssetDepartments(DepartmentName);
|
CREATE UNIQUE INDEX UX_AssetDepartments_DepartmentName ON AssetDepartments(DepartmentName);
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetProjects_ProjectName')
|
||||||
|
BEGIN
|
||||||
|
CREATE UNIQUE INDEX UX_AssetProjects_ProjectName ON AssetProjects(ProjectName);
|
||||||
|
END
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId')
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId')
|
||||||
BEGIN
|
BEGIN
|
||||||
CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);
|
CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);
|
||||||
@@ -325,7 +344,7 @@ END
|
|||||||
PRINT 'Indexes created successfully.';
|
PRINT 'Indexes created successfully.';
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 9. INSERT INITIAL DATA
|
-- 10. INSERT INITIAL DATA
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
|
|
||||||
-- Check if admin user exists
|
-- Check if admin user exists
|
||||||
@@ -349,7 +368,7 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 10. DISPLAY DATABASE INFORMATION
|
-- 11. DISPLAY DATABASE INFORMATION
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
PRINT '';
|
PRINT '';
|
||||||
PRINT '========================================';
|
PRINT '========================================';
|
||||||
|
|||||||
472
public/js/app.js
472
public/js/app.js
@@ -46,6 +46,8 @@ class AccountManager {
|
|||||||
this.assetBorrowProductLoading = false;
|
this.assetBorrowProductLoading = false;
|
||||||
this.assetDepartments = [];
|
this.assetDepartments = [];
|
||||||
this.assetDepartmentSearchTerm = '';
|
this.assetDepartmentSearchTerm = '';
|
||||||
|
this.assetProjects = [];
|
||||||
|
this.assetProjectSearchTerm = '';
|
||||||
this.selectedAssetIds = new Set();
|
this.selectedAssetIds = new Set();
|
||||||
this.mobileBreakpoint = 900;
|
this.mobileBreakpoint = 900;
|
||||||
this.boundResizeHandler = null;
|
this.boundResizeHandler = null;
|
||||||
@@ -56,6 +58,8 @@ class AccountManager {
|
|||||||
this.pendingBorrowAssetId = undefined;
|
this.pendingBorrowAssetId = undefined;
|
||||||
this.editingAssetDepartmentId = undefined;
|
this.editingAssetDepartmentId = undefined;
|
||||||
this.pendingDeleteAssetDepartmentId = undefined;
|
this.pendingDeleteAssetDepartmentId = undefined;
|
||||||
|
this.editingAssetProjectId = undefined;
|
||||||
|
this.pendingDeleteAssetProjectId = undefined;
|
||||||
this.assetBorrowRequestType = 'borrow';
|
this.assetBorrowRequestType = 'borrow';
|
||||||
this.pendingAssetRequestRejectId = undefined;
|
this.pendingAssetRequestRejectId = undefined;
|
||||||
this.assetBorrowAutoRefreshTimer = undefined;
|
this.assetBorrowAutoRefreshTimer = undefined;
|
||||||
@@ -174,6 +178,7 @@ class AccountManager {
|
|||||||
await this.fetchAssets();
|
await this.fetchAssets();
|
||||||
await this.fetchAssetBorrows();
|
await this.fetchAssetBorrows();
|
||||||
await this.fetchAssetDepartments();
|
await this.fetchAssetDepartments();
|
||||||
|
await this.fetchAssetProjects();
|
||||||
|
|
||||||
if (this.canCurrentUserManageAssets()) {
|
if (this.canCurrentUserManageAssets()) {
|
||||||
await this.fetchUsers();
|
await this.fetchUsers();
|
||||||
@@ -230,6 +235,10 @@ class AccountManager {
|
|||||||
mainContent.innerHTML = this.getAssetDepartmentsContent();
|
mainContent.innerHTML = this.getAssetDepartmentsContent();
|
||||||
this.setupAssetDepartmentListeners();
|
this.setupAssetDepartmentListeners();
|
||||||
this.setupAddButtonListeners();
|
this.setupAddButtonListeners();
|
||||||
|
} else if (page === 'asset-projects') {
|
||||||
|
mainContent.innerHTML = this.getAssetProjectsContent();
|
||||||
|
this.setupAssetProjectListeners();
|
||||||
|
this.setupAddButtonListeners();
|
||||||
} else if (page === 'accounts') {
|
} else if (page === 'accounts') {
|
||||||
mainContent.innerHTML = this.getAccountsContent();
|
mainContent.innerHTML = this.getAccountsContent();
|
||||||
this.setupAccountRowListeners();
|
this.setupAccountRowListeners();
|
||||||
@@ -784,6 +793,59 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUniqueAssetProjectNames() {
|
||||||
|
const rows = Array.isArray(this.assetProjects) ? this.assetProjects : [];
|
||||||
|
const seen = new Set();
|
||||||
|
return rows
|
||||||
|
.map(item => String(item?.ProjectName || '').trim())
|
||||||
|
.filter(name => {
|
||||||
|
if (!name) return false;
|
||||||
|
const key = name.toLowerCase();
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'vi', { sensitivity: 'base' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshAssetProjectOptions(selectedValue = '') {
|
||||||
|
const select = document.getElementById('assetProjectInput');
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedSelected = String(selectedValue || select.value || '').trim();
|
||||||
|
const projectNames = this.getUniqueAssetProjectNames();
|
||||||
|
select.innerHTML = '';
|
||||||
|
|
||||||
|
const emptyOption = document.createElement('option');
|
||||||
|
emptyOption.value = '';
|
||||||
|
emptyOption.textContent = '-- Chọn dự án --';
|
||||||
|
select.appendChild(emptyOption);
|
||||||
|
|
||||||
|
let hasSelected = false;
|
||||||
|
projectNames.forEach(name => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = name;
|
||||||
|
option.textContent = name;
|
||||||
|
if (normalizedSelected && name === normalizedSelected) {
|
||||||
|
option.selected = true;
|
||||||
|
hasSelected = true;
|
||||||
|
}
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (normalizedSelected && !hasSelected) {
|
||||||
|
const legacyOption = document.createElement('option');
|
||||||
|
legacyOption.value = normalizedSelected;
|
||||||
|
legacyOption.textContent = normalizedSelected;
|
||||||
|
legacyOption.selected = true;
|
||||||
|
select.appendChild(legacyOption);
|
||||||
|
} else if (!normalizedSelected) {
|
||||||
|
select.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchAssets() {
|
async fetchAssets() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${this.apiBase}/assets`);
|
const res = await fetch(`${this.apiBase}/assets`);
|
||||||
@@ -838,6 +900,21 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchAssetProjects() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${this.apiBase}/asset-projects`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.assetProjects = Array.isArray(data.data) ? data.data : [];
|
||||||
|
this.refreshAssetProjectOptions(document.getElementById('assetProjectInput')?.value || '');
|
||||||
|
} else {
|
||||||
|
console.error('Load asset projects failed:', data.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch asset projects error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fetchRoles() {
|
async fetchRoles() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${this.apiBase}/roles`);
|
const res = await fetch(`${this.apiBase}/roles`);
|
||||||
@@ -874,6 +951,7 @@ class AccountManager {
|
|||||||
this.setupAddButtonListeners();
|
this.setupAddButtonListeners();
|
||||||
this.setupFilters();
|
this.setupFilters();
|
||||||
this.refreshAssetDepartmentOptions(document.getElementById('assetDepartmentInput')?.value || '');
|
this.refreshAssetDepartmentOptions(document.getElementById('assetDepartmentInput')?.value || '');
|
||||||
|
this.refreshAssetProjectOptions(document.getElementById('assetProjectInput')?.value || '');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Lỗi load modals:', error);
|
console.error('Lỗi load modals:', error);
|
||||||
}
|
}
|
||||||
@@ -885,6 +963,7 @@ class AccountManager {
|
|||||||
const assetSearch = document.getElementById('assetSearch');
|
const assetSearch = document.getElementById('assetSearch');
|
||||||
const assetBorrowSearch = document.getElementById('assetBorrowSearch');
|
const assetBorrowSearch = document.getElementById('assetBorrowSearch');
|
||||||
const assetDepartmentSearch = document.getElementById('assetDepartmentSearch');
|
const assetDepartmentSearch = document.getElementById('assetDepartmentSearch');
|
||||||
|
const assetProjectSearch = document.getElementById('assetProjectSearch');
|
||||||
|
|
||||||
if (accountSearch && accountSearch.dataset.focused === 'true') {
|
if (accountSearch && accountSearch.dataset.focused === 'true') {
|
||||||
const pos = accountSearch.selectionStart || accountSearch.value.length;
|
const pos = accountSearch.selectionStart || accountSearch.value.length;
|
||||||
@@ -915,6 +994,12 @@ class AccountManager {
|
|||||||
assetDepartmentSearch.focus();
|
assetDepartmentSearch.focus();
|
||||||
assetDepartmentSearch.setSelectionRange(pos, pos);
|
assetDepartmentSearch.setSelectionRange(pos, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (assetProjectSearch && assetProjectSearch.dataset.focused === 'true') {
|
||||||
|
const pos = assetProjectSearch.selectionStart || assetProjectSearch.value.length;
|
||||||
|
assetProjectSearch.focus();
|
||||||
|
assetProjectSearch.setSelectionRange(pos, pos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
setupEventListeners() {
|
||||||
@@ -981,6 +1066,7 @@ class AccountManager {
|
|||||||
assetForm.dataset.boundSubmit = 'true';
|
assetForm.dataset.boundSubmit = 'true';
|
||||||
}
|
}
|
||||||
this.refreshAssetDepartmentOptions(document.getElementById('assetDepartmentInput')?.value || '');
|
this.refreshAssetDepartmentOptions(document.getElementById('assetDepartmentInput')?.value || '');
|
||||||
|
this.refreshAssetProjectOptions(document.getElementById('assetProjectInput')?.value || '');
|
||||||
this.setupAssetStockListeners();
|
this.setupAssetStockListeners();
|
||||||
this.setupAssetFormValidationListeners();
|
this.setupAssetFormValidationListeners();
|
||||||
}
|
}
|
||||||
@@ -1043,6 +1129,23 @@ class AccountManager {
|
|||||||
btn.dataset.boundClick = 'true';
|
btn.dataset.boundClick = 'true';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const assetProjectForm = document.getElementById('assetProjectForm');
|
||||||
|
if (assetProjectForm) {
|
||||||
|
if (!assetProjectForm.dataset.boundSubmit) {
|
||||||
|
assetProjectForm.addEventListener('submit', (e) => this.handleAssetProjectSubmit(e));
|
||||||
|
assetProjectForm.dataset.boundSubmit = 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.confirm-delete-asset-project').forEach(btn => {
|
||||||
|
if (btn.dataset.boundClick === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => this.confirmDeleteAssetProject());
|
||||||
|
btn.dataset.boundClick = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
// Close when clicking backdrop outside modal content
|
// Close when clicking backdrop outside modal content
|
||||||
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
||||||
backdrop.addEventListener('click', (evt) => {
|
backdrop.addEventListener('click', (evt) => {
|
||||||
@@ -2160,6 +2263,351 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFilteredAssetProjects() {
|
||||||
|
const search = String(this.assetProjectSearchTerm || '').toLowerCase();
|
||||||
|
const source = Array.isArray(this.assetProjects) ? this.assetProjects : [];
|
||||||
|
|
||||||
|
return source.filter(item => {
|
||||||
|
if (!search) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = String(item?.ProjectName || '').toLowerCase();
|
||||||
|
return name.includes(search);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAssetProjectsRowsHtml(projects = []) {
|
||||||
|
const canManageAssets = this.canCurrentUserManageAssets();
|
||||||
|
if (!projects.length) {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dự án nào.</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects.map((item, index) => {
|
||||||
|
const projectId = Number(item?.ProjectId);
|
||||||
|
const assetCount = Number(item?.AssetCount) || 0;
|
||||||
|
const projectName = this.escapeHtml(item?.ProjectName || '-');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="hover:bg-slate-50/80 transition-colors">
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${index + 1}</td>
|
||||||
|
<td class="px-4 py-3 text-sm font-semibold text-slate-700">${projectName}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${assetCount}</td>
|
||||||
|
<td class="px-4 py-3 text-right">
|
||||||
|
<div class="inline-flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
class="p-1.5 text-slate-400 transition-colors edit-asset-project ${canManageAssets ? 'hover:text-primary' : 'opacity-40 cursor-not-allowed'}"
|
||||||
|
data-project-id="${projectId}"
|
||||||
|
${canManageAssets ? '' : 'disabled'}
|
||||||
|
title="${canManageAssets ? 'Sửa dự án' : 'Chỉ xem'}"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg">edit</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1.5 text-slate-400 transition-colors delete-asset-project ${canManageAssets ? 'hover:text-error' : 'opacity-40 cursor-not-allowed'}"
|
||||||
|
data-project-id="${projectId}"
|
||||||
|
${canManageAssets ? '' : 'disabled'}
|
||||||
|
title="${canManageAssets ? 'Xóa dự án' : 'Chỉ xem'}"
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined text-lg">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetProjectsContent() {
|
||||||
|
const filteredProjects = this.getFilteredAssetProjects();
|
||||||
|
const canManageAssets = this.canCurrentUserManageAssets();
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="asset-projects-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">Quản Lý Dự Án</h1>
|
||||||
|
<p class="text-sm text-on-surface-variant">Thêm, sửa, xóa danh mục dự án sử dụng trong tài sản.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="addAssetProjectBtn"
|
||||||
|
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">workspaces</span>
|
||||||
|
Thêm dự án
|
||||||
|
</button>
|
||||||
|
</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="assetProjectSearch"
|
||||||
|
value="${this.escapeHtml(this.assetProjectSearchTerm)}"
|
||||||
|
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="Nhập tên dự án..."
|
||||||
|
>
|
||||||
|
</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">Dự án</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Sd tài sản</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 text-right">Thao tác</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100 asset-projects-table-body">
|
||||||
|
${this.buildAssetProjectsRowsHtml(filteredProjects)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-2 border-t border-slate-200 bg-slate-50 text-xs text-slate-600">
|
||||||
|
Tổng dự án: <span id="assetProjectCount">${filteredProjects.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAssetProjectsTableBody() {
|
||||||
|
const tbody = document.querySelector('.asset-projects-table-body');
|
||||||
|
if (!tbody) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredProjects = this.getFilteredAssetProjects();
|
||||||
|
tbody.innerHTML = this.buildAssetProjectsRowsHtml(filteredProjects);
|
||||||
|
|
||||||
|
const countElement = document.getElementById('assetProjectCount');
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = String(filteredProjects.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupAssetProjectActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAssetProjectActionListeners() {
|
||||||
|
document.querySelectorAll('.edit-asset-project').forEach(btn => {
|
||||||
|
if (btn.dataset.boundClick === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const projectId = Number(btn.dataset.projectId);
|
||||||
|
if (!Number.isFinite(projectId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleUpdateAssetProject(projectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.dataset.boundClick = 'true';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.delete-asset-project').forEach(btn => {
|
||||||
|
if (btn.dataset.boundClick === 'true') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const projectId = Number(btn.dataset.projectId);
|
||||||
|
if (!Number.isFinite(projectId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.handleDeleteAssetProject(projectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
btn.dataset.boundClick = 'true';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAssetProjectListeners() {
|
||||||
|
const searchInput = document.getElementById('assetProjectSearch');
|
||||||
|
if (searchInput && searchInput.dataset.boundInput !== 'true') {
|
||||||
|
searchInput.addEventListener('input', (event) => {
|
||||||
|
this.assetProjectSearchTerm = String(event.target.value || '').trim();
|
||||||
|
this.renderAssetProjectsTableBody();
|
||||||
|
});
|
||||||
|
searchInput.addEventListener('focus', () => {
|
||||||
|
searchInput.dataset.focused = 'true';
|
||||||
|
});
|
||||||
|
searchInput.addEventListener('blur', () => {
|
||||||
|
searchInput.dataset.focused = 'false';
|
||||||
|
});
|
||||||
|
searchInput.dataset.boundInput = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupAssetProjectActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshAssetProjectsUI() {
|
||||||
|
await this.fetchAssetProjects();
|
||||||
|
if (this.currentPage === 'asset-projects') {
|
||||||
|
this.renderAssetProjectsTableBody();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetProjectById(projectId) {
|
||||||
|
return this.assetProjects.find(item => Number(item?.ProjectId) === Number(projectId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
openAssetProjectModal(project = null) {
|
||||||
|
const modal = document.getElementById('assetProjectModal');
|
||||||
|
const titleNode = document.getElementById('assetProjectModalTitle');
|
||||||
|
const nameInput = document.getElementById('assetProjectNameInput');
|
||||||
|
if (!modal || !nameInput) {
|
||||||
|
this.notifyFailure('Không mở được biểu mẫu dự án');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editing = project && Number.isFinite(Number(project.ProjectId));
|
||||||
|
this.editingAssetProjectId = editing ? Number(project.ProjectId) : undefined;
|
||||||
|
|
||||||
|
if (titleNode) {
|
||||||
|
titleNode.textContent = editing ? 'Sửa dự án' : 'Thêm dự án';
|
||||||
|
}
|
||||||
|
nameInput.value = editing ? String(project.ProjectName || '') : '';
|
||||||
|
modal.classList.add('open');
|
||||||
|
nameInput.focus();
|
||||||
|
nameInput.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
openDeleteAssetProjectModal(project) {
|
||||||
|
const modal = document.getElementById('deleteAssetProjectModal');
|
||||||
|
const nameNode = document.getElementById('deleteAssetProjectName');
|
||||||
|
if (!modal) {
|
||||||
|
this.notifyFailure('Không mở được hộp thoại xóa dự án');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingDeleteAssetProjectId = Number(project?.ProjectId);
|
||||||
|
if (nameNode) {
|
||||||
|
nameNode.textContent = String(project?.ProjectName || '-');
|
||||||
|
}
|
||||||
|
modal.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleCreateAssetProject() {
|
||||||
|
if (!this.ensureAssetManagePermission('thêm dự án')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openAssetProjectModal(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAssetProjectSubmit(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!this.ensureAssetManagePermission('thêm hoặc sửa dự án')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameInput = document.getElementById('assetProjectNameInput');
|
||||||
|
const projectName = String(nameInput?.value || '').trim();
|
||||||
|
if (!projectName) {
|
||||||
|
this.notifyWarning('Tên dự án là bắt buộc');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEdit = Number.isFinite(Number(this.editingAssetProjectId));
|
||||||
|
const endpoint = isEdit
|
||||||
|
? `${this.apiBase}/asset-projects/${this.editingAssetProjectId}`
|
||||||
|
: `${this.apiBase}/asset-projects`;
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method,
|
||||||
|
headers: this.getAuthHeaders(true),
|
||||||
|
body: JSON.stringify({ projectName })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
|
this.notifyFailure(data.message || 'Lưu dự án thất bại');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editingAssetProjectId = undefined;
|
||||||
|
closeAssetProjectModal();
|
||||||
|
this.notifySuccess(isEdit ? 'Cập nhật dự án thành công' : 'Thêm dự án thành công');
|
||||||
|
await this.refreshAssetProjectsUI();
|
||||||
|
await this.refreshAssetsUI();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.notifyFailure('Lưu dự án thất bại');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleUpdateAssetProject(projectId) {
|
||||||
|
if (!this.ensureAssetManagePermission('sửa dự án')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = this.getAssetProjectById(projectId);
|
||||||
|
if (!targetProject) {
|
||||||
|
this.notifyWarning('Không tìm thấy dự án');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openAssetProjectModal(targetProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDeleteAssetProject(projectId) {
|
||||||
|
if (!this.ensureAssetManagePermission('xóa dự án')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetProject = this.getAssetProjectById(projectId);
|
||||||
|
if (!targetProject) {
|
||||||
|
this.notifyWarning('Không tìm thấy dự án');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openDeleteAssetProjectModal(targetProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmDeleteAssetProject() {
|
||||||
|
if (!this.ensureAssetManagePermission('xóa dự án')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(Number(this.pendingDeleteAssetProjectId))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/asset-projects/${this.pendingDeleteAssetProjectId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.getAuthHeaders(false)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
|
this.notifyFailure(data.message || 'Xóa dự án thất bại');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingDeleteAssetProjectId = undefined;
|
||||||
|
closeDeleteAssetProjectModal();
|
||||||
|
this.notifySuccess('Xóa dự án thành công');
|
||||||
|
await this.refreshAssetProjectsUI();
|
||||||
|
await this.refreshAssetsUI();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.notifyFailure('Xóa dự án thất bại');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildAssetBorrowRowHtml(item, rowNumber) {
|
buildAssetBorrowRowHtml(item, rowNumber) {
|
||||||
const assetName = item.AssetName || '-';
|
const assetName = item.AssetName || '-';
|
||||||
const assetCode = item.AssetCode ? `<div class="text-[11px] text-slate-500">${this.escapeHtml(item.AssetCode)}</div>` : '';
|
const assetCode = item.AssetCode ? `<div class="text-[11px] text-slate-500">${this.escapeHtml(item.AssetCode)}</div>` : '';
|
||||||
@@ -3351,7 +3799,7 @@ class AccountManager {
|
|||||||
document.getElementById('assetImportInPeriodInput').value = this.parseNonNegativeInteger(sourceAsset?.ImportInPeriod, 0);
|
document.getElementById('assetImportInPeriodInput').value = this.parseNonNegativeInteger(sourceAsset?.ImportInPeriod, 0);
|
||||||
document.getElementById('assetUnitInput').value = sourceAsset?.Unit || '';
|
document.getElementById('assetUnitInput').value = sourceAsset?.Unit || '';
|
||||||
this.refreshAssetDepartmentOptions(sourceAsset?.Department || '');
|
this.refreshAssetDepartmentOptions(sourceAsset?.Department || '');
|
||||||
document.getElementById('assetProjectInput').value = sourceAsset?.Project || '';
|
this.refreshAssetProjectOptions(sourceAsset?.Project || '');
|
||||||
document.getElementById('assetLocationInput').value = sourceAsset?.Location || '';
|
document.getElementById('assetLocationInput').value = sourceAsset?.Location || '';
|
||||||
this.refreshAssetCustodianOptions(sourceAsset?.Custodian || '');
|
this.refreshAssetCustodianOptions(sourceAsset?.Custodian || '');
|
||||||
|
|
||||||
@@ -3383,6 +3831,7 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.refreshAssetDepartmentOptions(document.getElementById('assetDepartmentInput')?.value || '');
|
this.refreshAssetDepartmentOptions(document.getElementById('assetDepartmentInput')?.value || '');
|
||||||
|
this.refreshAssetProjectOptions(document.getElementById('assetProjectInput')?.value || '');
|
||||||
|
|
||||||
if (!this.users.length) {
|
if (!this.users.length) {
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
@@ -3796,6 +4245,7 @@ class AccountManager {
|
|||||||
async refreshAssetsUI() {
|
async refreshAssetsUI() {
|
||||||
await this.fetchAssets();
|
await this.fetchAssets();
|
||||||
await this.fetchAssetDepartments();
|
await this.fetchAssetDepartments();
|
||||||
|
await this.fetchAssetProjects();
|
||||||
if (this.currentPage === 'assets') {
|
if (this.currentPage === 'assets') {
|
||||||
this.renderView('assets');
|
this.renderView('assets');
|
||||||
}
|
}
|
||||||
@@ -5110,6 +5560,12 @@ class AccountManager {
|
|||||||
addAssetDepartmentBtn.dataset.boundClick = 'true';
|
addAssetDepartmentBtn.dataset.boundClick = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addAssetProjectBtn = document.getElementById('addAssetProjectBtn');
|
||||||
|
if (addAssetProjectBtn && !addAssetProjectBtn.dataset.boundClick) {
|
||||||
|
addAssetProjectBtn.addEventListener('click', () => this.handleCreateAssetProject());
|
||||||
|
addAssetProjectBtn.dataset.boundClick = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
const addAssetBorrowRequestBtn = document.getElementById('addAssetBorrowRequestBtn');
|
const addAssetBorrowRequestBtn = document.getElementById('addAssetBorrowRequestBtn');
|
||||||
if (addAssetBorrowRequestBtn && !addAssetBorrowRequestBtn.dataset.boundClick) {
|
if (addAssetBorrowRequestBtn && !addAssetBorrowRequestBtn.dataset.boundClick) {
|
||||||
addAssetBorrowRequestBtn.addEventListener('click', () => this.openAssetBorrowRequestModal('borrow'));
|
addAssetBorrowRequestBtn.addEventListener('click', () => this.openAssetBorrowRequestModal('borrow'));
|
||||||
@@ -6406,6 +6862,20 @@ function closeDeleteAssetDepartmentModal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeAssetProjectModal() {
|
||||||
|
const modal = document.getElementById('assetProjectModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteAssetProjectModal() {
|
||||||
|
const modal = document.getElementById('deleteAssetProjectModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeUserModal() {
|
function closeUserModal() {
|
||||||
const userModalContainer = document.getElementById('userModalContainer');
|
const userModalContainer = document.getElementById('userModalContainer');
|
||||||
if (userModalContainer) {
|
if (userModalContainer) {
|
||||||
|
|||||||
@@ -265,7 +265,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Dự án</label>
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Dự án</label>
|
||||||
<input type="text" id="assetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="AGV / SS demo">
|
<select id="assetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
|
||||||
|
<option value="">-- Chọn dự án --</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Vị trí</label>
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Vị trí</label>
|
||||||
@@ -566,3 +568,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Asset Project Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetProjectModal">
|
||||||
|
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
|
||||||
|
<h3 class="text-base font-extrabold text-slate-900" id="assetProjectModalTitle">Thêm dự án</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetProjectModal()">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="assetProjectForm" class="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên dự án</label>
|
||||||
|
<input type="text" id="assetProjectNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Ví dụ: AGV">
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAssetProjectModal()">Hủy</button>
|
||||||
|
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Lưu</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Asset Project Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetProjectModal">
|
||||||
|
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
|
||||||
|
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
|
||||||
|
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
|
||||||
|
<h3 class="text-base font-extrabold text-red-700">Xóa dự án</h3>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa dự án <strong id="deleteAssetProjectName">-</strong>? Các tài sản đang gắn dự án này sẽ được để trống dự án.</p>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteAssetProjectModal()">Hủy</button>
|
||||||
|
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-asset-project">Xóa</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -248,6 +248,10 @@
|
|||||||
<span class="material-symbols-outlined">apartment</span>
|
<span class="material-symbols-outlined">apartment</span>
|
||||||
<span> Phòng Ban</span>
|
<span> Phòng Ban</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#asset-projects" data-nav="asset-projects" 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">workspaces</span>
|
||||||
|
<span>Dự án</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user