fixed complex strings

This commit is contained in:
2026-09-02 14:35:30 +03:00
parent b7a1609a39
commit f132e83288
8 changed files with 636 additions and 22 deletions
+45 -1
View File
@@ -23,11 +23,20 @@ from app.schemas import (
ProjectUpdate,
TableRowsPatch,
TableRowsResponse,
TableSettingsUpdate,
)
from app.services.excel_export import export_xlsx
from app.services.llm import chat_edit_table
from app.services.pdf_export import export_pdf
from app.services.project_service import get_project_full, ingest_zip, project_dir
from app.services.project_settings import (
TABLE_SETTINGS_META,
get_property_names,
get_project_params_map,
get_table_settings,
resolve_inscriptions,
update_table_settings,
)
from app.services.table_service import (
TABLE_TYPES,
apply_llm_edits,
@@ -204,6 +213,39 @@ def get_pcb(project_id: int, db: Session = Depends(get_db)):
}
@secured.get("/projects/{project_id}/property-names")
def read_property_names(project_id: int, db: Session = Depends(get_db)):
_get_project(db, project_id)
return {"property_names": get_property_names(db, project_id)}
@secured.get("/projects/{project_id}/table-settings/{table_type}")
def read_table_settings(project_id: int, table_type: str, db: Session = Depends(get_db)):
_get_project(db, project_id)
if table_type not in TABLE_TYPES:
raise HTTPException(400, f"Unknown table type. Use one of: {TABLE_TYPES}")
return get_table_settings(db, project_id, table_type)
@secured.patch("/projects/{project_id}/table-settings/{table_type}")
def patch_table_settings(
project_id: int,
table_type: str,
body: TableSettingsUpdate,
db: Session = Depends(get_db),
):
_get_project(db, project_id)
if table_type not in TABLE_TYPES:
raise HTTPException(400, f"Unknown table type")
return update_table_settings(db, project_id, table_type, body.model_dump(exclude_unset=True))
@secured.get("/projects/{project_id}/table-settings-meta")
def read_table_settings_meta(project_id: int, db: Session = Depends(get_db)):
_get_project(db, project_id)
return TABLE_SETTINGS_META
@secured.get("/projects/{project_id}/inscriptions")
def read_inscriptions(project_id: int, db: Session = Depends(get_db)):
_get_project(db, project_id)
@@ -272,6 +314,8 @@ def export_document(project_id: int, body: ExportRequest, db: Session = Depends(
if not rows:
raise HTTPException(400, "Table is empty. Generate it first.")
inscriptions = get_inscriptions(db, project_id)
params = get_project_params_map(db, project_id)
resolved_inscriptions = resolve_inscriptions(inscriptions, params)
if body.format == "xlsx":
data = export_xlsx(body.table_type, rows, title=project.name)
@@ -284,7 +328,7 @@ def export_document(project_id: int, body: ExportRequest, db: Session = Depends(
)
if body.format == "pdf":
try:
data = export_pdf(body.table_type, rows, inscriptions)
data = export_pdf(body.table_type, rows, resolved_inscriptions)
except ValueError as e:
raise HTTPException(400, str(e)) from e
return Response(
+6
View File
@@ -191,3 +191,9 @@ class DesignatorMappingOut(BaseModel):
prefix: str
singular_name: str
plural_name: str
class TableSettingsUpdate(BaseModel):
mappings: Optional[list[str]] = None
column_mappings: Optional[dict[str, str]] = None
simple_list: Optional[dict[str, Any]] = None
@@ -0,0 +1,310 @@
"""Per-project document settings (column mappings, param references)."""
from __future__ import annotations
import json
from typing import Any
from sqlalchemy import distinct, select
from sqlalchemy.orm import Session
from app.models import Component, ComponentProperty, ProjectParam, ProjectSetting
DOC_TYPE_KEYS = {
"perechen": "Perechen",
"specification_pcb": "SpecificationPCB",
"specification": "Specification",
"vedomost": "Vedomost",
"simple_list": "SimpleList",
}
TABLE_SETTINGS_META: dict[str, dict[str, Any]] = {
"perechen": {
"columns": [{"label": "Наименование", "key": "name", "mapping_index": 0}],
"defaults": ["Name"],
},
"specification_pcb": {
"columns": [
{"label": "Обозначение", "key": "designation", "mapping_index": 0},
{"label": "Наименование", "key": "name", "mapping_index": 1},
],
"defaults": ["Designator", "ManufacturerPartNumber", "", "", "", "", "", ""],
"project_params": [
{"key": "board_name", "label": "Название платы", "mapping_index": 6},
{"key": "decimal_number", "label": "Децимальный №", "mapping_index": 7},
],
},
"specification": {
"columns": [],
"defaults": [],
"project_params": [
{"key": "decimal_number", "label": "Децимальный №", "mapping_index": 7},
{"key": "document_name", "label": "Название документа", "mapping_index": 8},
],
},
"vedomost": {
"columns": [
{"label": "Наименование", "key": "name", "mapping_index": 0},
{"label": "Код продукции", "key": "product_code", "mapping_index": 1},
{"label": "Обозначение документа", "key": "document_code", "mapping_index": 2},
{"label": "Поставщик", "key": "supplier", "mapping_index": 3},
{"label": "Примечание", "key": "note", "mapping_index": 5},
],
"defaults": ["Name", "ProductCode", "DocumentCode", "Supplier", "", "Note"],
"project_params": [
{"key": "where_used", "label": "Куда входит", "mapping_index": 4},
],
},
"simple_list": {
"columns": [],
"defaults": [],
"component_fields": [
{"key": "name_field", "label": "Поле наименования", "setting_key": "SimpleList_nameField"},
],
"extra": [
{"key": "tech_reserve_percent", "label": "Тех. запас, %", "setting_key": "SimpleList_techReservePercent", "default": "10"},
{"key": "boards_count", "label": "Кол-во плат", "setting_key": "SimpleList_boardsCount", "default": "1"},
],
},
}
def _setting_key(table_type: str) -> str:
doc = DOC_TYPE_KEYS.get(table_type, table_type)
return f"columnMappings_{doc}"
def _get_setting(db: Session, project_id: int, key: str) -> str | None:
row = db.scalar(
select(ProjectSetting).where(
ProjectSetting.project_id == project_id,
ProjectSetting.key == key,
)
)
return row.value if row else None
def _set_setting(db: Session, project_id: int, key: str, value: str) -> None:
row = db.scalar(
select(ProjectSetting).where(
ProjectSetting.project_id == project_id,
ProjectSetting.key == key,
)
)
if row:
row.value = value
else:
db.add(ProjectSetting(project_id=project_id, key=key, value=value))
def get_project_params_map(db: Session, project_id: int) -> dict[str, str]:
rows = db.scalars(select(ProjectParam).where(ProjectParam.project_id == project_id)).all()
return {r.name: r.value for r in rows}
def resolve_param_value(stored: str, params: dict[str, str]) -> str:
if not stored or stored == "-- Не выбрано --":
return ""
if stored in params and params[stored]:
return params[stored]
return stored
def resolve_inscriptions(
inscriptions: dict[int, str], params: dict[str, str]
) -> dict[int, str]:
resolved: dict[int, str] = {}
for num, val in inscriptions.items():
resolved[int(num)] = resolve_param_value(val or "", params)
return resolved
def get_property_names(db: Session, project_id: int) -> list[str]:
keys = db.scalars(
select(distinct(ComponentProperty.key))
.join(Component, Component.id == ComponentProperty.component_id)
.where(Component.project_id == project_id)
.order_by(ComponentProperty.key)
).all()
names = sorted({k for k in keys if k})
if "Designator" not in names:
names.insert(0, "Designator")
return names
def get_column_mappings_list(db: Session, project_id: int, table_type: str) -> list[str]:
meta = TABLE_SETTINGS_META.get(table_type, {})
defaults = list(meta.get("defaults", []))
raw = _get_setting(db, project_id, _setting_key(table_type))
if not raw:
return defaults
try:
loaded = json.loads(raw)
if isinstance(loaded, list):
return [str(x) for x in loaded]
except json.JSONDecodeError:
pass
return defaults
def set_column_mappings_list(
db: Session, project_id: int, table_type: str, mappings: list[str]
) -> list[str]:
_set_setting(db, project_id, _setting_key(table_type), json.dumps(mappings, ensure_ascii=False))
db.commit()
return get_column_mappings_list(db, project_id, table_type)
def get_simple_list_settings(db: Session, project_id: int) -> dict[str, Any]:
name_field = _get_setting(db, project_id, "SimpleList_nameField") or "Name"
tech = _get_setting(db, project_id, "SimpleList_techReservePercent") or "10"
boards = _get_setting(db, project_id, "SimpleList_boardsCount") or "1"
return {
"name_field": name_field,
"tech_reserve_percent": float(tech),
"boards_count": int(boards),
}
def set_simple_list_settings(db: Session, project_id: int, data: dict[str, Any]) -> dict[str, Any]:
if "name_field" in data and data["name_field"] is not None:
_set_setting(db, project_id, "SimpleList_nameField", str(data["name_field"]))
if "tech_reserve_percent" in data and data["tech_reserve_percent"] is not None:
_set_setting(db, project_id, "SimpleList_techReservePercent", str(data["tech_reserve_percent"]))
if "boards_count" in data and data["boards_count"] is not None:
_set_setting(db, project_id, "SimpleList_boardsCount", str(data["boards_count"]))
db.commit()
return get_simple_list_settings(db, project_id)
def get_table_settings(db: Session, project_id: int, table_type: str) -> dict[str, Any]:
meta = TABLE_SETTINGS_META.get(table_type, {})
mappings = get_column_mappings_list(db, project_id, table_type)
columns = meta.get("columns", [])
column_mappings = {}
for col in columns:
idx = col.get("mapping_index", columns.index(col) if col in columns else 0)
column_mappings[col["key"]] = mappings[idx] if idx < len(mappings) else ""
result: dict[str, Any] = {
"table_type": table_type,
"columns": columns,
"column_mappings": column_mappings,
"mappings": mappings,
"property_names": get_property_names(db, project_id),
"project_params": [
{"name": n, "value": v} for n, v in get_project_params_map(db, project_id).items()
],
}
for pp in meta.get("project_params", []):
idx = pp["mapping_index"]
result["column_mappings"][pp["key"]] = mappings[idx] if idx < len(mappings) else ""
fields: list[dict[str, str]] = []
for col in columns:
fields.append({"key": col["key"], "label": col["label"], "source": "property"})
for pp in meta.get("project_params", []):
fields.append({"key": pp["key"], "label": pp["label"], "source": "project_param"})
for cf in meta.get("component_fields", []):
fields.append({"key": cf["key"], "label": cf["label"], "source": "component_field"})
result["fields"] = fields
if table_type == "simple_list":
result["simple_list"] = get_simple_list_settings(db, project_id)
sl = result["simple_list"]
result["column_mappings"]["name_field"] = sl["name_field"]
return result
def update_table_settings(
db: Session, project_id: int, table_type: str, body: dict[str, Any]
) -> dict[str, Any]:
meta = TABLE_SETTINGS_META.get(table_type, {})
mappings = get_column_mappings_list(db, project_id, table_type)
if "mappings" in body and body["mappings"] is not None:
mappings = [str(x) for x in body["mappings"]]
elif "column_mappings" in body and body["column_mappings"]:
cm = body["column_mappings"]
columns = meta.get("columns", [])
for col in columns:
key = col["key"]
if key not in cm:
continue
idx = col.get("mapping_index", 0)
while len(mappings) <= idx:
mappings.append("")
mappings[idx] = cm[key] or ""
for pp in meta.get("project_params", []):
key = pp["key"]
idx = pp["mapping_index"]
if key in cm:
while len(mappings) <= idx:
mappings.append("")
mappings[idx] = cm[key] or ""
set_column_mappings_list(db, project_id, table_type, mappings)
sl_patch = body.get("simple_list") or {}
if table_type == "simple_list":
cm = body.get("column_mappings") or {}
if "name_field" in cm:
sl_patch = {**sl_patch, "name_field": cm["name_field"]}
if sl_patch:
set_simple_list_settings(db, project_id, sl_patch)
return get_table_settings(db, project_id, table_type)
def vedomost_column_map(mappings: list[str]) -> dict[str, str]:
field_indices = {
"Name": 0,
"ProductCode": 1,
"DocumentCode": 2,
"Supplier": 3,
"Note": 5,
}
defaults = {
"Name": "Name",
"ProductCode": "ProductCode",
"DocumentCode": "DocumentCode",
"Supplier": "Supplier",
"Note": "Note",
}
return {
k: mappings[idx] if idx < len(mappings) and mappings[idx] else defaults[k]
for k, idx in field_indices.items()
}
def resolve_where_used(db: Session, project_id: int, mappings: list[str]) -> str:
params = get_project_params_map(db, project_id)
if len(mappings) > 4 and mappings[4]:
return resolve_param_value(mappings[4], params)
return ""
def resolve_decimal_and_board(
db: Session, project_id: int, project_decimal: str, project_board: str, table_type: str
) -> tuple[str, str]:
params = get_project_params_map(db, project_id)
mappings = get_column_mappings_list(db, project_id, table_type)
dec = project_decimal
board = project_board
if table_type == "specification_pcb":
if len(mappings) > 7 and mappings[7]:
dec = resolve_param_value(mappings[7], params)
elif dec:
dec = resolve_param_value(dec, params)
if len(mappings) > 6 and mappings[6]:
board = resolve_param_value(mappings[6], params)
elif board:
board = resolve_param_value(board, params)
elif dec:
dec = resolve_param_value(dec, params)
return dec, board
+2 -1
View File
@@ -147,6 +147,7 @@ def generate_specification_pcb(
board_name: str = "",
pcb_doc_name: str = "",
mappings: dict[str, tuple[str, str]] | None = None,
name_field: str = "ManufacturerPartNumber",
) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
dec = decimal_number or ""
@@ -226,7 +227,7 @@ def generate_specification_pcb(
for letter, comps in groups.items():
subgroups: OrderedDict[str, list[ComponentView]] = OrderedDict()
for c in comps:
key = c.get("ManufacturerPartNumber", "") or c.get("Name", "")
key = c.get(name_field, "") or c.get("ManufacturerPartNumber", "") or c.get("Name", "")
subgroups.setdefault(key, []).append(c)
for part_number, group in subgroups.items():
+44 -15
View File
@@ -19,6 +19,15 @@ from app.models import (
VariantProperty,
VedomostRow,
)
from app.services.project_settings import (
get_column_mappings_list,
get_project_params_map,
get_simple_list_settings,
resolve_decimal_and_board,
resolve_param_value,
resolve_where_used,
vedomost_column_map,
)
from app.services.table_generators import (
TABLE_TYPES,
ComponentView,
@@ -165,16 +174,23 @@ def generate_table(
mappings = _mappings_dict(db, project.id)
components = load_components_for_variant(db, project)
col_mappings = get_column_mappings_list(db, project.id, table_type)
if table_type == "perechen":
rows = generate_perechen(components, name_field=name_field or "Name", mappings=mappings)
nf = name_field or (col_mappings[0] if col_mappings else "Name") or "Name"
rows = generate_perechen(components, name_field=nf, mappings=mappings)
elif table_type == "specification_pcb":
dec, board = resolve_decimal_and_board(
db, project.id, project.decimal_number, project.board_name, table_type
)
name_prop = col_mappings[1] if len(col_mappings) > 1 and col_mappings[1] else "ManufacturerPartNumber"
rows = generate_specification_pcb(
components,
decimal_number=project.decimal_number,
board_name=project.board_name,
decimal_number=dec,
board_name=board,
pcb_doc_name=project.pcb_doc_name or "",
mappings=mappings,
name_field=name_prop,
)
elif table_type == "specification":
materials: list[MaterialView] = []
@@ -191,22 +207,35 @@ def generate_table(
layer_number=m.layer_number,
)
)
rows = generate_specification(materials, layer_count=layer_count, decimal_number=project.decimal_number)
dec, _ = resolve_decimal_and_board(
db, project.id, project.decimal_number, project.board_name, table_type
)
rows = generate_specification(materials, layer_count=layer_count, decimal_number=dec)
elif table_type == "vedomost":
inscriptions = {
i.field_number: i.field_value
for i in db.scalars(
select(TitleInscription).where(TitleInscription.project_id == project.id)
).all()
}
where_used = inscriptions.get(101) or inscriptions.get(1001) or project.decimal_number
rows = generate_vedomost(components, where_used=where_used, mappings=mappings)
params = get_project_params_map(db, project.id)
where_used = resolve_where_used(db, project.id, col_mappings)
if not where_used:
inscriptions = {
i.field_number: i.field_value
for i in db.scalars(
select(TitleInscription).where(TitleInscription.project_id == project.id)
).all()
}
raw = inscriptions.get(101) or inscriptions.get(1001) or project.decimal_number or ""
where_used = resolve_param_value(raw, params)
rows = generate_vedomost(
components,
where_used=where_used,
mappings=mappings,
column_mappings=vedomost_column_map(col_mappings),
)
else:
sl = get_simple_list_settings(db, project.id)
rows = generate_simple_list(
components,
name_field=name_field or "Name",
tech_reserve_percent=tech_reserve_percent if tech_reserve_percent is not None else 10.0,
boards_count=boards_count if boards_count is not None else 1,
name_field=name_field or sl["name_field"],
tech_reserve_percent=tech_reserve_percent if tech_reserve_percent is not None else sl["tech_reserve_percent"],
boards_count=boards_count if boards_count is not None else sl["boards_count"],
)
return replace_rows(db, project.id, table_type, rows)
+4
View File
@@ -109,6 +109,10 @@ export const api = {
},
getComponents: (id) => request(`/api/projects/${id}/components`),
getParams: (id) => request(`/api/projects/${id}/params`),
getPropertyNames: (id) => request(`/api/projects/${id}/property-names`),
getTableSettings: (id, type) => request(`/api/projects/${id}/table-settings/${type}`),
patchTableSettings: (id, type, data) =>
request(`/api/projects/${id}/table-settings/${type}`, { method: "PATCH", json: data }),
getPcb: (id) => request(`/api/projects/${id}/pcb`),
getInscriptions: (id) => request(`/api/projects/${id}/inscriptions`),
patchInscriptions: (id, inscriptions) =>
+219 -5
View File
@@ -79,6 +79,78 @@ const STATUS_LABELS = {
error: "ошибка",
};
/** Выпадающий список параметров проекта + свой текст (как в десктопе). */
function ParamCombo({ value, params, onChange, disabled }) {
const known = params.some((p) => p.name === value);
const [custom, setCustom] = useState(value && !known);
useEffect(() => {
setCustom(Boolean(value && !params.some((p) => p.name === value)));
}, [value, params]);
if (custom) {
return (
<div className="row" style={{ gap: "0.35rem" }}>
<input
style={{ flex: 1 }}
value={value || ""}
disabled={disabled}
placeholder="Свой текст"
onChange={(e) => onChange(e.target.value)}
/>
<button
type="button"
className="secondary"
disabled={disabled}
onClick={() => {
setCustom(false);
onChange("");
}}
>
Список
</button>
</div>
);
}
return (
<select
value={value || ""}
disabled={disabled}
onChange={(e) => {
if (e.target.value === "__custom__") {
setCustom(true);
onChange("");
return;
}
onChange(e.target.value);
}}
>
<option value=""> не выбрано </option>
{params.map((p) => (
<option key={p.name} value={p.name}>
{p.value ? `${p.name} (${p.value})` : p.name}
</option>
))}
<option value="__custom__">Свой текст</option>
</select>
);
}
/** Свойство компонента из списка Altium. */
function PropertySelect({ value, properties, onChange, disabled }) {
return (
<select value={value || ""} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
<option value=""> не выбрано </option>
{properties.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
);
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
@@ -107,6 +179,8 @@ export default function ProjectPage() {
const [pendingEdits, setPendingEdits] = useState(null);
const [tab, setTab] = useState("tables");
const [uploadState, setUploadState] = useState(null);
const [tableSettings, setTableSettings] = useState(null);
const [settingsDirty, setSettingsDirty] = useState(false);
const uploading = uploadState && uploadState.phase !== "done";
@@ -128,17 +202,36 @@ export default function ProjectPage() {
setRows(data.rows || []);
}, [projectId, tableType]);
const loadTableSettings = useCallback(async () => {
const data = await api.getTableSettings(projectId, tableType);
setTableSettings(data);
setSettingsDirty(false);
}, [projectId, tableType]);
useEffect(() => {
(async () => {
try {
setError("");
await loadProject();
await loadTable();
await loadTableSettings();
} catch (e) {
setError(e.message);
}
})();
}, [loadProject, loadTable]);
}, [loadProject, loadTable, loadTableSettings]);
useEffect(() => {
if (!project) return;
(async () => {
try {
await loadTable();
await loadTableSettings();
} catch (e) {
setError(e.message);
}
})();
}, [tableType, project, loadTable, loadTableSettings]);
const columns = useMemo(() => COLUMNS[tableType] || [], [tableType]);
@@ -158,7 +251,7 @@ export default function ProjectPage() {
try {
const updated = await api.uploadZip(projectId, file, method, true, setUploadState);
setProject(updated);
await Promise.all([loadProject(), loadTable()]);
await Promise.all([loadProject(), loadTable(), loadTableSettings()]);
setUploadState({
phase: "done",
percent: 100,
@@ -222,6 +315,47 @@ export default function ProjectPage() {
}
}
function updateColumnMapping(key, val) {
setTableSettings((prev) => {
if (!prev) return prev;
return {
...prev,
column_mappings: { ...prev.column_mappings, [key]: val },
};
});
setSettingsDirty(true);
}
function updateSimpleListSetting(key, val) {
setTableSettings((prev) => {
if (!prev) return prev;
return {
...prev,
simple_list: { ...prev.simple_list, [key]: val },
};
});
setSettingsDirty(true);
}
async function saveTableSettings() {
if (!tableSettings) return;
setBusy(true);
setError("");
try {
const body = { column_mappings: tableSettings.column_mappings };
if (tableType === "simple_list" && tableSettings.simple_list) {
body.simple_list = tableSettings.simple_list;
}
const data = await api.patchTableSettings(projectId, tableType, body);
setTableSettings(data);
setSettingsDirty(false);
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
}
async function sendChat() {
if (!chatInput.trim()) return;
const message = chatInput.trim();
@@ -432,16 +566,22 @@ export default function ProjectPage() {
{tab === "frame" && (
<div className="card stack">
<h3>Поля основной надписи</h3>
<p className="muted">
Выберите параметр проекта из списка или введите свой текст. При экспорте PDF имя параметра
подставится автоматически.
</p>
<div className="grid" style={{ gridTemplateColumns: "repeat(auto-fill,minmax(280px,1fr))" }}>
{FRAME_FIELDS.map(([num, label]) => (
<label key={num} className="stack">
<span className="muted">
{num}. {label}
</span>
<input
<ParamCombo
value={inscriptions[num] || inscriptions[String(num)] || ""}
onChange={(e) =>
setInscriptions((prev) => ({ ...prev, [num]: e.target.value }))
params={params}
disabled={busy}
onChange={(val) =>
setInscriptions((prev) => ({ ...prev, [num]: val }))
}
/>
</label>
@@ -485,6 +625,80 @@ export default function ProjectPage() {
</button>
)}
</div>
{tableSettings?.fields?.length > 0 && (
<div className="settings-panel stack">
<strong>Настройки таблицы</strong>
<p className="muted" style={{ margin: 0 }}>
Сопоставление колонок со свойствами компонентов и параметрами проекта Altium.
</p>
<div className="grid" style={{ gridTemplateColumns: "repeat(auto-fill,minmax(240px,1fr))" }}>
{tableSettings.fields.map((field) => (
<label key={field.key} className="stack">
<span className="muted">{field.label}</span>
{field.source === "project_param" ? (
<ParamCombo
value={tableSettings.column_mappings[field.key] || ""}
params={tableSettings.project_params || params}
disabled={busy}
onChange={(val) => updateColumnMapping(field.key, val)}
/>
) : field.source === "component_field" ? (
<PropertySelect
value={tableSettings.column_mappings[field.key] || ""}
properties={tableSettings.property_names || []}
disabled={busy}
onChange={(val) => updateColumnMapping(field.key, val)}
/>
) : (
<PropertySelect
value={tableSettings.column_mappings[field.key] || ""}
properties={tableSettings.property_names || []}
disabled={busy}
onChange={(val) => updateColumnMapping(field.key, val)}
/>
)}
</label>
))}
{tableType === "simple_list" && tableSettings.simple_list && (
<>
<label className="stack">
<span className="muted">Тех. запас, %</span>
<input
type="number"
min="0"
max="100"
value={tableSettings.simple_list.tech_reserve_percent ?? 10}
disabled={busy}
onChange={(e) =>
updateSimpleListSetting("tech_reserve_percent", Number(e.target.value))
}
/>
</label>
<label className="stack">
<span className="muted">Кол-во плат</span>
<input
type="number"
min="1"
value={tableSettings.simple_list.boards_count ?? 1}
disabled={busy}
onChange={(e) =>
updateSimpleListSetting("boards_count", Number(e.target.value))
}
/>
</label>
</>
)}
</div>
<div className="row">
<button disabled={busy || !settingsDirty} onClick={saveTableSettings}>
Сохранить настройки
</button>
{settingsDirty && <span className="muted">Есть несохранённые изменения</span>}
</div>
</div>
)}
<div className="table-wrap">
<table className="data">
<thead>
+6
View File
@@ -75,6 +75,12 @@ table.data tr.header-row td { font-weight: 700; text-decoration: underline; }
.tab.active { background: var(--accent); color: white; }
.row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.stack { display: flex; flex-direction: column; gap: 0.6rem; }
.settings-panel {
border: 1px dashed var(--line);
border-radius: 8px;
padding: 0.75rem;
background: #faf7f0;
}
.chat {
display: flex; flex-direction: column; gap: 0.5rem; max-height: 50vh; overflow: auto;
border: 1px solid var(--line); border-radius: 8px; padding: 0.6rem; background: #fff;