test commit
This commit is contained in:
@@ -20,8 +20,8 @@ type Props = {
|
|||||||
error: unknown
|
error: unknown
|
||||||
uploadError: unknown
|
uploadError: unknown
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => void
|
onSubmit: (params: { rating: number; text: string; imageUrl: string | null }) => Promise<void>
|
||||||
onUploadImage: (file: File) => void
|
onUploadImage: (file: File) => Promise<{ url: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
function reviewSubmitErrorMessage(err: unknown): string {
|
function reviewSubmitErrorMessage(err: unknown): string {
|
||||||
@@ -55,11 +55,13 @@ export function ReviewDialog({
|
|||||||
const [rating, setRating] = useState<number>(5)
|
const [rating, setRating] = useState<number>(5)
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||||
|
const [localUploadError, setLocalUploadError] = useState<string | null>(null)
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setRating(5)
|
setRating(5)
|
||||||
setText('')
|
setText('')
|
||||||
setImageUrl(null)
|
setImageUrl(null)
|
||||||
|
setLocalUploadError(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@@ -68,9 +70,9 @@ export function ReviewDialog({
|
|||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
if (isPending) return
|
if (isPending) return
|
||||||
onSubmit({ rating, text: text.trim(), imageUrl })
|
await onSubmit({ rating, text: text.trim(), imageUrl })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,11 +98,19 @@ export function ReviewDialog({
|
|||||||
hidden
|
hidden
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/png,image/jpeg,image/webp"
|
accept="image/png,image/jpeg,image/webp"
|
||||||
onChange={(e) => {
|
onChange={async (e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
onUploadImage(file)
|
|
||||||
e.currentTarget.value = ''
|
e.currentTarget.value = ''
|
||||||
|
setLocalUploadError(null)
|
||||||
|
try {
|
||||||
|
const result = await onUploadImage(file)
|
||||||
|
setImageUrl(result.url)
|
||||||
|
} catch (err) {
|
||||||
|
setLocalUploadError(
|
||||||
|
err instanceof Error ? err.message : 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.',
|
||||||
|
)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -126,11 +136,13 @@ export function ReviewDialog({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{uploadError ? (
|
{uploadError || localUploadError ? (
|
||||||
<Alert severity="error" sx={{ mt: 2 }}>
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
{uploadError instanceof Error
|
{localUploadError
|
||||||
? uploadError.message
|
? localUploadError
|
||||||
: 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.'}
|
: uploadError instanceof Error
|
||||||
|
? uploadError.message
|
||||||
|
: 'Не удалось загрузить фото. Разрешены png, jpg, jpeg, webp.'}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
{error ? (
|
{error ? (
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ type Props = {
|
|||||||
isUploadPending: boolean
|
isUploadPending: boolean
|
||||||
submitError: unknown
|
submitError: unknown
|
||||||
uploadError: unknown
|
uploadError: unknown
|
||||||
onSubmitReview: (params: { productId: string; rating: number; text: string; imageUrl: string | null }) => void
|
onSubmitReview: (params: {
|
||||||
|
productId: string
|
||||||
|
rating: number
|
||||||
|
text: string
|
||||||
|
imageUrl: string | null
|
||||||
|
}) => Promise<void>
|
||||||
onUploadImage: (file: File) => Promise<{ url: string }>
|
onUploadImage: (file: File) => Promise<{ url: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,17 +80,20 @@ export function ReviewSection({
|
|||||||
setTarget(null)
|
setTarget(null)
|
||||||
setUploadedImageUrl(null)
|
setUploadedImageUrl(null)
|
||||||
}}
|
}}
|
||||||
onSubmit={(params) => {
|
onSubmit={async (params) => {
|
||||||
if (!target) return
|
if (!target) return
|
||||||
onSubmitReview({
|
await onSubmitReview({
|
||||||
productId: target.productId,
|
productId: target.productId,
|
||||||
...params,
|
...params,
|
||||||
imageUrl: uploadedImageUrl,
|
imageUrl: uploadedImageUrl,
|
||||||
})
|
})
|
||||||
|
setTarget(null)
|
||||||
|
setUploadedImageUrl(null)
|
||||||
}}
|
}}
|
||||||
onUploadImage={async (file) => {
|
onUploadImage={async (file) => {
|
||||||
const result = await onUploadImage(file)
|
const result = await onUploadImage(file)
|
||||||
setUploadedImageUrl(result.url)
|
setUploadedImageUrl(result.url)
|
||||||
|
return result
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import TableRow from '@mui/material/TableRow'
|
|||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useUnit } from 'effector-react'
|
||||||
import { Controller, useForm } from 'react-hook-form'
|
import { Controller, useForm } from 'react-hook-form'
|
||||||
import { Link as RouterLink } from 'react-router-dom'
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
import { createAdminUser, deleteAdminUser, fetchAdminUsers, updateAdminUser } from '@/entities/user/api/user-api'
|
import { createAdminUser, deleteAdminUser, fetchAdminUsers, updateAdminUser } from '@/entities/user/api/user-api'
|
||||||
@@ -16,6 +17,7 @@ import type { AdminUser } from '@/entities/user/model/types'
|
|||||||
import { getErrorMessage } from '@/shared/lib/get-error-message'
|
import { getErrorMessage } from '@/shared/lib/get-error-message'
|
||||||
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
import { invalidateQueryKeys } from '@/shared/lib/invalidate-query-keys'
|
||||||
import { useEditDialogState } from '@/shared/lib/use-edit-dialog-state'
|
import { useEditDialogState } from '@/shared/lib/use-edit-dialog-state'
|
||||||
|
import { $user } from '@/shared/model/auth'
|
||||||
import { AdminDialog } from '@/shared/ui/AdminDialog/AdminDialog'
|
import { AdminDialog } from '@/shared/ui/AdminDialog/AdminDialog'
|
||||||
import { AdminTable } from '@/shared/ui/AdminTable'
|
import { AdminTable } from '@/shared/ui/AdminTable'
|
||||||
import { EntityRowActions } from '@/shared/ui/EntityRowActions'
|
import { EntityRowActions } from '@/shared/ui/EntityRowActions'
|
||||||
@@ -44,6 +46,8 @@ export function AdminUsersPage() {
|
|||||||
const [q, setQ] = useState('')
|
const [q, setQ] = useState('')
|
||||||
const [page, setPage] = useState(0)
|
const [page, setPage] = useState(0)
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(20)
|
const [rowsPerPage, setRowsPerPage] = useState(20)
|
||||||
|
const currentUser = useUnit($user)
|
||||||
|
const currentUserId = currentUser?.id
|
||||||
|
|
||||||
const userForm = useForm<UserFormState>({
|
const userForm = useForm<UserFormState>({
|
||||||
defaultValues: emptyUserForm(),
|
defaultValues: emptyUserForm(),
|
||||||
@@ -192,7 +196,7 @@ export function AdminUsersPage() {
|
|||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<EntityRowActions
|
<EntityRowActions
|
||||||
onEdit={() => openEdit(u)}
|
onEdit={() => openEdit(u)}
|
||||||
onDelete={() => deleteMut.mutate(u.id)}
|
onDelete={u.id === currentUserId ? undefined : () => deleteMut.mutate(u.id)}
|
||||||
deleteDisabled={deleteMut.isPending}
|
deleteDisabled={deleteMut.isPending}
|
||||||
confirmDeleteMessage={`Удалить пользователя ${u.email}?`}
|
confirmDeleteMessage={`Удалить пользователя ${u.email}?`}
|
||||||
/>
|
/>
|
||||||
@@ -237,7 +241,15 @@ export function AdminUsersPage() {
|
|||||||
<Controller
|
<Controller
|
||||||
control={userForm.control}
|
control={userForm.control}
|
||||||
name="email"
|
name="email"
|
||||||
render={({ field }) => <TextField label="Почта" fullWidth required {...field} />}
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
label="Почта"
|
||||||
|
fullWidth
|
||||||
|
required
|
||||||
|
disabled={Boolean(editing && editing.id === currentUserId)}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={userForm.control}
|
control={userForm.control}
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ import {
|
|||||||
fetchUserNotificationSettings,
|
fetchUserNotificationSettings,
|
||||||
updateUserNotificationSettings,
|
updateUserNotificationSettings,
|
||||||
} from '@/entities/notification/api/notifications-api'
|
} from '@/entities/notification/api/notifications-api'
|
||||||
|
import type { UserNotificationSettings } from '@/entities/notification/api/notifications-api'
|
||||||
|
|
||||||
const eventFields = [
|
function isOrderStatusChangesOn(s: UserNotificationSettings): boolean {
|
||||||
{ key: 'orderCreated' as const, label: 'Заказ создан' },
|
return s.orderCreated && s.orderStatusChanged && s.paymentStatusChanged && s.deliveryFeeAdjusted
|
||||||
{ key: 'orderStatusChanged' as const, label: 'Изменение статуса заказа' },
|
}
|
||||||
{ key: 'orderMessageReceived' as const, label: 'Сообщение в чате заказа' },
|
|
||||||
{ key: 'paymentStatusChanged' as const, label: 'Изменение статуса оплаты' },
|
const orderStatusChangesPayload = (on: boolean) => ({
|
||||||
{ key: 'deliveryFeeAdjusted' as const, label: 'Корректировка стоимости доставки' },
|
orderCreated: on,
|
||||||
]
|
orderStatusChanged: on,
|
||||||
|
paymentStatusChanged: on,
|
||||||
|
deliveryFeeAdjusted: on,
|
||||||
|
})
|
||||||
|
|
||||||
export function NotificationsPage() {
|
export function NotificationsPage() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -45,9 +49,11 @@ export function NotificationsPage() {
|
|||||||
|
|
||||||
const handleToggle = (field: string, value: boolean) => {
|
const handleToggle = (field: string, value: boolean) => {
|
||||||
setError(null)
|
setError(null)
|
||||||
mutation.mutate({ [field]: value } as Record<string, boolean>)
|
mutation.mutate({ [field]: value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const statusChangesOn = isOrderStatusChangesOn(settings)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
@@ -80,19 +86,26 @@ export function NotificationsPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ pl: 4 }}>
|
<Box sx={{ pl: 4 }}>
|
||||||
{eventFields.map(({ key, label }) => (
|
<FormControlLabel
|
||||||
<FormControlLabel
|
control={
|
||||||
key={key}
|
<Switch
|
||||||
control={
|
checked={statusChangesOn}
|
||||||
<Switch
|
disabled={!settings.globalEnabled}
|
||||||
checked={settings[key]}
|
onChange={(e) => mutation.mutate(orderStatusChangesPayload(e.target.checked))}
|
||||||
disabled={!settings.globalEnabled}
|
/>
|
||||||
onChange={(e) => handleToggle(key, e.target.checked)}
|
}
|
||||||
/>
|
label="Изменения статуса заказа"
|
||||||
}
|
/>
|
||||||
label={label}
|
<FormControlLabel
|
||||||
/>
|
control={
|
||||||
))}
|
<Switch
|
||||||
|
checked={settings.orderMessageReceived}
|
||||||
|
disabled={!settings.globalEnabled}
|
||||||
|
onChange={(e) => handleToggle('orderMessageReceived', e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Сообщения в чате заказа"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Link from '@mui/material/Link'
|
|||||||
import Stack from '@mui/material/Stack'
|
import Stack from '@mui/material/Stack'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Link as RouterLink, useParams, useSearchParams } from 'react-router-dom'
|
import { Link as RouterLink, useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
confirmOrderReceived,
|
confirmOrderReceived,
|
||||||
createOrderPayment,
|
createOrderPayment,
|
||||||
@@ -30,6 +30,7 @@ import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
|
|||||||
export function OrderDetailPage() {
|
export function OrderDetailPage() {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const paidParam = searchParams.get('paid')
|
const paidParam = searchParams.get('paid')
|
||||||
@@ -58,6 +59,14 @@ export function OrderDetailPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const data = paymentStatusQuery.data
|
||||||
|
if (data && (data.paid || data.status === 'canceled') && paidParam === '1') {
|
||||||
|
qc.invalidateQueries({ queryKey: ['me', 'orders', id] })
|
||||||
|
navigate(`/me/orders/${id}`, { replace: true })
|
||||||
|
}
|
||||||
|
}, [paymentStatusQuery.data, paidParam, qc, id, navigate])
|
||||||
|
|
||||||
const confirmMut = useMutation({
|
const confirmMut = useMutation({
|
||||||
mutationFn: () => confirmOrderReceived(id!),
|
mutationFn: () => confirmOrderReceived(id!),
|
||||||
onSuccess: () =>
|
onSuccess: () =>
|
||||||
@@ -224,7 +233,7 @@ export function OrderDetailPage() {
|
|||||||
{PICKUP_ADDRESS_FULL}
|
{PICKUP_ADDRESS_FULL}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography color="text.secondary" variant="body2">
|
<Typography color="text.secondary" variant="body2">
|
||||||
Заберите заказ точно ко времени, которое согласуем по телефону или в чате заказа.
|
Заберите заказ ко времени, которое согласуем в чате заказа.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
@@ -274,7 +283,9 @@ export function OrderDetailPage() {
|
|||||||
isUploadPending={uploadReviewImageMut.isPending}
|
isUploadPending={uploadReviewImageMut.isPending}
|
||||||
submitError={reviewMut.error}
|
submitError={reviewMut.error}
|
||||||
uploadError={uploadReviewImageMut.error}
|
uploadError={uploadReviewImageMut.error}
|
||||||
onSubmitReview={(params) => reviewMut.mutate(params)}
|
onSubmitReview={async (params) => {
|
||||||
|
await reviewMut.mutateAsync(params)
|
||||||
|
}}
|
||||||
onUploadImage={async (file) => {
|
onUploadImage={async (file) => {
|
||||||
const result = await uploadReviewImageMut.mutateAsync(file)
|
const result = await uploadReviewImageMut.mutateAsync(file)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function ReviewsBlock() {
|
|||||||
<OptimizedImage
|
<OptimizedImage
|
||||||
src={r.imageUrl}
|
src={r.imageUrl}
|
||||||
alt="Фото к отзыву"
|
alt="Фото к отзыву"
|
||||||
widths={[160, 320]}
|
widths={[320, 640]}
|
||||||
sizes="80px"
|
sizes="80px"
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
Binary file not shown.
@@ -92,6 +92,7 @@ export async function registerAdminUserRoutes(fastify) {
|
|||||||
fastify.patch('/api/admin/users/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
fastify.patch('/api/admin/users/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
const body = request.body ?? {}
|
const body = request.body ?? {}
|
||||||
|
const adminUserId = request.user.sub
|
||||||
|
|
||||||
const existing = await prisma.user.findUnique({ where: { id } })
|
const existing = await prisma.user.findUnique({ where: { id } })
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -99,9 +100,15 @@ export async function registerAdminUserRoutes(fastify) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSelf = id === adminUserId
|
||||||
|
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|
||||||
if (body.email !== undefined) {
|
if (body.email !== undefined) {
|
||||||
|
if (isSelf) {
|
||||||
|
reply.code(403).send({ error: 'Нельзя изменить свою почту через панель администратора' })
|
||||||
|
return
|
||||||
|
}
|
||||||
const email = normalizeEmail(body.email)
|
const email = normalizeEmail(body.email)
|
||||||
if (!email || !email.includes('@')) {
|
if (!email || !email.includes('@')) {
|
||||||
reply.code(400).send({ error: 'Некорректная почта' })
|
reply.code(400).send({ error: 'Некорректная почта' })
|
||||||
@@ -139,6 +146,13 @@ export async function registerAdminUserRoutes(fastify) {
|
|||||||
|
|
||||||
fastify.delete('/api/admin/users/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
fastify.delete('/api/admin/users/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => {
|
||||||
const { id } = request.params
|
const { id } = request.params
|
||||||
|
const adminUserId = request.user.sub
|
||||||
|
|
||||||
|
if (id === adminUserId) {
|
||||||
|
reply.code(403).send({ error: 'Нельзя удалить свою учётную запись' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await prisma.user.delete({ where: { id } })
|
await prisma.user.delete({ where: { id } })
|
||||||
reply.code(204).send()
|
reply.code(204).send()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function registerUserCartRoutes(fastify) {
|
|||||||
const product = await prisma.product.findFirst({ where: { id: productId, published: true } })
|
const product = await prisma.product.findFirst({ where: { id: productId, published: true } })
|
||||||
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
|
if (!product) return reply.code(404).send({ error: 'Товар не найден' })
|
||||||
|
|
||||||
const available = product.inStock ? product.quantity : 1
|
const available = product.quantity
|
||||||
const existing = await prisma.cartItem.findUnique({ where: { userId_productId: { userId, productId } } })
|
const existing = await prisma.cartItem.findUnique({ where: { userId_productId: { userId, productId } } })
|
||||||
const nextQty = (existing?.qty ?? 0) + Math.floor(qty)
|
const nextQty = (existing?.qty ?? 0) + Math.floor(qty)
|
||||||
if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` })
|
if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` })
|
||||||
@@ -57,7 +57,7 @@ export async function registerUserCartRoutes(fastify) {
|
|||||||
return reply.code(204).send()
|
return reply.code(204).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
const available = existing.product.inStock ? existing.product.quantity : 1
|
const available = existing.product.quantity
|
||||||
const nextQty = Math.floor(qty)
|
const nextQty = Math.floor(qty)
|
||||||
if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` })
|
if (nextQty > available) return reply.code(409).send({ error: `Доступно: ${available} шт.` })
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
if (cartItems.length === 0) return reply.code(400).send({ error: 'Корзина пуста' })
|
if (cartItems.length === 0) return reply.code(400).send({ error: 'Корзина пуста' })
|
||||||
|
|
||||||
for (const ci of cartItems) {
|
for (const ci of cartItems) {
|
||||||
const available = ci.product.inStock ? ci.product.quantity : 1
|
const available = ci.product.quantity
|
||||||
if (ci.qty > available) {
|
if (ci.qty > available) {
|
||||||
return reply.code(409).send({
|
return reply.code(409).send({
|
||||||
error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.`,
|
error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.`,
|
||||||
@@ -112,8 +112,6 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
try {
|
try {
|
||||||
created = await prisma.$transaction(async (tx) => {
|
created = await prisma.$transaction(async (tx) => {
|
||||||
for (const ci of cartItems) {
|
for (const ci of cartItems) {
|
||||||
if (!ci.product.inStock) continue
|
|
||||||
|
|
||||||
const res = await tx.product.updateMany({
|
const res = await tx.product.updateMany({
|
||||||
where: { id: ci.productId, quantity: { gte: ci.qty } },
|
where: { id: ci.productId, quantity: { gte: ci.qty } },
|
||||||
data: { quantity: { decrement: ci.qty } },
|
data: { quantity: { decrement: ci.qty } },
|
||||||
|
|||||||
Reference in New Issue
Block a user