done manager account

This commit is contained in:
2026-03-31 14:14:01 +07:00
parent 900a569c51
commit 58dbefa155
4 changed files with 194 additions and 343 deletions

View File

@@ -72,11 +72,50 @@ class AccountManager {
await this.fetchAccounts();
this.setupEventListeners();
this.loadModals(); // Load modals từ file riêng
// Render dashboard content (only for index.html)
// Single-page navigation based on hash
this.handleRoute(location.hash || '#dashboard');
window.addEventListener('hashchange', () => this.handleRoute(location.hash));
}
handleRoute(hash) {
const route = (hash || '#dashboard').replace('#', '') || 'dashboard';
this.renderView(route);
}
renderView(page) {
this.currentPage = page;
const mainContent = document.getElementById('mainContent');
if (mainContent && window.location.pathname.endsWith('index.html')) {
if (!mainContent) return;
if (page === 'applications') {
mainContent.innerHTML = this.getApplicationsContent();
this.setupAccountRowListeners();
this.setupAddButtonListeners();
this.setupFilters();
} else if (page === 'accounts') {
mainContent.innerHTML = this.getAccountsContent();
this.setupAccountRowListeners();
this.setupAddButtonListeners();
this.setupFilters();
} else {
mainContent.innerHTML = this.renderDashboard();
}
this.restoreSearchFocus();
this.setActiveNav(page);
}
setActiveNav(page) {
document.querySelectorAll('[data-nav]').forEach(link => {
const isActive = link.dataset.nav === page;
link.classList.toggle('border-l-4', isActive);
link.classList.toggle('border-blue-600', isActive);
link.classList.toggle('bg-slate-200/80', isActive);
link.classList.toggle('dark:bg-slate-800', isActive);
link.classList.toggle('text-slate-900', isActive);
link.classList.toggle('dark:text-slate-50', isActive);
link.classList.toggle('font-bold', isActive);
});
}
async fetchApplications() {
@@ -103,22 +142,47 @@ class AccountManager {
async loadModals() {
try {
const existingContainer = document.getElementById('modalsContainer');
if (existingContainer && existingContainer.children.length) {
return;
}
const response = await fetch('../modals.html');
const modalsHTML = await response.text();
const modalsContainer = document.getElementById('modalsContainer');
if (modalsContainer) {
modalsContainer.innerHTML = modalsHTML;
// Re-attach event listeners after modals are loaded
setTimeout(() => {
this.setupAccountRowListeners();
this.setupFormListeners();
}, 50);
const container = existingContainer || document.createElement('div');
if (!container.id) container.id = 'modalsContainer';
container.innerHTML = modalsHTML;
if (!container.parentElement) {
document.body.appendChild(container);
}
this.setupFormListeners();
this.setupAccountRowListeners();
this.setupAddButtonListeners();
this.setupFilters();
} catch (error) {
console.error('Lỗi load modals:', error);
}
}
restoreSearchFocus() {
const accountSearch = document.getElementById('accountSearch');
const appSearch = document.getElementById('appSearch');
if (accountSearch && accountSearch.dataset.focused === 'true') {
const pos = accountSearch.selectionStart || accountSearch.value.length;
accountSearch.focus();
accountSearch.setSelectionRange(pos, pos);
}
if (appSearch && appSearch.dataset.focused === 'true') {
const pos = appSearch.selectionStart || appSearch.value.length;
appSearch.focus();
appSearch.setSelectionRange(pos, pos);
}
}
setupEventListeners() {
// Modal close buttons
document.querySelectorAll('[data-close-modal]').forEach(btn => {
@@ -341,7 +405,7 @@ class AccountManager {
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
<tbody class="divide-y divide-slate-100 accounts-table-body">
${filteredAccounts.map(acc => `
<tr class="hover:bg-slate-50/80 transition-colors group account-row" data-account-id="${acc.AccountId}">
<td class="px-4 py-3 text-sm font-medium text-slate-900">${acc.Email || '-'}</td>
@@ -447,7 +511,7 @@ class AccountManager {
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-outline-variant/5">
<tbody class="divide-y divide-outline-variant/5 apps-table-body">
${filteredApps.map(app => `
<tr class="hover:bg-surface-container-low/30 transition-colors group">
<td class="px-6 py-3">
@@ -493,6 +557,76 @@ class AccountManager {
`;
}
renderAccountsTableBody() {
const tbody = document.querySelector('.accounts-table-body');
if (!tbody) return;
const filteredAccounts = this.getFilteredAccounts();
tbody.innerHTML = filteredAccounts.map(acc => `
<tr class="hover:bg-slate-50/80 transition-colors group account-row" data-account-id="${acc.AccountId}">
<td class="px-4 py-3 text-sm font-medium text-slate-900">${acc.Email || '-'}</td>
<td class="px-4 py-3 text-sm text-slate-600">${acc.AccountUsername || '-'}</td>
<td class="px-4 py-3 text-sm">
<span class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-semibold">${acc.AppName || '-'}</span>
</td>
<td class="px-4 py-3 text-right">
<button class="p-1.5 text-slate-400 hover:text-slate-600 transition-colors view-account" data-account-id="${acc.AccountId}" title="View Details">
<span class="material-symbols-outlined text-lg">info</span>
</button>
<button class="p-1.5 text-slate-400 hover:text-primary transition-colors edit-account" data-account-id="${acc.AccountId}">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button class="p-1.5 text-slate-400 hover:text-error transition-colors delete-account" data-account-id="${acc.AccountId}">
<span class="material-symbols-outlined text-lg">delete</span>
</button>
</td>
</tr>
`).join('');
this.setupAccountRowListeners();
}
renderApplicationsTableBody() {
const tbody = document.querySelector('.apps-table-body');
if (!tbody) return;
const filteredApps = this.getFilteredApplications();
tbody.innerHTML = filteredApps.map(app => `
<tr class="hover:bg-surface-container-low/30 transition-colors group">
<td class="px-6 py-3">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-surface-container-highest flex items-center justify-center text-primary">
<span class="material-symbols-outlined text-sm">${app.Icon || 'apps'}</span>
</div>
<span class="font-bold text-sm text-on-surface">${app.Name}</span>
</div>
</td>
<td class="px-6 py-3">
<span class="px-2 py-0.5 rounded-full text-[9px] font-black uppercase bg-surface-container-highest text-on-surface-variant">${app.Type}</span>
</td>
<td class="px-6 py-3 text-sm text-on-surface-variant max-w-xs truncate" title="${app.Description || ''}">${app.Description || '-'}</td>
<td class="px-6 py-3 text-sm text-primary max-w-xs truncate">${(app.Url || app.url) ? `<a href="${app.Url || app.url}" target="_blank" class="underline">${app.Url || app.url}</a>` : '-'}</td>
<td class="px-6 py-3">
<div class="flex items-center gap-1.5">
<div class="w-1.5 h-1.5 rounded-full ${(app.Status || app.status) === 'online' ? 'bg-primary' : 'bg-error'} ring-2 ${(app.Status || app.status) === 'online' ? 'ring-primary/20' : 'ring-error/20'}"></div>
<span class="text-xs font-medium ${(app.Status || app.status) === 'online' ? 'text-on-primary-fixed-variant' : 'text-error'}">${(app.Status || app.status) === 'online' ? 'Online' : 'Offline'}</span>
</div>
</td>
<td class="px-6 py-3 text-right">
<div class="flex items-center justify-end gap-1">
<button class="p-1.5 text-on-surface-variant hover:text-on-surface transition-colors view-app" data-app-id="${app.AppId}" title="View Details">
<span class="material-symbols-outlined text-lg">info</span>
</button>
<button class="p-1.5 text-on-surface-variant hover:text-primary transition-colors edit-app" data-app-id="${app.AppId}">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button class="p-1.5 text-on-surface-variant hover:text-error transition-colors delete-app" data-app-id="${app.AppId}">
<span class="material-symbols-outlined text-lg">delete</span>
</button>
</div>
</td>
</tr>
`).join('');
this.setupAccountRowListeners();
}
setupAccountRowListeners() {
// View Account listeners
document.querySelectorAll('.view-account').forEach(btn => {
@@ -504,8 +638,10 @@ class AccountManager {
document.getElementById('viewAccountService').textContent = account?.AppName || '-';
document.getElementById('viewAccountOwner').textContent = account?.Email || '-';
document.getElementById('viewAccountUsername').textContent = account?.AccountUsername || '-';
document.getElementById('viewAccountPassword').textContent = '••••••••';
document.getElementById('viewAccountPassword').dataset.visible = 'false';
const passwordEl = document.getElementById('viewAccountPassword');
passwordEl.dataset.password = account?.AccountPassword || '';
passwordEl.textContent = '••••••••';
passwordEl.dataset.visible = 'false';
document.getElementById('toggleIcon').textContent = 'visibility';
document.getElementById('viewAccountModal').classList.add('open');
});
@@ -596,14 +732,15 @@ class AccountManager {
btn.addEventListener('click', () => {
const passwordEl = document.getElementById('viewAccountPassword');
const toggleIcon = document.getElementById('toggleIcon');
const storedPwd = passwordEl.dataset.password || this.currentViewAccount?.AccountPassword || '';
const isVisible = passwordEl.dataset.visible === 'true';
if (isVisible) {
passwordEl.textContent = '••••••••';
passwordEl.dataset.visible = 'false';
toggleIcon.textContent = 'visibility';
} else {
passwordEl.textContent = this.currentViewAccount?.AccountPassword || '';
passwordEl.textContent = storedPwd || '(no password stored)';
passwordEl.dataset.visible = 'true';
toggleIcon.textContent = 'visibility_off';
}
@@ -733,26 +870,46 @@ class AccountManager {
serviceFilter.value = this.accountServiceFilter || '';
serviceFilter.addEventListener('change', (e) => {
this.accountServiceFilter = e.target.value;
this.refreshAccountsUI();
this.renderAccountsTableBody();
});
}
const accountSearch = document.getElementById('accountSearch');
if (accountSearch) {
accountSearch.value = this.accountSearchTerm;
accountSearch.addEventListener('input', (e) => {
this.accountSearchTerm = e.target.value.toLowerCase();
this.refreshAccountsUI();
});
const handleAccountSearch = event => {
this.accountSearchTerm = event.target.value.toLowerCase();
this.renderAccountsTableBody();
};
accountSearch.addEventListener('input', handleAccountSearch);
// Restore focus/selection after renders to avoid typing interruptions
accountSearch.addEventListener('focus', () => {
accountSearch.dataset.focused = 'true';
});
accountSearch.addEventListener('blur', () => {
accountSearch.dataset.focused = 'false';
});
}
const appSearch = document.getElementById('appSearch');
if (appSearch) {
appSearch.value = this.applicationSearchTerm;
appSearch.addEventListener('input', (e) => {
this.applicationSearchTerm = e.target.value.toLowerCase();
this.refreshApplicationsUI();
});
const handleApplicationSearch = event => {
this.applicationSearchTerm = event.target.value.toLowerCase();
this.renderApplicationsTableBody();
};
appSearch.addEventListener('input', handleApplicationSearch);
// Restore focus/selection after renders to avoid typing interruptions
appSearch.addEventListener('focus', () => {
appSearch.dataset.focused = 'true';
});
appSearch.addEventListener('blur', () => {
appSearch.dataset.focused = 'false';
});
}
}
@@ -820,12 +977,8 @@ class AccountManager {
async refreshAccountsUI() {
await this.fetchAccounts();
const mainContent = document.getElementById('mainContent');
if (mainContent) {
mainContent.innerHTML = this.getAccountsContent();
this.setupAccountRowListeners();
this.setupAddButtonListeners();
this.setupFilters();
if (this.currentPage === 'accounts') {
this.renderView('accounts');
}
}
@@ -865,12 +1018,8 @@ class AccountManager {
async refreshApplicationsUI() {
await this.fetchApplications();
const mainContent = document.getElementById('mainContent');
if (mainContent) {
mainContent.innerHTML = this.getApplicationsContent();
this.setupAccountRowListeners();
this.setupAddButtonListeners();
this.setupFilters();
if (this.currentPage === 'applications') {
this.renderView('applications');
}
}