docs: consolidate documentation into organized structure

- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
This commit is contained in:
2025-10-05 11:05:04 +02:00
parent 887847dde6
commit 5050c7d73a
36686 changed files with 196456 additions and 12398919 deletions

View File

@@ -0,0 +1,295 @@
import { Module } from '../core/module.js';
/**
* Admin Data Table Module
*
* Provides interactive functionality for admin data tables:
* - AJAX loading and pagination
* - Sorting by columns
* - Search/filtering
* - Auto-refresh
*/
export class AdminDataTable extends Module {
constructor(element) {
super(element);
this.resource = element.dataset.resource;
this.apiEndpoint = element.dataset.apiEndpoint;
this.sortable = element.dataset.sortable === 'true';
this.searchable = element.dataset.searchable === 'true';
this.paginated = element.dataset.paginated === 'true';
this.perPage = parseInt(element.dataset.perPage) || 25;
this.currentPage = 1;
this.currentSort = null;
this.currentSearch = '';
}
async init() {
if (!this.apiEndpoint) {
console.warn('AdminDataTable: No API endpoint configured');
return;
}
this.tbody = this.element.querySelector('tbody');
this.thead = this.element.querySelector('thead');
if (!this.tbody) {
console.warn('AdminDataTable: No tbody found');
return;
}
this.setupEventListeners();
// Only load data if table is initially empty
if (this.tbody.children.length === 0) {
await this.loadData();
}
}
setupEventListeners() {
// Sortable columns
if (this.sortable && this.thead) {
this.thead.querySelectorAll('th').forEach((th, index) => {
const columnKey = th.dataset.column || this.getColumnKeyFromIndex(index);
if (columnKey) {
th.style.cursor = 'pointer';
th.classList.add('sortable');
th.addEventListener('click', () => {
this.sortByColumn(columnKey, th);
});
}
});
}
// Search input
if (this.searchable) {
const searchInput = this.getSearchInput();
if (searchInput) {
searchInput.addEventListener('input', this.debounce(() => {
this.currentSearch = searchInput.value;
this.currentPage = 1;
this.loadData();
}, 300));
}
}
// Pagination links (if exist)
this.setupPaginationListeners();
}
setupPaginationListeners() {
const paginationContainer = this.getPaginationContainer();
if (!paginationContainer) return;
paginationContainer.addEventListener('click', (e) => {
const pageLink = e.target.closest('[data-page]');
if (pageLink) {
e.preventDefault();
const page = parseInt(pageLink.dataset.page);
if (page && page !== this.currentPage) {
this.currentPage = page;
this.loadData();
}
}
});
}
async sortByColumn(columnKey, th) {
// Toggle sort direction
const currentDir = th.dataset.sortDir;
const newDir = currentDir === 'asc' ? 'desc' : 'asc';
// Update sort state
this.currentSort = { column: columnKey, direction: newDir };
// Update UI
this.thead.querySelectorAll('th').forEach(header => {
header.removeAttribute('data-sort-dir');
header.classList.remove('sort-asc', 'sort-desc');
});
th.dataset.sortDir = newDir;
th.classList.add(`sort-${newDir}`);
// Reload data
await this.loadData();
}
async loadData() {
const params = new URLSearchParams({
page: this.currentPage.toString(),
per_page: this.perPage.toString(),
});
if (this.currentSearch) {
params.set('search', this.currentSearch);
}
if (this.currentSort) {
params.set('sort_by', this.currentSort.column);
params.set('sort_dir', this.currentSort.direction);
}
const url = `${this.apiEndpoint}?${params}`;
try {
this.setLoadingState(true);
const response = await fetch(url);
const result = await response.json();
if (result.success) {
this.renderRows(result.data);
if (this.paginated && result.pagination) {
this.renderPagination(result.pagination);
}
} else {
this.showError(result.error || 'Failed to load data');
}
} catch (error) {
console.error('AdminDataTable: Failed to load data', error);
this.showError('Failed to load data');
} finally {
this.setLoadingState(false);
}
}
renderRows(data) {
if (!data || data.length === 0) {
this.tbody.innerHTML = this.getEmptyRow();
return;
}
// Get column keys from header
const columnKeys = this.getColumnKeys();
this.tbody.innerHTML = data.map(item => {
const cells = columnKeys.map(key => {
const value = item[key] !== undefined ? item[key] : '';
return `<td>${this.escapeHtml(value)}</td>`;
}).join('');
return `<tr data-id="${item.id || ''}">${cells}</tr>`;
}).join('');
}
renderPagination(pagination) {
const container = this.getPaginationContainer();
if (!container) return;
const { page, pages, total } = pagination;
if (pages <= 1) {
container.innerHTML = '';
return;
}
const links = [];
// Previous
if (page > 1) {
links.push(`<a href="#" data-page="${page - 1}" class="page-link">Previous</a>`);
}
// Page numbers
for (let i = 1; i <= pages; i++) {
if (i === page) {
links.push(`<span class="page-link active">${i}</span>`);
} else {
links.push(`<a href="#" data-page="${i}" class="page-link">${i}</a>`);
}
}
// Next
if (page < pages) {
links.push(`<a href="#" data-page="${page + 1}" class="page-link">Next</a>`);
}
container.innerHTML = `
<div class="pagination">
${links.join('')}
<span class="pagination-info">Total: ${total}</span>
</div>
`;
}
setLoadingState(loading) {
if (loading) {
this.element.classList.add('loading');
this.tbody.style.opacity = '0.5';
} else {
this.element.classList.remove('loading');
this.tbody.style.opacity = '1';
}
}
showError(message) {
const columnCount = this.getColumnKeys().length;
this.tbody.innerHTML = `
<tr>
<td colspan="${columnCount}" class="error-message">
${this.escapeHtml(message)}
</td>
</tr>
`;
}
getEmptyRow() {
const columnCount = this.getColumnKeys().length;
return `
<tr>
<td colspan="${columnCount}" class="empty-message">
No data available
</td>
</tr>
`;
}
getColumnKeys() {
if (!this.thead) return [];
const headers = this.thead.querySelectorAll('th');
return Array.from(headers).map((th, index) =>
th.dataset.column || this.getColumnKeyFromIndex(index)
).filter(Boolean);
}
getColumnKeyFromIndex(index) {
// Fallback: use column index as key
return `col_${index}`;
}
getSearchInput() {
return document.querySelector(`[data-table-search="${this.resource}"]`);
}
getPaginationContainer() {
return document.querySelector(`[data-table-pagination="${this.resource}"]`) ||
this.element.parentElement.querySelector('.pagination-container');
}
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
debounce(func, wait) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
}
// Auto-initialize all admin data tables
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-resource][data-api-endpoint]').forEach(table => {
new AdminDataTable(table).init();
});
});