Files
mechanic-pwa/mechanic-pwa/frontend/src/pages/ActionMenu.tsx
T
tremble7681andclaude-flow a5bd7308b5 feat(sto): activate Repair section in PWA (PR-4..PR-7)
Активация раздела «Ремонт» в Premium Мехник PWA: лента + создание ремонта,
фоновая сверка прав (У35), различающие сообщения при отказе входа (У36).

Backend для всех новых API в taxi-dashboard (отдельный commit eccaebe).
Тулится на `ENABLE_STO_MODULE=true` в backend env.

PR-4: Login + useMeSync.
- src/api/me.ts: тип Me расширен (is_admin, can_login_pwa, departments,
  support — для У35/У36).
- src/hooks/useMeSync.ts: фоновая сверка `/me` раз в 5 мин (У35). При
  403 NO_PWA_ACCESS — сброс токена, revocation info в sessionStorage.
  IndexedDB-черновики не трогаются.
- src/pages/Login.tsx: после успешного login делает getMe() и при 403
  показывает баннер «У вас нет доступа» с кликабельным TG/телефонным
  контактом (У36). Баннер также рендерится при revocation после
  фоновой сверки.
- src/App.tsx: useMeSync() подключён в корне.

PR-5: лента ремонтов.
- src/pages/ActionMenu.tsx: снят `comingSoon` с тайла «Ремонт».
- src/pages/repairs/RepairsFeedPage.tsx: лента с infinite scroll (через
  IntersectionObserver), плейсхолдер «В парке пока нет ремонтов» (У41),
  ellipsis-имена работ + плюрализация «+N работ» (У40).
- src/hooks/useRepairsFeed.ts: useInfiniteQuery + stale-while-revalidate
  + visibility refresh (У38).
- src/lib/timeFormat.ts: формат «Сегодня / Вчера / 23 мая» (У39).
- src/api/repairsFeed.ts: GET /api/v1/mechanic/repairs клиент.

PR-7: создание ремонта (online-only).
- src/api/repairsCreate.ts: searchCars, getCarContext, searchWorks,
  suggestParts, presignPhoto + uploadPhotoToTmp (PUT с правильным
  Content-Type), createRepair (POST с Idempotency-Key).
- src/pages/repairs/CarSelectPage.tsx: поиск машины с debounce 200мс.
- src/pages/repairs/CreateRepairPage.tsx: карточка создания ремонта с 5
  inline-блоками (Шапка / Пробег / Фото / Работы / Запчасти / Замена
  масла). UUID v4 как Idempotency-Key. Карандашик-разблокировка цены
  с обязательным комментарием (У19). Мягкая подписка пробега масла
  на шапочный (У24). Дефолт next_in_km=10000 (У25). Полноэкранная
  модалка поиска работ.

Routes: /repairs, /repairs/new, /repairs/new/:carId.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 00:16:44 +10:00

99 lines
3.1 KiB
TypeScript

import { useNavigate } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
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: () => navigate("/repairs"),
},
];
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>
);
}