diff --git a/mechanic-pwa/frontend/src/api/repairsFeed.ts b/mechanic-pwa/frontend/src/api/repairsFeed.ts index e2d181f..75b3c44 100644 --- a/mechanic-pwa/frontend/src/api/repairsFeed.ts +++ b/mechanic-pwa/frontend/src/api/repairsFeed.ts @@ -38,6 +38,15 @@ export async function getRepairsFeed(params: FeedParams = {}): Promise(); } +export interface RepairsMonthStats { + total: number; + count: number; +} + +export async function getRepairsMonthStats(): Promise { + return api.get("repairs/stats/month").json(); +} + // ── Детальная карточка ремонта ───────────────────────────────────────────── export interface RepairDetailWork { diff --git a/mechanic-pwa/frontend/src/pages/repairs/RepairsFeedPage.tsx b/mechanic-pwa/frontend/src/pages/repairs/RepairsFeedPage.tsx index 074d046..4fa570a 100644 --- a/mechanic-pwa/frontend/src/pages/repairs/RepairsFeedPage.tsx +++ b/mechanic-pwa/frontend/src/pages/repairs/RepairsFeedPage.tsx @@ -7,13 +7,14 @@ * * Пустая лента — плейсхолдер «В парке пока нет ремонтов» (У41). */ -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { useNavigate } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; import { ChevronLeft, Plus, Wrench } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useRepairsFeed } from "@/hooks/useRepairsFeed"; import { formatFeedTimestamp } from "@/lib/timeFormat"; -import type { FeedItem } from "@/api/repairsFeed"; +import { getRepairsMonthStats, type FeedItem } from "@/api/repairsFeed"; export default function RepairsFeedPage() { const navigate = useNavigate(); @@ -28,6 +29,17 @@ export default function RepairsFeedPage() { isFetching, } = useRepairsFeed(); + // Сумма и количество ремонтов за текущий месяц (отдельный endpoint — + // не зависит от пагинации ленты). + const monthStatsQuery = useQuery({ + queryKey: ["repairs-month-stats"], + queryFn: getRepairsMonthStats, + staleTime: 60_000, + }); + + // Группировка items по локальной дате (Asia/Vladivostok). + const grouped = useMemo(() => groupByLocalDay(items), [items]); + // IntersectionObserver для бесконечной прокрутки. const sentinelRef = useRef(null); useEffect(() => { @@ -83,16 +95,18 @@ export default function RepairsFeedPage() {
- {/* Итого слева — сумма работ загруженных ремонтов; Обновить справа. */} + {/* Итого слева — сумма работ и количество ремонтов за текущий месяц + (с 1-го числа по now). Обновить справа. */}
- Итого:{" "} + За этот месяц:{" "} - {fmtMoney(items.reduce((s, it) => s + (it.works_total || 0), 0))} ₽ + {fmtMoney(monthStatsQuery.data?.total ?? 0)} ₽ - {items.length > 0 && ( + {(monthStatsQuery.data?.count ?? 0) > 0 && ( - ({items.length} {pluralRepairs(items.length)}) + ({monthStatsQuery.data!.count}{" "} + {pluralRepairs(monthStatsQuery.data!.count)}) )}
@@ -124,9 +138,16 @@ export default function RepairsFeedPage() { {items.length > 0 && (
    - {items.map((item) => ( -
  • - handleOpen(item)} /> + {grouped.map((group) => ( +
  • + +
      + {group.items.map((item) => ( +
    • + handleOpen(item)} /> +
    • + ))} +
  • ))}
@@ -149,6 +170,71 @@ export default function RepairsFeedPage() { } +// ── Группировка по локальной дате (Asia/Vladivostok) ──────────────────────── + +interface DayGroup { + key: string; // YYYY-MM-DD (VLAT) + items: FeedItem[]; + total: number; +} + +function groupByLocalDay(items: FeedItem[]): DayGroup[] { + const map = new Map(); + for (const it of items) { + const key = toLocalDateKey(it.created_at); + let g = map.get(key); + if (!g) { + g = { key, items: [], total: 0 }; + map.set(key, g); + } + g.items.push(it); + g.total += it.works_total || 0; + } + // Items already sorted DESC by created_at, so keys naturally come in + // newest-first order from the Map. + return Array.from(map.values()); +} + +function toLocalDateKey(iso: string): string { + // ISO → date in Asia/Vladivostok (+10). + const d = new Date(iso); + // ru-RU sv-style → YYYY-MM-DD via Intl with explicit TZ. + const fmt = new Intl.DateTimeFormat("en-CA", { + timeZone: "Asia/Vladivostok", + year: "numeric", month: "2-digit", day: "2-digit", + }); + return fmt.format(d); // 2026-05-26 +} + +function formatDayLabel(key: string): string { + // key = YYYY-MM-DD → «26 мая» / «Сегодня» / «Вчера» + const today = toLocalDateKey(new Date().toISOString()); + if (key === today) return "Сегодня"; + // Yesterday in same TZ: + const now = new Date(); + const yest = new Date(now.getTime() - 24 * 3600 * 1000); + if (key === toLocalDateKey(yest.toISOString())) return "Вчера"; + const [y, m, d] = key.split("-").map(Number); + const dt = new Date(Date.UTC(y, m - 1, d)); + return new Intl.DateTimeFormat("ru-RU", { + day: "numeric", month: "long", timeZone: "UTC", + }).format(dt); +} + +function DaySeparator({ dateKey, total, count }: { dateKey: string; total: number; count: number }) { + return ( +
+ {formatDayLabel(dateKey)} +
+ + {fmtMoney(total)} ₽ + ({count} {pluralRepairs(count)}) + +
+ ); +} + + function RepairRow({ item, onOpen }: { item: FeedItem; onOpen: () => void }) { const carLine = [item.car_plate, item.car_make, item.car_model] .filter(Boolean)