diff --git a/mechanic-pwa/frontend/src/App.tsx b/mechanic-pwa/frontend/src/App.tsx index de7a2fa..34bc5f4 100644 --- a/mechanic-pwa/frontend/src/App.tsx +++ b/mechanic-pwa/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { Routes, Route, Navigate } from "react-router-dom"; import { useAuth } from "@/store/auth"; import Login from "@/pages/Login"; +import ActionMenu from "@/pages/ActionMenu"; import Home from "@/pages/Home"; import VehicleCard from "@/pages/VehicleCard"; import InspectionEditor from "@/pages/InspectionEditor"; @@ -16,7 +17,8 @@ export default function App() { return ( } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/mechanic-pwa/frontend/src/api/inspections.ts b/mechanic-pwa/frontend/src/api/inspections.ts index 1b8ea5c..a921650 100644 --- a/mechanic-pwa/frontend/src/api/inspections.ts +++ b/mechanic-pwa/frontend/src/api/inspections.ts @@ -18,6 +18,7 @@ export interface InspectionCreateInput { vehicle_id: number; type: InspectionType; driver_id?: number; + mileage?: number; } export interface PhotoSummary { @@ -62,6 +63,7 @@ export interface InspectionDetail { meta: Record; photos: PhotoSummary[]; markers: MarkerSummary[]; + mileage?: number | null; } export async function createInspection( @@ -76,7 +78,7 @@ export async function getInspection(id: number): Promise { export async function patchInspection( id: number, - body: { status?: InspectionStatus; notes?: string; finished_at?: string } + body: { status?: InspectionStatus; notes?: string; finished_at?: string; mileage?: number } ): Promise { return api.patch(`inspections/${id}`, { json: body }).json(); } diff --git a/mechanic-pwa/frontend/src/api/vehicles.ts b/mechanic-pwa/frontend/src/api/vehicles.ts index 5a0766e..80fb5a7 100644 --- a/mechanic-pwa/frontend/src/api/vehicles.ts +++ b/mechanic-pwa/frontend/src/api/vehicles.ts @@ -20,6 +20,8 @@ export interface InspectionSummary { status: string; started_at: string; finished_at?: string | null; + mileage?: number | null; + photos_count?: number; } export async function listVehicles(q?: string): Promise { diff --git a/mechanic-pwa/frontend/src/pages/ActionMenu.tsx b/mechanic-pwa/frontend/src/pages/ActionMenu.tsx new file mode 100644 index 0000000..5b3a6b7 --- /dev/null +++ b/mechanic-pwa/frontend/src/pages/ActionMenu.tsx @@ -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 ( +
+
+
+

Premium Механик

+ {meQuery.data && ( +

{meQuery.data.name}

+ )} +
+ +
+ +
+

+ Что делаем? +

+
+ {tiles.map((t) => ( + +
+
{t.emoji}
+
+
+

{t.title}

+ {t.comingSoon && ( + + скоро + + )} +
+

+ {t.description} +

+
+
+
+ ))} +
+
+
+ ); +} diff --git a/mechanic-pwa/frontend/src/pages/Home.tsx b/mechanic-pwa/frontend/src/pages/Home.tsx index 33bfa62..f6be092 100644 --- a/mechanic-pwa/frontend/src/pages/Home.tsx +++ b/mechanic-pwa/frontend/src/pages/Home.tsx @@ -1,22 +1,22 @@ import { useState } from "react"; 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 { getMe } from "@/api/me"; -import { logout } from "@/api/auth"; import { Input } from "@/components/ui/input"; import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; + +const ACTION_TITLES: Record = { + inspect: "Осмотр", + repair: "Ремонт", +}; export default function Home() { const [q, setQ] = useState(""); + const [searchParams] = useSearchParams(); const navigate = useNavigate(); - const meQuery = useQuery({ - queryKey: ["me"], - queryFn: getMe, - staleTime: 60_000, - }); + const action = searchParams.get("action") || undefined; + const subtitle = action ? ACTION_TITLES[action] : undefined; const vehiclesQuery = useQuery({ queryKey: ["vehicles", q], @@ -25,29 +25,21 @@ export default function Home() { return (
-
-
-

Premium Механик

- {meQuery.data && ( -

- {meQuery.data.name} -

- )} -
- + ← Меню + +
+

+ Машины{subtitle && ` — ${subtitle}`} +

+
-

Машины

navigate(`/vehicles/${v.id}`)} + onClick={() => + navigate(`/vehicles/${v.id}${action ? `?action=${action}` : ""}`) + } >
{v.license_plate}
diff --git a/mechanic-pwa/frontend/src/pages/VehicleCard.tsx b/mechanic-pwa/frontend/src/pages/VehicleCard.tsx index 1464622..7cb34ff 100644 --- a/mechanic-pwa/frontend/src/pages/VehicleCard.tsx +++ b/mechanic-pwa/frontend/src/pages/VehicleCard.tsx @@ -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 { toast } from "sonner"; import { getVehicle } from "@/api/vehicles"; import { createInspection, type InspectionType } from "@/api/inspections"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; const INSPECTION_TYPE_LABELS: Record = { handover: "Передача", @@ -27,22 +30,34 @@ const STATUS_LABELS: Record = { export default function VehicleCard() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const vehicleId = Number(id); + const action = searchParams.get("action") || undefined; + + const [pendingType, setPendingType] = useState(null); + const [mileageInput, setMileageInput] = useState(""); + const { data: v, isLoading, isError } = useQuery({ queryKey: ["vehicle", vehicleId], queryFn: () => getVehicle(vehicleId), }); const startInspection = useMutation({ - mutationFn: (type: InspectionType) => - createInspection({ vehicle_id: vehicleId, type }), + mutationFn: (input: { type: InspectionType; mileage: number | undefined }) => + createInspection({ vehicle_id: vehicleId, type: input.type, mileage: input.mileage }), onSuccess: (inspection) => { navigate(`/inspections/${inspection.id}/edit`); }, 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) return
Загружаю…
; if (isError || !v) @@ -51,7 +66,7 @@ export default function VehicleCard() { return (
+ +
+ + +
+
+ )} ); }