fixed complex strings
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user