inspection-review: TT-фото повреждений, счётчик и lightbox для всех маркеров
1. Photo counter теперь учитывает уникальные tt_image_id из markers, не только
ins.photos + photo_id. На осмотрах, где почти все маркеры пришли из Element
carry-over, счётчик показывал 1 вместо 15+ — теперь корректно.
2. В галерее «Фото» добавлена секция карточек для TT-image carry-over (label
«из Element»). Картинки тянутся через ttImageUrl(hash), кликабельны.
3. Lightbox state поменялся с `number | null` на дискриминированный union
{ kind: 'photo', id } | { kind: 'tt', ttId }. Полноразмерное превью
выбирает между AuthImg (наши MinIO-фото) и обычным <img> (Element).
4. Превью маркеров в секции «Метки» теперь кликабельны (раньше только грид
с основными фото открывал lightbox). Работает и для photo_id, и для
tt_image_id маркеров.
Backend-зависимость: MarkerOut.tt_image_id (Pydantic) и
MechanicDamageMarker.tt_image_id (ORM) добавлены в PremiumCRM коммитом
d405e17 на crm.pptaxi.ru — без них фронт получает tt_image_id=undefined.
This commit is contained in:
@@ -66,6 +66,8 @@ export interface InspectionDetail {
|
|||||||
photos: PhotoSummary[];
|
photos: PhotoSummary[];
|
||||||
markers: MarkerSummary[];
|
markers: MarkerSummary[];
|
||||||
mileage?: number | null;
|
mileage?: number | null;
|
||||||
|
/** ФИО механика, выполнившего осмотр (lookup users.full_name по performed_by). */
|
||||||
|
inspector_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInspection(
|
export async function createInspection(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -114,6 +115,12 @@ export default function InspectionReview() {
|
|||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lightbox: либо ID нашего фото в MinIO (через AuthImg), либо vendor TT
|
||||||
|
// image hash (через ttImageUrl).
|
||||||
|
const [preview, setPreview] = useState<
|
||||||
|
{ kind: "photo"; id: number } | { kind: "tt"; ttId: string } | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
if (isLoading) return <div className="p-8 text-muted-foreground">Загружаю…</div>;
|
if (isLoading) return <div className="p-8 text-muted-foreground">Загружаю…</div>;
|
||||||
if (isError || !ins)
|
if (isError || !ins)
|
||||||
return <div className="p-8 text-destructive">Осмотр не найден.</div>;
|
return <div className="p-8 text-destructive">Осмотр не найден.</div>;
|
||||||
@@ -122,6 +129,25 @@ export default function InspectionReview() {
|
|||||||
const photosById = new Map<number, typeof ins.photos[0]>();
|
const photosById = new Map<number, typeof ins.photos[0]>();
|
||||||
for (const p of ins.photos) photosById.set(p.id, p);
|
for (const p of ins.photos) photosById.set(p.id, p);
|
||||||
|
|
||||||
|
// Все уникальные фото осмотра: основные + photo_id из маркеров (наши локальные
|
||||||
|
// фото повреждений) + tt_image_id из маркеров (carry-over из Element). Без
|
||||||
|
// tt_image_id counter недоучитывал импортированные дефекты — видно на
|
||||||
|
// осмотрах, где почти все маркеры пришли из вендора.
|
||||||
|
const markerPhotoIds = ins.markers
|
||||||
|
.map((m: MarkerSummary) => m.photo_id)
|
||||||
|
.filter((id): id is number => id != null);
|
||||||
|
const markerTtIds = ins.markers
|
||||||
|
.map((m: MarkerSummary) => m.tt_image_id)
|
||||||
|
.filter((id): id is string => !!id);
|
||||||
|
const uniquePhotoIds = Array.from(
|
||||||
|
new Set([...ins.photos.map((p: PhotoSummary) => p.id), ...markerPhotoIds])
|
||||||
|
);
|
||||||
|
const uniqueTtIds = Array.from(new Set(markerTtIds));
|
||||||
|
const totalPhotoCount = uniquePhotoIds.length + uniqueTtIds.length;
|
||||||
|
// Дополнительные фото-карточки в галерее: photo_id маркеров, которых нет в
|
||||||
|
// основном списке (`ins.photos` — это side-photos осмотра).
|
||||||
|
const extraMarkerPhotos = markerPhotoIds.filter((id) => !photosById.has(id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background p-4 space-y-4">
|
<div className="min-h-screen bg-background p-4 space-y-4">
|
||||||
<button
|
<button
|
||||||
@@ -142,6 +168,12 @@ export default function InspectionReview() {
|
|||||||
<span className="font-semibold">Статус: </span>
|
<span className="font-semibold">Статус: </span>
|
||||||
{STATUS_LABELS[ins.status] ?? ins.status}
|
{STATUS_LABELS[ins.status] ?? ins.status}
|
||||||
</div>
|
</div>
|
||||||
|
{ins.inspector_name && (
|
||||||
|
<div>
|
||||||
|
<span className="font-semibold">Механик: </span>
|
||||||
|
{ins.inspector_name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold">Начат: </span>
|
<span className="font-semibold">Начат: </span>
|
||||||
{new Date(ins.started_at).toLocaleString("ru-RU")}
|
{new Date(ins.started_at).toLocaleString("ru-RU")}
|
||||||
@@ -179,14 +211,18 @@ export default function InspectionReview() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-semibold text-muted-foreground mb-2">
|
<h2 className="text-sm font-semibold text-muted-foreground mb-2">
|
||||||
Фото ({ins.photos.length})
|
Фото ({totalPhotoCount})
|
||||||
</h2>
|
</h2>
|
||||||
{ins.photos.length === 0 ? (
|
{totalPhotoCount === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground">Нет фото.</div>
|
<div className="text-sm text-muted-foreground">Нет фото.</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{ins.photos.map((p: PhotoSummary) => (
|
{ins.photos.map((p: PhotoSummary) => (
|
||||||
<Card key={p.id} className="overflow-hidden">
|
<Card
|
||||||
|
key={p.id}
|
||||||
|
className="overflow-hidden cursor-pointer"
|
||||||
|
onClick={() => setPreview({ kind: "photo", id: p.id })}
|
||||||
|
>
|
||||||
<AuthImg
|
<AuthImg
|
||||||
src={`photos/${p.id}/thumb`}
|
src={`photos/${p.id}/thumb`}
|
||||||
alt={p.side}
|
alt={p.side}
|
||||||
@@ -195,6 +231,37 @@ export default function InspectionReview() {
|
|||||||
<div className="text-xs text-muted-foreground p-2">{p.side}</div>
|
<div className="text-xs text-muted-foreground p-2">{p.side}</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
{/* Фото-маркеров, которых нет в основном списке (наши локальные) */}
|
||||||
|
{extraMarkerPhotos.map((id) => (
|
||||||
|
<Card
|
||||||
|
key={`m-${id}`}
|
||||||
|
className="overflow-hidden cursor-pointer"
|
||||||
|
onClick={() => setPreview({ kind: "photo", id })}
|
||||||
|
>
|
||||||
|
<AuthImg
|
||||||
|
src={`photos/${id}/thumb`}
|
||||||
|
alt="повреждение"
|
||||||
|
className="w-full aspect-[4/3] object-cover bg-muted"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground p-2">повреждение</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
{/* TT-фото carry-over дефектов из Element */}
|
||||||
|
{uniqueTtIds.map((ttId) => (
|
||||||
|
<Card
|
||||||
|
key={`tt-${ttId}`}
|
||||||
|
className="overflow-hidden cursor-pointer"
|
||||||
|
onClick={() => setPreview({ kind: "tt", ttId })}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={ttImageUrl(ttId)}
|
||||||
|
alt="повреждение (Element)"
|
||||||
|
loading="lazy"
|
||||||
|
className="w-full aspect-[4/3] object-cover bg-muted"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground p-2">из Element</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -249,7 +316,12 @@ export default function InspectionReview() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{m.photo_id ? (
|
{m.photo_id ? (
|
||||||
<div className="mt-2 max-w-[8rem]">
|
<div
|
||||||
|
className="mt-2 max-w-[8rem] cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setPreview({ kind: "photo", id: m.photo_id as number })
|
||||||
|
}
|
||||||
|
>
|
||||||
<AuthImg
|
<AuthImg
|
||||||
src={
|
src={
|
||||||
photosById.get(m.photo_id)?.annotated_key
|
photosById.get(m.photo_id)?.annotated_key
|
||||||
@@ -261,7 +333,12 @@ export default function InspectionReview() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : m.tt_image_id ? (
|
) : m.tt_image_id ? (
|
||||||
<div className="mt-2 max-w-[8rem]">
|
<div
|
||||||
|
className="mt-2 max-w-[8rem] cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setPreview({ kind: "tt", ttId: m.tt_image_id as string })
|
||||||
|
}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={ttImageUrl(m.tt_image_id)}
|
src={ttImageUrl(m.tt_image_id)}
|
||||||
alt="Фото из Element Mechanic"
|
alt="Фото из Element Mechanic"
|
||||||
@@ -295,6 +372,45 @@ export default function InspectionReview() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Lightbox preview — click photo to enlarge, click background to close.
|
||||||
|
Соответствует UX в TtStateDetail (импортированные из Element осмотры). */}
|
||||||
|
{preview != null && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black/90 flex flex-col"
|
||||||
|
onClick={() => setPreview(null)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-3">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setPreview(null);
|
||||||
|
}}
|
||||||
|
className="text-white text-2xl px-3"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex-1 flex items-center justify-center p-4"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{preview.kind === "photo" ? (
|
||||||
|
<AuthImg
|
||||||
|
src={`photos/${preview.id}`}
|
||||||
|
alt="Просмотр"
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={ttImageUrl(preview.ttId)}
|
||||||
|
alt="Просмотр (Element)"
|
||||||
|
className="max-w-full max-h-full object-contain"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user