first commit
This commit is contained in:
800
public/app.js
Normal file
800
public/app.js
Normal file
@@ -0,0 +1,800 @@
|
||||
// Global State
|
||||
const state = {
|
||||
currentTab: 'dashboard-tab',
|
||||
dbPage: 1,
|
||||
dbLimit: 12,
|
||||
dbSearch: '',
|
||||
dbCategory: '',
|
||||
dbCompany: '',
|
||||
selectedProduct: null,
|
||||
sseConnected: false
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const elements = {
|
||||
tabs: document.querySelectorAll('.nav-btn'),
|
||||
tabContents: document.querySelectorAll('.tab-content'),
|
||||
pageTitle: document.getElementById('page-title'),
|
||||
pageSubtitle: document.getElementById('page-subtitle'),
|
||||
|
||||
// Stats elements
|
||||
statTotal: document.getElementById('stat-total'),
|
||||
statCompleted: document.getElementById('stat-completed'),
|
||||
statPending: document.getElementById('stat-pending'),
|
||||
statFailed: document.getElementById('stat-failed'),
|
||||
progressPercent: document.getElementById('progress-percent'),
|
||||
progressFillSuccess: document.getElementById('progress-fill-success'),
|
||||
progressFillError: document.getElementById('progress-fill-error'),
|
||||
activeThreadsBadge: document.getElementById('active-threads-badge'),
|
||||
|
||||
// Scraper Controls
|
||||
inputConcurrency: document.getElementById('input-concurrency'),
|
||||
inputDelay: document.getElementById('input-delay'),
|
||||
btnToggleScrape: document.getElementById('btn-toggle-scrape'),
|
||||
btnRetryFailed: document.getElementById('btn-retry-failed'),
|
||||
consoleLogs: document.getElementById('console-logs'),
|
||||
btnClearLogs: document.getElementById('btn-clear-logs'),
|
||||
|
||||
// Database tab elements
|
||||
dbSearchInput: document.getElementById('db-search'),
|
||||
filterCategory: document.getElementById('filter-category'),
|
||||
filterCompany: document.getElementById('filter-company'),
|
||||
btnClearFilters: document.getElementById('btn-clear-filters'),
|
||||
productsGrid: document.getElementById('products-grid-list'),
|
||||
dbEmptyState: document.getElementById('db-empty-state'),
|
||||
dbLoadingState: document.getElementById('db-loading-state'),
|
||||
paginationControls: document.getElementById('pagination-controls'),
|
||||
btnPrevPage: document.getElementById('btn-prev-page'),
|
||||
btnNextPage: document.getElementById('btn-next-page'),
|
||||
pageNumbersContainer: document.getElementById('page-numbers-container'),
|
||||
|
||||
// Form tab elements
|
||||
productForm: document.getElementById('product-form'),
|
||||
formProductId: document.getElementById('form-product-id'),
|
||||
formBarcode: document.getElementById('form-barcode'),
|
||||
formName: document.getElementById('form-name'),
|
||||
formCategory: document.getElementById('form-category'),
|
||||
formSubcategory: document.getElementById('form-subcategory'),
|
||||
formCompany: document.getElementById('form-company'),
|
||||
formImage: document.getElementById('form-image'),
|
||||
btnSaveProduct: document.getElementById('btn-save-product'),
|
||||
btnResetForm: document.getElementById('btn-reset-form'),
|
||||
|
||||
// Modal elements
|
||||
productDetailModal: document.getElementById('product-detail-modal'),
|
||||
btnCloseModal: document.getElementById('btn-close-modal'),
|
||||
modalProductImage: document.getElementById('modal-product-image'),
|
||||
modalProductName: document.getElementById('modal-product-name'),
|
||||
modalProductCategory: document.getElementById('modal-product-category'),
|
||||
modalProductCompany: document.getElementById('modal-product-company'),
|
||||
modalCleanName: document.getElementById('modal-clean-name'),
|
||||
modalAmountRaw: document.getElementById('modal-amount-raw'),
|
||||
modalAmount: document.getElementById('modal-amount'),
|
||||
modalUnit: document.getElementById('modal-unit'),
|
||||
modalBarcodeSvg: document.getElementById('modal-barcode-svg'),
|
||||
modalQrcodeCanvas: document.getElementById('modal-qrcode-canvas'),
|
||||
downloadButtons: document.querySelectorAll('.download-btn'),
|
||||
|
||||
// Toast notification
|
||||
toast: document.getElementById('toast')
|
||||
};
|
||||
|
||||
// API Base URL
|
||||
const API_URL = ''; // Same host
|
||||
|
||||
// Toast Notification Helper
|
||||
function showToast(message, isError = false) {
|
||||
const toast = elements.toast;
|
||||
const icon = toast.querySelector('.toast-icon');
|
||||
const msg = toast.querySelector('.toast-message');
|
||||
|
||||
msg.textContent = message;
|
||||
|
||||
if (isError) {
|
||||
toast.classList.add('error');
|
||||
icon.className = 'fa-solid fa-circle-xmark toast-icon';
|
||||
} else {
|
||||
toast.classList.remove('error');
|
||||
icon.className = 'fa-solid fa-circle-check toast-icon';
|
||||
}
|
||||
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
/* Event Handlers & Core Functions */
|
||||
|
||||
// Initialize client
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTabs();
|
||||
initScraperControl();
|
||||
initDatabase();
|
||||
initForm();
|
||||
initModal();
|
||||
connectSSE();
|
||||
});
|
||||
|
||||
// 1. Navigation Tab Switches
|
||||
function initTabs() {
|
||||
elements.tabs.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const targetTab = btn.getAttribute('data-tab');
|
||||
state.currentTab = targetTab;
|
||||
|
||||
// Toggle active classes
|
||||
elements.tabs.forEach(t => t.classList.remove('active'));
|
||||
elements.tabContents.forEach(c => c.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
document.getElementById(targetTab).classList.add('active');
|
||||
|
||||
// Header Content Updates
|
||||
if (targetTab === 'dashboard-tab') {
|
||||
elements.pageTitle.textContent = 'Kontrol Paneli';
|
||||
elements.pageSubtitle.textContent = 'Veri tarama sürecini ve sunucu istatistiklerini canlı takip edin';
|
||||
} else if (targetTab === 'database-tab') {
|
||||
elements.pageTitle.textContent = 'Ürün Veritabanı';
|
||||
elements.pageSubtitle.textContent = 'Taranan barkod verilerini arayın, inceleyin, düzenleyin veya yeni kodlar üretin';
|
||||
fetchProducts();
|
||||
} else if (targetTab === 'add-product-tab') {
|
||||
elements.pageTitle.textContent = 'Ürün Ekle';
|
||||
elements.pageSubtitle.textContent = 'Veritabanına manuel olarak yeni bir barkod kaydı girin';
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 2. SSE Scraper Stream Connection
|
||||
let sseSource = null;
|
||||
let pollingInterval = null;
|
||||
|
||||
function connectSSE() {
|
||||
if (sseSource) {
|
||||
sseSource.close();
|
||||
}
|
||||
|
||||
const indicator = document.querySelector('.status-indicator');
|
||||
|
||||
// Set a timeout to switch to polling if SSE doesn't connect within 2.5 seconds
|
||||
const sseTimeout = setTimeout(() => {
|
||||
if (!state.sseConnected) {
|
||||
console.log('SSE connection timeout. Falling back to HTTP polling...');
|
||||
startPolling();
|
||||
}
|
||||
}, 2500);
|
||||
|
||||
try {
|
||||
if (!window.EventSource) {
|
||||
throw new Error('EventSource is not supported by this browser.');
|
||||
}
|
||||
|
||||
sseSource = new EventSource(`${API_URL}/api/scrape/stream`);
|
||||
|
||||
sseSource.onopen = () => {
|
||||
clearTimeout(sseTimeout);
|
||||
stopPolling();
|
||||
state.sseConnected = true;
|
||||
indicator.className = 'status-indicator connected';
|
||||
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Aktif (SSE)';
|
||||
};
|
||||
|
||||
sseSource.onerror = (e) => {
|
||||
clearTimeout(sseTimeout);
|
||||
state.sseConnected = false;
|
||||
indicator.className = 'status-indicator disconnected';
|
||||
indicator.querySelector('.status-text').textContent = 'SSE Bağlantısı Kesildi - Polling Aktif';
|
||||
startPolling();
|
||||
};
|
||||
|
||||
sseSource.onmessage = (event) => {
|
||||
try {
|
||||
const stats = JSON.parse(event.data);
|
||||
updateScraperDashboard(stats);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse SSE data:', error);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
clearTimeout(sseTimeout);
|
||||
console.warn('SSE initialization failed. Using polling fallback:', err.message);
|
||||
startPolling();
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollingInterval) return;
|
||||
|
||||
const indicator = document.querySelector('.status-indicator');
|
||||
indicator.className = 'status-indicator connected';
|
||||
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Aktif (Polling)';
|
||||
|
||||
// Run once immediately
|
||||
fetchStats();
|
||||
|
||||
pollingInterval = setInterval(fetchStats, 1500);
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
const indicator = document.querySelector('.status-indicator');
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/stats`);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const stats = await response.json();
|
||||
updateScraperDashboard(stats);
|
||||
indicator.className = 'status-indicator connected';
|
||||
if (!state.sseConnected) {
|
||||
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Aktif (Polling)';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
indicator.className = 'status-indicator disconnected';
|
||||
indicator.querySelector('.status-text').textContent = 'Sunucu Bağlantısı Koptu';
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update Dashboard Stats & Progress
|
||||
let lastLogTimestamp = null;
|
||||
function updateScraperDashboard(stats) {
|
||||
// 1. Text values
|
||||
elements.statTotal.textContent = stats.totalUrls.toLocaleString();
|
||||
elements.statCompleted.textContent = stats.completedCount.toLocaleString();
|
||||
elements.statPending.textContent = stats.pendingCount.toLocaleString();
|
||||
elements.statFailed.textContent = stats.failedCount.toLocaleString();
|
||||
|
||||
elements.activeThreadsBadge.textContent = `${stats.activeWorkers} Aktif İşçi`;
|
||||
|
||||
// 2. Concurrency inputs update if not focused
|
||||
if (document.activeElement !== elements.inputConcurrency) {
|
||||
elements.inputConcurrency.value = stats.concurrency;
|
||||
}
|
||||
if (document.activeElement !== elements.inputDelay) {
|
||||
elements.inputDelay.value = stats.delayMs;
|
||||
}
|
||||
|
||||
// 3. Play/Pause Button State
|
||||
if (stats.isRunning) {
|
||||
elements.btnToggleScrape.className = 'btn btn-danger btn-large';
|
||||
elements.btnToggleScrape.innerHTML = '<i class="fa-solid fa-pause"></i> Tarayıcıyı Duraklat';
|
||||
} else {
|
||||
elements.btnToggleScrape.className = 'btn btn-primary btn-large';
|
||||
elements.btnToggleScrape.innerHTML = '<i class="fa-solid fa-play"></i> Tarayıcıyı Başlat';
|
||||
}
|
||||
|
||||
// 4. Progress Bars
|
||||
const total = stats.totalUrls || 1;
|
||||
const completedPercent = (stats.completedCount / total) * 100;
|
||||
const failedPercent = (stats.failedCount / total) * 100;
|
||||
const totalPercent = Math.min(100, completedPercent + failedPercent);
|
||||
|
||||
elements.progressPercent.textContent = `${totalPercent.toFixed(1)}%`;
|
||||
elements.progressFillSuccess.style.width = `${completedPercent}%`;
|
||||
elements.progressFillError.style.width = `${failedPercent}%`;
|
||||
|
||||
// 5. Build Scrolling Logs
|
||||
if (stats.logs && stats.logs.length > 0) {
|
||||
const consoleBox = elements.consoleLogs;
|
||||
|
||||
// Clear default loading line on first actual logs
|
||||
if (consoleBox.innerHTML.includes('Sistem yükleniyor')) {
|
||||
consoleBox.innerHTML = '';
|
||||
}
|
||||
|
||||
// Find logs that are newer than what is currently rendered
|
||||
const existingLinesCount = consoleBox.childElementCount;
|
||||
const newLogs = stats.logs.slice(existingLinesCount);
|
||||
|
||||
newLogs.forEach(log => {
|
||||
const line = document.createElement('div');
|
||||
line.className = `console-line ${log.type}`;
|
||||
|
||||
const time = document.createElement('span');
|
||||
time.className = 'console-time';
|
||||
time.textContent = new Date(log.timestamp).toLocaleTimeString();
|
||||
|
||||
const msg = document.createElement('span');
|
||||
msg.textContent = log.message;
|
||||
|
||||
line.appendChild(time);
|
||||
line.appendChild(msg);
|
||||
consoleBox.appendChild(line);
|
||||
|
||||
// Limit lines in browser DOM to 300 for memory efficiency
|
||||
while (consoleBox.childElementCount > 300) {
|
||||
consoleBox.removeChild(consoleBox.firstChild);
|
||||
}
|
||||
|
||||
// Scroll to bottom
|
||||
consoleBox.scrollTop = consoleBox.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Scraper Control Actions
|
||||
function initScraperControl() {
|
||||
// Start/Pause Button Toggle
|
||||
elements.btnToggleScrape.addEventListener('click', async () => {
|
||||
const isRunningText = elements.btnToggleScrape.textContent.includes('Duraklat');
|
||||
const endpoint = isRunningText ? '/api/scrape/pause' : '/api/scrape/start';
|
||||
|
||||
const payload = isRunningText ? {} : {
|
||||
concurrency: parseInt(elements.inputConcurrency.value) || 5,
|
||||
delayMs: parseInt(elements.inputDelay.value) !== undefined ? parseInt(elements.inputDelay.value) : 500
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(data.message);
|
||||
} else {
|
||||
showToast(data.error || 'Tarama durumunu değiştirme işlemi başarısız oldu.', true);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Sunucu ile iletişim kurulamadı.', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Retry Failed
|
||||
elements.btnRetryFailed.addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch('/api/scrape/retry', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast(data.message);
|
||||
} else {
|
||||
showToast(data.error || 'Hatalı adresleri sıfırlama işlemi başarısız.', true);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('İşlem başarısız.', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear Logs Console View
|
||||
elements.btnClearLogs.addEventListener('click', () => {
|
||||
elements.consoleLogs.innerHTML = '<div class="console-line system">Konsol temizlendi. Yeni güncellemeler bekleniyor...</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Product Database Tab (Search, Filter, Pagination, Render)
|
||||
function initDatabase() {
|
||||
// Debounce search input
|
||||
let searchTimeout = null;
|
||||
elements.dbSearchInput.addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
state.dbSearch = elements.dbSearchInput.value.trim();
|
||||
state.dbPage = 1;
|
||||
fetchProducts();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
// Select filters change
|
||||
elements.filterCategory.addEventListener('change', () => {
|
||||
state.dbCategory = elements.filterCategory.value;
|
||||
state.dbPage = 1;
|
||||
fetchProducts();
|
||||
});
|
||||
|
||||
elements.filterCompany.addEventListener('change', () => {
|
||||
state.dbCompany = elements.filterCompany.value;
|
||||
state.dbPage = 1;
|
||||
fetchProducts();
|
||||
});
|
||||
|
||||
// Clear Filters
|
||||
elements.btnClearFilters.addEventListener('click', () => {
|
||||
elements.dbSearchInput.value = '';
|
||||
elements.filterCategory.value = '';
|
||||
elements.filterCompany.value = '';
|
||||
state.dbSearch = '';
|
||||
state.dbCategory = '';
|
||||
state.dbCompany = '';
|
||||
state.dbPage = 1;
|
||||
fetchProducts();
|
||||
});
|
||||
|
||||
// Pagination Click Listeners
|
||||
elements.btnPrevPage.addEventListener('click', () => {
|
||||
if (state.dbPage > 1) {
|
||||
state.dbPage--;
|
||||
fetchProducts();
|
||||
}
|
||||
});
|
||||
|
||||
elements.btnNextPage.addEventListener('click', () => {
|
||||
state.dbPage++;
|
||||
fetchProducts();
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch products from database API
|
||||
async function fetchProducts() {
|
||||
elements.dbLoadingState.classList.remove('hidden');
|
||||
elements.productsGrid.classList.add('hidden');
|
||||
elements.dbEmptyState.classList.add('hidden');
|
||||
elements.paginationControls.classList.add('hidden');
|
||||
|
||||
// Build URL query parameters
|
||||
const params = new URLSearchParams({
|
||||
page: state.dbPage,
|
||||
limit: state.dbLimit,
|
||||
q: state.dbSearch,
|
||||
category: state.dbCategory,
|
||||
company: state.dbCompany
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/products?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
elements.dbLoadingState.classList.add('hidden');
|
||||
|
||||
if (data.products && data.products.length > 0) {
|
||||
renderProductsGrid(data.products);
|
||||
renderPagination(data.pagination);
|
||||
|
||||
// Only update filters dropdown lists on page 1 search/initial load
|
||||
if (state.dbPage === 1 && data.filters) {
|
||||
populateFilters(data.filters);
|
||||
}
|
||||
} else {
|
||||
elements.dbEmptyState.classList.remove('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load products:', error);
|
||||
elements.dbLoadingState.classList.add('hidden');
|
||||
elements.dbEmptyState.classList.remove('hidden');
|
||||
showToast('Ürün veritabanı yüklenirken hata oluştu.', true);
|
||||
}
|
||||
}
|
||||
|
||||
// Populates filter select dropdown arrays
|
||||
function populateFilters(filters) {
|
||||
// 1. Categories dropdown
|
||||
const currentCategory = elements.filterCategory.value;
|
||||
elements.filterCategory.innerHTML = '<option value="">Tüm Kategoriler</option>';
|
||||
filters.categories.forEach(cat => {
|
||||
const option = document.createElement('option');
|
||||
option.value = cat.name;
|
||||
option.textContent = `${cat.name} (${cat.count})`;
|
||||
if (cat.name === currentCategory) option.selected = true;
|
||||
elements.filterCategory.appendChild(option);
|
||||
});
|
||||
|
||||
// 2. Companies dropdown
|
||||
const currentCompany = elements.filterCompany.value;
|
||||
elements.filterCompany.innerHTML = '<option value="">Tüm Şirketler</option>';
|
||||
filters.companies.forEach(comp => {
|
||||
const option = document.createElement('option');
|
||||
option.value = comp.name;
|
||||
option.textContent = `${comp.name} (${comp.count})`;
|
||||
if (comp.name === currentCompany) option.selected = true;
|
||||
elements.filterCompany.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Renders the Grid of Product Cards
|
||||
function renderProductsGrid(products) {
|
||||
const grid = elements.productsGrid;
|
||||
grid.innerHTML = '';
|
||||
grid.classList.remove('hidden');
|
||||
|
||||
products.forEach(product => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'product-card';
|
||||
card.dataset.id = product.id;
|
||||
|
||||
// Fallback placeholder image URL
|
||||
const imageSrc = product.image || `https://placehold.co/400x400/1e293b/cbd5e1?text=${encodeURIComponent(product.cleanName || 'Ürün Resmi')}`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="product-img-wrapper">
|
||||
<img src="${imageSrc}" alt="${product.cleanName || 'Ürün Görseli'}" onerror="this.src='https://placehold.co/400x400/1e293b/cbd5e1?text=%C3%9Cr%C3%BCn+G%C3%B6rseli'">
|
||||
</div>
|
||||
<div class="product-card-body">
|
||||
<span class="product-barcode"><i class="fa-solid fa-barcode"></i> ${product.barcode}</span>
|
||||
<h3 class="product-title" title="${product.name}">${product.name}</h3>
|
||||
|
||||
<div class="product-tags">
|
||||
${product.category ? `<span class="badge badge-category"><i class="fa-solid fa-tags"></i> ${product.category}</span>` : ''}
|
||||
${product.company ? `<span class="badge badge-company"><i class="fa-solid fa-building"></i> ${product.company}</span>` : ''}
|
||||
${product.amountRaw ? `<span class="badge badge-qty"><i class="fa-solid fa-cubes"></i> ${product.amountRaw} ${product.unit || ''}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-actions">
|
||||
<button class="btn btn-secondary btn-small view-details-btn"><i class="fa-solid fa-eye"></i> İncele</button>
|
||||
<button class="btn btn-secondary btn-small edit-product-btn" title="Düzenle"><i class="fa-solid fa-pen"></i></button>
|
||||
<button class="btn btn-danger btn-small delete-product-btn" title="Sil"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Card button click bindings
|
||||
card.querySelector('.view-details-btn').addEventListener('click', () => openDetailModal(product));
|
||||
card.querySelector('.edit-product-btn').addEventListener('click', () => editProductForm(product));
|
||||
card.querySelector('.delete-product-btn').addEventListener('click', () => deleteProduct(product));
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Renders Page Numbers
|
||||
function renderPagination(pageInfo) {
|
||||
elements.paginationControls.classList.remove('hidden');
|
||||
|
||||
// Enable/Disable navigation buttons
|
||||
elements.btnPrevPage.disabled = pageInfo.page <= 1;
|
||||
elements.btnNextPage.disabled = pageInfo.page >= pageInfo.totalPages;
|
||||
|
||||
const container = elements.pageNumbersContainer;
|
||||
container.innerHTML = '';
|
||||
|
||||
const current = pageInfo.page;
|
||||
const total = pageInfo.totalPages;
|
||||
|
||||
// Helper logic to build numbers with dots (e.g. 1 2 ... 5 6 7 ... 9 10)
|
||||
let pages = [];
|
||||
if (total <= 5) {
|
||||
for (let i = 1; i <= total; i++) pages.push(i);
|
||||
} else {
|
||||
if (current <= 3) {
|
||||
pages = [1, 2, 3, 4, '...', total];
|
||||
} else if (current >= total - 2) {
|
||||
pages = [1, '...', total - 3, total - 2, total - 1, total];
|
||||
} else {
|
||||
pages = [1, '...', current - 1, current, current + 1, '...', total];
|
||||
}
|
||||
}
|
||||
|
||||
pages.forEach(p => {
|
||||
const span = document.createElement('span');
|
||||
if (p === '...') {
|
||||
span.textContent = '...';
|
||||
span.className = 'dots';
|
||||
} else {
|
||||
span.textContent = p;
|
||||
if (p === current) {
|
||||
span.className = 'active';
|
||||
}
|
||||
span.addEventListener('click', () => {
|
||||
state.dbPage = p;
|
||||
fetchProducts();
|
||||
});
|
||||
}
|
||||
container.appendChild(span);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete Product Operation
|
||||
async function deleteProduct(product) {
|
||||
if (!confirm(`"${product.name}" adlı ürünü silmek istediğinize emin misiniz?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/products/${product.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast('Ürün başarıyla silindi.');
|
||||
fetchProducts();
|
||||
} else {
|
||||
showToast(data.error || 'Ürün silinirken bir hata oluştu.', true);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('İşlem gerçekleştirilemedi.', true);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Form Submissions (Add / Edit Product Manual Entry)
|
||||
function initForm() {
|
||||
elements.productForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const id = elements.formProductId.value;
|
||||
const payload = {
|
||||
barcode: elements.formBarcode.value.trim(),
|
||||
name: elements.formName.value.trim(),
|
||||
category: elements.formCategory.value.trim(),
|
||||
subcategory: elements.formSubcategory.value.trim(),
|
||||
company: elements.formCompany.value.trim(),
|
||||
image: elements.formImage.value.trim()
|
||||
};
|
||||
|
||||
const isEdit = id !== '';
|
||||
const endpoint = isEdit ? `/api/products/${id}` : '/api/products';
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
elements.btnSaveProduct.disabled = true;
|
||||
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Kaydediliyor...';
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
elements.btnSaveProduct.disabled = false;
|
||||
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-save"></i> Ürünü Kaydet';
|
||||
|
||||
if (response.ok) {
|
||||
showToast(isEdit ? 'Ürün başarıyla güncellendi!' : 'Ürün başarıyla eklendi!');
|
||||
resetForm();
|
||||
|
||||
// Switch to Database Tab
|
||||
document.getElementById('btn-tab-database').click();
|
||||
} else {
|
||||
showToast(data.error || 'Kayıt başarısız oldu.', true);
|
||||
}
|
||||
} catch (error) {
|
||||
elements.btnSaveProduct.disabled = false;
|
||||
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-save"></i> Ürünü Kaydet';
|
||||
showToast('Sunucu bağlantı hatası.', true);
|
||||
}
|
||||
});
|
||||
|
||||
elements.btnResetForm.addEventListener('click', resetForm);
|
||||
}
|
||||
|
||||
// Redirects and fills Form for Edit Action
|
||||
function editProductForm(product) {
|
||||
// Fill the inputs
|
||||
elements.formProductId.value = product.id;
|
||||
elements.formBarcode.value = product.barcode;
|
||||
elements.formName.value = product.name;
|
||||
elements.formCategory.value = product.category || '';
|
||||
elements.formSubcategory.value = product.subcategory || '';
|
||||
elements.formCompany.value = product.company || '';
|
||||
elements.formImage.value = product.image || '';
|
||||
|
||||
// Update labels and display
|
||||
document.getElementById('btn-tab-add').click();
|
||||
elements.pageTitle.textContent = 'Ürün Düzenle';
|
||||
elements.pageSubtitle.textContent = `"${product.cleanName}" barkod kaydını güncelleyin`;
|
||||
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-check"></i> Değişiklikleri Kaydet';
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
elements.productForm.reset();
|
||||
elements.formProductId.value = '';
|
||||
elements.btnSaveProduct.innerHTML = '<i class="fa-solid fa-save"></i> Ürünü Kaydet';
|
||||
if (state.currentTab === 'add-product-tab') {
|
||||
elements.pageTitle.textContent = 'Ürün Ekle';
|
||||
elements.pageSubtitle.textContent = 'Veritabanına manuel olarak yeni bir barkod kaydı girin';
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Product Detail Modal & Barcode Renders
|
||||
function initModal() {
|
||||
elements.btnCloseModal.addEventListener('click', () => {
|
||||
elements.productDetailModal.classList.remove('active');
|
||||
state.selectedProduct = null;
|
||||
});
|
||||
|
||||
// Close on overlay overlay click
|
||||
elements.productDetailModal.addEventListener('click', (e) => {
|
||||
if (e.target === elements.productDetailModal) {
|
||||
elements.productDetailModal.classList.remove('active');
|
||||
state.selectedProduct = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle code downloads (SVG for Barcode, Canvas for QR Code)
|
||||
elements.downloadButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const type = btn.getAttribute('data-type');
|
||||
if (!state.selectedProduct) return;
|
||||
|
||||
const barcodeVal = state.selectedProduct.barcode;
|
||||
const productName = state.selectedProduct.cleanName || 'barkod';
|
||||
|
||||
if (type === 'barcode') {
|
||||
// EAN-13 SVG Download
|
||||
const svgSerializer = new XMLSerializer();
|
||||
const svgString = svgSerializer.serializeToString(elements.modalBarcodeSvg);
|
||||
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = `EAN13_${barcodeVal}_${productName.toLowerCase().replace(/[^a-z0-9]/g, '_')}.svg`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
} else if (type === 'qrcode') {
|
||||
// QR Code PNG Download
|
||||
const canvas = elements.modalQrcodeCanvas;
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = dataUrl;
|
||||
a.download = `QR_${barcodeVal}_${productName.toLowerCase().replace(/[^a-z0-9]/g, '_')}.png`;
|
||||
a.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openDetailModal(product) {
|
||||
state.selectedProduct = product;
|
||||
|
||||
// Fill text labels
|
||||
elements.modalProductName.textContent = product.name;
|
||||
elements.modalCleanName.textContent = product.cleanName || '-';
|
||||
elements.modalAmountRaw.textContent = product.amountRaw || 'Miktar Belirtilmemiş';
|
||||
elements.modalAmount.textContent = product.amount !== null ? product.amount : '-';
|
||||
elements.modalUnit.textContent = product.unit || '-';
|
||||
|
||||
// Badges
|
||||
elements.modalProductCategory.innerHTML = product.category
|
||||
? `<i class="fa-solid fa-tags"></i> ${product.category}`
|
||||
: '<i class="fa-solid fa-tags"></i> Kategorisiz';
|
||||
|
||||
elements.modalProductCompany.innerHTML = product.company
|
||||
? `<i class="fa-solid fa-building"></i> ${product.company}`
|
||||
: '<i class="fa-solid fa-building"></i> Şirket Belirsiz';
|
||||
|
||||
// Product Image
|
||||
const imageSrc = product.image || `https://placehold.co/400x400/1e293b/cbd5e1?text=${encodeURIComponent(product.cleanName)}`;
|
||||
elements.modalProductImage.src = imageSrc;
|
||||
|
||||
// GENERATE BARKOD (EAN-13 or Code128)
|
||||
const barcodeVal = product.barcode;
|
||||
try {
|
||||
// EAN-13 requires exactly 12 or 13 numerical digits.
|
||||
// If it's something else, we fallback to CODE128 format so it successfully renders.
|
||||
const isNumericOnly = /^\d+$/.test(barcodeVal);
|
||||
const format = (isNumericOnly && (barcodeVal.length === 12 || barcodeVal.length === 13)) ? "EAN13" : "CODE128";
|
||||
|
||||
JsBarcode("#modal-barcode-svg", barcodeVal, {
|
||||
format: format,
|
||||
width: 2,
|
||||
height: 90,
|
||||
displayValue: true,
|
||||
fontSize: 14,
|
||||
font: 'var(--font-mono)',
|
||||
background: "#ffffff",
|
||||
lineColor: "#000000",
|
||||
margin: 10
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Barcode generation error:', err);
|
||||
elements.modalBarcodeSvg.innerHTML = `<text x="10" y="50" fill="red">Barkod Oluşturulamadı (${err.message})</text>`;
|
||||
}
|
||||
|
||||
// GENERATE QR CODE
|
||||
try {
|
||||
QRCode.toCanvas(elements.modalQrcodeCanvas, barcodeVal, {
|
||||
width: 150,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: "#000000",
|
||||
light: "#ffffff"
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('QR Code generation error:', err);
|
||||
}
|
||||
|
||||
// Toggle Modal class
|
||||
elements.productDetailModal.classList.add('active');
|
||||
}
|
||||
Reference in New Issue
Block a user