801 lines
30 KiB
JavaScript
801 lines
30 KiB
JavaScript
// 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');
|
||
}
|