// ============================================= // Recycle Object — Каталог фурнитуры из Китая // Catalog of hardware/packaging from China // with delivery cost calculator // ============================================= const ChinaCatalog = { _items: [], _filter: '', _search: '', // Delivery rates (USD per kg) DELIVERY_METHODS: { avia_fast: { label: 'Авиа быстрая', rate_usd: 38, days: '3 дн' }, avia: { label: 'Авиа', rate_usd: 33, days: '5-7 дн' }, auto: { label: 'Авто', rate_usd: 4.8, days: '18-25 дн' }, }, // Surcharges: withdrawal + crypto card + unfavorable rates ITEM_SURCHARGE: 0.035, // +3.5% на товар (1.5% вывод + 2% крипта) DELIVERY_SURCHARGE: 0.10, // +10% на доставку (вывод + невыгодный курс) // Default exchange rates (overridden from settings) _cnyRate: 12.5, _usdRate: 90, async load() { try { // Load rates from settings const params = App.params || {}; this._cnyRate = params.china_cny_rate || 12.5; this._usdRate = params.china_usd_rate || 90; // Override delivery rates from settings if available if (params.china_delivery_avia_fast) this.DELIVERY_METHODS.avia_fast.rate_usd = params.china_delivery_avia_fast; if (params.china_delivery_avia) this.DELIVERY_METHODS.avia.rate_usd = params.china_delivery_avia; if (params.china_delivery_auto) this.DELIVERY_METHODS.auto.rate_usd = params.china_delivery_auto; // Surcharges if (params.china_item_surcharge !== undefined) this.ITEM_SURCHARGE = params.china_item_surcharge; if (params.china_delivery_surcharge !== undefined) this.DELIVERY_SURCHARGE = params.china_delivery_surcharge; // Load catalog: from localStorage first, then seed from JSON this._items = await this._loadItems(); this.render(); } catch (err) { console.error('ChinaCatalog.load() error:', err); const el = document.getElementById('china-catalog-container'); if (el) el.innerHTML = '
Ошибка загрузки каталога: ' + (err.message || err) + '
'; } }, async _loadItems() { // Try localStorage first let items = getLocal('ro_calc_china_catalog'); // Always load seed JSON to merge missing fields (photos, links) let seedItems = []; try { const resp = await fetch('data/china_catalog.json'); if (resp.ok) seedItems = await resp.json(); } catch (e) { console.warn('Failed to load china_catalog.json:', e); } if (items && items.length > 0) { // Build lookup by ID from seed JSON const seedMap = new Map(); seedItems.forEach(si => seedMap.set(si.id, si)); // Merge missing photo_url and link_1688 from JSON let migrated = false; items.forEach(item => { const seed = seedMap.get(item.id); if (!item.photo_url && seed && seed.photo_url) { item.photo_url = seed.photo_url; migrated = true; } if (!item.link_1688 && seed && seed.link_1688) { item.link_1688 = seed.link_1688; migrated = true; } }); if (migrated) setLocal('ro_calc_china_catalog', items); return items; } // No localStorage — seed from JSON if (seedItems.length > 0) { setLocal('ro_calc_china_catalog', seedItems); return seedItems; } return []; }, _saveItems() { setLocal('ro_calc_china_catalog', this._items); }, // ========================================== // CATEGORIES // ========================================== getCategories() { const cats = new Map(); this._items.forEach(item => { if (!cats.has(item.category)) { cats.set(item.category, item.category_ru || item.category); } }); return cats; // Map: key -> label_ru }, // ========================================== // RENDERING // ========================================== render() { const container = document.getElementById('china-catalog-container'); if (!container) return; const categories = this.getCategories(); // Rates bar const itemPct = Math.round(this.ITEM_SURCHARGE * 100); const delPct = Math.round(this.DELIVERY_SURCHARGE * 100); let html = `
Курсы: ¥ = ${this._cnyRate} $ = ${this._usdRate} | ${Object.entries(this.DELIVERY_METHODS).map(([k, m]) => `${m.label}: $${m.rate_usd}/кг (${m.days})` ).join('')} | Товар +${itemPct}% · Дост. +${delPct}%
`; // Category filter + search html += `
`; categories.forEach((label, key) => { const count = this._items.filter(i => i.category === key).length; html += ``; }); html += `
`; // Items table const filtered = this._getFiltered(); if (filtered.length === 0) { html += '

Нет позиций по фильтру

'; } else { html += this._renderTable(filtered); } container.innerHTML = html; }, _getFiltered() { let items = [...this._items]; if (this._filter) items = items.filter(i => i.category === this._filter); if (this._search) { const q = this._search.toLowerCase(); items = items.filter(i => (i.name || '').toLowerCase().includes(q) || (i.category_ru || '').toLowerCase().includes(q) || (i.size || '').toLowerCase().includes(q) || (i.notes || '').toLowerCase().includes(q) ); } return items; }, _renderTable(items) { let html = `
`; items.forEach(item => { const priceRub = round2(item.price_cny * this._cnyRate); const isRussia = item.category === 'russia'; // Price column const priceHtml = isRussia ? `
${formatRub(item.price_rub || 0)}
` : `
${item.price_cny}¥
${formatRub(priceRub)}
`; // Delivery (short labels for compact table) const shortLabels = { avia_fast: 'Быстр', avia: 'Авиа', auto: 'Авто' }; const deliverySelect = isRussia ? `Россия` : ``; // Photo const photoSrc = this._proxyPhoto(item.photo_url || ''); const photoHtml = photoSrc ? `📦` : `📦`; // Link icon const linkIcon = item.link_1688 ? `🔗` : ''; // Size + weight subtitle const details = [item.category_ru, item.size, item.weight_grams ? item.weight_grams + 'г' : ''].filter(Boolean).join(' · '); html += ``; }); html += '
Позиция Цена Доставка Кол Итого/шт
${photoHtml}
${this._esc(item.name)}
${this._esc(details)}
${item.notes ? `
${this._esc(item.notes)}
` : ''}
${priceHtml} ${deliverySelect} ${isRussia ? formatRub(item.price_rub || 0) : formatRub(round2(priceRub * (1 + this.ITEM_SURCHARGE)))}
${isRussia ? '' : 'без дост.'}
${linkIcon}
'; return html; }, // ========================================== // DELIVERY CALCULATOR // ========================================== recalcRow(id) { const item = this._items.find(i => i.id === id); if (!item) return; const isRussia = item.category === 'russia'; const totalEl = document.getElementById('cc-total-' + id); const detailEl = document.getElementById('cc-detail-' + id); if (!totalEl) return; if (isRussia) { // Russian items — no delivery calc, price is already in RUB const price = item.price_rub || 0; totalEl.innerHTML = `${formatRub(price)}`; if (detailEl) detailEl.textContent = 'Россия'; return; } const deliveryEl = document.getElementById('cc-delivery-' + id); const qtyEl = document.getElementById('cc-qty-' + id); const method = deliveryEl ? deliveryEl.value : 'auto'; const qty = parseInt(qtyEl?.value) || 1; const result = this.calcDelivery(item, method, qty); totalEl.innerHTML = `${formatRub(result.totalPerUnit)}`; if (detailEl) { detailEl.innerHTML = `${formatRub(result.priceWithSurcharge)} + дост. ${formatRub(result.deliveryWithSurcharge)}`; } }, /** * Calculate delivery cost per unit with surcharges * @param {Object} item - catalog item * @param {string} method - delivery method key (avia_fast/avia/auto) * @param {number} qty - quantity * @returns {{ priceRub, priceWithSurcharge, deliveryPerUnit, deliveryWithSurcharge, totalPerUnit, deliveryTotal }} */ calcDelivery(item, method, qty) { // Item price + 3.5% surcharge (withdrawal + crypto) const priceRub = round2(item.price_cny * this._cnyRate); const priceWithSurcharge = round2(priceRub * (1 + this.ITEM_SURCHARGE)); // Delivery + 10% surcharge (withdrawal + unfavorable rate) const weightKg = (item.weight_grams || 0) / 1000; const rate = this.DELIVERY_METHODS[method]?.rate_usd || 4.8; const deliveryTotal = round2(weightKg * qty * rate * this._usdRate); const deliveryPerUnit = qty > 0 ? round2(deliveryTotal / qty) : 0; const deliveryWithSurcharge = round2(deliveryPerUnit * (1 + this.DELIVERY_SURCHARGE)); const totalPerUnit = round2(priceWithSurcharge + deliveryWithSurcharge); return { priceRub, priceWithSurcharge, deliveryPerUnit, deliveryWithSurcharge, totalPerUnit, deliveryTotal }; }, // ========================================== // FILTERS // ========================================== setFilter(cat) { this._filter = cat; this.render(); }, onSearch(val) { this._search = val; this.render(); }, // ========================================== // RATES MODAL // ========================================== openRatesModal() { const html = `

