Feature: таблица товаров без скролла, товар обрезается, цена/стоимость с разделителем разрядов

Made-with: Cursor
This commit is contained in:
2026-02-26 12:26:28 +00:00
parent 646b3ea3fc
commit 39cc12fae4
4 changed files with 168 additions and 26 deletions

View File

@@ -2,6 +2,16 @@
# История изменений ERP WaterSurf
## 2025-02-25 22:35 UTC Табличная часть заказа: без скролла, цена/стоимость с разделителем
**Проблема**: В таблице товаров появлялась горизонтальная полоса прокрутки; поле «Товар» слишком широкое; в полях «Цена» и «Стоимость» не было разделителя разрядов.
**Решение**: Включён table-layout: fixed и заданы доли колонок в % (Товар 28%, Цена 14%, Валюта 12%, Количество 10%, Стоимость 14%, Удалить 5%), чтобы таблица помещалась без скролла. Поле «Товар» ограничено по ширине, select с overflow: hidden. Поля «Цена» и «Стоимость»: выравнивание по правому краю, узкое поле под 8-значную сумму. В JS добавлены formatNum/parseNum: разделитель тысяч (неразрывный пробел) и запятая как десятичный разделитель; при blur цена форматируется, при focus и при submit — пробелы снимаются перед отправкой. Стоимость в ячейке выводится с разделителем. Аналогичные правки в форме заказа поставщику.
**Изменения**: order_form.html, supplier_order_form.html (colgroup, классы ячеек, JS форматирование), theme-compact.css (ширины колонок).
---
## 2025-02-25 22:25 UTC Временное отключение авторизации для отладки
**Проблема**: Для быстрой разработки и отладки нужно работать без входа.

View File

@@ -84,14 +84,54 @@
max-width: 100%;
}
/* Таблица товаров: колонка Товар шире, Количество уже */
/* Таблица товаров: без горизонтального скролла, компактные колонки */
.ws-table-wrap {
overflow-x: auto;
max-width: 100%;
}
.ws-table-items {
table-layout: fixed;
width: 100%;
min-width: 0;
}
/* Товар: ограниченная ширина, длинное название обрезается */
.ws-table-items .ws-col-product {
width: 35%;
min-width: 14rem;
width: 28%;
overflow: hidden;
}
.ws-table-items .ws-col-product select {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
/* Цена: поле под 8 знаков, значение справа */
.ws-table-items .ws-col-price {
width: 14%;
}
.ws-table-items .ws-col-price input {
width: 100% !important;
max-width: 100%;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* Валюта */
.ws-table-items .ws-col-currency {
width: 12%;
}
.ws-table-items .ws-col-currency select {
max-width: 100%;
}
.ws-table-items .ws-col-qty {
width: 5rem;
width: 10%;
min-width: 4rem;
}
.ws-table-items td.ws-col-qty input,
@@ -101,6 +141,14 @@
box-sizing: border-box;
}
.ws-table-items .ws-col-del {
width: 4rem;
/* Стоимость: справа, разделитель в JS */
.ws-table-items .ws-col-cost {
width: 14%;
text-align: right;
font-variant-numeric: tabular-nums;
}
.ws-table-items .ws-col-del {
width: 5%;
min-width: 3rem;
}

View File

@@ -48,10 +48,10 @@
<table class="ws-table ws-table-items" id="order-items">
<colgroup>
<col class="ws-col-product">
<col>
<col>
<col class="ws-col-price">
<col class="ws-col-currency">
<col class="ws-col-qty">
<col>
<col class="ws-col-cost">
<col class="ws-col-del">
</colgroup>
<thead>
@@ -67,11 +67,11 @@
<tbody id="order-items-body">
{% for f in formset %}
<tr class="item-row">
<td>{{ f.id }}{{ f.product }}</td>
<td>{{ f.price }}</td>
<td>{{ f.currency }}</td>
<td class="ws-col-product">{{ f.id }}{{ f.product }}</td>
<td class="ws-col-price">{{ f.price }}</td>
<td class="ws-col-currency">{{ f.currency }}</td>
<td class="ws-col-qty">{{ f.quantity }}</td>
<td class="row-amount ws-num"></td>
<td class="row-amount ws-col-cost ws-num"></td>
<td>{% if f.DELETE %}{{ f.DELETE }}{% endif %}</td>
</tr>
{% endfor %}
@@ -101,17 +101,52 @@
return name.replace(/-TOTAL_FORMS$/, '');
}
function formatNum(x) {
var n = parseFloat(x);
if (isNaN(n)) return '—';
var parts = n.toFixed(2).split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '\u202f');
return parts.join(',');
}
function parseNum(s) {
return parseFloat(String(s).replace(/\s/g, '').replace(',', '.')) || 0;
}
function updateRowAmounts() {
document.querySelectorAll('#order-items .item-row').forEach(function(row) {
var price = parseFloat(row.querySelector('input[name$="-price"]')?.value) || 0;
var qty = parseFloat(row.querySelector('input[name$="-quantity"]')?.value) || 0;
var priceInput = row.querySelector('input[name$="-price"]');
var qty = parseNum(row.querySelector('input[name$="-quantity"]')?.value);
var price = parseNum(priceInput?.value);
var el = row.querySelector('.row-amount');
if (el) el.textContent = (price * qty).toFixed(2);
if (el) el.textContent = formatNum(price * qty);
});
}
form.addEventListener('input', updateRowAmounts);
form.querySelectorAll('#order-items input[name$="-price"]').forEach(function(input) {
input.addEventListener('blur', function() {
var v = parseNum(this.value);
if (!isNaN(v) && this.value.trim() !== '') this.value = formatNum(v).replace(',', '.');
});
input.addEventListener('focus', function() {
this.value = String(this.value).replace(/\s/g, '').replace(',', '.');
});
});
form.addEventListener('submit', function() {
form.querySelectorAll('input[name$="-price"]').forEach(function(input) {
input.value = String(input.value).replace(/\s/g, '').replace(',', '.');
});
});
document.querySelectorAll('#order-items input[name$="-price"]').forEach(function(input) {
if (input.value && input.value.trim() !== '') {
var v = parseNum(input.value);
input.value = formatNum(v).replace(',', '.');
}
});
updateRowAmounts();
document.getElementById('add-order-row').addEventListener('click', function() {
var rows = tbody.querySelectorAll('.item-row');
var lastRow = rows[rows.length - 1];
@@ -135,6 +170,13 @@
});
var amountCell = clone.querySelector('.row-amount');
if (amountCell) amountCell.textContent = '—';
clone.querySelector('input[name$="-price"]')?.addEventListener('blur', function() {
var v = parseNum(this.value);
if (!isNaN(v) && this.value.trim() !== '') this.value = formatNum(v).replace(',', '.');
});
clone.querySelector('input[name$="-price"]')?.addEventListener('focus', function() {
this.value = String(this.value).replace(/\s/g, '').replace(',', '.');
});
tbody.appendChild(clone);
totalInput.value = nextIndex + 1;
});

