Files
hBarkodDB/public/app.js
2026-05-31 15:23:37 +03:00

801 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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');
}