// =============================================
// Recycle Object — China Purchases Page
// =============================================
const ChinaPurchases = {
allPurchases: [],
allShipments: [],
currentView: 'dashboard',
editingPurchaseId: null,
consolidationEditingShipmentId: null,
consolidationSelection: {},
itemCounter: 0,
_pendingBoxPdfName: null,
_pendingBoxPdfData: null,
STATUSES: [
{ key: 'ordered', label: 'Заказано', color: 'accent', icon: '🛒' },
{ key: 'in_china_warehouse', label: 'На складе в Китае', color: 'yellow', icon: '📦' },
{ key: 'in_transit', label: 'В пути', color: 'blue', icon: '✈' },
{ key: 'delivered', label: 'Доставлено', color: 'green', icon: '✓' },
{ key: 'received', label: 'Принято на склад', color: 'green', icon: '☑' },
],
DELIVERY_TYPES: [
{ key: 'auto_slow', label: 'Авто (долго)', days: '35-50' },
{ key: 'air_fast', label: 'Авиа (быстро)', days: '7-12' },
{ key: 'air', label: 'Авиа (обычная)', days: '10-18' },
{ key: 'auto_fast', label: 'Авто быстро', days: '20-30' },
],
// ==========================================
// LIFECYCLE
// ==========================================
async load() {
this.allPurchases = await loadChinaPurchases({});
this.allShipments = await loadShipments();
this.allPurchases = this.allPurchases.map(p => {
if (!p) return p;
if (p.status === 'consolidating') p.status = 'in_transit';
return p;
});
this.showView(this.currentView);
},
showView(view) {
this.currentView = view;
document.querySelectorAll('.china-view').forEach(el => el.style.display = 'none');
document.querySelectorAll('.china-tab').forEach(el => {
el.classList.toggle('active', el.dataset.view === view);
});
switch (view) {
case 'dashboard':
document.getElementById('china-view-dashboard').style.display = '';
this.renderDashboard();
break;
case 'list':
document.getElementById('china-view-list').style.display = '';
this.renderList();
break;
case 'consolidation':
document.getElementById('china-view-consolidation').style.display = '';
this.renderConsolidationView();
break;
case 'form':
document.getElementById('china-view-form').style.display = '';
break;
case 'detail':
document.getElementById('china-view-detail').style.display = '';
break;
}
},
// ==========================================
// HELPERS
// ==========================================
statusLabel(status) {
const s = this.STATUSES.find(s => s.key === status);
return s ? s.label : status || '—';
},
statusColor(status) {
const s = this.STATUSES.find(s => s.key === status);
return s ? s.color : 'muted';
},
boxStatusLabel(status) {
const map = {
draft: 'Черновик коробки',
in_transit: 'В пути',
delivered: 'Доставлено',
received: 'Принято на склад',
};
return map[status] || 'Черновик коробки';
},
deliveryLabel(type) {
const d = this.DELIVERY_TYPES.find(d => d.key === type);
return d ? d.label : type || '—';
},
formatCny(n) {
if (!n) return '0 \u00A5';
return new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 2 }).format(n) + ' \u00A5';
},
_proxyPhoto(url) {
if (!url) return '';
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;
},
_normStr(v) {
return String(v || '').trim().toLowerCase();
},
_resolveItemPhoto(item) {
const direct = this._proxyPhoto(item && item.photo_url ? item.photo_url : '');
if (direct) return direct;
try {
const catalog = getLocal('ro_calc_china_catalog') || [];
const itemName = this._normStr(item && item.name);
if (!itemName) return '';
const match = catalog.find(c => this._normStr(c.name) === itemName && c.photo_url);
return match ? this._proxyPhoto(match.photo_url) : '';
} catch (e) {
return '';
}
},
esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
},
_selectedConsolidationIds() {
return Object.entries(this.consolidationSelection || {})
.filter(([, selected]) => !!selected)
.map(([id]) => parseInt(id, 10))
.filter(Boolean);
},
_findShipment(id) {
return (this.allShipments || []).find(sh => String(sh.id) === String(id)) || null;
},
_getBoxStatus(shipment) {
if (!shipment) return 'draft';
if (shipment.status === 'received') return 'received';
return shipment.china_box_status || 'draft';
},
_getChinaBoxes() {
return (this.allShipments || []).filter(sh => sh && sh.source === 'china_consolidation');
},
_appendStatusHistory(purchase, status, note) {
purchase.status_history = Array.isArray(purchase.status_history) ? purchase.status_history : [];
purchase.status_history.push({
status,
date: new Date().toISOString(),
note: note || '',
});
},
_availableConsolidationPurchases() {
const editingShipmentId = this.consolidationEditingShipmentId;
return (this.allPurchases || []).filter(p => {
if (!p) return false;
if (editingShipmentId && String(p.shipment_id || '') === String(editingShipmentId)) return true;
return p.status === 'in_china_warehouse';
});
},
_buildShipmentItemsFromPurchases(purchases) {
const items = [];
(purchases || []).forEach(purchase => {
const rows = Array.isArray(purchase.items) ? purchase.items : [];
const purchaseTotal = rows.reduce((sum, item) => {
return sum + ((parseFloat(item.qty) || 0) * (parseFloat(item.price_cny) || 0));
}, 0);
const localDelivery = parseFloat(purchase.delivery_cost_cny) || 0;
const fallbackDivisor = rows.length || 1;
rows.forEach((item, index) => {
const qty = parseFloat(item.qty) || 0;
const priceCny = parseFloat(item.price_cny) || 0;
const lineTotal = qty * priceCny;
let deliveryShare = 0;
if (localDelivery > 0) {
if (purchaseTotal > 0 && lineTotal > 0) {
deliveryShare = localDelivery * (lineTotal / purchaseTotal);
} else {
deliveryShare = localDelivery / fallbackDivisor;
}
}
items.push({
source: item.warehouse_item_id ? 'existing' : 'new',
warehouse_item_id: item.warehouse_item_id || null,
name: item.name || `Позиция ${index + 1}`,
sku: item.sku || '',
category: item.category || 'other',
size: item.size || '',
color: item.color || '',
unit: item.unit || 'шт',
photo_url: item.photo_url || '',
photo_thumbnail: '',
qty_received: qty,
weight_grams: parseFloat(item.weight_grams) || 0,
purchase_price_cny: Math.round((lineTotal + deliveryShare) * 100) / 100,
purchase_price_rub: 0,
delivery_allocated: 0,
total_cost_per_unit: 0,
china_purchase_id: purchase.id,
china_purchase_name: purchase.purchase_name || '',
linked_order_id: purchase.order_id || '',
linked_order_name: purchase.order_name || purchase.order_label || '',
notes: purchase.purchase_name ? `Закупка: ${purchase.purchase_name}` : '',
});
});
});
return items;
},
_buildShipmentSupplier(purchases) {
const suppliers = [...new Set((purchases || []).map(p => String(p.supplier_name || '').trim()).filter(Boolean))];
if (!suppliers.length) return 'Склад в Китае';
if (suppliers.length === 1) return suppliers[0];
return suppliers.slice(0, 3).join(', ') + (suppliers.length > 3 ? '…' : '');
},
// ==========================================
// DASHBOARD VIEW
// ==========================================
renderDashboard() {
const all = this.allPurchases;
const boxes = this._getChinaBoxes();
const counts = {};
this.STATUSES.forEach(s => counts[s.key] = 0);
all.forEach(p => {
if (p.status === 'ordered' || p.status === 'in_china_warehouse') {
if (counts[p.status] !== undefined) counts[p.status]++;
}
});
boxes.forEach(box => {
const boxStatus = this._getBoxStatus(box);
if (counts[boxStatus] !== undefined) counts[boxStatus]++;
});
// Pipeline
const pipeEl = document.getElementById('china-pipeline');
pipeEl.innerHTML = this.STATUSES.map(s => `
${counts[s.key]}
${s.label}
`).join('→
');
// Stats
let totalCny = 0, deliveryCny = 0, inTransit = 0;
all.forEach(p => {
totalCny += p.total_cny || 0;
deliveryCny += p.delivery_cost_cny || 0;
});
inTransit = boxes.filter(box => this._getBoxStatus(box) === 'in_transit').length;
document.getElementById('china-stat-total').textContent = all.length;
document.getElementById('china-stat-transit').textContent = inTransit;
document.getElementById('china-stat-cny').textContent = this.formatCny(totalCny);
document.getElementById('china-stat-delivery').textContent = this.formatCny(deliveryCny);
// Recent table
const tbody = document.getElementById('china-recent-body');
const recent = [...all]
.sort((a, b) => new Date(b.date || b.created_at || 0) - new Date(a.date || a.created_at || 0))
.slice(0, 5);
if (recent.length === 0) {
tbody.innerHTML = '| Нет закупок. Нажмите «+ Новая закупка» |
';
return;
}
tbody.innerHTML = recent.map(p => `
| ${this.esc(p.purchase_name)} |
${this.esc(p.supplier_name || '—')} |
${this.statusLabel(p.status)} |
${this.deliveryLabel(p.delivery_type)} |
${this.formatCny(p.total_cny)} |
${App.formatDate(p.date)} |
`).join('');
},
filterByStatus(status) {
if (['in_transit', 'delivered', 'received'].includes(status)) {
const boxFilter = document.getElementById('china-box-filter-status');
if (boxFilter) boxFilter.value = status;
this.openConsolidation();
return;
}
document.getElementById('china-filter-status').value = status;
this.showView('list');
},
// ==========================================
// LIST VIEW
// ==========================================
renderList() {
const status = document.getElementById('china-filter-status').value;
const delivery = document.getElementById('china-filter-delivery').value;
const q = (document.getElementById('china-search').value || '').toLowerCase().trim();
let list = [...this.allPurchases].filter(p => p.status !== 'in_transit' && p.status !== 'delivered' && p.status !== 'received');
if (status) list = list.filter(p => p.status === status);
if (delivery) list = list.filter(p => p.delivery_type === delivery);
if (q) list = list.filter(p =>
(p.purchase_name || '').toLowerCase().includes(q)
|| (p.supplier_name || '').toLowerCase().includes(q)
|| (p.order_name || '').toLowerCase().includes(q)
|| ((p.shipment_id ? (this._findShipment(p.shipment_id)?.china_tracking_number || '') : '')).toLowerCase().includes(q)
);
const tbody = document.getElementById('china-list-body');
if (list.length === 0) {
tbody.innerHTML = '| Нет закупок по фильтру |
';
return;
}
tbody.innerHTML = list.map(p => `
| ${this.esc(p.purchase_name)} |
${this.esc(p.supplier_name || '—')} |
${this.esc(p.order_name || '—')} |
${this.statusLabel(p.status)} |
${p.shipment_id ? this.deliveryLabel(this._findShipment(p.shipment_id)?.china_delivery_type) : '—'} |
${this.formatCny(p.total_cny)} |
${this.esc((p.shipment_id ? this._findShipment(p.shipment_id)?.china_tracking_number : '') || '—')} |
|
`).join('');
},
applyFilters() { this.renderList(); },
// ==========================================
// CONSOLIDATION VIEW
// ==========================================
async openConsolidation(purchaseIds = [], shipmentId = null) {
if (!Array.isArray(purchaseIds)) purchaseIds = purchaseIds ? [purchaseIds] : [];
this.allPurchases = await loadChinaPurchases({});
this.allShipments = await loadShipments();
this.resetConsolidationForm(false);
if (shipmentId) {
const shipment = this._findShipment(shipmentId);
this.consolidationEditingShipmentId = shipmentId;
const linkedIds = (shipment && Array.isArray(shipment.china_purchase_ids) && shipment.china_purchase_ids.length)
? shipment.china_purchase_ids
: this.allPurchases.filter(p => String(p.shipment_id || '') === String(shipmentId)).map(p => p.id);
this.consolidationSelection = {};
linkedIds.forEach(id => { this.consolidationSelection[id] = true; });
if (shipment) {
document.getElementById('china-cons-name').value = shipment.shipment_name || '';
document.getElementById('china-cons-date').value = shipment.date || App.todayLocalYMD();
document.getElementById('china-cons-supplier').value = shipment.supplier || '';
document.getElementById('china-cons-type').value = shipment.china_delivery_type || '';
document.getElementById('china-cons-days').value = shipment.china_estimated_days || '';
document.getElementById('china-cons-tracking').value = shipment.china_tracking_number || '';
document.getElementById('china-cons-estimated-usd').value = shipment.china_delivery_estimated_usd || '';
document.getElementById('china-cons-delivery-rub').value = shipment.delivery_china_to_russia || 0;
document.getElementById('china-cons-moscow-rub').value = shipment.delivery_moscow || 0;
document.getElementById('china-cons-notes').value = shipment.notes || '';
this._pendingBoxPdfName = shipment.waybill_pdf_name || null;
this._pendingBoxPdfData = shipment.waybill_pdf_data || null;
const pdfInfo = document.getElementById('china-cons-pdf-info');
if (pdfInfo) pdfInfo.textContent = this._pendingBoxPdfName || 'Файл не выбран';
}
} else {
this.consolidationEditingShipmentId = null;
this.consolidationSelection = {};
purchaseIds.forEach(id => { this.consolidationSelection[id] = true; });
if (purchaseIds.length === 1) {
const purchase = this.allPurchases.find(p => String(p.id) === String(purchaseIds[0]));
if (purchase) {
document.getElementById('china-cons-name').value = `Коробка: ${purchase.purchase_name || 'закупка'}`;
document.getElementById('china-cons-supplier').value = purchase.supplier_name || '';
}
}
}
this.showView('consolidation');
},
resetConsolidationForm(clearSelection = true) {
if (clearSelection) this.consolidationSelection = {};
this.consolidationEditingShipmentId = null;
this._pendingBoxPdfName = null;
this._pendingBoxPdfData = null;
const today = App.todayLocalYMD();
const defaults = {
'china-cons-name': '',
'china-cons-date': today,
'china-cons-supplier': '',
'china-cons-type': '',
'china-cons-days': '',
'china-cons-tracking': '',
'china-cons-estimated-usd': '',
'china-cons-delivery-rub': '',
'china-cons-moscow-rub': '',
'china-cons-notes': '',
};
Object.entries(defaults).forEach(([id, value]) => {
const el = document.getElementById(id);
if (el) el.value = value;
});
const btn = document.getElementById('china-cons-open-shipment-btn');
if (btn) btn.style.display = 'none';
const pdfInfo = document.getElementById('china-cons-pdf-info');
if (pdfInfo) pdfInfo.textContent = 'Файл не выбран';
if (this.currentView === 'consolidation') this.renderConsolidationView();
},
toggleConsolidationPurchase(purchaseId, checked) {
this.consolidationSelection[purchaseId] = !!checked;
this.renderConsolidationView();
},
renderConsolidationView() {
const available = this._availableConsolidationPurchases();
const listEl = document.getElementById('china-cons-purchase-list');
const selectedIds = this._selectedConsolidationIds();
const selectedSet = new Set(selectedIds.map(String));
const btn = document.getElementById('china-cons-open-shipment-btn');
if (btn) btn.style.display = this.consolidationEditingShipmentId ? '' : 'none';
if (!available.length) {
listEl.innerHTML = 'Нет закупок на складе в Китае для сборки коробки.
';
} else {
listEl.innerHTML = `${available.map(p => {
const checked = selectedSet.has(String(p.id));
const itemCount = Array.isArray(p.items) ? p.items.length : 0;
const localDelivery = parseFloat(p.delivery_cost_cny) || 0;
const linkedBadge = p.shipment_id ? `
${String(p.shipment_id) === String(this.consolidationEditingShipmentId) ? 'В этой коробке' : 'Уже в коробке'}` : '';
return `
`;
}).join('')}
`;
}
document.getElementById('china-cons-count').textContent = `Выбрано: ${selectedIds.length}`;
this.renderConsolidationSummary();
this.renderBoxesList();
},
renderConsolidationSummary() {
const selectedIds = this._selectedConsolidationIds();
const selectedPurchases = (this.allPurchases || []).filter(p => selectedIds.includes(p.id));
const summaryEl = document.getElementById('china-cons-summary');
if (!selectedPurchases.length) {
summaryEl.innerHTML = 'Ничего не выбрано';
return;
}
const items = this._buildShipmentItemsFromPurchases(selectedPurchases);
const totalCny = items.reduce((sum, item) => sum + (parseFloat(item.purchase_price_cny) || 0), 0);
const totalQty = items.reduce((sum, item) => sum + (parseFloat(item.qty_received) || 0), 0);
const localDeliveryCny = selectedPurchases.reduce((sum, p) => sum + (parseFloat(p.delivery_cost_cny) || 0), 0);
const totalWeight = items.reduce((sum, item) => sum + (parseFloat(item.weight_grams) || 0), 0);
const deliveryRub = (parseFloat(document.getElementById('china-cons-delivery-rub').value) || 0)
+ (parseFloat(document.getElementById('china-cons-moscow-rub').value) || 0);
const estimatedUsd = parseFloat(document.getElementById('china-cons-estimated-usd').value) || 0;
summaryEl.innerHTML = `
Закупок
${selectedPurchases.length}
Позиций в коробке
${items.length}
Общее кол-во
${new Intl.NumberFormat('ru-RU').format(totalQty)}
Товары + локал. доставка
${this.formatCny(totalCny)}
Локальная доставка CNY
${this.formatCny(localDeliveryCny)}
Вес в приёмке
${new Intl.NumberFormat('ru-RU').format(totalWeight)} г
План перевозки USD
${estimatedUsd ? new Intl.NumberFormat('ru-RU').format(estimatedUsd) + ' $' : '—'}
Доставка RUB на коробку
${Math.round(deliveryRub).toLocaleString('ru-RU')} ₽
`;
},
renderBoxesList() {
const container = document.getElementById('china-boxes-list');
if (!container) return;
const filterStatus = document.getElementById('china-box-filter-status')?.value || '';
let boxes = this._getChinaBoxes().sort((a, b) => new Date(b.updated_at || b.created_at || 0) - new Date(a.updated_at || a.created_at || 0));
if (filterStatus) boxes = boxes.filter(box => this._getBoxStatus(box) === filterStatus);
if (!boxes.length) {
container.innerHTML = 'Нет коробок по выбранному фильтру.
';
return;
}
container.innerHTML = `
| Коробка | Статус | Доставка | Трек | Факт RUB | PDF | |
${boxes.map(box => {
const status = this._getBoxStatus(box);
const pdfHtml = box.waybill_pdf_name && box.waybill_pdf_data
? `PDF`
: '—';
const nextAction = status === 'draft'
? ``
: status === 'in_transit'
? ``
: status === 'delivered'
? ``
: ``;
return `
| ${this.esc(box.shipment_name || ('Коробка #' + box.id))} |
${this.boxStatusLabel(status)} |
${this.deliveryLabel(box.china_delivery_type)} |
${this.esc(box.china_tracking_number || '—')} |
${Math.round(box.delivery_china_to_russia || 0).toLocaleString('ru-RU')} ₽ |
${pdfHtml} |
${nextAction} |
`;
}).join('')}
`;
},
async updateBoxStatus(shipmentId, status) {
const shipment = this._findShipment(shipmentId);
if (!shipment) return;
shipment.china_box_status = status;
shipment.status = status;
await saveShipment(shipment);
const linkedIds = Array.isArray(shipment.china_purchase_ids)
? shipment.china_purchase_ids
: this.allPurchases.filter(p => String(p.shipment_id || '') === String(shipmentId)).map(p => p.id);
for (const purchaseId of linkedIds) {
const purchase = await loadChinaPurchase(purchaseId);
if (!purchase) continue;
purchase.status = status;
purchase.shipment_id = shipmentId;
purchase.delivery_type = shipment.china_delivery_type || '';
purchase.tracking_number = shipment.china_tracking_number || '';
purchase.estimated_days = shipment.china_estimated_days || 0;
this._appendStatusHistory(purchase, status, `Статус коробки «${shipment.shipment_name || ''}»`);
await saveChinaPurchase(purchase);
}
this.allPurchases = await loadChinaPurchases({});
this.allShipments = await loadShipments();
this.renderBoxesList();
App.toast(`Коробка: ${this.boxStatusLabel(status)}`);
},
handleBoxPdfUpload(input) {
const file = input.files[0];
if (!file) return;
if (file.size > 500 * 1024) {
App.toast('PDF слишком большой (макс. 500 КБ)');
input.value = '';
return;
}
const reader = new FileReader();
reader.onload = (e) => {
this._pendingBoxPdfName = file.name;
this._pendingBoxPdfData = e.target.result;
const info = document.getElementById('china-cons-pdf-info');
if (info) info.textContent = file.name + ' (' + Math.round(file.size / 1024) + ' КБ)';
};
reader.readAsDataURL(file);
},
async saveConsolidation() {
const selectedIds = this._selectedConsolidationIds();
if (!selectedIds.length) {
App.toast('Выберите хотя бы одну закупку');
return;
}
const shipmentName = (document.getElementById('china-cons-name').value || '').trim();
if (!shipmentName) {
App.toast('Укажите название коробки');
return;
}
const selectedPurchases = (this.allPurchases || []).filter(p => selectedIds.includes(p.id));
const shipmentItems = this._buildShipmentItemsFromPurchases(selectedPurchases);
if (!shipmentItems.length) {
App.toast('В выбранных закупках нет позиций');
return;
}
const currentShipment = this.consolidationEditingShipmentId ? this._findShipment(this.consolidationEditingShipmentId) : null;
const rate = currentShipment?.cny_rate || selectedPurchases.find(p => p.cny_rate)?.cny_rate || App?.params?.china_cny_rate || 12.5;
const feeCashout = currentShipment?.fee_cashout_percent ?? 1.5;
const feeCrypto = currentShipment?.fee_crypto_percent ?? 2;
const fee1688 = currentShipment?.fee_1688_percent ?? 3;
const feeTotalPct = feeCashout + feeCrypto + fee1688;
const totalPurchaseCny = shipmentItems.reduce((sum, item) => sum + (parseFloat(item.purchase_price_cny) || 0), 0);
const chinaRub = parseFloat(document.getElementById('china-cons-delivery-rub').value) || 0;
const moscowRub = parseFloat(document.getElementById('china-cons-moscow-rub').value) || 0;
const estimatedUsd = parseFloat(document.getElementById('china-cons-estimated-usd').value) || 0;
const shipmentData = {
id: currentShipment?.id,
date: document.getElementById('china-cons-date').value || App.todayLocalYMD(),
shipment_name: shipmentName,
supplier: (document.getElementById('china-cons-supplier').value || '').trim() || this._buildShipmentSupplier(selectedPurchases),
total_purchase_cny: Math.round(totalPurchaseCny * 100) / 100,
cny_rate: rate,
fee_cashout_percent: feeCashout,
fee_crypto_percent: feeCrypto,
fee_1688_percent: fee1688,
fee_total_percent: feeTotalPct,
total_purchase_rub: totalPurchaseCny * rate * (1 + feeTotalPct / 100),
delivery_china_to_russia: chinaRub,
delivery_moscow: moscowRub,
customs_fees: currentShipment?.customs_fees || 0,
total_delivery: chinaRub + moscowRub,
pricing_mode: currentShipment?.pricing_mode || 'weighted_avg',
total_weight_grams: shipmentItems.reduce((sum, item) => sum + (parseFloat(item.weight_grams) || 0), 0),
items: shipmentItems,
notes: (document.getElementById('china-cons-notes').value || '').trim(),
status: currentShipment?.status || currentShipment?.china_box_status || 'in_transit',
china_box_status: currentShipment?.china_box_status || currentShipment?.status || 'in_transit',
source: 'china_consolidation',
china_purchase_ids: selectedIds,
china_delivery_type: document.getElementById('china-cons-type').value || '',
china_estimated_days: parseInt(document.getElementById('china-cons-days').value, 10) || 0,
china_tracking_number: (document.getElementById('china-cons-tracking').value || '').trim(),
china_delivery_estimated_usd: estimatedUsd,
waybill_pdf_name: this._pendingBoxPdfName || currentShipment?.waybill_pdf_name || '',
waybill_pdf_data: this._pendingBoxPdfData || currentShipment?.waybill_pdf_data || '',
};
const shipmentId = await saveShipment(shipmentData);
const previousSelectedIds = currentShipment && Array.isArray(currentShipment.china_purchase_ids)
? currentShipment.china_purchase_ids.map(id => parseInt(id, 10)).filter(Boolean)
: this.allPurchases.filter(p => String(p.shipment_id || '') === String(this.consolidationEditingShipmentId)).map(p => p.id);
const removedIds = previousSelectedIds.filter(id => !selectedIds.includes(id));
const purchaseTotals = selectedPurchases.map(p => ({
id: p.id,
grandCny: (parseFloat(p.total_cny) || 0) + (parseFloat(p.delivery_cost_cny) || 0),
}));
const grandCnyTotal = purchaseTotals.reduce((sum, row) => sum + row.grandCny, 0);
for (const purchase of selectedPurchases) {
const next = JSON.parse(JSON.stringify(purchase));
next.shipment_id = shipmentId;
next.delivery_type = shipmentData.china_delivery_type || next.delivery_type || '';
next.estimated_days = shipmentData.china_estimated_days || next.estimated_days || 0;
next.tracking_number = shipmentData.china_tracking_number || next.tracking_number || '';
const grandCny = purchaseTotals.find(row => row.id === purchase.id)?.grandCny || 0;
next.delivery_cost_rub = grandCnyTotal > 0
? Math.round((chinaRub * (grandCny / grandCnyTotal)) * 100) / 100
: 0;
if (next.status !== 'in_transit') {
next.status = 'in_transit';
this._appendStatusHistory(next, 'in_transit', `Добавлено в коробку «${shipmentName}»`);
}
await saveChinaPurchase(next);
}
for (const purchaseId of removedIds) {
const purchase = this.allPurchases.find(p => p.id === purchaseId);
if (!purchase) continue;
const next = JSON.parse(JSON.stringify(purchase));
next.shipment_id = null;
next.delivery_type = '';
next.estimated_days = 0;
next.tracking_number = '';
next.delivery_cost_rub = 0;
if (next.status === 'in_transit') {
next.status = 'in_china_warehouse';
this._appendStatusHistory(next, 'in_china_warehouse', `Убрано из коробки «${shipmentName}»`);
}
await saveChinaPurchase(next);
}
this.allPurchases = await loadChinaPurchases({});
this.allShipments = await loadShipments();
this.consolidationEditingShipmentId = shipmentId;
App.toast('Коробка сохранена');
this.showView('consolidation');
},
openLinkedShipment(shipmentId) {
if (!shipmentId) {
App.toast('У этой закупки ещё нет коробки');
return;
}
App.navigate('warehouse');
setTimeout(async () => {
Warehouse.setView('shipments');
await Warehouse.loadShipmentsList();
Warehouse.editShipment(shipmentId);
}, 250);
},
// ==========================================
// FORM VIEW
// ==========================================
async openNewForm(orderId = null) {
this.editingPurchaseId = null;
this.itemCounter = 0;
this.resetFormFields();
await this.loadOrderOptions();
if (orderId != null && orderId !== '') {
document.getElementById('china-f-order').value = String(orderId);
}
document.getElementById('china-form-heading').textContent = 'Новая закупка';
this.showView('form');
},
async openForm(purchaseId) {
if (purchaseId == null || purchaseId === '') {
await this.openNewForm();
return;
}
const p = await loadChinaPurchase(purchaseId);
if (!p) { App.toast('Закупка не найдена'); return; }
this.editingPurchaseId = purchaseId;
await this.loadOrderOptions();
this.populateForm(p);
document.getElementById('china-form-heading').textContent = 'Редактирование';
this.showView('form');
},
resetFormFields() {
document.getElementById('china-f-name').value = '';
document.getElementById('china-f-date').value = App.todayLocalYMD();
document.getElementById('china-f-supplier').value = '';
document.getElementById('china-f-url').value = '';
document.getElementById('china-f-order').value = '';
document.getElementById('china-f-del-cny').value = '';
document.getElementById('china-f-notes').value = '';
document.getElementById('china-f-items').innerHTML = '';
document.getElementById('china-f-total').textContent = '0 \u00A5';
},
populateForm(p) {
document.getElementById('china-f-name').value = p.purchase_name || '';
document.getElementById('china-f-date').value = p.date || '';
document.getElementById('china-f-supplier').value = p.supplier_name || '';
document.getElementById('china-f-url').value = p.supplier_url || '';
document.getElementById('china-f-order').value = p.order_id || '';
document.getElementById('china-f-del-cny').value = p.delivery_cost_cny || '';
document.getElementById('china-f-notes').value = p.notes || '';
document.getElementById('china-f-items').innerHTML = '';
this.itemCounter = 0;
(p.items || []).forEach(item => this.addItemRow(item));
this.recalcTotal();
},
addItemRow(data) {
const idx = this.itemCounter++;
const d = data || { name: '', description: '', qty: 0, price_cny: 0, photo_url: '' };
const container = document.getElementById('china-f-items');
container.insertAdjacentHTML('beforeend', `
`);
},
removeItemRow(idx) {
const el = document.getElementById('china-item-' + idx);
if (el) el.remove();
this.recalcTotal();
},
recalcTotal() {
let total = 0;
document.querySelectorAll('.china-item-row').forEach(row => {
const qty = parseFloat(row.querySelector('.ci-qty').value) || 0;
const price = parseFloat(row.querySelector('.ci-price').value) || 0;
total += qty * price;
});
document.getElementById('china-f-total').textContent = this.formatCny(total);
},
gatherFormData() {
const items = [];
document.querySelectorAll('.china-item-row').forEach(row => {
items.push({
name: row.querySelector('.ci-name').value.trim(),
description: row.querySelector('.ci-desc').value.trim(),
qty: parseFloat(row.querySelector('.ci-qty').value) || 0,
price_cny: parseFloat(row.querySelector('.ci-price').value) || 0,
photo_url: row.querySelector('.ci-photo').value.trim(),
warehouse_item_id: null,
});
});
let totalCny = 0;
items.forEach(i => totalCny += i.qty * i.price_cny);
return {
id: this.editingPurchaseId || undefined,
purchase_name: document.getElementById('china-f-name').value.trim(),
date: document.getElementById('china-f-date').value || null,
supplier_name: document.getElementById('china-f-supplier').value.trim(),
supplier_url: document.getElementById('china-f-url').value.trim(),
order_id: parseInt(document.getElementById('china-f-order').value) || null,
order_name: '',
items,
total_cny: Math.round(totalCny * 100) / 100,
delivery_cost_cny: parseFloat(document.getElementById('china-f-del-cny').value) || 0,
status: 'ordered',
status_history: [],
shipment_id: null,
notes: document.getElementById('china-f-notes').value.trim(),
created_by: '',
};
},
async saveForm() {
const data = this.gatherFormData();
if (!data.purchase_name) { App.toast('Введите название закупки'); return; }
// Cache order name
if (data.order_id) {
const orderData = await loadOrder(data.order_id);
if (orderData) data.order_name = orderData.order.order_name || '';
}
// Preserve existing status/history/pdf if editing
if (this.editingPurchaseId) {
const existing = await loadChinaPurchase(this.editingPurchaseId);
if (existing) {
data.status = existing.status;
data.status_history = existing.status_history || [];
data.shipment_id = existing.shipment_id;
data.created_by = existing.created_by;
data.delivery_type = existing.delivery_type || '';
data.delivery_cost_rub = existing.delivery_cost_rub || 0;
data.cny_rate = existing.cny_rate || 0;
data.estimated_days = existing.estimated_days || 0;
data.tracking_number = existing.tracking_number || '';
}
}
const id = await saveChinaPurchase(data);
if (id) {
App.toast('Закупка сохранена');
this.allPurchases = await loadChinaPurchases({});
this.openDetail(id);
}
},
async loadOrderOptions() {
const orders = await loadOrders({});
const sel = document.getElementById('china-f-order');
sel.innerHTML = '';
orders.forEach(o => {
sel.innerHTML += ``;
});
},
// ==========================================
// DETAIL VIEW
// ==========================================
async openDetail(purchaseId) {
const p = await loadChinaPurchase(purchaseId);
if (!p) { App.toast('Закупка не найдена'); return; }
this.editingPurchaseId = purchaseId;
this.renderDetail(p);
this.showView('detail');
},
renderDetail(p) {
document.getElementById('china-d-title').textContent = p.purchase_name;
document.getElementById('china-d-date').textContent = App.formatDate(p.date);
const badge = document.getElementById('china-d-badge');
badge.className = 'china-badge china-badge-' + this.statusColor(p.status);
badge.textContent = this.statusLabel(p.status);
// Timeline
const tl = document.getElementById('china-d-timeline');
const currentIdx = this.STATUSES.findIndex(s => s.key === p.status);
tl.innerHTML = this.STATUSES.map((s, i) => {
const entry = (p.status_history || []).find(h => h.status === s.key);
const passed = i <= currentIdx;
const current = i === currentIdx;
return `
${s.label}
${entry ? `
${App.formatDate(entry.date)}
` : ''}
${entry && entry.note ? `
${this.esc(entry.note)}
` : ''}
`;
}).join('');
// Info
document.getElementById('china-d-supplier').textContent = p.supplier_name || '—';
const urlEl = document.getElementById('china-d-url');
urlEl.innerHTML = p.supplier_url
? `${this.esc(p.supplier_url.substring(0, 60))}${p.supplier_url.length > 60 ? '...' : ''}`
: '—';
document.getElementById('china-d-order').textContent = p.order_name || '—';
const shipment = p.shipment_id ? this._findShipment(p.shipment_id) : null;
const shipmentEl = document.getElementById('china-d-shipment');
shipmentEl.innerHTML = shipment
? `${this.esc(shipment.shipment_name || ('Коробка #' + shipment.id))}`
: '—';
document.getElementById('china-d-del-type').textContent = this.statusLabel(p.status);
document.getElementById('china-d-tracking').textContent = shipment ? `${shipment.shipment_name || ''}${shipment.china_tracking_number ? ' · ' + shipment.china_tracking_number : ''}` : '—';
document.getElementById('china-d-days').textContent = shipment?.china_estimated_days ? shipment.china_estimated_days + ' дн' : '—';
document.getElementById('china-d-rate').textContent = p.cny_rate ? p.cny_rate + ' \u20BD' : '—';
// Financial
document.getElementById('china-d-total-cny').textContent = this.formatCny(p.total_cny);
document.getElementById('china-d-del-cny').textContent = this.formatCny(p.delivery_cost_cny);
document.getElementById('china-d-del-rub').textContent = p.delivery_cost_rub ? (new Intl.NumberFormat('ru-RU').format(p.delivery_cost_rub) + ' \u20BD') : '—';
const grandCny = (p.total_cny || 0) + (p.delivery_cost_cny || 0);
document.getElementById('china-d-grand-cny').textContent = this.formatCny(grandCny);
const grandRub = p.cny_rate ? Math.round((grandCny * p.cny_rate + (p.delivery_cost_rub || 0)) * 100) / 100 : 0;
document.getElementById('china-d-grand-rub').textContent = p.cny_rate
? (new Intl.NumberFormat('ru-RU').format(grandRub) + ' \u20BD') : '—';
// Items
const itemsBody = document.getElementById('china-d-items-body');
if (p.items && p.items.length > 0) {
itemsBody.innerHTML = p.items.map((item, i) => {
const photoSrc = this._resolveItemPhoto(item);
const photoHtml = photoSrc
? `
`
: '';
const fallbackHtml = `📦`;
return `
| ${i + 1} |
${photoHtml}${fallbackHtml}
${this.esc(item.name)}
${item.description ? ` ${this.esc(item.description)} ` : ''}
|
${item.qty} |
${this.formatCny(item.price_cny)} |
${this.formatCny(item.qty * item.price_cny)} |
`;
}).join('');
} else {
itemsBody.innerHTML = '| Нет позиций |
';
}
// PDF
const pdfEl = document.getElementById('china-d-pdf');
pdfEl.innerHTML = (shipment?.waybill_pdf_name && shipment?.waybill_pdf_data)
? `📄 ${this.esc(shipment.waybill_pdf_name)}`
: 'Нет накладной';
// Notes
document.getElementById('china-d-notes').textContent = p.notes || '—';
// Actions
this.renderStatusActions(p);
},
renderStatusActions(p) {
const container = document.getElementById('china-d-actions');
const idx = this.STATUSES.findIndex(s => s.key === p.status);
let html = '';
if (p.status === 'in_china_warehouse' || p.shipment_id) {
html += ``;
}
if (p.shipment_id) {
html += ``;
}
const canAdvanceDirectly = p.status === 'ordered' || (!!p.shipment_id && idx < this.STATUSES.length - 1);
if (canAdvanceDirectly && idx < this.STATUSES.length - 1) {
const next = this.STATUSES[idx + 1];
html += ``;
}
html += ``;
html += ``;
container.innerHTML = html;
},
async promptStatusChange(purchaseId, newStatus) {
const note = prompt('Комментарий к статусу (необязательно):') || '';
const purchase = await loadChinaPurchase(purchaseId);
if (!purchase) return;
let targetIds = [purchaseId];
if (purchase.shipment_id && ['in_transit', 'delivered', 'received'].includes(newStatus)) {
const shipment = this._findShipment(purchase.shipment_id);
if (shipment) {
await this.updateBoxStatus(shipment.id, newStatus);
this.openDetail(purchaseId);
return;
}
}
for (const id of targetIds) {
await updateChinaPurchaseStatus(id, newStatus, note);
}
App.toast(targetIds.length > 1
? `Статус обновлён для всей коробки: ${this.statusLabel(newStatus)}`
: 'Статус: ' + this.statusLabel(newStatus));
this.allPurchases = await loadChinaPurchases({});
this.allShipments = await loadShipments();
this.openDetail(purchaseId);
},
async confirmDelete(purchaseId) {
if (!confirm('Удалить эту закупку?')) return;
await deleteChinaPurchase(purchaseId);
App.toast('Закупка удалена');
this.allPurchases = await loadChinaPurchases({});
this.showView('dashboard');
},
};