View File

@@ -55,10 +55,10 @@
<table class="ws-table ws-table-items" id="supplier-order-items">
<colgroup>
<col class="ws-col-product">
<col>
<col>
<col class="ws-col-price">
<col class="ws-col-currency">
<col class="ws-col-qty">
<col>
<col class="ws-col-cost">
<col class="ws-col-del">
</colgroup>
<thead>
@@ -74,11 +74,11 @@
<tbody id="supplier-order-items-body">
{% for f in formset %}
<tr class="item-row">
<td>{{ f.id }}{{ f.product }}</td>
<td>{{ f.price }}</td>
<td>{{ f.currency }}</td>
<td class="ws-col-product">{{ f.id }}{{ f.product }}</td>
<td class="ws-col-price">{{ f.price }}</td>
<td class="ws-col-currency">{{ f.currency }}</td>
<td class="ws-col-qty">{{ f.quantity }}</td>
<td class="row-amount ws-num"></td>
<td class="row-amount ws-col-cost ws-num"></td>
<td>{% if f.DELETE %}{{ f.DELETE }}{% endif %}</td>
</tr>
{% endfor %}
@@ -107,17 +107,52 @@
return name.replace(/-TOTAL_FORMS$/, '');
}
function formatNum(x) {
var n = parseFloat(x);
if (isNaN(n)) return '—';
var parts = n.toFixed(2).split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '\u202f');
return parts.join(',');
}
function parseNum(s) {
return parseFloat(String(s).replace(/\s/g, '').replace(',', '.')) || 0;
}
function updateRowAmounts() {
document.querySelectorAll('#supplier-order-items .item-row').forEach(function(row) {
var price = parseFloat(row.querySelector('input[name$="-price"]')?.value) || 0;
var qty = parseFloat(row.querySelector('input[name$="-quantity"]')?.value) || 0;
var priceInput = row.querySelector('input[name$="-price"]');
var qty = parseNum(row.querySelector('input[name$="-quantity"]')?.value);
var price = parseNum(priceInput?.value);
var el = row.querySelector('.row-amount');
if (el) el.textContent = (price * qty).toFixed(2);
if (el) el.textContent = formatNum(price * qty);
});
}
form.addEventListener('input', updateRowAmounts);
form.querySelectorAll('#supplier-order-items input[name$="-price"]').forEach(function(input) {
input.addEventListener('blur', function() {
var v = parseNum(this.value);
if (!isNaN(v) && this.value.trim() !== '') this.value = formatNum(v).replace(',', '.');
});
input.addEventListener('focus', function() {
this.value = String(this.value).replace(/\s/g, '').replace(',', '.');
});
});
form.addEventListener('submit', function() {
form.querySelectorAll('input[name$="-price"]').forEach(function(input) {
input.value = String(input.value).replace(/\s/g, '').replace(',', '.');
});
});
document.querySelectorAll('#supplier-order-items input[name$="-price"]').forEach(function(input) {
if (input.value && input.value.trim() !== '') {
var v = parseNum(input.value);
input.value = formatNum(v).replace(',', '.');
}
});
updateRowAmounts();
document.getElementById('add-supplier-order-row').addEventListener('click', function() {
var rows = tbody.querySelectorAll('.item-row');
var lastRow = rows[rows.length - 1];
@@ -141,6 +176,13 @@
});
var amountCell = clone.querySelector('.row-amount');
if (amountCell) amountCell.textContent = '—';
clone.querySelector('input[name$="-price"]')?.addEventListener('blur', function() {
var v = parseNum(this.value);
if (!isNaN(v) && this.value.trim() !== '') this.value = formatNum(v).replace(',', '.');
});
clone.querySelector('input[name$="-price"]')?.addEventListener('focus', function() {
this.value = String(this.value).replace(/\s/g, '').replace(',', '.');
});
tbody.appendChild(clone);
totalInput.value = nextIndex + 1;
});