seed vehicle trims catalog

This commit is contained in:
VPN SaaS Dev
2026-05-12 04:44:19 +09:00
parent f7a3b8be54
commit b5012ec6e7
9 changed files with 335 additions and 21 deletions

View File

@@ -0,0 +1,46 @@
"""car trims
Revision ID: 202605120002
Revises: 202605120001
Create Date: 2026-05-12
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "202605120002"
down_revision: str | None = "202605120001"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.add_column("cars", sa.Column("trim", sa.String(length=120), nullable=True))
op.create_table(
"car_trims",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("model_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=120), nullable=False),
sa.Column("body_type", sa.String(length=60), nullable=True),
sa.Column("fuel_type", sa.String(length=32), nullable=True),
sa.Column("transmission", sa.String(length=32), nullable=True),
sa.Column("drive_type", sa.String(length=32), nullable=True),
sa.Column("year_from", sa.Integer(), nullable=True),
sa.Column("year_to", sa.Integer(), nullable=True),
sa.Column("market", sa.String(length=80), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["model_id"], ["car_models.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("model_id", "name", name="uq_car_trims_model_name"),
)
op.create_index(op.f("ix_car_trims_model_id"), "car_trims", ["model_id"])
op.create_index(op.f("ix_car_trims_name"), "car_trims", ["name"])
def downgrade() -> None:
op.drop_index(op.f("ix_car_trims_name"), table_name="car_trims")
op.drop_index(op.f("ix_car_trims_model_id"), table_name="car_trims")
op.drop_table("car_trims")
op.drop_column("cars", "trim")

View File

@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db.session import get_session
from app.models.car import CarMake
from app.models.car import CarMake, CarModel
from app.schemas.car import CarMakeRead
router = APIRouter(prefix="/catalog", tags=["catalog"])
@@ -13,9 +13,13 @@ router = APIRouter(prefix="/catalog", tags=["catalog"])
@router.get("/makes", response_model=list[CarMakeRead])
async def list_makes(session: AsyncSession = Depends(get_session)) -> list[CarMake]:
result = await session.execute(
select(CarMake).options(selectinload(CarMake.models)).order_by(CarMake.name)
select(CarMake)
.options(selectinload(CarMake.models).selectinload(CarModel.trims))
.order_by(CarMake.name)
)
makes = list(result.scalars())
for make in makes:
make.models.sort(key=lambda model: model.name)
for model in make.models:
model.trims.sort(key=lambda trim: trim.name)
return makes

View File

@@ -1,4 +1,5 @@
import asyncio
import argparse
from datetime import date
from decimal import Decimal
@@ -7,25 +8,25 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.db.session import async_session_factory
from app.models.car import Car, CarMake, CarModel
from app.models.car import Car, CarMake, CarModel, CarTrim
from app.models.expense import FuelEntry, ServiceEntry, ServiceType
from app.models.user import User
from app.services.catalog_data import CAR_CATALOG
from app.services.catalog_data import CAR_CATALOG, CAR_TRIMS, COMMON_TRIMS, MAKE_COUNTRIES
MOCK_PLATE_PREFIX = "MOCK"
MOCK_CARS = [
("KIA Sportage", "KIA", "Sportage", 2021, "gasoline", 36200, Decimal("2450000")),
("Toyota Camry", "Toyota", "Camry", 2020, "gasoline", 58400, Decimal("2850000")),
("Hyundai Tucson", "Hyundai", "Tucson", 2022, "gasoline", 27100, Decimal("2750000")),
("Volkswagen Tiguan", "Volkswagen", "Tiguan", 2019, "gasoline", 73400, Decimal("2300000")),
("BMW X3", "BMW", "X3", 2021, "diesel", 48900, Decimal("4350000")),
("Mercedes GLC", "Mercedes", "GLC", 2020, "gasoline", 52200, Decimal("4500000")),
("Nissan X-Trail", "Nissan", "X-Trail", 2018, "gasoline", 91400, Decimal("1850000")),
("Skoda Octavia", "Skoda", "Octavia", 2021, "gasoline", 46800, Decimal("2050000")),
("Tesla Model 3", "Tesla", "Model 3", 2022, "electric", 33800, Decimal("3900000")),
("Haval Jolion", "Haval", "Jolion", 2023, "gasoline", 19600, Decimal("2150000")),
("KIA Sportage", "KIA", "Sportage", "GT-Line 1.6T DCT AWD", 2021, "gasoline", 36200, Decimal("2450000")),
("Toyota Camry", "Toyota", "Camry", "Comfort 2.5 AT", 2020, "gasoline", 58400, Decimal("2850000")),
("Hyundai Tucson", "Hyundai", "Tucson", "Prestige 2.0 AT AWD", 2022, "gasoline", 27100, Decimal("2750000")),
("Volkswagen Tiguan", "Volkswagen", "Tiguan", "Status 2.0 TSI DSG 4Motion", 2019, "gasoline", 73400, Decimal("2300000")),
("BMW X3", "BMW", "X3", "20d xDrive", 2021, "diesel", 48900, Decimal("4350000")),
("Mercedes GLC", "Mercedes", "GLC", "GLC 300 4MATIC AMG Line", 2020, "gasoline", 52200, Decimal("4500000")),
("Nissan X-Trail", "Nissan", "X-Trail", "Premium", 2018, "gasoline", 91400, Decimal("1850000")),
("Skoda Octavia", "Skoda", "Octavia", "Comfort", 2021, "gasoline", 46800, Decimal("2050000")),
("Tesla Model 3", "Tesla", "Model 3", "Long Range AWD", 2022, "electric", 33800, Decimal("3900000")),
("Haval Jolion", "Haval", "Jolion", "Premium", 2023, "gasoline", 19600, Decimal("2150000")),
]
@@ -43,15 +44,40 @@ async def seed_catalog(session: AsyncSession) -> None:
for make_name, model_names in CAR_CATALOG.items():
make = existing.get(make_name)
if make is None:
make = CarMake(name=make_name)
make = CarMake(name=make_name, country=MAKE_COUNTRIES.get(make_name))
session.add(make)
await session.flush()
existing_models = set()
else:
if not make.country and MAKE_COUNTRIES.get(make_name):
make.country = MAKE_COUNTRIES[make_name]
existing_models = {model.name for model in make.models}
for model_name in model_names:
if model_name not in existing_models:
session.add(CarModel(make_id=make.id, name=model_name))
await session.flush()
await seed_trims(session)
async def seed_trims(session: AsyncSession) -> None:
result = await session.execute(
select(CarMake).options(selectinload(CarMake.models).selectinload(CarModel.trims))
)
makes = {make.name: make for make in result.scalars()}
for make in makes.values():
for model in make.models:
trim_rows = CAR_TRIMS.get((make.name, model.name)) or [
{**item, "body_type": infer_body_type(model.name)} for item in COMMON_TRIMS
]
existing = {trim.name for trim in model.trims}
for row in trim_rows:
if row["name"] not in existing:
session.add(CarTrim(model_id=model.id, **row))
def infer_body_type(model_name: str) -> str:
suv_markers = ("Q", "X", "CX", "CR-V", "RAV", "Tiguan", "Tucson", "Sportage", "Jolion")
return "SUV" if any(marker in model_name for marker in suv_markers) else "sedan"
async def pick_owner(session: AsyncSession) -> User:
@@ -82,12 +108,16 @@ async def seed_mock_usage(session: AsyncSession, owner: User) -> None:
await clear_previous_mock(session)
today = date.today()
for index, (name, make, model, year, fuel_type, start_odo, price) in enumerate(MOCK_CARS, start=1):
for index, (name, make, model, trim, year, fuel_type, start_odo, price) in enumerate(
MOCK_CARS,
start=1,
):
car = Car(
owner_id=owner.id,
name=name,
make=make,
model=model,
trim=trim,
year=year,
plate_number=f"{MOCK_PLATE_PREFIX}-{index:02d}",
fuel_type=fuel_type,
@@ -201,9 +231,13 @@ async def seed_mock_usage(session: AsyncSession, owner: User) -> None:
car.current_odometer = odometer
async def main() -> None:
async def main(catalog_only: bool = False) -> None:
async with async_session_factory() as session:
await seed_catalog(session)
if catalog_only:
await session.commit()
print("Seeded vehicle catalog")
return
owner = await pick_owner(session)
await seed_mock_usage(session, owner)
await session.commit()
@@ -211,4 +245,7 @@ async def main() -> None:
if __name__ == "__main__":
asyncio.run(main())
parser = argparse.ArgumentParser()
parser.add_argument("--catalog-only", action="store_true")
args = parser.parse_args()
asyncio.run(main(catalog_only=args.catalog_only))

View File

@@ -1,7 +1,7 @@
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Date, DateTime, ForeignKey, Numeric, String, UniqueConstraint, func
from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
@@ -15,6 +15,7 @@ class Car(Base):
name: Mapped[str] = mapped_column(String(160))
make: Mapped[str | None] = mapped_column(String(80))
model: Mapped[str | None] = mapped_column(String(80))
trim: Mapped[str | None] = mapped_column(String(120))
year: Mapped[int | None]
plate_number: Mapped[str | None] = mapped_column(String(32))
vin: Mapped[str | None] = mapped_column(String(32))
@@ -53,3 +54,23 @@ class CarModel(Base):
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
make = relationship("CarMake", back_populates="models")
trims = relationship("CarTrim", back_populates="model", cascade="all, delete-orphan")
class CarTrim(Base):
__tablename__ = "car_trims"
__table_args__ = (UniqueConstraint("model_id", "name", name="uq_car_trims_model_name"),)
id: Mapped[int] = mapped_column(primary_key=True)
model_id: Mapped[int] = mapped_column(ForeignKey("car_models.id", ondelete="CASCADE"), index=True)
name: Mapped[str] = mapped_column(String(120), index=True)
body_type: Mapped[str | None] = mapped_column(String(60))
fuel_type: Mapped[str | None] = mapped_column(String(32))
transmission: Mapped[str | None] = mapped_column(String(32))
drive_type: Mapped[str | None] = mapped_column(String(32))
year_from: Mapped[int | None] = mapped_column(Integer)
year_to: Mapped[int | None] = mapped_column(Integer)
market: Mapped[str | None] = mapped_column(String(80))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
model = relationship("CarModel", back_populates="trims")

View File

@@ -8,6 +8,7 @@ class CarBase(BaseModel):
name: str
make: str | None = None
model: str | None = None
trim: str | None = None
year: int | None = None
plate_number: str | None = None
vin: str | None = None
@@ -25,6 +26,7 @@ class CarUpdate(BaseModel):
name: str | None = None
make: str | None = None
model: str | None = None
trim: str | None = None
year: int | None = None
plate_number: str | None = None
vin: str | None = None
@@ -42,9 +44,24 @@ class CarRead(CarBase):
model_config = ConfigDict(from_attributes=True)
class CarTrimRead(BaseModel):
id: int
name: str
body_type: str | None = None
fuel_type: str | None = None
transmission: str | None = None
drive_type: str | None = None
year_from: int | None = None
year_to: int | None = None
market: str | None = None
model_config = ConfigDict(from_attributes=True)
class CarModelRead(BaseModel):
id: int
name: str
trims: list[CarTrimRead] = []
model_config = ConfigDict(from_attributes=True)

View File

@@ -55,3 +55,103 @@ CAR_CATALOG: dict[str, list[str]] = {
"Zeekr": ["001", "007", "009", "X"],
"УАЗ": ["Буханка", "Патриот", "Пикап", "Хантер"],
}
MAKE_COUNTRIES: dict[str, str] = {
"BMW": "Germany",
"Mercedes": "Germany",
"Volkswagen": "Germany",
"Audi": "Germany",
"Porsche": "Germany",
"Skoda": "Czech Republic",
"Toyota": "Japan",
"Lexus": "Japan",
"Nissan": "Japan",
"Honda": "Japan",
"Mazda": "Japan",
"Subaru": "Japan",
"Mitsubishi": "Japan",
"Hyundai": "South Korea",
"KIA": "South Korea",
"Genesis": "South Korea",
"Tesla": "USA",
"Ford": "USA",
"Chevrolet": "USA",
"Haval": "China",
"Chery": "China",
"Geely": "China",
"BYD": "China",
"LADA": "Russia",
"УАЗ": "Russia",
}
COMMON_TRIMS = [
{
"name": "Base",
"body_type": None,
"fuel_type": "gasoline",
"transmission": "AT",
"drive_type": "FWD",
"market": "Global",
},
{
"name": "Comfort",
"body_type": None,
"fuel_type": "gasoline",
"transmission": "AT",
"drive_type": "FWD",
"market": "Global",
},
{
"name": "Premium",
"body_type": None,
"fuel_type": "gasoline",
"transmission": "AT",
"drive_type": "AWD",
"market": "Global",
},
]
CAR_TRIMS: dict[tuple[str, str], list[dict[str, str | int | None]]] = {
("Toyota", "Camry"): [
{"name": "Standard 2.0 AT", "body_type": "sedan", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
{"name": "Comfort 2.5 AT", "body_type": "sedan", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
{"name": "Prestige 2.5 AT", "body_type": "sedan", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
{"name": "Hybrid 2.5 e-CVT", "body_type": "sedan", "fuel_type": "hybrid", "transmission": "e-CVT", "drive_type": "FWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
],
("Toyota", "RAV4"): [
{"name": "Comfort 2.0 CVT", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "CVT", "drive_type": "FWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
{"name": "Style 2.0 CVT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "CVT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
{"name": "Hybrid 2.5 e-CVT AWD", "body_type": "SUV", "fuel_type": "hybrid", "transmission": "e-CVT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
],
("KIA", "Sportage"): [
{"name": "Classic 2.0 AT", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
{"name": "Comfort 2.0 AT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
{"name": "Prestige 2.0 Diesel AWD", "body_type": "SUV", "fuel_type": "diesel", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2024, "market": "Global"},
{"name": "GT-Line 1.6T DCT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DCT", "drive_type": "AWD", "year_from": 2021, "year_to": 2025, "market": "Global"},
],
("Hyundai", "Tucson"): [
{"name": "Lifestyle 2.0 AT", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "FWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
{"name": "Prestige 2.0 AT AWD", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
{"name": "Hybrid 1.6T AWD", "body_type": "SUV", "fuel_type": "hybrid", "transmission": "AT", "drive_type": "AWD", "year_from": 2021, "year_to": 2025, "market": "Global"},
],
("Volkswagen", "Tiguan"): [
{"name": "Respect 1.4 TSI DSG", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DSG", "drive_type": "FWD", "year_from": 2017, "year_to": 2024, "market": "Global"},
{"name": "Status 2.0 TSI DSG 4Motion", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DSG", "drive_type": "AWD", "year_from": 2017, "year_to": 2024, "market": "Global"},
{"name": "R-Line 2.0 TSI DSG 4Motion", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "DSG", "drive_type": "AWD", "year_from": 2017, "year_to": 2024, "market": "Global"},
],
("BMW", "X3"): [
{"name": "20i xDrive", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
{"name": "20d xDrive", "body_type": "SUV", "fuel_type": "diesel", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
{"name": "30i xDrive M Sport", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2018, "year_to": 2025, "market": "Global"},
],
("Mercedes", "GLC"): [
{"name": "GLC 200 4MATIC", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
{"name": "GLC 220d 4MATIC", "body_type": "SUV", "fuel_type": "diesel", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
{"name": "GLC 300 4MATIC AMG Line", "body_type": "SUV", "fuel_type": "gasoline", "transmission": "AT", "drive_type": "AWD", "year_from": 2019, "year_to": 2025, "market": "Global"},
],
("Tesla", "Model 3"): [
{"name": "Rear-Wheel Drive", "body_type": "sedan", "fuel_type": "electric", "transmission": "single-speed", "drive_type": "RWD", "year_from": 2019, "year_to": 2026, "market": "Global"},
{"name": "Long Range AWD", "body_type": "sedan", "fuel_type": "electric", "transmission": "single-speed", "drive_type": "AWD", "year_from": 2019, "year_to": 2026, "market": "Global"},
{"name": "Performance AWD", "body_type": "sedan", "fuel_type": "electric", "transmission": "single-speed", "drive_type": "AWD", "year_from": 2019, "year_to": 2026, "market": "Global"},
],
}

View File

@@ -266,10 +266,28 @@
Модель
<select name="model" id="modelSelect" required></select>
</label>
<label>
Комплектация
<select name="trim" id="trimSelect"></select>
</label>
<div class="catalog-preview" id="catalogPreview">
<strong>Выбери модель</strong>
<span>Покажем кузов, топливо, привод и годы выпуска.</span>
</div>
<label>
Год
<input name="year" type="number" min="1900" max="2100" />
</label>
<label>
Тип топлива
<select name="fuel_type" id="fuelTypeSelect">
<option value="">Авто</option>
<option value="gasoline">Бензин</option>
<option value="diesel">Дизель</option>
<option value="hybrid">Гибрид</option>
<option value="electric">Электро</option>
</select>
</label>
<button type="submit">Добавить авто</button>
</form>
</section>

View File

@@ -614,11 +614,50 @@ async function loadCatalog() {
function initCarCatalog() {
const makeSelect = document.querySelector("#makeSelect");
const modelSelect = document.querySelector("#modelSelect");
const trimSelect = document.querySelector("#trimSelect");
const fuelTypeSelect = document.querySelector("#fuelTypeSelect");
const preview = document.querySelector("#catalogPreview");
const makes = [...state.catalog].sort((a, b) => a.name.localeCompare(b.name, "ru"));
makeSelect.innerHTML = `<option value="">${t("Выбери марку")}</option>` + makes
.map((make) => `<option value="${make.name}">${make.name}</option>`)
.join("");
function selectedModel() {
const make = state.catalog.find((item) => item.name === makeSelect.value);
return make?.models.find((model) => model.name === modelSelect.value) || null;
}
function syncPreview() {
const model = selectedModel();
const trim = model?.trims?.find((item) => item.name === trimSelect.value);
if (!model) {
preview.innerHTML = `<strong>${t("Выбери модель")}</strong><span>Покажем кузов, топливо, привод и годы выпуска.</span>`;
return;
}
const chips = [
trim?.body_type,
trim?.fuel_type,
trim?.transmission,
trim?.drive_type,
trim?.year_from && trim?.year_to ? `${trim.year_from}-${trim.year_to}` : null,
].filter(Boolean);
preview.innerHTML = `
<strong>${makeSelect.value} ${model.name}${trim ? ` · ${trim.name}` : ""}</strong>
<span>${chips.length ? chips.join(" · ") : "Базовые параметры можно уточнить позже"}</span>
`;
if (trim?.fuel_type && !fuelTypeSelect.value) fuelTypeSelect.value = trim.fuel_type;
}
function syncTrims() {
const model = selectedModel();
const trims = model?.trims || [];
trimSelect.disabled = !trims.length;
trimSelect.innerHTML = trims.length
? `<option value="">Комплектация не выбрана</option>` + trims.map((trim) => `<option value="${trim.name}">${trim.name}</option>`).join("")
: `<option value="">Сначала модель</option>`;
syncPreview();
}
function syncModels() {
const make = makeSelect.value;
const models = state.catalog.find((item) => item.name === make)?.models || [];
@@ -626,24 +665,32 @@ function initCarCatalog() {
modelSelect.innerHTML = models.length
? `<option value="">${t("Выбери модель")}</option>` + models.map((model) => `<option value="${model.name}">${model.name}</option>`).join("")
: `<option value="">${t("Сначала марка")}</option>`;
syncTrims();
}
makeSelect.addEventListener("change", syncModels);
modelSelect.addEventListener("change", syncTrims);
trimSelect.addEventListener("change", syncPreview);
syncModels();
}
function resetCarCatalog() {
document.querySelector("#makeSelect").value = "";
const modelSelect = document.querySelector("#modelSelect");
const trimSelect = document.querySelector("#trimSelect");
modelSelect.disabled = true;
modelSelect.innerHTML = `<option value="">${t("Сначала марка")}</option>`;
trimSelect.disabled = true;
trimSelect.innerHTML = `<option value="">Сначала модель</option>`;
document.querySelector("#catalogPreview").innerHTML =
`<strong>${t("Выбери модель")}</strong><span>Покажем кузов, топливо, привод и годы выпуска.</span>`;
}
function updateHero(stats) {
const car = selectedCar();
document.querySelector("#selectedCarTitle").textContent = car?.name || t("Не выбран");
document.querySelector("#selectedCarMeta").textContent = car
? [car.make, car.model, car.year].filter(Boolean).join(" ") || t("Без деталей")
? [car.make, car.model, car.trim, car.year].filter(Boolean).join(" ") || t("Без деталей")
: t("Добавь авто или выбери из списка");
document.querySelector("#summaryTotal").textContent = money(stats?.total_cost);
document.querySelector("#summaryConsumption").textContent = stats?.avg_consumption_l_per_100km
@@ -670,7 +717,7 @@ function renderCars() {
<span class="car-badge">${(car.make || car.name).slice(0, 2).toUpperCase()}</span>
<span>
<strong>${car.name}</strong>
<small>${[car.make, car.model, car.year].filter(Boolean).join(" ") || t("Без деталей")}</small>
<small>${[car.make, car.model, car.trim, car.year].filter(Boolean).join(" ") || t("Без деталей")}</small>
</span>
</button>
`,
@@ -1084,7 +1131,9 @@ document.querySelector("#carForm").addEventListener("submit", async (event) => {
name: data.name,
make: data.make || null,
model: data.model || null,
trim: data.trim || null,
year: data.year ? Number(data.year) : null,
fuel_type: data.fuel_type || null,
}),
});
form.reset();

View File

@@ -1199,6 +1199,28 @@ select {
gap: 10px;
}
.catalog-preview {
align-self: stretch;
display: grid;
gap: 6px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background:
linear-gradient(135deg, rgba(18, 115, 95, 0.08), rgba(47, 111, 159, 0.08)),
#fff;
}
.catalog-preview strong {
color: var(--text);
font-size: 14px;
}
.catalog-preview span {
color: var(--muted);
font-size: 12px;
}
@keyframes toastIn {
from {
opacity: 0;