Курсы и ставки доставки

₽ за 1 ¥
₽ за 1 $

Ставки доставки ($/кг)

Наценки (%)

% (вывод + крипта)
% (вывод + курс)
`; document.body.insertAdjacentHTML('beforeend', html); }, closeRatesModal() { const el = document.getElementById('cc-rates-overlay'); if (el) el.remove(); }, async saveRates() { this._cnyRate = parseFloat(document.getElementById('cc-rate-cny').value) || 12.5; this._usdRate = parseFloat(document.getElementById('cc-rate-usd').value) || 90; this.DELIVERY_METHODS.avia_fast.rate_usd = parseFloat(document.getElementById('cc-rate-avia-fast').value) || 38; this.DELIVERY_METHODS.avia.rate_usd = parseFloat(document.getElementById('cc-rate-avia').value) || 33; this.DELIVERY_METHODS.auto.rate_usd = parseFloat(document.getElementById('cc-rate-auto').value) || 4.8; this.ITEM_SURCHARGE = (parseFloat(document.getElementById('cc-surcharge-item').value) || 3.5) / 100; this.DELIVERY_SURCHARGE = (parseFloat(document.getElementById('cc-surcharge-delivery').value) || 10) / 100; // Save to settings await saveSetting('china_cny_rate', this._cnyRate); await saveSetting('china_usd_rate', this._usdRate); await saveSetting('china_delivery_avia_fast', this.DELIVERY_METHODS.avia_fast.rate_usd); await saveSetting('china_delivery_avia', this.DELIVERY_METHODS.avia.rate_usd); await saveSetting('china_delivery_auto', this.DELIVERY_METHODS.auto.rate_usd); await saveSetting('china_item_surcharge', this.ITEM_SURCHARGE); await saveSetting('china_delivery_surcharge', this.DELIVERY_SURCHARGE); // Update App.params if (App.params) { App.params.china_cny_rate = this._cnyRate; App.params.china_usd_rate = this._usdRate; App.params.china_delivery_avia_fast = this.DELIVERY_METHODS.avia_fast.rate_usd; App.params.china_delivery_avia = this.DELIVERY_METHODS.avia.rate_usd; App.params.china_delivery_auto = this.DELIVERY_METHODS.auto.rate_usd; App.params.china_item_surcharge = this.ITEM_SURCHARGE; App.params.china_delivery_surcharge = this.DELIVERY_SURCHARGE; } this.closeRatesModal(); this.render(); App.toast('Курсы сохранены'); }, // ========================================== // CRUD // ========================================== _editingId: null, showAddForm() { this._editingId = null; document.getElementById('cc-form-title').textContent = 'Новая позиция'; document.getElementById('cc-item-name').value = ''; document.getElementById('cc-item-category').value = ''; document.getElementById('cc-item-size').value = ''; document.getElementById('cc-item-weight').value = ''; document.getElementById('cc-item-price-cny').value = ''; document.getElementById('cc-item-price-rub').value = ''; document.getElementById('cc-item-link').value = ''; document.getElementById('cc-item-photo').value = ''; document.getElementById('cc-item-notes').value = ''; this._updatePhotoPreview(''); document.getElementById('cc-delete-btn').style.display = 'none'; document.getElementById('cc-edit-form').style.display = ''; document.getElementById('cc-edit-form').scrollIntoView({ behavior: 'smooth' }); }, editItem(id) { const item = this._items.find(i => i.id === id); if (!item) return; this._editingId = id; document.getElementById('cc-form-title').textContent = 'Редактировать: ' + (item.name || ''); document.getElementById('cc-item-name').value = item.name || ''; document.getElementById('cc-item-category').value = item.category || ''; document.getElementById('cc-item-size').value = item.size || ''; document.getElementById('cc-item-weight').value = item.weight_grams || ''; document.getElementById('cc-item-price-cny').value = item.price_cny || ''; document.getElementById('cc-item-price-rub').value = item.price_rub || ''; document.getElementById('cc-item-link').value = item.link_1688 || ''; document.getElementById('cc-item-photo').value = item.photo_url || ''; document.getElementById('cc-item-notes').value = item.notes || ''; this._updatePhotoPreview(item.photo_url || ''); document.getElementById('cc-delete-btn').style.display = ''; document.getElementById('cc-edit-form').style.display = ''; document.getElementById('cc-edit-form').scrollIntoView({ behavior: 'smooth' }); }, hideForm() { document.getElementById('cc-edit-form').style.display = 'none'; this._editingId = null; }, saveItem() { const name = document.getElementById('cc-item-name').value.trim(); if (!name) { App.toast('Введите название'); return; } const category = document.getElementById('cc-item-category').value.trim() || 'misc'; // Determine category_ru from existing items or use category as-is const existingCat = this._items.find(i => i.category === category); const category_ru = existingCat?.category_ru || category; const data = { name, category, category_ru, size: document.getElementById('cc-item-size').value.trim(), weight_grams: parseFloat(document.getElementById('cc-item-weight').value) || 0, price_cny: parseFloat(document.getElementById('cc-item-price-cny').value) || 0, price_rub: parseFloat(document.getElementById('cc-item-price-rub').value) || 0, link_1688: document.getElementById('cc-item-link').value.trim(), photo_url: document.getElementById('cc-item-photo').value.trim(), notes: document.getElementById('cc-item-notes').value.trim(), }; if (this._editingId) { const idx = this._items.findIndex(i => i.id === this._editingId); if (idx >= 0) { this._items[idx] = { ...this._items[idx], ...data }; } } else { const maxId = this._items.reduce((max, i) => Math.max(max, i.id || 0), 0); data.id = maxId + 1; this._items.push(data); } this._saveItems(); this.hideForm(); this.render(); App.toast('Позиция сохранена'); }, deleteItem(id) { const item = this._items.find(i => i.id === id); if (!item) return; if (!confirm(`Удалить "${item.name}"?`)) return; this._items = this._items.filter(i => i.id !== id); this._saveItems(); this.render(); App.toast('Удалено'); }, deleteFromForm() { if (!this._editingId) return; this.deleteItem(this._editingId); this.hideForm(); }, // ========================================== // UTILS // ========================================== // Proxy alicdn images to bypass hotlink protection _proxyPhoto(url) { if (!url) return ''; // alicdn/1688 images need proxy if (url.includes('alicdn.com') || url.includes('1688.com')) { return 'https://images.weserv.nl/?url=' + encodeURIComponent(url) + '&w=80&h=80&fit=cover&default=1'; } return url; }, _updatePhotoPreview(url) { const preview = document.getElementById('cc-item-photo-preview'); if (!preview) return; if (url) { preview.src = this._proxyPhoto(url); preview.style.display = ''; preview.onerror = () => { preview.style.display = 'none'; }; } else { preview.style.display = 'none'; } }, _esc(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }, };