From 226b1ca6c14e5bbdba9112b295300453f9bcfb58 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Wed, 29 Jul 2026 19:16:26 +1000 Subject: [PATCH] =?UTF-8?q?repairs:=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D1=81=D0=BA=D0=BB=D0=B0=D0=B4=D1=81=D0=BA?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=B0=D1=82=D0=B0=D0=BB=D0=BE=D0=B3?= =?UTF-8?q?=D0=B0=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D1=81=D0=B2?= =?UTF-8?q?=D0=BE=D0=B1=D0=BE=D0=B4=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Блок «Запчасти» — модалка-пикер по складскому каталогу (searchParts): поиск по названию/артикулу, остаток и пометка «подходит», тумблер «Показать все» (снимает фильтр применимости). Выбор шлёт part_id — бэкенд списывает деталь со склада. Свободный ввод убран. Co-Authored-By: Claude Opus 4.8 --- .../frontend/src/api/repairsCreate.ts | 31 ++- .../src/pages/repairs/CreateRepairPage.tsx | 195 ++++++++++++++---- 2 files changed, 182 insertions(+), 44 deletions(-) diff --git a/mechanic-pwa/frontend/src/api/repairsCreate.ts b/mechanic-pwa/frontend/src/api/repairsCreate.ts index 2d56f3c..1036889 100644 --- a/mechanic-pwa/frontend/src/api/repairsCreate.ts +++ b/mechanic-pwa/frontend/src/api/repairsCreate.ts @@ -29,6 +29,18 @@ export interface PartSuggestion { name: string; } +/** Позиция складского каталога для подбора детали в ремонт. */ +export interface PartCatalogItem { + id: number; + name: string; + article: string | null; + brand: string | null; + unit: string; + in_stock: number; // суммарный остаток (может быть < 0) + price_hint: number | string | null; + fits: boolean; // подходит этой машине +} + export interface PresignResponse { photo_uuid: string; extension: string; @@ -53,8 +65,9 @@ export interface CreateWorkPayload { } export interface CreatePartPayload { - name: string; + part_id: number; qty: number; + name?: string; // снимок имени (сервер возьмёт из каталога, если пусто) } export interface CreateOilChangePayload { @@ -102,6 +115,22 @@ export async function suggestParts(q: string): Promise { .json(); } +/** + * Поиск детали в складском каталоге для конкретной машины. + * По умолчанию — только подходящие этой машине + без привязки к марке; + * showAll=true снимает фильтр применимости. + */ +export async function searchParts( + q: string, + carId: number, + showAll = false, +): Promise { + const params = new URLSearchParams({ car_id: String(carId) }); + if (q.trim()) params.set("q", q.trim()); + if (showAll) params.set("all", "true"); + return api.get(`parts/search?${params.toString()}`).json(); +} + export async function presignPhoto(extension: string, photo_uuid?: string): Promise { return api .post("repairs/photos/presign", { diff --git a/mechanic-pwa/frontend/src/pages/repairs/CreateRepairPage.tsx b/mechanic-pwa/frontend/src/pages/repairs/CreateRepairPage.tsx index 54130d4..735571f 100644 --- a/mechanic-pwa/frontend/src/pages/repairs/CreateRepairPage.tsx +++ b/mechanic-pwa/frontend/src/pages/repairs/CreateRepairPage.tsx @@ -26,11 +26,12 @@ import { getCarContext, presignPhoto, searchWorks, - suggestParts, + searchParts, uploadPhotoToTmp, type CarContext, type CreatePhotoPayload, type MechWork, + type PartCatalogItem, type PresignResponse, } from "@/api/repairsCreate"; @@ -57,7 +58,10 @@ interface WorkRow { interface PartRow { rowKey: string; + part_id: number; name: string; + article: string | null; + in_stock: number; qty: number; } @@ -238,20 +242,49 @@ export default function CreateRepairPage() { // ── Parts ──────────────────────────────────────────────────────────────── - const [partsDraft, setPartsDraft] = useState<{ name: string; suggestions: string[] }>({ name: "", suggestions: [] }); + const [partsPicker, setPartsPicker] = useState<{ + open: boolean; q: string; showAll: boolean; results: PartCatalogItem[]; loading: boolean; + }>({ open: false, q: "", showAll: false, results: [], loading: false }); - const queryPartSuggestions = async (q: string) => { - setPartsDraft({ name: q, suggestions: [] }); - if (!q.trim()) return; - const list = await suggestParts(q); - setPartsDraft({ name: q, suggestions: list.map((s) => s.name) }); + const loadParts = async (q: string, showAll: boolean) => { + setPartsPicker((s) => ({ ...s, loading: true })); + try { + const list = await searchParts(q, carId, showAll); + setPartsPicker((s) => ({ ...s, results: list, loading: false })); + } catch { + setPartsPicker((s) => ({ ...s, results: [], loading: false })); + } }; - const addPart = (name: string) => { - const trimmed = name.trim(); - if (!trimmed) return; - setParts((arr) => [...arr, { rowKey: uuid4(), name: trimmed, qty: 1 }]); - setPartsDraft({ name: "", suggestions: [] }); + const openPartsPicker = () => { + setPartsPicker({ open: true, q: "", showAll: false, results: [], loading: true }); + void loadParts("", false); + }; + + const queryParts = (q: string) => { + setPartsPicker((s) => ({ ...s, q })); + void loadParts(q, partsPicker.showAll); + }; + + const togglePartsShowAll = () => { + const next = !partsPicker.showAll; + setPartsPicker((s) => ({ ...s, showAll: next })); + void loadParts(partsPicker.q, next); + }; + + const addPartFromCatalog = (item: PartCatalogItem) => { + setParts((arr) => { + // Та же деталь уже в списке — увеличиваем количество, а не плодим строку. + const existing = arr.find((p) => p.part_id === item.id); + if (existing) { + return arr.map((p) => (p.part_id === item.id ? { ...p, qty: p.qty + 1 } : p)); + } + return [ + ...arr, + { rowKey: uuid4(), part_id: item.id, name: item.name, article: item.article, in_stock: item.in_stock, qty: 1 }, + ]; + }); + setPartsPicker({ open: false, q: "", showAll: false, results: [], loading: false }); }; const removePart = (rowKey: string) => @@ -300,7 +333,7 @@ export default function CreateRepairPage() { manual_override: w.unlocked, override_comment: w.unlocked ? w.override_comment.trim() : null, })), - parts: parts.map((p) => ({ name: p.name, qty: p.qty })), + parts: parts.map((p) => ({ part_id: p.part_id, qty: p.qty, name: p.name })), photos: photoPayload, oil_changes: oilChange.enabled ? (["engine", "transmission"] as const) @@ -518,14 +551,34 @@ export default function CreateRepairPage() { )} - {/* Запчасти */} + {/* Запчасти — из складского каталога (списываются со склада) */}
- - {parts.length > 0 && ( +
+ + +
+ {parts.length === 0 ? ( +
+ Детали выбираются со склада — спишутся при сохранении +
+ ) : (
    {parts.map((p) => (
  • - {p.name} +
    +
    {p.name}
    +
    + {p.article ? `арт. ${p.article} · ` : ""} + {p.in_stock > 0 + ? `на складе: ${p.in_stock}` + : p.in_stock < 0 + ? `на складе: ${p.in_stock}` + : "нет в наличии"} +
    +
    )} -
    - queryPartSuggestions(e.target.value)} - placeholder="Например, Свеча NGK" - onKeyDown={(e) => e.key === "Enter" && addPart(partsDraft.name)} - /> - -
    - {partsDraft.suggestions.length > 0 && ( -
      - {partsDraft.suggestions.map((s) => ( -
    • - -
    • - ))} -
    - )}
{/* Замена масла */} @@ -716,6 +743,88 @@ export default function CreateRepairPage() { )} + + {/* Parts picker modal — каталог склада */} + {partsPicker.open && ( +
+
+ + queryParts(e.target.value)} + /> +
+
+ + {partsPicker.showAll ? "Все детали каталога" : "Подходящие этой машине"} + + +
+
+
    + {partsPicker.results.map((p) => ( +
  • + +
  • + ))} + {!partsPicker.loading && partsPicker.results.length === 0 && ( +
  • + {partsPicker.q + ? "Ничего не найдено. Если детали нет в каталоге — заведите карточку на складе." + : "Нет подходящих деталей. Нажмите «Показать все» или заведите карточку на складе."} +
  • + )} + {partsPicker.loading && ( +
  • Загрузка…
  • + )} +
+
+
+ )} ); }