base commit

This commit is contained in:
@kirill.komarov
2026-04-30 22:34:55 +05:00
parent 123d86091d
commit 9139a24093
46 changed files with 2023 additions and 153 deletions
@@ -3,6 +3,7 @@ import { apiClient } from '@/shared/api/client'
export type AdminOrderListItem = {
id: string
status: string
deliveryType: 'delivery' | 'pickup'
totalCents: number
currency: string
createdAt: string
@@ -22,9 +23,12 @@ export type AdminOrderDetailResponse = {
item: {
id: string
status: string
deliveryType: 'delivery' | 'pickup'
itemsSubtotalCents: number
deliveryFeeCents: number
totalCents: number
currency: string
addressSnapshotJson: string
addressSnapshotJson: string | null
comment: string | null
createdAt: string
updatedAt: string
@@ -45,9 +49,16 @@ export type AdminOrderDetailResponse = {
}
}
export async function fetchAdminOrdersSummary(token: string): Promise<{ attentionCount: number }> {
const { data } = await apiClient.get<{ attentionCount: number }>('admin/orders/summary', {
headers: { Authorization: `Bearer ${token}` },
})
return data
}
export async function fetchAdminOrders(
token: string,
params?: { status?: string; q?: string; page?: number; pageSize?: number },
params?: { status?: string; deliveryType?: 'delivery' | 'pickup'; q?: string; page?: number; pageSize?: number },
): Promise<AdminOrdersListResponse> {
const { data } = await apiClient.get<AdminOrdersListResponse>('admin/orders', {
params,
+29 -4
View File
@@ -16,9 +16,12 @@ export type OrderDetailResponse = {
item: {
id: string
status: string
deliveryType: 'delivery' | 'pickup'
itemsSubtotalCents: number
deliveryFeeCents: number
totalCents: number
currency: string
addressSnapshotJson: string
addressSnapshotJson: string | null
comment: string | null
createdAt: string
updatedAt: string
@@ -38,7 +41,11 @@ export type OrderDetailResponse = {
}
}
export async function createOrder(body: { addressId: string; comment?: string | null }): Promise<{ orderId: string }> {
export async function createOrder(body: {
deliveryType: 'delivery' | 'pickup'
addressId?: string | null
comment?: string | null
}): Promise<{ orderId: string }> {
const { data } = await apiClient.post<{ orderId: string }>('me/orders', body)
return data
}
@@ -57,6 +64,24 @@ export async function postOrderMessage(id: string, text: string): Promise<void>
await apiClient.post(`me/orders/${id}/messages`, { text })
}
export async function payOrderStub(id: string): Promise<void> {
await apiClient.post(`me/orders/${id}/pay`)
export async function payOrderStub(id: string): Promise<{ ok: boolean; status: string }> {
const { data } = await apiClient.post<{ ok: boolean; status: string }>(`me/orders/${id}/pay`)
return data
}
export async function confirmOrderReceived(id: string): Promise<{ ok: boolean; status: string }> {
const { data } = await apiClient.post<{ ok: boolean; status: string }>(`me/orders/${id}/confirm-received`)
return data
}
export type ReviewEligibilityItem = { productId: string; title: string; hasReview: boolean }
export async function fetchOrderReviewEligibility(orderId: string): Promise<{
canReview: boolean
items: ReviewEligibilityItem[]
}> {
const { data } = await apiClient.get<{ canReview: boolean; items: ReviewEligibilityItem[] }>(
`me/orders/${orderId}/review-eligibility`,
)
return data
}
@@ -12,6 +12,7 @@ export async function fetchPublicProducts(params?: {
categorySlug?: string
q?: string
sort?: 'price_asc' | 'price_desc' | ''
availability?: 'all' | 'in_stock' | 'made_to_order'
page?: number
pageSize?: number
priceMinCents?: number
@@ -22,6 +23,7 @@ export async function fetchPublicProducts(params?: {
categorySlug: params?.categorySlug || undefined,
q: params?.q || undefined,
sort: params?.sort || undefined,
availability: params?.availability || undefined,
page: params?.page || undefined,
pageSize: params?.pageSize || undefined,
priceMin: params?.priceMinCents ?? undefined,
@@ -0,0 +1,54 @@
import { apiClient } from '@/shared/api/client'
export async function postProductReview(
productId: string,
body: { rating: number; text?: string | null },
): Promise<void> {
await apiClient.post(`products/${productId}/reviews`, body)
}
export type PublicReviewFeedItem = {
id: string
rating: number
text: string | null
createdAt: string
authorDisplay: string
productId: string
productTitle: string
}
export type PublicReviewsLatestResponse = {
items: PublicReviewFeedItem[]
}
export async function fetchLatestApprovedReviews(limit = 5): Promise<PublicReviewsLatestResponse> {
const { data } = await apiClient.get<PublicReviewsLatestResponse>('reviews/latest', {
params: { limit },
})
return data
}
export type PublicProductReviewItem = {
id: string
rating: number
text: string | null
createdAt: string
authorDisplay: string
}
export type PublicProductReviewsResponse = {
items: PublicProductReviewItem[]
total: number
page: number
pageSize: number
}
export async function fetchPublicProductReviews(
productId: string,
params?: { page?: number; pageSize?: number },
): Promise<PublicProductReviewsResponse> {
const { data } = await apiClient.get<PublicProductReviewsResponse>(`products/${productId}/reviews`, {
params: { page: params?.page, pageSize: params?.pageSize },
})
return data
}
@@ -5,6 +5,12 @@ export type Category = {
sort: number
}
export type ProductReviewsSummary = {
approvedReviewCount: number
avgRating: number | null
latestApprovedText: string | null
}
export type Product = {
id: string
title: string
@@ -24,4 +30,6 @@ export type Product = {
updatedAt: string
category?: Category
images?: { id: string; url: string; sort: number }[]
/** Для опубликованных товаров с публичного API. */
reviewsSummary?: ProductReviewsSummary | null
}
@@ -6,6 +6,7 @@ import CardContent from '@mui/material/CardContent'
import CardMedia from '@mui/material/CardMedia'
import Chip from '@mui/material/Chip'
import Link from '@mui/material/Link'
import Rating from '@mui/material/Rating'
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import { Link as RouterLink } from 'react-router-dom'
@@ -13,6 +14,7 @@ import { Swiper, SwiperSlide } from 'swiper/react'
import 'swiper/css'
import type { Product } from '@/entities/product/model/types'
import { formatPriceRub } from '@/shared/lib/format-price'
import { reviewsCountRu } from '@/shared/lib/reviews-count-ru'
import type { Swiper as SwiperType } from 'swiper/types'
type Props = { product: Product; mediaHeight?: number; actions?: ReactNode }
@@ -117,6 +119,7 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
<CardContent sx={{ flexGrow: 1 }}>
<Stack spacing={1}>
{product.category && <Chip label={product.category.name} size="small" />}
{product.inStock && product.quantity === 0 && <Chip label="Нет в наличии" size="small" color="default" />}
{!product.inStock && (
<Chip
label={`Под заказ · ${product.leadTimeDays ?? '—'} дн.`}
@@ -157,6 +160,38 @@ export function ProductCard({ product, mediaHeight = 200, actions }: Props) {
<Typography variant="h6" color="primary">
{formatPriceRub(product.priceCents)}
</Typography>
{product.reviewsSummary && product.reviewsSummary.approvedReviewCount > 0 && (
<Stack spacing={0.5}>
<Stack direction="row" spacing={0.75} sx={{ alignItems: 'center' }}>
<Rating
size="small"
value={product.reviewsSummary.avgRating ?? 0}
precision={0.25}
readOnly
sx={{ color: 'warning.main', fontSize: 18 }}
/>
<Typography variant="caption" color="text.secondary">
{reviewsCountRu(product.reviewsSummary.approvedReviewCount)}
</Typography>
</Stack>
{product.reviewsSummary.latestApprovedText ? (
<Typography
variant="caption"
color="text.secondary"
sx={{
fontStyle: 'italic',
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
title={product.reviewsSummary.latestApprovedText}
>
«{product.reviewsSummary.latestApprovedText}»
</Typography>
) : null}
</Stack>
)}
{actions}
</Stack>
</CardContent>
@@ -0,0 +1,24 @@
import { apiClient } from '@/shared/api/client'
export async function fetchUnreadMessageCount(): Promise<{ count: number }> {
const { data } = await apiClient.get<{ count: number }>('me/messages/unread-count')
return data
}
export async function markOrderMessagesRead(orderId: string): Promise<void> {
await apiClient.post(`me/orders/${orderId}/messages/read`)
}
export type ConversationSummary = {
orderId: string
status: string
deliveryType: 'delivery' | 'pickup'
lastMessageAt: string
preview: string
unreadCount: number
}
export async function fetchMyConversations(): Promise<{ items: ConversationSummary[] }> {
const { data } = await apiClient.get<{ items: ConversationSummary[] }>('me/conversations')
return data
}