feat(mechanic-pwa): action menu + vehicle list at /vehicles + mileage dialog + photo icon
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Routes, Route, Navigate } from "react-router-dom";
|
import { Routes, Route, Navigate } from "react-router-dom";
|
||||||
import { useAuth } from "@/store/auth";
|
import { useAuth } from "@/store/auth";
|
||||||
import Login from "@/pages/Login";
|
import Login from "@/pages/Login";
|
||||||
|
import ActionMenu from "@/pages/ActionMenu";
|
||||||
import Home from "@/pages/Home";
|
import Home from "@/pages/Home";
|
||||||
import VehicleCard from "@/pages/VehicleCard";
|
import VehicleCard from "@/pages/VehicleCard";
|
||||||
import InspectionEditor from "@/pages/InspectionEditor";
|
import InspectionEditor from "@/pages/InspectionEditor";
|
||||||
@@ -16,7 +17,8 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/" element={<RequireAuth><Home /></RequireAuth>} />
|
<Route path="/" element={<RequireAuth><ActionMenu /></RequireAuth>} />
|
||||||
|
<Route path="/vehicles" element={<RequireAuth><Home /></RequireAuth>} />
|
||||||
<Route path="/vehicles/:id" element={<RequireAuth><VehicleCard /></RequireAuth>} />
|
<Route path="/vehicles/:id" element={<RequireAuth><VehicleCard /></RequireAuth>} />
|
||||||
<Route path="/inspections/:id/edit" element={<RequireAuth><InspectionEditor /></RequireAuth>} />
|
<Route path="/inspections/:id/edit" element={<RequireAuth><InspectionEditor /></RequireAuth>} />
|
||||||
<Route path="/inspections/:id" element={<RequireAuth><InspectionReview /></RequireAuth>} />
|
<Route path="/inspections/:id" element={<RequireAuth><InspectionReview /></RequireAuth>} />
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface InspectionCreateInput {
|
|||||||
vehicle_id: number;
|
vehicle_id: number;
|
||||||
type: InspectionType;
|
type: InspectionType;
|
||||||
driver_id?: number;
|
driver_id?: number;
|
||||||
|
mileage?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PhotoSummary {
|
export interface PhotoSummary {
|
||||||
@@ -62,6 +63,7 @@ export interface InspectionDetail {
|
|||||||
meta: Record<string, unknown>;
|
meta: Record<string, unknown>;
|
||||||
photos: PhotoSummary[];
|
photos: PhotoSummary[];
|
||||||
markers: MarkerSummary[];
|
markers: MarkerSummary[];
|
||||||
|
mileage?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createInspection(
|
export async function createInspection(
|
||||||
@@ -76,7 +78,7 @@ export async function getInspection(id: number): Promise<InspectionDetail> {
|
|||||||
|
|
||||||
export async function patchInspection(
|
export async function patchInspection(
|
||||||
id: number,
|
id: number,
|
||||||
body: { status?: InspectionStatus; notes?: string; finished_at?: string }
|
body: { status?: InspectionStatus; notes?: string; finished_at?: string; mileage?: number }
|
||||||
): Promise<InspectionDetail> {
|
): Promise<InspectionDetail> {
|
||||||
return api.patch(`inspections/${id}`, { json: body }).json<InspectionDetail>();
|
return api.patch(`inspections/${id}`, { json: body }).json<InspectionDetail>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export interface InspectionSummary {
|
|||||||
status: string;
|
status: string;
|
||||||
started_at: string;
|
started_at: string;
|
||||||
finished_at?: string | null;
|
finished_at?: string | null;
|
||||||
|
mileage?: number | null;
|
||||||
|
photos_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listVehicles(q?: string): Promise<VehicleSummary[]> {
|
export async function listVehicles(q?: string): Promise<VehicleSummary[]> {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { getMe } from "@/api/me";
|
||||||
|
import { logout } from "@/api/auth";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface ActionTile {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
emoji: string;
|
||||||
|
onClick: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
comingSoon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ActionMenu() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const meQuery = useQuery({ queryKey: ["me"], queryFn: getMe, staleTime: 60_000 });
|
||||||
|
|
||||||
|
const tiles: ActionTile[] = [
|
||||||
|
{
|
||||||
|
key: "inspect",
|
||||||
|
title: "Осмотр",
|
||||||
|
description: "Создать или продолжить осмотр машины",
|
||||||
|
emoji: "🔍",
|
||||||
|
onClick: () => navigate("/vehicles?action=inspect"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "repair",
|
||||||
|
title: "Ремонт",
|
||||||
|
description: "Регистрация ремонта (в разработке)",
|
||||||
|
emoji: "🔧",
|
||||||
|
onClick: () => toast.info("Раздел «Ремонт» в разработке"),
|
||||||
|
comingSoon: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<header className="flex items-center justify-between p-4 border-b">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold">Premium Механик</h1>
|
||||||
|
{meQuery.data && (
|
||||||
|
<p className="text-xs text-muted-foreground">{meQuery.data.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
logout();
|
||||||
|
navigate("/login", { replace: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выйти
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="container max-w-2xl py-8 space-y-4">
|
||||||
|
<h2 className="text-lg font-semibold text-muted-foreground">
|
||||||
|
Что делаем?
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{tiles.map((t) => (
|
||||||
|
<Card
|
||||||
|
key={t.key}
|
||||||
|
onClick={t.disabled ? undefined : t.onClick}
|
||||||
|
className={
|
||||||
|
"p-5 transition-shadow " +
|
||||||
|
(t.disabled
|
||||||
|
? "opacity-50 cursor-not-allowed"
|
||||||
|
: "cursor-pointer hover:shadow-md active:shadow-sm")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-3xl leading-none">{t.emoji}</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-base">{t.title}</h3>
|
||||||
|
{t.comingSoon && (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide bg-muted text-muted-foreground rounded px-1.5 py-0.5">
|
||||||
|
скоро
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
{t.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { listVehicles } from "@/api/vehicles";
|
import { listVehicles } from "@/api/vehicles";
|
||||||
import { getMe } from "@/api/me";
|
|
||||||
import { logout } from "@/api/auth";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
const ACTION_TITLES: Record<string, string> = {
|
||||||
|
inspect: "Осмотр",
|
||||||
|
repair: "Ремонт",
|
||||||
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [q, setQ] = useState("");
|
const [q, setQ] = useState("");
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const meQuery = useQuery({
|
const action = searchParams.get("action") || undefined;
|
||||||
queryKey: ["me"],
|
const subtitle = action ? ACTION_TITLES[action] : undefined;
|
||||||
queryFn: getMe,
|
|
||||||
staleTime: 60_000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const vehiclesQuery = useQuery({
|
const vehiclesQuery = useQuery({
|
||||||
queryKey: ["vehicles", q],
|
queryKey: ["vehicles", q],
|
||||||
@@ -25,29 +25,21 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<header className="flex items-center justify-between p-4 border-b">
|
<header className="flex items-center gap-3 p-4 border-b">
|
||||||
<div>
|
<button
|
||||||
<h1 className="text-xl font-semibold">Premium Механик</h1>
|
onClick={() => navigate("/")}
|
||||||
{meQuery.data && (
|
className="text-sm text-muted-foreground hover:text-foreground"
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{meQuery.data.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
logout();
|
|
||||||
navigate("/login", { replace: true });
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Выйти
|
← Меню
|
||||||
</Button>
|
</button>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-lg font-semibold">
|
||||||
|
Машины{subtitle && ` — ${subtitle}`}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="p-4">
|
<main className="p-4">
|
||||||
<h2 className="text-lg font-semibold mb-3">Машины</h2>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Поиск по номеру или VIN"
|
placeholder="Поиск по номеру или VIN"
|
||||||
value={q}
|
value={q}
|
||||||
@@ -67,7 +59,9 @@ export default function Home() {
|
|||||||
<Card
|
<Card
|
||||||
key={v.id}
|
key={v.id}
|
||||||
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
|
className="p-4 cursor-pointer hover:shadow-md transition-shadow"
|
||||||
onClick={() => navigate(`/vehicles/${v.id}`)}
|
onClick={() =>
|
||||||
|
navigate(`/vehicles/${v.id}${action ? `?action=${action}` : ""}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="font-semibold text-lg">{v.license_plate}</div>
|
<div className="font-semibold text-lg">{v.license_plate}</div>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useState } from "react";
|
||||||
|
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { getVehicle } from "@/api/vehicles";
|
import { getVehicle } from "@/api/vehicles";
|
||||||
import { createInspection, type InspectionType } from "@/api/inspections";
|
import { createInspection, type InspectionType } from "@/api/inspections";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
const INSPECTION_TYPE_LABELS: Record<InspectionType, string> = {
|
const INSPECTION_TYPE_LABELS: Record<InspectionType, string> = {
|
||||||
handover: "Передача",
|
handover: "Передача",
|
||||||
@@ -27,22 +30,34 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
export default function VehicleCard() {
|
export default function VehicleCard() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const vehicleId = Number(id);
|
const vehicleId = Number(id);
|
||||||
|
|
||||||
|
const action = searchParams.get("action") || undefined;
|
||||||
|
|
||||||
|
const [pendingType, setPendingType] = useState<InspectionType | null>(null);
|
||||||
|
const [mileageInput, setMileageInput] = useState("");
|
||||||
|
|
||||||
const { data: v, isLoading, isError } = useQuery({
|
const { data: v, isLoading, isError } = useQuery({
|
||||||
queryKey: ["vehicle", vehicleId],
|
queryKey: ["vehicle", vehicleId],
|
||||||
queryFn: () => getVehicle(vehicleId),
|
queryFn: () => getVehicle(vehicleId),
|
||||||
});
|
});
|
||||||
|
|
||||||
const startInspection = useMutation({
|
const startInspection = useMutation({
|
||||||
mutationFn: (type: InspectionType) =>
|
mutationFn: (input: { type: InspectionType; mileage: number | undefined }) =>
|
||||||
createInspection({ vehicle_id: vehicleId, type }),
|
createInspection({ vehicle_id: vehicleId, type: input.type, mileage: input.mileage }),
|
||||||
onSuccess: (inspection) => {
|
onSuccess: (inspection) => {
|
||||||
navigate(`/inspections/${inspection.id}/edit`);
|
navigate(`/inspections/${inspection.id}/edit`);
|
||||||
},
|
},
|
||||||
onError: () => toast.error("Не удалось создать осмотр"),
|
onError: () => toast.error("Не удалось создать осмотр"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleStartClick = (type: InspectionType) => {
|
||||||
|
setPendingType(type);
|
||||||
|
const lastWithMileage = v?.recent_inspections.find((i) => i.mileage != null);
|
||||||
|
setMileageInput(lastWithMileage?.mileage ? String(lastWithMileage.mileage) : "");
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return <div className="p-8 text-muted-foreground">Загружаю…</div>;
|
return <div className="p-8 text-muted-foreground">Загружаю…</div>;
|
||||||
if (isError || !v)
|
if (isError || !v)
|
||||||
@@ -51,7 +66,7 @@ export default function VehicleCard() {
|
|||||||
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
|
||||||
onClick={() => navigate("/")}
|
onClick={() => navigate(`/vehicles${action ? `?action=${action}` : ""}`)}
|
||||||
className="text-sm text-muted-foreground hover:text-foreground"
|
className="text-sm text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
← К списку
|
← К списку
|
||||||
@@ -73,62 +88,62 @@ export default function VehicleCard() {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => startInspection.mutate("handover")}
|
onClick={() => handleStartClick("handover")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Передача
|
Передача
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => startInspection.mutate("return")}
|
onClick={() => handleStartClick("return")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Приёмка
|
Приёмка
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("periodic")}
|
onClick={() => handleStartClick("periodic")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Плановый
|
Плановый
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("ad-hoc")}
|
onClick={() => handleStartClick("ad-hoc")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Свободный
|
Свободный
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("initial")}
|
onClick={() => handleStartClick("initial")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Первичный
|
Первичный
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("seizure")}
|
onClick={() => handleStartClick("seizure")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Изъятие
|
Изъятие
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("equipment-change")}
|
onClick={() => handleStartClick("equipment-change")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Комплектация
|
Комплектация
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("pre-repair")}
|
onClick={() => handleStartClick("pre-repair")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
В ремонт
|
В ремонт
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => startInspection.mutate("post-repair")}
|
onClick={() => handleStartClick("post-repair")}
|
||||||
disabled={startInspection.isPending}
|
disabled={startInspection.isPending}
|
||||||
>
|
>
|
||||||
Из ремонта
|
Из ремонта
|
||||||
@@ -152,22 +167,108 @@ export default function VehicleCard() {
|
|||||||
className="p-3 cursor-pointer hover:shadow-md transition-shadow"
|
className="p-3 cursor-pointer hover:shadow-md transition-shadow"
|
||||||
onClick={() => navigate(`/inspections/${i.id}`)}
|
onClick={() => navigate(`/inspections/${i.id}`)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{INSPECTION_TYPE_LABELS[i.type as InspectionType] ?? i.type}
|
{INSPECTION_TYPE_LABELS[i.type as InspectionType] ?? i.type}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
{(i.photos_count ?? 0) > 0 && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
|
||||||
|
title={`Фото: ${i.photos_count}`}
|
||||||
|
>
|
||||||
|
📷 {i.photos_count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1 flex items-center gap-2 flex-wrap">
|
||||||
|
<span>{new Date(i.started_at).toLocaleString("ru-RU")}</span>
|
||||||
|
{i.mileage != null && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground/50">·</span>
|
||||||
|
<span>{i.mileage.toLocaleString("ru-RU")} км</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground shrink-0">
|
||||||
{STATUS_LABELS[i.status] ?? i.status}
|
{STATUS_LABELS[i.status] ?? i.status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground mt-1">
|
|
||||||
{new Date(i.started_at).toLocaleString("ru-RU")}
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{pendingType && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 bg-black/40 flex items-end sm:items-center justify-center p-0 sm:p-4"
|
||||||
|
onClick={() => setPendingType(null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-sm bg-card rounded-t-2xl sm:rounded-2xl shadow-xl p-5 space-y-4"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold">
|
||||||
|
{INSPECTION_TYPE_LABELS[pendingType]}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Введите пробег машины на текущий момент.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="mileage">Пробег, км</Label>
|
||||||
|
<Input
|
||||||
|
id="mileage"
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
value={mileageInput}
|
||||||
|
onChange={(e) => setMileageInput(e.target.value.replace(/\D/g, ""))}
|
||||||
|
placeholder="123456"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setPendingType(null)}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={startInspection.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
const m = mileageInput.trim() ? Number(mileageInput) : undefined;
|
||||||
|
startInspection.mutate({
|
||||||
|
type: pendingType,
|
||||||
|
mileage: m && Number.isFinite(m) ? m : undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{startInspection.isPending ? "Создаём…" : "Начать"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
startInspection.mutate({ type: pendingType, mileage: undefined });
|
||||||
|
}}
|
||||||
|
className="w-full text-xs text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
Пропустить пробег
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user