first commit

This commit is contained in:
hOLOlu
2026-05-31 15:23:37 +03:00
commit c7b07602b7
16 changed files with 371587 additions and 0 deletions

800
public/app.js Normal file
View 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');
}