feat: add pricelist CSV export and improve description display
- Add CSV export functionality for pricelists with download button - Export includes all pricelist items with proper UTF-8 encoding - Support both warehouse and estimate pricelist sources - Remove description column from admin pricing tables - Show description as tooltip on row hover instead - Improve table layout by removing redundant column Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -684,7 +684,6 @@ function renderLots(lots, total) {
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Категория</th>';
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>';
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">p/n</th>';
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>';
|
||||
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Популярность</th>';
|
||||
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Котировок</th>';
|
||||
html += '<th class="px-3 py-2 text-center text-xs font-medium text-gray-500 uppercase">Конкуренты</th>';
|
||||
@@ -694,7 +693,7 @@ function renderLots(lots, total) {
|
||||
lots.forEach(lot => {
|
||||
const category = lot.category ? escapeHtml(lot.category) : '—';
|
||||
const lotName = lot.lot_name ? escapeHtml(lot.lot_name) : '—';
|
||||
const description = lot.lot_description ? escapeHtml(lot.lot_description) : '—';
|
||||
const description = lot.lot_description ? lot.lot_description : '';
|
||||
const popularity = Number.isFinite(lot.popularity) ? Number(lot.popularity).toFixed(2) : '0.00';
|
||||
const estimateCount = Number.isFinite(lot.estimate_count) ? lot.estimate_count.toLocaleString('ru-RU') : '0';
|
||||
const stockQty = lot.stock_qty === null || lot.stock_qty === undefined
|
||||
@@ -706,11 +705,10 @@ function renderLots(lots, total) {
|
||||
? `<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full bg-gray-100 text-gray-600 text-xs">+${partnumbers.length - 1}</span>`
|
||||
: '';
|
||||
|
||||
html += '<tr class="hover:bg-gray-50">';
|
||||
html += '<tr class="hover:bg-gray-50" title="' + escapeHtml(description) + '">';
|
||||
html += '<td class="px-3 py-2 text-sm">' + category + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm font-medium">' + lotName + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm">' + firstPart + more + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-gray-600">' + description + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-right">' + popularity + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-right">' + estimateCount + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-center">—</td>';
|
||||
@@ -731,7 +729,6 @@ function renderComponents(components, total) {
|
||||
let html = '<div class="overflow-x-auto"><table class="w-full"><thead class="bg-gray-50"><tr>';
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Артикул</th>';
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Категория</th>';
|
||||
html += '<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>';
|
||||
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Популярность</th>';
|
||||
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Кол-во котировок</th>';
|
||||
html += '<th class="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">Цена</th>';
|
||||
@@ -741,7 +738,7 @@ function renderComponents(components, total) {
|
||||
components.forEach((c, idx) => {
|
||||
const price = c.current_price ? '$' + parseFloat(c.current_price).toLocaleString('en-US', {minimumFractionDigits: 2}) : '—';
|
||||
const category = c.category ? c.category.code : '—';
|
||||
const desc = c.lot && c.lot.lot_description ? c.lot.lot_description : '—';
|
||||
const desc = c.lot && c.lot.lot_description ? c.lot.lot_description : '';
|
||||
const quoteCount = c.quote_count || 0;
|
||||
const popularity = c.popularity_score ? c.popularity_score.toFixed(2) : '0.00';
|
||||
const isHidden = c.is_hidden || quoteCount === 0;
|
||||
@@ -807,10 +804,9 @@ function renderComponents(components, total) {
|
||||
settingsHtml = settings.join(' | ');
|
||||
}
|
||||
|
||||
html += '<tr class="hover:bg-gray-50 cursor-pointer" onclick="openModal(' + idx + ')">';
|
||||
html += '<tr class="hover:bg-gray-50 cursor-pointer" onclick="openModal(' + idx + ')" title="' + escapeHtml(desc) + '">';
|
||||
html += '<td class="px-3 py-2 text-sm font-mono"><span class="inline-flex items-center gap-2"><span class="w-2.5 h-2.5 rounded-full ' + dotColor + ' flex-shrink-0" title="' + escapeHtml(dotTitle) + '"></span>' + escapeHtml(c.lot_name) + '</span></td>';
|
||||
html += '<td class="px-3 py-2 text-sm">' + escapeHtml(category) + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-gray-500 max-w-xs truncate">' + escapeHtml(desc) + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-right">' + popularity + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-right">' + quoteCount + '</td>';
|
||||
html += '<td class="px-3 py-2 text-sm text-right font-medium">' + price + '</td>';
|
||||
|
||||
@@ -2,13 +2,21 @@
|
||||
|
||||
{{define "content"}}
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="/pricelists" class="text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="/pricelists" class="text-gray-500 hover:text-gray-700">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 id="page-title" class="text-2xl font-bold text-gray-900">Загрузка...</h1>
|
||||
</div>
|
||||
<button onclick="exportToCSV()" class="px-4 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 flex items-center space-x-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 id="page-title" class="text-2xl font-bold text-gray-900">Загрузка...</h1>
|
||||
<span>Экспорт в CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="pricelist-info" class="bg-white rounded-lg shadow p-6">
|
||||
@@ -305,6 +313,40 @@
|
||||
}, 300);
|
||||
});
|
||||
|
||||
async function exportToCSV() {
|
||||
try {
|
||||
const resp = await fetch(`/api/pricelists/${pricelistId}/export-csv`);
|
||||
if (!resp.ok) {
|
||||
throw new Error('Ошибка экспорта');
|
||||
}
|
||||
|
||||
const blob = await resp.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// Get filename from Content-Disposition header or use default
|
||||
const contentDisposition = resp.headers.get('Content-Disposition');
|
||||
let filename = `pricelist_${pricelistId}.csv`;
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
showToast('CSV файл экспортирован', 'success');
|
||||
} catch (e) {
|
||||
showToast('Ошибка экспорта: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadPricelistInfo();
|
||||
loadItems(1);
|
||||
|
||||
Reference in New Issue
Block a user