From 3c6dacb29bdc968915fbeed54ad42a7b5d7f26e1 Mon Sep 17 00:00:00 2001 From: vladtechno Date: Sun, 17 May 2026 12:21:06 +1000 Subject: [PATCH] feat(mechanic-pwa): multi-view vendor scheme, exact tap coords, per-marker photo upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace SVG sedan zone-click model with 5-view PNG scheme (top/front/back/left/right) - New MultiViewVehicleScheme: tab selector, tap-to-exact-coord marker placement, touchAction:manipulation prevents iOS double-tap zoom, marker dots at normalized x/y - New AddMarkerDialog: bottom-sheet modal with damage_type (16 opts), severity (4 opts), description, optional CameraCapture → uploadPhoto(side="free") → photo_id on submit - InspectionEditor: removes addZoneMarker mutation and zone-count machinery; wires pendingTap state → dialog open/close → query invalidation - InspectionReview: adds read-only MultiViewVehicleScheme; both pages show photo_id thumbnails via AuthImg in the marker list - humanizeMarkerSide updated for new view names; keeps zone-* legacy mapping - Deletes VehicleScheme.tsx and sedan-top.svg (no remaining imports) - No backend changes, no new dependencies Co-Authored-By: claude-flow --- mechanic-pwa/frontend/public/scheme/back.png | Bin 0 -> 15197 bytes mechanic-pwa/frontend/public/scheme/front.png | Bin 0 -> 1248 bytes mechanic-pwa/frontend/public/scheme/left.png | Bin 0 -> 1842 bytes mechanic-pwa/frontend/public/scheme/right.png | Bin 0 -> 1793 bytes mechanic-pwa/frontend/public/scheme/top.png | Bin 0 -> 1899 bytes .../components/inspection/AddMarkerDialog.tsx | 230 ++++++++++++++++++ .../vehicle-scheme/MultiViewVehicleScheme.tsx | 143 +++++++++++ .../vehicle-scheme/VehicleScheme.tsx | 110 --------- .../vehicle-scheme/schemes/sedan-top.svg | 15 -- .../frontend/src/pages/InspectionEditor.tsx | 99 ++++---- .../frontend/src/pages/InspectionReview.tsx | 94 ++++++- 11 files changed, 519 insertions(+), 172 deletions(-) create mode 100644 mechanic-pwa/frontend/public/scheme/back.png create mode 100644 mechanic-pwa/frontend/public/scheme/front.png create mode 100644 mechanic-pwa/frontend/public/scheme/left.png create mode 100644 mechanic-pwa/frontend/public/scheme/right.png create mode 100644 mechanic-pwa/frontend/public/scheme/top.png create mode 100644 mechanic-pwa/frontend/src/components/inspection/AddMarkerDialog.tsx create mode 100644 mechanic-pwa/frontend/src/components/vehicle-scheme/MultiViewVehicleScheme.tsx delete mode 100644 mechanic-pwa/frontend/src/components/vehicle-scheme/VehicleScheme.tsx delete mode 100644 mechanic-pwa/frontend/src/components/vehicle-scheme/schemes/sedan-top.svg diff --git a/mechanic-pwa/frontend/public/scheme/back.png b/mechanic-pwa/frontend/public/scheme/back.png new file mode 100644 index 0000000000000000000000000000000000000000..2fb060cc691616b9270e2f8fa2abbbc7256f1770 GIT binary patch literal 15197 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 KMNUMnLSTabz%+ya literal 0 HcmV?d00001 diff --git a/mechanic-pwa/frontend/public/scheme/left.png b/mechanic-pwa/frontend/public/scheme/left.png new file mode 100644 index 0000000000000000000000000000000000000000..a623055e5f09a814cd2d6d0d3a21e27afedae674 GIT binary patch literal 1842 zcmV-22hI42P) z<46!qc7i2vpJ3D{08W58K{qD=P5@?$;0ybU2v&0f+$R`w0^BFSoM1F3XipIE!8-4$ ztK5YeTUC<0h~`BoRI+8Ml&>-?D>LN*d50(#i@Ax332$(4&>I*S$nmr1=Vvc`9=`Y0 zcmB)EOXKeD&c|;X(#IH(41>f>PEO82@N+nv1vzsNyoYv9;-i7<)Is7k_CEIY?d|QF z^a(OY*x}(}et39z0mS67k*Fl(SOetMaKDSEr>EM^&Q4u=giH`dk|40J*p{VB1VzBd zR=dByFYWH``qD-8EnzfwGcz-5FiGo@fFzLIfM&eHSZF6(8 zB3(e=5k_qq8XDT8$#eM~iQ#;MCg+|yOjmtmWW+BN3XPMKlPhID17Obk#LwTl&cVR} z=R^<9Jod8={qV7sI!#>P5Eh3hoXgSu9nBi%wR$TOj*pLj$S*BzK&YR@5~ru99tHwU z^0H;$e3;6`PMOM{6SlvTCAiV4lioOzH2VH$KWVD{#@z;1I|4&VO`t=y9o z&{e|F=~D=-ENx7nnxp1ZAWGxay&{C`3~C#$xfpd_48URTlONxNmqAB{Kk0H3lPK25`uVJuVs`Xqu_}EG}H#gNdam$Vqro6yQX|pKh=;SC> zb3>|t3BEMVFPwnSopQOkG=Yh{v^_sgxT~Xt*^mWP@sO)&yT2OHuz$?^W8Qi5nb}v} zhF6)>&IJ)Pg@)SKi*<*5A^=R0z}Q3_NHMjKxFc)Wgty zZZ}_@!FyVdx;8w@<~86kdb+J55*opMjqkKDSIp~~bB%@h>NP6icx5cY)Y>L5KT{g{ zxDQT~U>th7p<;ji5ajI#Fc#We7FZF+Q9eCAO>J3_2(9@*XP}*>mW0u!kB^TptGrWQ zKG19U!J04pY20Y}2RaXMveXPsnc*UA@`1iYJ|IlH-x=_l{%F8Sn+ZZwD!t*!2l^UR zs6ozIVwsBygW)9XD%e)B;G1U@L2vYGxXwyDcGvGUcPz@# zev5~NBj{NLp1okEgD@CbI~nD``Iz69%;-EgpbSqX0wl~jfr*n#nkdI38+MerjTtQ8CHl;ALi6qP=Yf$*&;4rr!X^q-^VqSP@2M&3ZeieA!V$f^{A% zn0P?qLH3zs7_5lWZI(T#!D|i3ucAEk03fVj^E+D&YYJC+e9m4aK&PWQ!P3K&>}pv> zTN_#qRr%!%kbZ6q;<}BN*$~zUO_WLM64s3lHgvgk)_{TcO}9rzMv4+Yd*Q*uT+s)Z z+b*Jh5J?#8&~=-6Fi98@aqVC~f>p-4jX}2{2>>n28`q62g|pGHYvbacBn0c#bvvU` zkcJKxC~M(KGBo88wdjVt{v%;+PvV=2M%#LUZ3FY#8G_m8G!nUIB$JC$nlf{cer zOi)An=>_jJz-nbl04PD(<$UMbdfcEWG3PRdx8$86sAP(1)|oGKt#)=Mj5lDNoH&B{ zvu?cy+-)%36k~qBjw+&k3$fl_VdR4uRNGFfsCH4_DOA*E(LvLy=7QM;iAuHgn~1pi zLO7eO-tnrzgqB>BommEBt&qi<%7wjxdMCS-WFAlLWeENDJ(mUM8~pNzf`b`hxZv-T g=$g~RPD3951L~9687IJ!6951J07*qoM6N<$f{~DPfB*mh literal 0 HcmV?d00001 diff --git a/mechanic-pwa/frontend/public/scheme/right.png b/mechanic-pwa/frontend/public/scheme/right.png new file mode 100644 index 0000000000000000000000000000000000000000..66ee856abc517de20f6c3dd7ffbf9e078e23ae2d GIT binary patch literal 1793 zcmV+c2mbhpP)1ls=ch{GkqT{5wl7;*3aQaDI z1mV@&3Gd|O?j^R8ra34MKi-OuCl*)h^+R7^}v zT-cC>d+HF5gMH~CG9yj-ST|zmm}bBTuYjy` z$pFxo>k#5uR_}(6q*c3TL7tCUx6rFuSCkAueF4I5r43=U>>$*ItO!%@^dr}vqyP+= z+Q!Dl7c^I+?~Z^2^d7TxPkMmVq(Ogu$I`+X)>3~_064y8t;{h9{K)Tkk^x8_-nD=k zcPZ_Ezlz!UgR6{m5s68oDG$PK@Q2kT#^8VemZ1lH$rxi}V}9g!K1)Vmf8lJeNc^l@ zqFU%Can?Fb#|6>+CJrH|EOl5jV5I*x`VwH4e3FboV+DgV&VBJ=2rb)KuW8YeH0{@* zIfs!}P8ztSpTo0`WDp?oU)|ObdUa1DLa)DrRTn~91Y@_QsTv^qyV$btgYbs`~3VID$dc-QIF+r;+C%(2Y!VvG#U^=sJSd zRefPuxZ~rXuMtN1RUrCS`W#WhRNQt?#ckW+L#>G-?$&l7lQb*qksSoF6@9HU)fQDv z9{UAfBP#vr>3Ur7K9NirU_3q0|R4O9AHJ7>R_Nd7?-JTf_0NP zF0nC*#ax3Hq8X_`fZ_^&&z!?6KA7+v$g~w~AC4uB!Zka{B~g;4ZPuy(^aYgnrSvaC z0jpvl7s52O3Eu#XJKG0iN#m%}Z3_K8Mg2W1jddHoOUJU=9YzqT`A=NjL$hDcHm+nk zVI2Eh+zqsPbemS9qyPywJ1Itvs$ZbW)*DbOhbPHVLQo=vvU&zb+Z)X~4aueiA?&0%Ao>N7KBtxkh?;e|e{ym% zDAJSfrU`CZje0BY5Y}jHP-(;-X@bKF^3hzRAVFeD6~oYY2nEYES9$q~0RzO>ZAMP; zJ9X(Jpuc{zc2bynjI+3zE2OH_uC?*zRLdW&op%L0q{^-b0&4foZ%F~$ojxX<@@ZhY zCNYLfJJ3m&0fLK}-V7YXNh-3ONK|i>*9`u~ZA^$l z2UlePq2iAf$Hy8Olcf6_5|tZpV}h;4*(`lAXy6KhfxK)JPgPsKD$x{|3sP+=gd*k! z^s^SBEtW%H(&Fhwq=V=FUCObj(e-u!45CQ8&^87Cyx&f6O jF7W`7cifhh)!_XPzh4(wJNz;?00000NkvXXu0mjfFtIvZ literal 0 HcmV?d00001 diff --git a/mechanic-pwa/frontend/public/scheme/top.png b/mechanic-pwa/frontend/public/scheme/top.png new file mode 100644 index 0000000000000000000000000000000000000000..63d16f04198d684635824cc4f2cbc75d6e08eb55 GIT binary patch literal 1899 zcmV-x2bB1UP)ru@fQV9!AQ7cl$}2#DLJJfmp-{j=fj-YaO1Cq2d%Jt>?Jk=6%|Cm)-_7@ZvvV`w zd@~0Mg+ifFC=?2XLZMJ76bgkxp-?Ck3WY+U`2XXvx&9;u(}ga?g;A4_63$Z05suqz zeUpf14*T%gT5_05OSc`>XBwq8J6~T)dB$yL{b|p3hn%l3tM8_M5YJ(U-S^=mkLbpk z8}31{H9HvWcdwj1oC>R!c61||7Sg?qOvD#vZ$ieyHtD^bCzFSWv_;pwWO7>4_Kw@` z!ve{mKj>hyzF5`?`f3^BuKTIf2LUtJ8x1cjU2_8vJRsAPcM>efqLu8rH?19C)=WAA`G zOHg$?UR@2A$i(V$MmSWQ^>?u}}PAj+lG$4t!+}5$YhIJ(9eeiPR#&vXRI{5b9$3ej;1x8 zW26)#Y^b~WvDD$y+(;@qNCE}I$kqe836sSMI=gM310)ME!XkJ>`%ue+ZrkP%n>3#{ zN6=5Z{VfOGw#*@RXji=>=&ybF&276JqgcD@6(KBU3f#8KF)FmH7G7bQc+=9C+mb>o zkyMe-VQ#tY3#}MI2O{xM#6e0!ub^QH9BL>R!6RgmD=m$y$zhsnOQPOnlFcnChACtL z9ZkO}E|)DfK%&8sx2wGBcseC6)c|q) zV45B9;p11EykSGCJA0fNcg z6h!lcsIi_nzrpBTTitIV9l|VOe1JqGZ%KVKM>mBtUKqgHp?!GMPb`9iLRFszT_8^u zrp8JOlThPXF4&VBbSr)UmjpST2k;rgypHDp`9v(6@wlz!kNv>wxv>r36{QQA}jtd2l+h{&k3D&kz@gpBr!y?tcdybmnjCje?Ceq8Tg$+4dE{RE!Z>Dq>frrfIDw_ zC-bNbT2+mu2B>P{8xDnP3o^!5S2B;C@~wd!3ffwoEGR4p;~O?BQ!Lo?xxx1j=?~P+ zSsG+}>}gYV_Za?&!d*VG&;Y@f?^jqV9_06*PCWEs2t!E5%P?(5U)nHS1YtDU+Vx(G zxusA^()RZcn4U?B<+A2xA?JZ=^2sHalUV*^&1S9D%IR$=870K%7iQZc@HzYEu+WkcN+JosF|-R!YO)Ivd@G&XftdPVu;o%uA9?K~v8RZmHs4ASFSs7 zV$8!=0eY)B!jGJ5WV~QXC%?BGtd#MT?d}73b+VLtrPlutOyg|NGniPSsi1%Y{-VrPdlU+VLZMJ76bgkxp-?Ck l3WY+UP$(1%g+g&x@-O5-3M0x5 void; + onCreated: (marker: MarkerSummary) => void; +} + +export function AddMarkerDialog({ + open, + inspectionId, + view, + x, + y, + onClose, + onCreated, +}: Props) { + const [damageType, setDamageType] = useState("scratch"); + const [severity, setSeverity] = useState("minor"); + const [description, setDescription] = useState(""); + const [photoId, setPhotoId] = useState(undefined); + const [uploading, setUploading] = useState(false); + + async function handlePhoto(file: File) { + setUploading(true); + try { + // Marker close-ups land in the "free" slot — backend has no slot-uniqueness constraint + const result = await uploadPhoto(inspectionId, "free", 0, file); + setPhotoId(result.id); + toast.success("Фото повреждения загружено"); + } catch { + toast.error("Ошибка загрузки фото"); + } finally { + setUploading(false); + } + } + + const createMut = useMutation({ + mutationFn: (body: MarkerCreateInput) => createMarker(inspectionId, body), + onSuccess: (marker) => { + onCreated(marker); + reset(); + }, + onError: () => toast.error("Не удалось добавить метку"), + }); + + function reset() { + setDamageType("scratch"); + setSeverity("minor"); + setDescription(""); + setPhotoId(undefined); + } + + function handleClose() { + reset(); + onClose(); + } + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > +
+
+

Новая метка

+

+ {view} · {(x * 100).toFixed(1)}%, {(y * 100).toFixed(1)}% +

+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + setDescription(e.target.value)} + placeholder="Например: правый угол, длина ~10см" + /> +
+ +
+ + {photoId ? ( +
+ + +
+ ) : ( + + )} +
+ +
+ + +
+
+
+ ); +} diff --git a/mechanic-pwa/frontend/src/components/vehicle-scheme/MultiViewVehicleScheme.tsx b/mechanic-pwa/frontend/src/components/vehicle-scheme/MultiViewVehicleScheme.tsx new file mode 100644 index 0000000..5add8e4 --- /dev/null +++ b/mechanic-pwa/frontend/src/components/vehicle-scheme/MultiViewVehicleScheme.tsx @@ -0,0 +1,143 @@ +import { useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +export type ViewName = "top" | "front" | "back" | "left" | "right"; + +export interface SchemeMarker { + id: number; + side?: string | null; + x?: number | null; + y?: number | null; + severity?: string | null; + damage_type?: string | null; + resolved: boolean; +} + +interface Props { + markers: SchemeMarker[]; + onTap?: (view: ViewName, x: number, y: number) => void; + onMarkerClick?: (markerId: number) => void; + editable?: boolean; +} + +const VIEWS: { key: ViewName; label: string; src: string }[] = [ + { key: "top", label: "Сверху", src: "/scheme/top.png" }, + { key: "front", label: "Перед", src: "/scheme/front.png" }, + { key: "back", label: "Зад", src: "/scheme/back.png" }, + { key: "left", label: "Левый", src: "/scheme/left.png" }, + { key: "right", label: "Правый", src: "/scheme/right.png" }, +]; + +export function MultiViewVehicleScheme({ + markers, + onTap, + onMarkerClick, + editable = true, +}: Props) { + const [active, setActive] = useState("top"); + const imgWrapRef = useRef(null); + + const visibleMarkers = markers.filter( + (m) => m.side === active && m.x != null && m.y != null + ); + + function handleClick(e: React.MouseEvent) { + if (!editable) return; + // Don't intercept marker dot clicks — they have their own handler. + if ((e.target as HTMLElement).dataset.markerId) return; + + const el = imgWrapRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const x = (e.clientX - rect.left) / rect.width; + const y = (e.clientY - rect.top) / rect.height; + // Clamp 0..1 (defensive — touch on edges might overshoot by sub-pixel) + const clampedX = Math.max(0, Math.min(1, x)); + const clampedY = Math.max(0, Math.min(1, y)); + onTap?.(active, clampedX, clampedY); + } + + return ( +
+ {/* View tabs */} +
+ {VIEWS.map((v) => { + const count = markers.filter((m) => m.side === v.key).length; + return ( + + ); + })} +
+ + {/* Image + marker overlay */} +
+ v.key === active)!.src} + alt={active} + className="absolute inset-0 w-full h-full object-contain p-4" + style={{ imageRendering: "auto" }} + draggable={false} + /> + {visibleMarkers.map((m) => ( + + ))} +
+ + {editable && ( +

+ Тапните на машину, чтобы поставить метку повреждения. +

+ )} +
+ ); +} diff --git a/mechanic-pwa/frontend/src/components/vehicle-scheme/VehicleScheme.tsx b/mechanic-pwa/frontend/src/components/vehicle-scheme/VehicleScheme.tsx deleted file mode 100644 index 45f0368..0000000 --- a/mechanic-pwa/frontend/src/components/vehicle-scheme/VehicleScheme.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from "react"; -import sedanSrc from "./schemes/sedan-top.svg?raw"; - -interface MarkerDot { - x: number; - y: number; - severity?: string | null; -} - -interface Props { - /** Normalized 0..1 dots overlaid on the scheme. */ - markers?: MarkerDot[]; - /** Click handler — receives the zone id like "zone-front". */ - onZoneClick?: (zoneId: string) => void; - /** Per-zone count overlays (e.g. {"zone-front": 2}). Optional. */ - zoneCounts?: Record; -} - -const ZONES = [ - "zone-front", - "zone-rear", - "zone-left", - "zone-right", - "zone-hood", - "zone-trunk", - "zone-roof", -]; - -const ZONE_LABELS: Record = { - "zone-front": "Передний бампер", - "zone-rear": "Задний бампер", - "zone-left": "Левый бок", - "zone-right": "Правый бок", - "zone-hood": "Капот", - "zone-trunk": "Багажник", - "zone-roof": "Крыша", -}; - -// Approximate badge position per zone (percentage of container) -const ZONE_BADGE_POSITIONS: Record = { - "zone-front": { x: "50%", y: "10%" }, - "zone-rear": { x: "50%", y: "90%" }, - "zone-left": { x: "12%", y: "50%" }, - "zone-right": { x: "88%", y: "50%" }, - "zone-hood": { x: "50%", y: "10%" }, - "zone-trunk": { x: "50%", y: "90%" }, - "zone-roof": { x: "50%", y: "50%" }, -}; - -export function VehicleScheme({ markers = [], onZoneClick, zoneCounts }: Props) { - const [hoverZone, setHoverZone] = useState(null); - - return ( -
-
{ - const target = e.target as Element; - const id = target.getAttribute?.("id"); - if (id && ZONES.includes(id)) onZoneClick?.(id); - }} - onMouseMove={(e) => { - const target = e.target as Element; - const id = target.getAttribute?.("id"); - setHoverZone(id && ZONES.includes(id) ? id : null); - }} - onMouseLeave={() => setHoverZone(null)} - /> - - {/* Point markers (red dots) overlaid on the scheme via normalized coords */} - {markers.map((m, i) => ( -
- ))} - - {/* Zone counts as small badges */} - {zoneCounts && - Object.entries(zoneCounts).map(([zone, count]) => { - if (count <= 0) return null; - const pos = ZONE_BADGE_POSITIONS[zone]; - if (!pos) return null; - return ( -
- {count} -
- ); - })} - -
- {hoverZone ? ZONE_LABELS[hoverZone] : "Тапни по зоне, чтобы добавить метку"} -
-
- ); -} diff --git a/mechanic-pwa/frontend/src/components/vehicle-scheme/schemes/sedan-top.svg b/mechanic-pwa/frontend/src/components/vehicle-scheme/schemes/sedan-top.svg deleted file mode 100644 index 9c74c31..0000000 --- a/mechanic-pwa/frontend/src/components/vehicle-scheme/schemes/sedan-top.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - diff --git a/mechanic-pwa/frontend/src/pages/InspectionEditor.tsx b/mechanic-pwa/frontend/src/pages/InspectionEditor.tsx index 3b8e6ad..9fbea36 100644 --- a/mechanic-pwa/frontend/src/pages/InspectionEditor.tsx +++ b/mechanic-pwa/frontend/src/pages/InspectionEditor.tsx @@ -1,14 +1,16 @@ +import { useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { getInspection, patchInspection, - createMarker, type MarkerSummary, } from "@/api/inspections"; import { PhotoSlot } from "@/components/camera/PhotoSlot"; -import { VehicleScheme } from "@/components/vehicle-scheme/VehicleScheme"; +import { MultiViewVehicleScheme, type ViewName } from "@/components/vehicle-scheme/MultiViewVehicleScheme"; +import { AddMarkerDialog } from "@/components/inspection/AddMarkerDialog"; +import { AuthImg } from "@/components/AuthImg"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; @@ -51,7 +53,13 @@ const DAMAGE_TYPE_LABELS: Record = { function humanizeMarkerSide(side: string | null | undefined): string { if (!side) return "—"; - const zoneMap: Record = { + const map: Record = { + top: "Сверху", + front: "Перед", + back: "Зад", + left: "Левый", + right: "Правый", + // Legacy zone-* support (data from earlier prod) "zone-front": "Передний бампер", "zone-rear": "Задний бампер", "zone-left": "Левый бок", @@ -60,7 +68,7 @@ function humanizeMarkerSide(side: string | null | undefined): string { "zone-trunk": "Багажник", "zone-roof": "Крыша", }; - return zoneMap[side] ?? side; + return map[side] ?? side; } export default function InspectionEditor() { @@ -69,6 +77,12 @@ export default function InspectionEditor() { const navigate = useNavigate(); const qc = useQueryClient(); + const [pendingTap, setPendingTap] = useState<{ + view: ViewName; + x: number; + y: number; + } | null>(null); + const { data: ins, isLoading, isError } = useQuery({ queryKey: ["inspection", insId], queryFn: () => getInspection(insId), @@ -83,21 +97,6 @@ export default function InspectionEditor() { onError: () => toast.error("Не удалось завершить"), }); - const addZoneMarker = useMutation({ - mutationFn: (zone: string) => - createMarker(insId, { - side: zone, - damage_type: "damaged", - severity: "minor", - description: "Метка со схемы", - }), - onSuccess: () => { - void qc.invalidateQueries({ queryKey: ["inspection", insId] }); - toast.success("Метка добавлена"); - }, - onError: () => toast.error("Ошибка"), - }); - if (isLoading) return
Загружаю…
; if (isError || !ins) return
Осмотр не найден.
; @@ -106,23 +105,6 @@ export default function InspectionEditor() { const photosBySide = new Map(); for (const p of ins.photos) photosBySide.set(`${p.side}-${p.slot_index}`, p); - // Point markers (x/y populated) drawn as dots on the scheme - const dots = ins.markers - .filter((m: MarkerSummary) => m.x != null && m.y != null) - .map((m: MarkerSummary) => ({ - x: Number(m.x), - y: Number(m.y), - severity: m.severity ?? "minor", - })); - - // Zone counts (markers with side="zone-*", no x/y) - const zoneCounts: Record = {}; - for (const m of ins.markers) { - if (m.side?.startsWith("zone-")) { - zoneCounts[m.side] = (zoneCounts[m.side] ?? 0) + 1; - } - } - const editable = ins.status === "in_progress"; return ( @@ -145,16 +127,40 @@ export default function InspectionEditor() {
-

- Схема — тапни по зоне, чтобы добавить метку +

+ Схема — тапните на машину, чтобы добавить метку

- addZoneMarker.mutate(zone) : undefined} + ({ + id: m.id, + side: m.side, + x: m.x == null ? null : Number(m.x), + y: m.y == null ? null : Number(m.y), + severity: m.severity, + damage_type: m.damage_type, + resolved: m.resolved, + }))} + onTap={editable ? (view, x, y) => setPendingTap({ view, x, y }) : undefined} + editable={editable} />
+ {pendingTap && ( + setPendingTap(null)} + onCreated={() => { + setPendingTap(null); + void qc.invalidateQueries({ queryKey: ["inspection", insId] }); + toast.success("Метка добавлена"); + }} + /> + )} +

Фото

@@ -214,6 +220,15 @@ export default function InspectionEditor() { {m.description}
)} + {m.photo_id && ( +
+ +
+ )} ))} diff --git a/mechanic-pwa/frontend/src/pages/InspectionReview.tsx b/mechanic-pwa/frontend/src/pages/InspectionReview.tsx index af91fd9..e8f5bcb 100644 --- a/mechanic-pwa/frontend/src/pages/InspectionReview.tsx +++ b/mechanic-pwa/frontend/src/pages/InspectionReview.tsx @@ -2,6 +2,7 @@ import { useParams, useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { getInspection, type MarkerSummary, type PhotoSummary } from "@/api/inspections"; import { AuthImg } from "@/components/AuthImg"; +import { MultiViewVehicleScheme } from "@/components/vehicle-scheme/MultiViewVehicleScheme"; import { Card } from "@/components/ui/card"; const STATUS_LABELS: Record = { @@ -10,6 +11,52 @@ const STATUS_LABELS: Record = { cancelled: "Отменён", }; +const SEVERITY_LABELS: Record = { + cosmetic: "косметика", + minor: "лёгкое", + moderate: "среднее", + severe: "тяжёлое", +}; + +const DAMAGE_TYPE_LABELS: Record = { + damaged: "повреждено", + scratch: "царапина", + chip: "скол", + dent: "вмятина", + crack: "трещина", + scuff: "затёртость", + burn: "прожог", + stain: "пятно", + bent: "погнуто", + torn: "порвано", + cut: "порез", + puncture: "прокол", + bulge: "грыжа", + missing: "отсутствует", + removed: "снято", + "not-working": "не работает", +}; + +function humanizeMarkerSide(side: string | null | undefined): string { + if (!side) return "—"; + const map: Record = { + top: "Сверху", + front: "Перед", + back: "Зад", + left: "Левый", + right: "Правый", + // Legacy zone-* support (data from earlier prod) + "zone-front": "Передний бампер", + "zone-rear": "Задний бампер", + "zone-left": "Левый бок", + "zone-right": "Правый бок", + "zone-hood": "Капот", + "zone-trunk": "Багажник", + "zone-roof": "Крыша", + }; + return map[side] ?? side; +} + export default function InspectionReview() { const { id } = useParams<{ id: string }>(); const insId = Number(id); @@ -62,6 +109,26 @@ export default function InspectionReview() { )} + {ins.markers.length > 0 && ( + +

+ Схема повреждений +

+ ({ + id: m.id, + side: m.side, + x: m.x == null ? null : Number(m.x), + y: m.y == null ? null : Number(m.y), + severity: m.severity, + damage_type: m.damage_type, + resolved: m.resolved, + }))} + editable={false} + /> +
+ )} +

Фото ({ins.photos.length}) @@ -97,14 +164,22 @@ export default function InspectionReview() {
- {m.side ?? "—"} + + {humanizeMarkerSide(m.side)} + {" — "} - {m.damage_type ?? "—"} + + {m.damage_type + ? (DAMAGE_TYPE_LABELS[m.damage_type] ?? m.damage_type) + : "—"} + {m.severity && ( - + <> {" — "} - {m.severity} - + + {SEVERITY_LABELS[m.severity] ?? m.severity} + + )}
@@ -125,6 +200,15 @@ export default function InspectionReview() { {m.description}
)} + {m.photo_id && ( +
+ +
+ )} ))}