refresh token + base orders list and detail order
This commit is contained in:
Generated
+93
-43
@@ -22,7 +22,8 @@
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "15.4.5",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"next": "^15.5.3",
|
||||
"rc-rate": "^2.13.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -38,6 +39,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/moment-jalaali": "^0.7.9",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -820,9 +822,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.5.tgz",
|
||||
"integrity": "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz",
|
||||
"integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -836,9 +838,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.5.tgz",
|
||||
"integrity": "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz",
|
||||
"integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -852,9 +854,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.5.tgz",
|
||||
"integrity": "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz",
|
||||
"integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -868,9 +870,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.5.tgz",
|
||||
"integrity": "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz",
|
||||
"integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -884,9 +886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.5.tgz",
|
||||
"integrity": "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz",
|
||||
"integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -900,9 +902,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.5.tgz",
|
||||
"integrity": "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz",
|
||||
"integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -916,9 +918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.4.5.tgz",
|
||||
"integrity": "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz",
|
||||
"integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -932,9 +934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.5.tgz",
|
||||
"integrity": "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz",
|
||||
"integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -948,9 +950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.5.tgz",
|
||||
"integrity": "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz",
|
||||
"integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1853,6 +1855,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/moment-jalaali": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz",
|
||||
"integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": ">=2.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz",
|
||||
@@ -2723,9 +2735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -4777,6 +4789,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/jalaali-js": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz",
|
||||
"integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
|
||||
@@ -5309,6 +5327,38 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-jalaali": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz",
|
||||
"integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jalaali-js": "^1.2.7",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.46"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -5358,12 +5408,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.4.5",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.4.5.tgz",
|
||||
"integrity": "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g==",
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz",
|
||||
"integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.4.5",
|
||||
"@next/env": "15.5.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"postcss": "8.4.31",
|
||||
@@ -5376,14 +5426,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.4.5",
|
||||
"@next/swc-darwin-x64": "15.4.5",
|
||||
"@next/swc-linux-arm64-gnu": "15.4.5",
|
||||
"@next/swc-linux-arm64-musl": "15.4.5",
|
||||
"@next/swc-linux-x64-gnu": "15.4.5",
|
||||
"@next/swc-linux-x64-musl": "15.4.5",
|
||||
"@next/swc-win32-arm64-msvc": "15.4.5",
|
||||
"@next/swc-win32-x64-msvc": "15.4.5",
|
||||
"@next/swc-darwin-arm64": "15.5.3",
|
||||
"@next/swc-darwin-x64": "15.5.3",
|
||||
"@next/swc-linux-arm64-gnu": "15.5.3",
|
||||
"@next/swc-linux-arm64-musl": "15.5.3",
|
||||
"@next/swc-linux-x64-gnu": "15.5.3",
|
||||
"@next/swc-linux-x64-musl": "15.5.3",
|
||||
"@next/swc-win32-arm64-msvc": "15.5.3",
|
||||
"@next/swc-win32-x64-msvc": "15.5.3",
|
||||
"sharp": "^0.34.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
+3
-1
@@ -23,7 +23,8 @@
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"iconsax-react": "^0.0.8",
|
||||
"lucide-react": "^0.539.0",
|
||||
"next": "15.4.5",
|
||||
"moment-jalaali": "^0.10.4",
|
||||
"next": "^15.5.3",
|
||||
"rc-rate": "^2.13.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -39,6 +40,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/moment-jalaali": "^0.7.9",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import axios from "@/config/axios";
|
||||
import { AuthenticationType, VerifyOtpType } from "../types/Types";
|
||||
import {
|
||||
AuthenticationType,
|
||||
RefreshTokenType,
|
||||
VerifyOtpType,
|
||||
} from "../types/Types";
|
||||
|
||||
export const authentication = async (params: AuthenticationType) => {
|
||||
const { data } = await axios.post("/user/authenticate", params);
|
||||
@@ -10,3 +14,8 @@ export const verifyOtp = async (params: VerifyOtpType) => {
|
||||
const { data } = await axios.post("/user/login/otp", params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const refreshToken = async (params: RefreshTokenType) => {
|
||||
const { data } = await axios.post(`/auth/refresh`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -13,3 +13,7 @@ export type VerifyOtpType = {
|
||||
phone: string;
|
||||
otpCode: string;
|
||||
};
|
||||
|
||||
export type RefreshTokenType = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import * as yup from 'yup'
|
||||
import Modal from '@/components/Modal'
|
||||
import Select from '@/components/Select'
|
||||
import Input from '@/components/Input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
interface CancelOrderFormData {
|
||||
reason: string
|
||||
count: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface CancelReason {
|
||||
_id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface CancelOrderModalProps {
|
||||
showModalCancel: boolean
|
||||
setShowModalCancel: (show: boolean) => void
|
||||
onSubmit: (data: CancelOrderFormData) => void
|
||||
isLoading: boolean
|
||||
cancelReasons?: Array<{ _id: string; title: string }>
|
||||
}
|
||||
|
||||
const cancelOrderSchema = yup.object({
|
||||
reason: yup.string().required('دلیل لغو سفارش را انتخاب کنید'),
|
||||
count: yup.string().required('تعداد را وارد کنید'),
|
||||
description: yup.string().required('توضیحات را وارد کنید')
|
||||
})
|
||||
|
||||
const CancelOrderModal: React.FC<CancelOrderModalProps> = ({
|
||||
showModalCancel,
|
||||
setShowModalCancel,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
cancelReasons = []
|
||||
}) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
reset
|
||||
} = useForm<CancelOrderFormData>({
|
||||
resolver: yupResolver(cancelOrderSchema)
|
||||
})
|
||||
|
||||
const handleFormSubmit = (data: CancelOrderFormData) => {
|
||||
onSubmit(data)
|
||||
reset()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={showModalCancel}
|
||||
close={() => setShowModalCancel(false)}
|
||||
isHeader
|
||||
title='درخواست لغو سفارش'
|
||||
>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className='mt-6'>
|
||||
<Select
|
||||
placeholder='دلیل لغو سفارش'
|
||||
items={cancelReasons.map((item) => ({
|
||||
label: item.title,
|
||||
value: item._id
|
||||
}))}
|
||||
className='bg-white border'
|
||||
value={watch('reason')}
|
||||
name='reason'
|
||||
onChange={(e) => setValue('reason', e.target.value)}
|
||||
error_text={errors.reason?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
variant='floating_outlined'
|
||||
label='تعداد'
|
||||
className='mt-6'
|
||||
{...register('count')}
|
||||
error_text={errors.count?.message}
|
||||
/>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className="text-sm font-medium mb-2">توضیحات :</div>
|
||||
<textarea
|
||||
{...register('description')}
|
||||
className='h-[123px] rounded-xl p-3 text-sm border w-full resize-none focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||
placeholder='متن توضیحات خود را بنویسید'
|
||||
/>
|
||||
{errors.description && (
|
||||
<div className='text-xs text-red-500 mt-1'>
|
||||
{errors.description.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2 text-xs text-[#7F7F7F] mt-4 p-3 bg-blue-50 rounded-lg'>
|
||||
<div className="size-5 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-blue-600 text-xs">!</span>
|
||||
</div>
|
||||
<span>
|
||||
توجه داشته باشید که بعد از ثبت لغو سفارش طبق
|
||||
</span>
|
||||
<span className='text-primary font-medium'>
|
||||
جدول جرائم
|
||||
</span>
|
||||
<span>
|
||||
جریمه میشود
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
type="submit"
|
||||
className='w-full'
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
لغو سفارش
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CancelOrderModal
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import OrderProductItem from './OrderProductItem'
|
||||
import { OrderItemDetail } from '../../types/Types'
|
||||
|
||||
interface OrderMainContentProps {
|
||||
orderItems: OrderItemDetail[]
|
||||
orderId: string
|
||||
getStatusText: (status: string) => string
|
||||
onCancelClick: () => void
|
||||
}
|
||||
|
||||
const OrderMainContent: React.FC<OrderMainContentProps> = ({
|
||||
orderItems,
|
||||
orderId,
|
||||
getStatusText,
|
||||
onCancelClick
|
||||
}) => {
|
||||
// محاسبه تعداد مرسوله (length of orderItems)
|
||||
const shipmentCount = orderItems.length
|
||||
|
||||
// محاسبه تعداد اقلام (sum of all itemsCount)
|
||||
const totalItemsCount = orderItems.reduce((sum, item) => sum + item.itemsCount, 0)
|
||||
|
||||
// گرفتن اولین آیتم برای نمایش اطلاعات (در صورت نیاز)
|
||||
const firstOrderItem = orderItems[0]
|
||||
|
||||
// محاسبه مجموع هزینه ارسال و مبلغ مرسوله
|
||||
const totalShipmentCost = orderItems.reduce((sum, item) => sum + item.totalShipmentCost, 0)
|
||||
const totalPaymentPrice = orderItems.reduce((sum, item) => sum + item.totalPaymentPrice, 0)
|
||||
return (
|
||||
<div className='flex-1 w-full bg-white shadow-lg p-6 rounded-2xl'>
|
||||
<div className='flex md:flex-row flex-col gap-4 justify-between md:items-center'>
|
||||
<div className='flex md:flex-row flex-col gap-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>تعداد مرسوله :</div>
|
||||
<div>{shipmentCount}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>تعداد اقلام :</div>
|
||||
<div>
|
||||
{totalItemsCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='text-primary font-semibold'>
|
||||
{firstOrderItem ? getStatusText(firstOrderItem.status) : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-6'>
|
||||
<div className='text-[#7F7F7F]'>حداکثر مهلت ارسال :</div>
|
||||
<div>
|
||||
{firstOrderItem?.shipper.deliveryTime} روز
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex md:flex-row flex-col gap-3 justify-between mt-6'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>هزینه ارسال :</div>
|
||||
<div>
|
||||
{totalShipmentCost.toLocaleString()} تومان
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>مبلغ مرسوله :</div>
|
||||
<div>
|
||||
{totalPaymentPrice.toLocaleString()} تومان
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>شناسه سفارش :</div>
|
||||
<div className="font-mono">
|
||||
{orderId}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Items */}
|
||||
{orderItems.map((orderItem) =>
|
||||
orderItem.shipmentItems.map((item) => (
|
||||
<OrderProductItem
|
||||
key={item._id}
|
||||
item={item}
|
||||
shipperName={orderItem.shipper.name}
|
||||
onCancelClick={onCancelClick}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderMainContent
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import { MessageText, ShieldTick, Truck, CloseCircle } from 'iconsax-react'
|
||||
import { ShipmentItemDetail } from '../../types/Types'
|
||||
|
||||
interface OrderProductItemProps {
|
||||
item: ShipmentItemDetail
|
||||
shipperName: string
|
||||
onCancelClick: () => void
|
||||
}
|
||||
|
||||
const OrderProductItem: React.FC<OrderProductItemProps> = ({ item, shipperName, onCancelClick }) => {
|
||||
return (
|
||||
<div className='mt-8 py-7 border-t items-start flex gap-6 sm:flex-row flex-col'>
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
width={80}
|
||||
height={80}
|
||||
className='w-20 h-20 object-cover rounded-lg border'
|
||||
alt={item.product.title_fa}
|
||||
/>
|
||||
|
||||
<div className='flex-1 w-full'>
|
||||
<div className='truncate max-w-[90%] font-medium'>
|
||||
{item.product.title_fa}
|
||||
</div>
|
||||
|
||||
<div className='mt-4 text-[#999999] space-y-3'>
|
||||
<div className='flex gap-2'>
|
||||
<div>تعداد اقلام :</div>
|
||||
<div>{item.quantity}</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<div>تعداد کنسلی :</div>
|
||||
<div>{item.cancelled_quantity}</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<div>تعداد مرجوعی :</div>
|
||||
<div>{item.returned_quantity}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='size-3 bg-black rounded-full'></div>
|
||||
<div>
|
||||
{item.variant.meterage?.value ||
|
||||
item.variant.size?.value ||
|
||||
'ندارد'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<MessageText size={20} className="text-gray-400" />
|
||||
<div className="font-mono">
|
||||
SKP_{item.product._id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<ShieldTick size={20} className="text-green-500" />
|
||||
<div>
|
||||
گارانتی {item.variant.warranty.duration} {item.variant.warranty.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<Truck size={20} className="text-blue-500" />
|
||||
<div>
|
||||
{shipperName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex justify-between items-center'>
|
||||
<div>
|
||||
<div className='text-lg font-semibold'>
|
||||
{Intl.NumberFormat('fa-IR').format(item.selling_price)}
|
||||
</div>
|
||||
<div className='text-xs mt-1 text-gray-500'>تومان</div>
|
||||
</div>
|
||||
|
||||
{item.quantity !== item.cancelled_quantity && (
|
||||
<button
|
||||
onClick={onCancelClick}
|
||||
className='flex cursor-pointer gap-2 text-[#B3261E] hover:bg-red-50 p-2 rounded-lg transition-colors'
|
||||
>
|
||||
<div>لغو سفارش</div>
|
||||
<CloseCircle size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderProductItem
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import TrackCode from './TrackCode'
|
||||
import { Payment, User, ShipmentAddress } from '../../types/Types'
|
||||
import * as api from '../../service/Service'
|
||||
import { toast } from '@/components/Toast'
|
||||
|
||||
interface OrderSidebarProps {
|
||||
orderData: {
|
||||
payment: Payment
|
||||
order: {
|
||||
_id: string
|
||||
user: User
|
||||
shipmentAddress: ShipmentAddress
|
||||
createdAt: string
|
||||
}
|
||||
}
|
||||
orderId: number
|
||||
}
|
||||
|
||||
const OrderSidebar: React.FC<OrderSidebarProps> = ({ orderData, orderId }) => {
|
||||
const [isLoadingInvoice, setIsLoadingInvoice] = useState(false)
|
||||
|
||||
const handleInvoice = async () => {
|
||||
try {
|
||||
setIsLoadingInvoice(true)
|
||||
const blob = await api.getInvoice(orderId.toString())
|
||||
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `invoice-${orderId}.pdf`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
toast("فاکتور با موفقیت دانلود شد", "success")
|
||||
} catch (error) {
|
||||
toast("مشکلی در دانلود فاکتور رخ داد", "error")
|
||||
} finally {
|
||||
setIsLoadingInvoice(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className='lg:w-[280px] w-full p-5 rounded-2xl shadow-lg'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>شناسه تراکنش :</div>
|
||||
<div className="font-mono">
|
||||
{orderData.payment.transaction_id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F]'>گیرنده :</div>
|
||||
<div>
|
||||
{orderData.order.user.fullName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F]'>کدپستی :</div>
|
||||
<div>
|
||||
{orderData.order.shipmentAddress.postalCode}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F] whitespace-nowrap'>تاریخ ثبت سفارش :</div>
|
||||
<div>
|
||||
{orderData.order.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F]'>شماره موبایل :</div>
|
||||
<div>
|
||||
{orderData.order.shipmentAddress.phone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-start gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F] whitespace-nowrap'>آدرس :</div>
|
||||
<div>
|
||||
{orderData.order.shipmentAddress.address}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TrackCode orderId={orderId} />
|
||||
|
||||
<Button
|
||||
className='mt-3 w-full'
|
||||
variant="outline"
|
||||
onClick={handleInvoice}
|
||||
disabled={isLoadingInvoice}
|
||||
>
|
||||
{isLoadingInvoice ? 'در حال دانلود...' : 'مشاهده فاکتور'}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderSidebar
|
||||
@@ -1,115 +1,25 @@
|
||||
'use client'
|
||||
import { FC, useState } from 'react'
|
||||
import { ArrowRight2, ShieldTick, Truck, MessageText, CloseCircle } from 'iconsax-react'
|
||||
import { ArrowRight2 } from 'iconsax-react'
|
||||
import { useRouter, useParams } from 'next/navigation'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import * as yup from 'yup'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import TrackCode from './components/TrackCode'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Modal from '@/components/Modal'
|
||||
import Select from '@/components/Select'
|
||||
import Input from '@/components/Input'
|
||||
import Layout from '@/hoc/Layout'
|
||||
import Image from 'next/image'
|
||||
import Menu from '../../components/Menu'
|
||||
import OrderSidebar from './components/OrderSidebar'
|
||||
import OrderMainContent from './components/OrderMainContent'
|
||||
import CancelOrderModal from './components/CancelOrderModal'
|
||||
import { useGetOrderDetail } from '../hooks/useOrdersData'
|
||||
import * as api from '../service/Service'
|
||||
import { toast } from '@/components/Toast'
|
||||
|
||||
// Mock data
|
||||
const mockOrderDetail = {
|
||||
payment: {
|
||||
transaction_id: 'TRX123456789'
|
||||
},
|
||||
order: {
|
||||
_id: 'ORD001',
|
||||
user: {
|
||||
fullName: 'احمد محمدی'
|
||||
},
|
||||
shipmentAddress: {
|
||||
postalCode: '1234567890',
|
||||
phone: '09123456789',
|
||||
address: 'تهران، خیابان ولیعصر، پلاک 123'
|
||||
},
|
||||
createdAt: '1402/12/15',
|
||||
orderItems: {
|
||||
_id: 'ITEM001',
|
||||
itemsCount: 3,
|
||||
status: 'processing',
|
||||
totalShipmentCost: '25000 تومان',
|
||||
totalPaymentPrice: '450000',
|
||||
shipper: {
|
||||
name: 'پست ایران',
|
||||
deliveryTime: 3
|
||||
},
|
||||
shipmentItems: [
|
||||
{
|
||||
_id: 'SHIP001',
|
||||
product: {
|
||||
_id: 'PROD001',
|
||||
title_fa: 'لپتاپ اپل مکبوک پرو 13 اینچی',
|
||||
imagesUrl: {
|
||||
cover: 'https://via.placeholder.com/80x80'
|
||||
}
|
||||
},
|
||||
quantity: 1,
|
||||
cancelled_quantity: 0,
|
||||
returned_quantity: 0,
|
||||
selling_price: 450000,
|
||||
variant: {
|
||||
color: {
|
||||
name: 'نقرهای'
|
||||
},
|
||||
size: {
|
||||
name: '13 اینچ'
|
||||
},
|
||||
meterages: {
|
||||
name: '256GB'
|
||||
},
|
||||
warranty: {
|
||||
duration: '2 سال',
|
||||
name: 'گارانتی'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mockCancelReasons = [
|
||||
{ _id: 'REASON1', title: 'تغییر نظر در خرید' },
|
||||
{ _id: 'REASON2', title: 'یافتن قیمت بهتر' },
|
||||
{ _id: 'REASON3', title: 'مشکل در محصول' },
|
||||
{ _id: 'REASON4', title: 'تأخیر در ارسال' }
|
||||
]
|
||||
|
||||
// Form validation schema
|
||||
const cancelOrderSchema = yup.object({
|
||||
reason: yup.string().required('لطفا دلیل لغو سفارش را انتخاب کنید'),
|
||||
description: yup.string().required('لطفا توضیحات لغو سفارش را وارد کنید'),
|
||||
count: yup.string().required('تعداد لغو سفارش را وارد کنید')
|
||||
})
|
||||
|
||||
type CancelOrderFormData = yup.InferType<typeof cancelOrderSchema>
|
||||
|
||||
const OrderDetail: FC = () => {
|
||||
const { id } = useParams()
|
||||
const { data: orderDetail } = useGetOrderDetail(id as string)
|
||||
const [showModalCancel, setShowModalCancel] = useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const router = useRouter()
|
||||
|
||||
// Form with react-hook-form
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
reset
|
||||
} = useForm<CancelOrderFormData>({
|
||||
resolver: yupResolver(cancelOrderSchema)
|
||||
})
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'processing': 'در حال پردازش',
|
||||
@@ -120,17 +30,24 @@ const OrderDetail: FC = () => {
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
setIsLoading(true)
|
||||
const onSubmit = async (cancelData: { reason: string; count: string; description: string }) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
await api.cancelOrder(id as string, {
|
||||
reason: cancelData.reason,
|
||||
count: parseInt(cancelData.count),
|
||||
description: cancelData.description
|
||||
})
|
||||
|
||||
toast("درخواست لغو سفارش با موفقیت ثبت شد", "success")
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
setShowModalCancel(false)
|
||||
reset() // Reset form
|
||||
alert('درخواست لغو سفارش با موفقیت ثبت شد')
|
||||
router.push('/profile/orders')
|
||||
}, 1000)
|
||||
} catch {
|
||||
toast("مشکلی در ثبت درخواست لغو سفارش رخ داد", "error")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -151,269 +68,40 @@ const OrderDetail: FC = () => {
|
||||
|
||||
{isLoading ? (
|
||||
<PageLoading />
|
||||
) : (
|
||||
) : orderDetail?.results.order ? (
|
||||
<div className='mt-6 lg:flex-row flex-col flex gap-6 items-start'>
|
||||
{/* Sidebar */}
|
||||
<div className='lg:w-[280px] w-full p-5 rounded-2xl shadow-lg'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>شناسه تراکنش :</div>
|
||||
<div className="font-mono">
|
||||
{mockOrderDetail.payment.transaction_id}
|
||||
</div>
|
||||
</div>
|
||||
<OrderSidebar
|
||||
orderData={{
|
||||
payment: orderDetail.results.order.payment,
|
||||
order: {
|
||||
_id: orderDetail.results.order._id.toString(),
|
||||
user: orderDetail.results.order.user,
|
||||
shipmentAddress: orderDetail.results.order.shipmentAddress,
|
||||
createdAt: orderDetail.results.order.createdAt
|
||||
}
|
||||
}}
|
||||
orderId={orderDetail.results.order._id}
|
||||
/>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F]'>گیرنده :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.user.fullName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F]'>کدپستی :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.shipmentAddress.postalCode}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F] whitespace-nowrap'>تاریخ ثبت سفارش :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.createdAt}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F]'>شماره موبایل :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.shipmentAddress.phone}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-start gap-2 mt-4'>
|
||||
<div className='text-[#7F7F7F] whitespace-nowrap'>آدرس :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.shipmentAddress.address}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TrackCode orderId={Number(id) || 0} />
|
||||
|
||||
<Button
|
||||
className='mt-3 w-full'
|
||||
variant="outline"
|
||||
>
|
||||
مشاهده فاکتور
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className='flex-1 w-full bg-white shadow-lg p-6 rounded-2xl'>
|
||||
<div className='flex md:flex-row flex-col gap-4 justify-between md:items-center'>
|
||||
<div className='flex md:flex-row flex-col gap-4'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>تعداد مرسوله :</div>
|
||||
<div>1</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>تعداد اقلام :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.orderItems.itemsCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='text-primary font-semibold'>
|
||||
{getStatusText(mockOrderDetail.order.orderItems.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-2 mt-6'>
|
||||
<div className='text-[#7F7F7F]'>حداکثر مهلت ارسال :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.orderItems.shipper.deliveryTime} روز
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex md:flex-row flex-col gap-3 justify-between mt-6'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>هزینه ارسال :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.orderItems.totalShipmentCost}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>مبلغ مرسوله :</div>
|
||||
<div>
|
||||
{mockOrderDetail.order.orderItems.totalPaymentPrice} تومان
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-[#7F7F7F]'>شناسه سفارش :</div>
|
||||
<div className="font-mono">
|
||||
{mockOrderDetail.order._id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Items */}
|
||||
{mockOrderDetail.order.orderItems.shipmentItems.map((item) => (
|
||||
<div key={item.product._id} className='mt-8 py-7 border-t items-start flex gap-6 sm:flex-row flex-col'>
|
||||
<Image
|
||||
src={item.product.imagesUrl.cover}
|
||||
width={80}
|
||||
height={80}
|
||||
className='w-20 h-20 object-cover rounded-lg border'
|
||||
alt={item.product.title_fa}
|
||||
/>
|
||||
|
||||
<div className='flex-1 w-full'>
|
||||
<div className='truncate max-w-[90%] font-medium'>
|
||||
{item.product.title_fa}
|
||||
</div>
|
||||
|
||||
<div className='mt-4 text-[#999999] space-y-3'>
|
||||
<div className='flex gap-2'>
|
||||
<div>تعداد اقلام :</div>
|
||||
<div>{item.quantity}</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<div>تعداد کنسلی :</div>
|
||||
<div>{item.cancelled_quantity}</div>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<div>تعداد مرجوعی :</div>
|
||||
<div>{item.returned_quantity}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<div className='size-3 bg-black rounded-full'></div>
|
||||
<div>
|
||||
{item.variant.color?.name ||
|
||||
item.variant.size?.name ||
|
||||
item.variant.meterages?.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<MessageText size={20} className="text-gray-400" />
|
||||
<div className="font-mono">
|
||||
SKP_{item.product._id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<ShieldTick size={20} className="text-green-500" />
|
||||
<div>
|
||||
گارانتی {item.variant.warranty.duration} {item.variant.warranty.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 items-center'>
|
||||
<Truck size={20} className="text-blue-500" />
|
||||
<div>
|
||||
{mockOrderDetail.order.orderItems.shipper.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-8 flex justify-between items-center'>
|
||||
<div>
|
||||
<div className='text-lg font-semibold'>
|
||||
{Intl.NumberFormat('fa-IR').format(item.selling_price)}
|
||||
</div>
|
||||
<div className='text-xs mt-1 text-gray-500'>تومان</div>
|
||||
</div>
|
||||
|
||||
{item.quantity !== item.cancelled_quantity && (
|
||||
<button
|
||||
onClick={() => setShowModalCancel(true)}
|
||||
className='flex cursor-pointer gap-2 text-[#B3261E] hover:bg-red-50 p-2 rounded-lg transition-colors'
|
||||
>
|
||||
<div>لغو سفارش</div>
|
||||
<CloseCircle size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<OrderMainContent
|
||||
orderItems={orderDetail.results.order.orderItems}
|
||||
orderId={orderDetail.results.order._id.toString()}
|
||||
getStatusText={getStatusText}
|
||||
onCancelClick={() => setShowModalCancel(true)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-6 text-center'>در حال بارگذاری...</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cancel Order Modal */}
|
||||
<Modal
|
||||
open={showModalCancel}
|
||||
close={() => setShowModalCancel(false)}
|
||||
isHeader
|
||||
title='درخواست لغو سفارش'
|
||||
>
|
||||
<form onSubmit={handleSubmit(onSubmit)} className='mt-6'>
|
||||
<Select
|
||||
placeholder='دلیل لغو سفارش'
|
||||
items={mockCancelReasons.map((item) => ({
|
||||
label: item.title,
|
||||
value: item._id
|
||||
}))}
|
||||
className='bg-white border'
|
||||
value={watch('reason')}
|
||||
name='reason'
|
||||
onChange={(e) => setValue('reason', e.target.value)}
|
||||
error_text={errors.reason?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
variant='floating_outlined'
|
||||
label='تعداد'
|
||||
className='mt-6'
|
||||
{...register('count')}
|
||||
error_text={errors.count?.message}
|
||||
/>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className="text-sm font-medium mb-2">توضیحات :</div>
|
||||
<textarea
|
||||
{...register('description')}
|
||||
className='h-[123px] rounded-xl p-3 text-sm border w-full resize-none focus:outline-none focus:ring-2 focus:ring-primary/20'
|
||||
placeholder='متن توضیحات خود را بنویسید'
|
||||
/>
|
||||
{errors.description && (
|
||||
<div className='text-xs text-red-500 mt-1'>
|
||||
{errors.description.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex gap-2 text-xs text-[#7F7F7F] mt-4 p-3 bg-blue-50 rounded-lg'>
|
||||
<div className="size-5 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<span className="text-blue-600 text-xs">!</span>
|
||||
</div>
|
||||
<span>
|
||||
توجه داشته باشید که بعد از ثبت لغو سفارش طبق
|
||||
</span>
|
||||
<span className='text-primary font-medium'>
|
||||
جدول جرائم
|
||||
</span>
|
||||
<span>
|
||||
جریمه میشود
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<Button
|
||||
type="submit"
|
||||
className='w-full'
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
لغو سفارش
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
<CancelOrderModal
|
||||
showModalCancel={showModalCancel}
|
||||
setShowModalCancel={setShowModalCancel}
|
||||
onSubmit={onSubmit}
|
||||
isLoading={isLoading}
|
||||
cancelReasons={orderDetail?.results.order.cancelReasons}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
interface OrderButtonsProps {
|
||||
status: string
|
||||
orderId: string
|
||||
}
|
||||
|
||||
const OrderButtons: React.FC<OrderButtonsProps> = ({ status, orderId }) => {
|
||||
const statusLower = status.toLowerCase();
|
||||
const router = useRouter();
|
||||
|
||||
switch (statusLower) {
|
||||
case 'pending':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
onClick={() => router.push(`/profile/orders/${orderId}`)}
|
||||
>
|
||||
جزئیات سفارش
|
||||
</Button>
|
||||
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
|
||||
پرداخت
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'processing':
|
||||
case 'shipped':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
>
|
||||
پیگیری سفارش
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
onClick={() => router.push(`/profile/orders/${orderId}`)}
|
||||
>
|
||||
جزئیات سفارش
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'delivered':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
>
|
||||
امتیازدهی
|
||||
</Button>
|
||||
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
|
||||
خرید مجدد
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'returned':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
onClick={() => router.push(`/profile/orders/${orderId}`)}
|
||||
>
|
||||
جزئیات سفارش
|
||||
</Button>
|
||||
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
|
||||
خرید مجدد
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'cancelled':
|
||||
return (
|
||||
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
|
||||
سفارش مجدد
|
||||
</Button>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
onClick={() => router.push(`/profile/orders/${orderId}`)}
|
||||
>
|
||||
جزئیات سفارش
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default OrderButtons
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import OrderButtons from './OrderButtons'
|
||||
import OrderProducts from './OrderProducts'
|
||||
import { Order } from '../types/Types'
|
||||
import moment from 'moment-jalaali'
|
||||
|
||||
interface OrderItemProps {
|
||||
order: Order
|
||||
getStatusText: (status: string) => string
|
||||
}
|
||||
|
||||
const OrderItem: React.FC<OrderItemProps> = ({ order, getStatusText }) => {
|
||||
return (
|
||||
<div className='bg-white border border-[#E0E0E0] rounded-[8px] p-3 sm:p-4 md:p-5'>
|
||||
<div className='flex flex-col lg:flex-row gap-4 sm:gap-6'>
|
||||
<div className='lg:max-w-[234px] w-full'>
|
||||
<div className=''>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F] mb-1'>
|
||||
شماره سفارش: <span className='text-black'> {order._id.toString()}</span>
|
||||
</div>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
|
||||
مبلغ: <span className='text-black'> {(order.payment?.totalPrice || 0).toLocaleString('fa-IR')} تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4 sm:mb-6 mt-3 sm:mt-4'>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F]'>
|
||||
وضعیت سفارش : <span className='text-black'>{getStatusText(order.status || 'pending')}</span>
|
||||
</div>
|
||||
<div className='text-xs dltr text-right sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
|
||||
تاریخ ثبت سفارش: <span className='text-black'> {moment(order.payment?.createdAt, 'jYYYY/jMM/jDD HH:mm:ss').format('jYYYY-jMM-jDD')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col sm:flex-row gap-2 sm:gap-3'>
|
||||
<OrderButtons status={order.status || 'pending'} orderId={order._id.toString()} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OrderProducts products={order.orderItems.flatMap(item =>
|
||||
item.shipmentItems.map(shipmentItem => ({
|
||||
id: shipmentItem.product._id,
|
||||
image: shipmentItem.product.imagesUrl?.cover || '',
|
||||
name: shipmentItem.product.title_fa
|
||||
}))
|
||||
)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderItem
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import 'swiper/css';
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Product {
|
||||
id: number
|
||||
image: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface OrderProductsProps {
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
const OrderProducts: React.FC<OrderProductsProps> = ({ products }) => {
|
||||
return (
|
||||
<div className='flex-1 flex gap-3 sm:gap-4 lg:border-r lg:border-border lg:pr-5 overflow-hidden min-h-[200px] sm:min-h-[225px]'>
|
||||
<Swiper
|
||||
spaceBetween={12}
|
||||
slidesPerView={'auto'}
|
||||
className='h-full w-full'
|
||||
grabCursor={true}
|
||||
>
|
||||
{products.map((product) => (
|
||||
<SwiperSlide key={product.id} className='!w-[180px] sm:!w-[200px] lg:!w-[234px]'>
|
||||
<div className='flex w-[180px] sm:w-[200px] lg:w-[234px] h-[180px] sm:h-[200px] lg:h-[234px] flex-col p-4 sm:p-5 lg:p-7 items-center rounded-xl gap-2 border border-border'>
|
||||
<Image
|
||||
src={product.image}
|
||||
className='max-w-[100px] sm:max-w-[120px] lg:max-w-[140px] max-h-[100px] sm:max-h-[120px] lg:max-h-[140px] object-center'
|
||||
alt={product.name}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<div className='max-w-[160px] truncate h-full sm:max-w-[180px] lg:max-w-[200px] text-center text-xs sm:text-sm font-light mt-2 sm:mt-4'>
|
||||
{product.name}
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderProducts
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
interface Tab {
|
||||
key: 'Processing' | 'Delivered' | 'Cancelled'
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
|
||||
interface OrderTabsProps {
|
||||
activeTab: 'Processing' | 'Delivered' | 'Cancelled'
|
||||
setActiveTab: (tab: 'Processing' | 'Delivered' | 'Cancelled') => void
|
||||
tabCounts: {
|
||||
current: number
|
||||
delivered: number
|
||||
returned: number
|
||||
cancelled: number
|
||||
}
|
||||
}
|
||||
|
||||
const OrderTabs: React.FC<OrderTabsProps> = ({ activeTab, setActiveTab, tabCounts }) => {
|
||||
const tabs: Tab[] = [
|
||||
{ key: 'Processing', label: 'جاری', count: tabCounts.current },
|
||||
{ key: 'Delivered', label: 'تحویل شده', count: tabCounts.delivered },
|
||||
// { key: 'Returned', label: 'مرجوع شده', count: tabCounts.returned },
|
||||
{ key: 'Cancelled', label: 'لغو شده', count: tabCounts.cancelled }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14 border-b border-gray-200'>
|
||||
<div className='flex gap-2 sm:gap-4 lg:gap-8 overflow-x-auto'>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`pb-3 sm:pb-4 px-1 sm:px-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === tab.key
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<span className='ml-1 sm:ml-2 text-xs sm:text-sm'>{tab.label}</span>
|
||||
<span className={`inline-flex items-center justify-center min-w-[16px] sm:min-w-[20px] h-4 sm:h-5 px-1 sm:px-1.5 text-xs rounded-full ${activeTab === tab.key
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{tab.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrderTabs
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/Service";
|
||||
import { OrdersResponse, OrderDetailResponse } from "../types/Types";
|
||||
|
||||
export const useGetOrders = (activeTab: string) => {
|
||||
return useQuery<OrdersResponse>({
|
||||
queryKey: ["orders", activeTab],
|
||||
queryFn: () => api.getOrders(activeTab),
|
||||
enabled: !!activeTab,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrderDetail = (orderId: string) => {
|
||||
return useQuery<OrderDetailResponse>({
|
||||
queryKey: ["orderDetail", orderId],
|
||||
queryFn: () => api.getOrderDetail(orderId),
|
||||
enabled: !!orderId,
|
||||
});
|
||||
};
|
||||
+51
-118
@@ -1,139 +1,72 @@
|
||||
'use client'
|
||||
import Layout from '@/hoc/Layout'
|
||||
import { NextPage } from 'next'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import Menu from '../components/Menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import Image from 'next/image'
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import 'swiper/css';
|
||||
import { useGetOrders } from './hooks/useOrdersData'
|
||||
import { Order } from './types/Types'
|
||||
import OrderTabs from './components/OrderTabs'
|
||||
import OrderItem from './components/OrderItem'
|
||||
|
||||
|
||||
const Orders: NextPage = () => {
|
||||
const [activeTab, setActiveTab] = useState<'current' | 'delivered' | 'returned' | 'cancelled'>('current')
|
||||
|
||||
const mockOrders = [
|
||||
{
|
||||
id: '656151',
|
||||
amount: '18,500,000',
|
||||
status: 'در انتظار پرداخت',
|
||||
date: '1401 / 10 / 12',
|
||||
products: [
|
||||
{ id: 1, image: 'https://picsum.photos/80/80?random=1', name: 'گوشی موبایل سامسونگ مدل Galaxy S24 Ultra' },
|
||||
{ id: 2, image: 'https://picsum.photos/80/80?random=2', name: 'لحاف کرسی ستین مدل ساده' },
|
||||
{ id: 3, image: 'https://picsum.photos/80/80?random=3', name: 'روغنی ستین کد T-2112 طرح گلریز' },
|
||||
{ id: 4, image: 'https://picsum.photos/80/80?random=4', name: 'شارژر دیواری 65 وات ماگسیونگ' },
|
||||
{ id: 5, image: 'https://picsum.photos/80/80?random=5', name: 'گوشی موبایل سامسونگ مدل Galaxy S24 Ultra' },
|
||||
{ id: 6, image: 'https://picsum.photos/80/80?random=6', name: 'لحاف کرسی ستین مدل ساده' },
|
||||
{ id: 7, image: 'https://picsum.photos/80/80?random=7', name: 'روغنی ستین کد T-2112 طرح گلریز' },
|
||||
{ id: 8, image: 'https://picsum.photos/80/80?random=8', name: 'شارژر دیواری 65 وات ماگسیونگ' },
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '846511',
|
||||
amount: '254,500,000',
|
||||
status: 'وضعیت سفارش: در حال ارسال',
|
||||
date: '1401 / 8 / 10',
|
||||
products: [
|
||||
{ id: 1, image: 'https://picsum.photos/80/80?random=5', name: 'گوشی موبایل سامسونگ مدل Galaxy S24 Ultra' },
|
||||
{ id: 2, image: 'https://picsum.photos/80/80?random=6', name: 'لحاف کرسی ستین مدل ساده' },
|
||||
{ id: 3, image: 'https://picsum.photos/80/80?random=7', name: 'روغنی ستین کد T-2112 طرح گلریز' },
|
||||
{ id: 4, image: 'https://picsum.photos/80/80?random=8', name: 'شارژر دیواری 65 وات ماگسیونگ' }
|
||||
]
|
||||
}
|
||||
]
|
||||
const [activeTab, setActiveTab] = useState<'Processing' | 'Delivered' | 'Cancelled'>('Processing')
|
||||
const { data: ordersData } = useGetOrders(activeTab);
|
||||
|
||||
|
||||
// محاسبه تعداد سفارشات در هر تب از دادههای اصلی
|
||||
const tabCounts = useMemo(() => {
|
||||
if (!ordersData?.results?.orders) return { current: 0, delivered: 0, returned: 0, cancelled: 0 };
|
||||
|
||||
return ordersData.results.orders.reduce((acc, order: Order) => {
|
||||
const status = (order.status || 'pending').toLowerCase();
|
||||
if (['pending', 'processing', 'shipped'].includes(status)) {
|
||||
acc.current++;
|
||||
} else if (status === 'delivered') {
|
||||
acc.delivered++;
|
||||
} else if (status === 'returned') {
|
||||
acc.returned++;
|
||||
} else if (status === 'cancelled') {
|
||||
acc.cancelled++;
|
||||
}
|
||||
return acc;
|
||||
}, { current: 0, delivered: 0, returned: 0, cancelled: 0 });
|
||||
}, [ordersData]);
|
||||
|
||||
// تابع تبدیل وضعیت به فارسی
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: { [key: string]: string } = {
|
||||
'pending': 'در انتظار پرداخت',
|
||||
'processing': 'در حال پردازش',
|
||||
'shipped': 'در حال ارسال',
|
||||
'delivered': 'تحویل شده',
|
||||
'returned': 'مرجوع شده',
|
||||
'cancelled': 'لغو شده'
|
||||
};
|
||||
return statusMap[status.toLowerCase()] || status;
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ key: 'current' as const, label: 'جاری', count: 10 },
|
||||
{ key: 'delivered' as const, label: 'تحویل شده', count: 3 },
|
||||
{ key: 'returned' as const, label: 'مرجوع شده', count: 3 },
|
||||
{ key: 'cancelled' as const, label: 'لغو شده', count: 3 }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='p-3 sm:p-4 md:p-6'>
|
||||
<Menu pageActive='orders' />
|
||||
|
||||
{/* Tabs */}
|
||||
<div className='mt-6 sm:mt-8 md:mt-10 lg:mt-14 border-b border-gray-200'>
|
||||
<div className='flex gap-2 sm:gap-4 lg:gap-8 overflow-x-auto'>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`pb-3 sm:pb-4 px-1 sm:px-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === tab.key
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-gray-600 hover:text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<span className='ml-1 sm:ml-2 text-xs sm:text-sm'>{tab.label}</span>
|
||||
<span className={`inline-flex items-center justify-center min-w-[16px] sm:min-w-[20px] h-4 sm:h-5 px-1 sm:px-1.5 text-xs rounded-full ${activeTab === tab.key
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-gray-200 text-gray-600'
|
||||
}`}>
|
||||
{tab.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<OrderTabs
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
tabCounts={tabCounts}
|
||||
/>
|
||||
|
||||
{/* Orders List */}
|
||||
<div className='mt-4 sm:mt-6 space-y-3 sm:space-y-4'>
|
||||
{mockOrders.map((order) => (
|
||||
<div key={order.id} className='bg-white border border-[#E0E0E0] rounded-[8px] p-3 sm:p-4 md:p-5'>
|
||||
<div className='flex flex-col lg:flex-row gap-4 sm:gap-6'>
|
||||
<div className='lg:max-w-[234px] w-full'>
|
||||
<div className=''>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F] mb-1'>
|
||||
شماره سفارش: <span className='text-black'> {order.id}</span>
|
||||
</div>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
|
||||
مبلغ: <span className='text-black'> {order.amount} تومان</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-4 sm:mb-6 mt-3 sm:mt-4'>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F]'>
|
||||
وضعیت سفارش : <span className='text-black'>{order.status}</span>
|
||||
</div>
|
||||
<div className='text-xs sm:text-sm text-[#7F7F7F] mt-3 sm:mt-4'>
|
||||
تاریخ ثبت سفارش: <span className='text-black'> {order.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col sm:flex-row gap-2 sm:gap-3'>
|
||||
<Button
|
||||
variant="outline"
|
||||
className='px-4 sm:px-6 py-2 h-8 sm:h-9 border-[#E0E0E0] text-[#666666] hover:bg-gray-50 text-xs sm:text-sm'
|
||||
>
|
||||
جزئیات سفارش
|
||||
</Button>
|
||||
<Button className='bg-primary hover:bg-primary/80 px-4 sm:px-6 py-2 h-8 sm:h-9 text-white text-xs sm:text-sm'>
|
||||
پرداخت
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex-1 flex gap-3 sm:gap-4 lg:border-r lg:border-border lg:pr-5 overflow-hidden min-h-[200px] sm:min-h-[225px]'>
|
||||
<Swiper
|
||||
spaceBetween={12}
|
||||
slidesPerView={'auto'}
|
||||
className='h-full w-full'
|
||||
grabCursor={true}
|
||||
>
|
||||
{order.products.map((product) => (
|
||||
<SwiperSlide key={product.id} className='!w-[180px] sm:!w-[200px] lg:!w-[234px]'>
|
||||
<div className='flex w-[180px] sm:w-[200px] lg:w-[234px] h-[180px] sm:h-[200px] lg:h-[224px] flex-col p-4 sm:p-5 lg:p-7 items-center rounded-xl gap-2 border border-border'>
|
||||
<Image src={product.image} className='max-w-[100px] sm:max-w-[120px] lg:max-w-[140px] max-h-[100px] sm:max-h-[120px] lg:max-h-[140px] object-center' alt={product.name} width={100} height={100} />
|
||||
<div className='max-w-[160px] sm:max-w-[180px] lg:max-w-[200px] text-center text-xs sm:text-sm font-light mt-2 sm:mt-4'>{product.name}</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ordersData?.results?.orders.map((order) => (
|
||||
<OrderItem
|
||||
key={order._id.toString()}
|
||||
order={order}
|
||||
getStatusText={getStatusText}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import axios from "@/config/axios";
|
||||
import { OrdersResponse, OrderDetailResponse } from "../types/Types";
|
||||
|
||||
export const getOrders = async (activeTab: string): Promise<OrdersResponse> => {
|
||||
const { data } = await axios.get(`/user/profile/orders?status=${activeTab}`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getOrderDetail = async (
|
||||
orderId: string
|
||||
): Promise<OrderDetailResponse> => {
|
||||
const { data } = await axios.get(`/order/${orderId}/user`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const cancelOrder = async (
|
||||
orderId: string,
|
||||
cancelData: {
|
||||
reason: string;
|
||||
count: number;
|
||||
description: string;
|
||||
}
|
||||
) => {
|
||||
const { data } = await axios.post(`/order/${orderId}/cancel`, cancelData);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getInvoice = async (orderId: string) => {
|
||||
const { data } = await axios.get(`/order/${orderId}/invoice`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,293 @@
|
||||
export interface ProductImage {
|
||||
cover: string;
|
||||
list: string[];
|
||||
}
|
||||
|
||||
export interface ProductSpecification {
|
||||
title: string;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface Brand {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface Variant {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
_id: number;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
seoTitle: string | null;
|
||||
seoDescription: string | null;
|
||||
source: string;
|
||||
description: string;
|
||||
metaDescription: string;
|
||||
tags: string[];
|
||||
advantages: string[];
|
||||
disAdvantages: string[];
|
||||
imagesUrl: ProductImage;
|
||||
isFake: string;
|
||||
voice: string | null;
|
||||
specifications: ProductSpecification[];
|
||||
brand: Brand;
|
||||
category: Category;
|
||||
variants: Variant[];
|
||||
}
|
||||
|
||||
export interface Price {
|
||||
order_limit: number;
|
||||
retailPrice: number;
|
||||
selling_price: number;
|
||||
is_specialSale: boolean;
|
||||
discount_percent: number;
|
||||
specialSale_order_limit: number | null;
|
||||
specialSale_quantity: number | null;
|
||||
specialSale_endDate: string | null;
|
||||
}
|
||||
|
||||
export interface Shop {
|
||||
_id: string;
|
||||
}
|
||||
|
||||
export interface ProductVariant {
|
||||
_id: string;
|
||||
market_status: string;
|
||||
price: Price;
|
||||
stock: number;
|
||||
postingTime: number;
|
||||
isFreeShip: boolean;
|
||||
isWholeSale: boolean;
|
||||
shop: Shop;
|
||||
warranty: number;
|
||||
meterage?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
product: Product;
|
||||
variant: ProductVariant;
|
||||
quantity: number;
|
||||
cancelled_quantity: number;
|
||||
returned_quantity: number;
|
||||
selling_price: number;
|
||||
retail_price: number;
|
||||
discount_percent: number;
|
||||
shipmentCost: number;
|
||||
}
|
||||
|
||||
export interface PaymentMethod {
|
||||
_id: string;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
description: string;
|
||||
provider: string;
|
||||
isActive: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface PriceDetails {
|
||||
shipping_cost: number;
|
||||
process_cost: number;
|
||||
total_retail_price: number;
|
||||
total_payable_price: number;
|
||||
total_discount: number;
|
||||
coupon_discount: number;
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
_id: string;
|
||||
transaction_id: string | null;
|
||||
payment_method: PaymentMethod;
|
||||
priceDetails: PriceDetails;
|
||||
totalPrice: number;
|
||||
paymentStatus: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ShipmentAddress {
|
||||
address: string;
|
||||
city: string;
|
||||
province: string;
|
||||
plaque: string;
|
||||
postalCode: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export interface MappedOrder {
|
||||
orderId: number;
|
||||
orderItems: OrderItem[];
|
||||
payment: Payment;
|
||||
shipmentAddress: ShipmentAddress;
|
||||
orderStatus: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ShopDetails {
|
||||
_id: string;
|
||||
shopName: string;
|
||||
shopCode: number;
|
||||
owner: string;
|
||||
shopDescription: string;
|
||||
telephoneNumber: string;
|
||||
isChatActive: boolean;
|
||||
shopPostalCode: string;
|
||||
logo: string;
|
||||
}
|
||||
|
||||
export interface ShipmentItem {
|
||||
product: Product;
|
||||
variant: ProductVariant;
|
||||
quantity: number;
|
||||
cancelled_quantity: number;
|
||||
returned_quantity: number;
|
||||
selling_price: number;
|
||||
retail_price: number;
|
||||
discount_percent: number;
|
||||
shipmentCost: number;
|
||||
}
|
||||
|
||||
export interface OrderItemDetails {
|
||||
_id: string;
|
||||
shop: ShopDetails;
|
||||
shipmentItems: ShipmentItem[];
|
||||
}
|
||||
|
||||
export interface Shipper {
|
||||
_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
deliveryTime: number;
|
||||
deliveryType: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
_id: number;
|
||||
payment: Payment;
|
||||
orderItems: OrderItemDetails[];
|
||||
shipper: Shipper;
|
||||
trackCode: string | null;
|
||||
itemsCount: number;
|
||||
totalPaymentPrice: number;
|
||||
totalRetailPrice: number;
|
||||
totalSellingPrice: number;
|
||||
totalShipmentCost: number;
|
||||
postingDate: string | null;
|
||||
postingReceipt: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
_id: string;
|
||||
fullName: string;
|
||||
phoneNumber: string;
|
||||
dateOfBirth: string | null;
|
||||
address: {
|
||||
_id: string;
|
||||
};
|
||||
nationalCode: string | null;
|
||||
homeNumber: string | null;
|
||||
}
|
||||
|
||||
export interface Warranty {
|
||||
_id: number;
|
||||
duration: string;
|
||||
logoUrl: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Meterage {
|
||||
_id: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
_id: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ProductVariantDetail {
|
||||
_id: string;
|
||||
market_status: string;
|
||||
price: Price;
|
||||
stock: number;
|
||||
postingTime: number;
|
||||
isFreeShip: boolean;
|
||||
isWholeSale: boolean;
|
||||
shop: ShopDetails;
|
||||
warranty: Warranty;
|
||||
meterage?: Meterage;
|
||||
size?: Size;
|
||||
}
|
||||
|
||||
export interface ShipmentItemDetail {
|
||||
_id: string;
|
||||
product: Product;
|
||||
variant: ProductVariantDetail;
|
||||
quantity: number;
|
||||
cancelled_quantity: number;
|
||||
returned_quantity: number;
|
||||
selling_price: number;
|
||||
retail_price: number;
|
||||
discount_percent: number;
|
||||
shipmentCost: number;
|
||||
}
|
||||
|
||||
export interface OrderItemDetail {
|
||||
_id: string;
|
||||
order_id: number;
|
||||
shop: ShopDetails;
|
||||
shipmentItems: ShipmentItemDetail[];
|
||||
shipper: Shipper;
|
||||
trackCode: string | null;
|
||||
itemsCount: number;
|
||||
totalPaymentPrice: number;
|
||||
totalRetailPrice: number;
|
||||
totalSellingPrice: number;
|
||||
totalShipmentCost: number;
|
||||
postingDate: string | null;
|
||||
postingReceipt: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface CancelReason {
|
||||
_id: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface OrderDetail {
|
||||
_id: number;
|
||||
user: User;
|
||||
payment: Payment;
|
||||
orderItems: OrderItemDetail[];
|
||||
shipmentAddress: ShipmentAddress;
|
||||
orderStatus: string;
|
||||
createdAt: string;
|
||||
cancelReasons?: CancelReason[];
|
||||
}
|
||||
|
||||
export interface OrderDetailResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
order: OrderDetail;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrdersResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
mappedOrders: MappedOrder[];
|
||||
orders: Order[];
|
||||
shipmentAddress: ShipmentAddress;
|
||||
orderStatus: string;
|
||||
createdAt: string;
|
||||
};
|
||||
}
|
||||
+72
-8
@@ -1,16 +1,83 @@
|
||||
import axios from "axios";
|
||||
import { getToken } from "./func";
|
||||
import {
|
||||
getToken,
|
||||
getRefreshToken,
|
||||
setToken,
|
||||
setRefreshToken,
|
||||
removeToken,
|
||||
removeRefreshToken,
|
||||
} from "./func";
|
||||
import { refreshToken } from "../app/auth/service/Service";
|
||||
import { BASE_URL } from "./const";
|
||||
// import { Pages } from "./Pages";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
isRefreshingToken?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
// baseURL: process.env.NEXT_PUBLIC_BASE_URL,
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error?.response?.status === 401 && !originalRequest._retry) {
|
||||
if (window.isRefreshingToken) {
|
||||
// Wait for the refresh to complete
|
||||
await new Promise((resolve) => {
|
||||
const checkComplete = setInterval(() => {
|
||||
if (!window.isRefreshingToken) {
|
||||
clearInterval(checkComplete);
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
window.isRefreshingToken = true;
|
||||
|
||||
try {
|
||||
const refreshTokenValue = await getRefreshToken();
|
||||
if (!refreshTokenValue) {
|
||||
await removeRefreshToken();
|
||||
await removeToken();
|
||||
window.location.href = `/auth`;
|
||||
return;
|
||||
}
|
||||
const { data } = await refreshToken({
|
||||
token: refreshTokenValue,
|
||||
});
|
||||
|
||||
if (data?.accessToken?.token) {
|
||||
await setToken(data?.accessToken?.token);
|
||||
if (data.refreshToken?.token) {
|
||||
await setRefreshToken(data.refreshToken?.token);
|
||||
}
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${data.accessToken.token}`;
|
||||
return axiosInstance(originalRequest);
|
||||
} else {
|
||||
await removeRefreshToken();
|
||||
await removeToken();
|
||||
window.location.href = `/auth`;
|
||||
return;
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
await removeToken();
|
||||
await removeRefreshToken();
|
||||
window.location.href = `/auth`;
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
window.isRefreshingToken = false;
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@@ -18,10 +85,7 @@ axiosInstance.interceptors.response.use(
|
||||
axiosInstance.interceptors.request.use(async function (config) {
|
||||
const tokenValue = await getToken();
|
||||
const token = tokenValue ? tokenValue : "";
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = "Bearer " + token;
|
||||
}
|
||||
config.headers.Authorization = "Bearer " + token;
|
||||
return config;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user