diff --git a/src/app/[locale]/profile/refresh-order/page.tsx b/src/app/[locale]/profile/refresh-order/page.tsx new file mode 100644 index 0000000..18a5fa2 --- /dev/null +++ b/src/app/[locale]/profile/refresh-order/page.tsx @@ -0,0 +1,7 @@ +import RefreshOrder from '@/features/profile/ui/RefreshOrder'; + +const Page = () => { + return ; +}; + +export default Page; diff --git a/src/features/cart/lib/api.ts b/src/features/cart/lib/api.ts index f665883..91b7256 100644 --- a/src/features/cart/lib/api.ts +++ b/src/features/cart/lib/api.ts @@ -24,6 +24,8 @@ export interface OrderCreateBody { contact_number: string; comment: string; name: string; + long: number; + lat: number; } export const cart_api = { diff --git a/src/features/cart/lib/form.ts b/src/features/cart/lib/form.ts index e179db3..688b713 100644 --- a/src/features/cart/lib/form.ts +++ b/src/features/cart/lib/form.ts @@ -1,8 +1,7 @@ import { z } from 'zod'; export const orderForm = z.object({ - firstName: z.string().min(1, { message: 'Majburiy maydon' }), - lastName: z.string().min(1, { message: 'Majburiy maydon' }), + username: z.string().min(1, { message: 'Majburiy maydon' }), phone: z.string().min(12, { message: 'Xato raqam kiritildi' }), long: z.string().min(1, { message: 'Majburiy maydon' }), lat: z.string().min(1, { message: 'Majburiy maydon' }), diff --git a/src/features/cart/ui/CartPage.tsx b/src/features/cart/ui/CartPage.tsx index 20e3878..12e0c46 100644 --- a/src/features/cart/ui/CartPage.tsx +++ b/src/features/cart/ui/CartPage.tsx @@ -158,7 +158,7 @@ const CartPage = () => { width={500} height={500} className="object-cover" - style={{ width: '100%', height: 'auto' }} + style={{ width: '100%', height: '100%' }} /> diff --git a/src/features/cart/ui/OrderPage.tsx b/src/features/cart/ui/OrderPage.tsx index d35762f..0eb740d 100644 --- a/src/features/cart/ui/OrderPage.tsx +++ b/src/features/cart/ui/OrderPage.tsx @@ -57,11 +57,10 @@ const OrderPage = () => { const form = useForm>({ resolver: zodResolver(orderForm), defaultValues: { - firstName: '', + username: '', comment: '', - lastName: '', - lat: '', - long: '', + lat: '41.311081', + long: '69.240562', phone: '+998', }, }); @@ -232,9 +231,11 @@ const OrderPage = () => { comment: value.comment, contact_number: onlyNumber(value.phone), delivery_type: deliveryMethod, - name: value.firstName + ' ' + value.lastName, + name: value.username, payment_type: paymentMethod, items: items, + long: Number(value.long), + lat: Number(value.lat), }); } @@ -288,37 +289,17 @@ const OrderPage = () => {
( - - - - )} - /> - - ( - - - - diff --git a/src/features/profile/lib/api.ts b/src/features/profile/lib/api.ts index 96aec0a..a54850d 100644 --- a/src/features/profile/lib/api.ts +++ b/src/features/profile/lib/api.ts @@ -3,9 +3,12 @@ import { API_URLS } from '@/shared/config/api/URLs'; import { AxiosResponse } from 'axios'; export interface OrderList { - count: number; - next: string; - previous: string; + total: number; + page: number; + page_size: number; + total_pages: number; + has_next: boolean; + has_previous: boolean; results: OrderListRes[]; } diff --git a/src/features/profile/lib/order.ts b/src/features/profile/lib/order.ts new file mode 100644 index 0000000..3bf7f09 --- /dev/null +++ b/src/features/profile/lib/order.ts @@ -0,0 +1,35 @@ +// store/orderStore.ts +import { create } from 'zustand'; +import { OrderListRes } from './api'; + +type State = { + order: OrderListRes | null; +}; + +type Actions = { + setOrder: (order: OrderListRes) => void; +}; + +const getInitialOrder = (): OrderListRes | null => { + if (typeof window === 'undefined') return null; // SSR check + const stored = localStorage.getItem('order'); + if (!stored) return null; + try { + return JSON.parse(stored); + } catch (err) { + console.error('Failed to parse order from localStorage', err); + return null; + } +}; + +const useOrderStore = create((set) => ({ + order: getInitialOrder(), + setOrder: (order: OrderListRes) => { + if (typeof window !== 'undefined') { + localStorage.setItem('order', JSON.stringify(order)); + } + set({ order }); + }, +})); + +export default useOrderStore; diff --git a/src/features/profile/ui/History.tsx b/src/features/profile/ui/History.tsx index 9a1afc5..d3daedd 100644 --- a/src/features/profile/ui/History.tsx +++ b/src/features/profile/ui/History.tsx @@ -1,95 +1,245 @@ +import { usePathname, useRouter } from '@/shared/config/i18n/navigation'; +import formatDate from '@/shared/lib/formatDate'; +import formatPrice from '@/shared/lib/formatPrice'; +import { cn } from '@/shared/lib/utils'; import { Button } from '@/shared/ui/button'; import { Card, CardContent } from '@/shared/ui/card'; +import { GlobalPagination } from '@/shared/ui/global-pagination'; import { useQuery } from '@tanstack/react-query'; -import { Calendar, CheckCircle, Clock, RefreshCw } from 'lucide-react'; +import { + Calendar, + CheckCircle, + Clock, + Loader2, + Package, + RefreshCw, +} from 'lucide-react'; import { useTranslations } from 'next-intl'; -import Image from 'next/image'; -import { order_api } from '../lib/api'; -import { orders } from '../lib/data'; +import { useSearchParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { order_api, OrderListRes } from '../lib/api'; +import useOrderStore from '../lib/order'; const HistoryTabs = () => { const t = useTranslations(); + const searchParams = useSearchParams(); + const { setOrder } = useOrderStore(); + const [page, setPage] = useState(1); + const PAGE_SIZE = 36; + const router = useRouter(); + const pathname = usePathname(); - const { data } = useQuery({ - queryKey: ['order_list'], - queryFn: () => order_api.list({ page: 1, page_size: 1 }), + const { data, isLoading } = useQuery({ + queryKey: ['order_list', page], + queryFn: () => order_api.list({ page, page_size: PAGE_SIZE }), + select(data) { + return data.data; + }, }); - console.log(data); + useEffect(() => { + const urlPage = Number(searchParams.get('page')) || 1; + setPage(urlPage); + }, [searchParams]); + + const handlePageChange = (newPage: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set('page', newPage.toString()); + + router.push(`${pathname}?${params.toString()}`, { + scroll: true, + }); + }; + + const getStatusConfig = (status: 'NEW' | 'DONE') => { + return status === 'DONE' + ? { + bgColor: 'bg-emerald-100', + textColor: 'text-emerald-600', + icon: CheckCircle, + text: 'Yetkazildi', + } + : { + bgColor: 'bg-yellow-100', + textColor: 'text-yellow-600', + icon: Clock, + text: 'Kutilmoqda', + }; + }; + + const getPaymentTypeText = (type: 'CASH' | 'ACCOUNT_NUMBER') => { + return type === 'CASH' ? 'Naqd pul' : 'Hisob raqami'; + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!data?.results || data.results.length === 0) { + return ( +
+ +

+ {t('Buyurtmalar topilmadi')} +

+

+ {t('Hali buyurtma qilmagansiz')} +

+
+ ); + } return ( <>

- {t('Tarix')} + {t('Buyurtmalar tarixi')}

+
+ {data.results.length} ta buyurtma +
+
- {orders - .filter((o) => o.status === 'delivered') - .map((order, idx) => ( + {data.results.map((order: OrderListRes, idx: number) => { + const statusConfig = getStatusConfig(order.status); + const StatusIcon = statusConfig.icon; + + return (
+ {/* Status Timeline */}
-
- +
+
- {idx < - orders.filter((o) => o.status === 'delivered').length - 1 && ( -
+ + {idx < data.results.length - 1 && ( +
)}
- + + {/* Order Card */} + -
-
-

- {order.id} -

+ {/* Header */} +
+
+
+

+ #{order.order_number} +

+ + {statusConfig.text} + +
- {order.date} - - - - {order.time} + {formatDate.format( + order.created_at, + 'DD.MM.YYYY HH:mm', + )}
-

- {(order.total + order.deliveryFee).toLocaleString()}{' '} - {"so'm"} -

+
+

+ {formatPrice( + order.total_price + order.delivery_price, + true, + )} +

+

+ {getPaymentTypeText(order.payment_type)} +

+
-
- {order.items.map((item, i) => ( - {item.name} - ))} - - {order.items.map((i) => i.name).join(', ')} - + + {order.comment && ( +
+

+ Izoh: +

+

{order.comment}

+
+ )} + + {/* Total Price Breakdown */} +
+
+ + Mahsulotlar narxi: + + + {formatPrice(order.total_price, true)} + +
+
+ Yetkazish: + + {formatPrice(order.delivery_price, true)} + +
+
+ Jami: + + {formatPrice( + order.total_price + order.delivery_price, + true, + )} + +
-
+ + {/* Actions */} +
- ))} + ); + })} +
+ +
+
); diff --git a/src/features/profile/ui/RefreshOrder.tsx b/src/features/profile/ui/RefreshOrder.tsx new file mode 100644 index 0000000..ff9ad29 --- /dev/null +++ b/src/features/profile/ui/RefreshOrder.tsx @@ -0,0 +1,637 @@ +'use client'; + +import { cart_api, OrderCreateBody } from '@/features/cart/lib/api'; +import { orderForm } from '@/features/cart/lib/form'; +import formatPhone from '@/shared/lib/formatPhone'; +import formatPrice from '@/shared/lib/formatPrice'; +import onlyNumber from '@/shared/lib/onlyNumber'; +import { Button } from '@/shared/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/shared/ui/form'; +import { Input } from '@/shared/ui/input'; +import { Label } from '@/shared/ui/label'; +import { Textarea } from '@/shared/ui/textarea'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + Map, + Placemark, + Polygon, + YMaps, + ZoomControl, +} from '@pbe/react-yandex-maps'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + CheckCircle2, + Clock, + CreditCard, + Loader2, + LocateFixed, + MapPin, + Package, + Truck, + User, + Wallet, +} from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import Image from 'next/image'; +import { useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import z from 'zod'; +import useOrderStore from '../lib/order'; + +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; +} + +const RefreshOrder = () => { + const { order: initialValues } = useOrderStore(); + const t = useTranslations(); + const queryClient = useQueryClient(); + + const initialCartItems = initialValues?.items.map((item) => ({ + id: item.id, + product_id: item.product.id, + product_name: item.product.name, + product_price: item.price, + product_image: + item.product.image || + item.product.images?.[0]?.image || + '/placeholder.svg', + quantity: item.quantity, + })); + + const cartItems = initialCartItems; + + const form = useForm>({ + resolver: zodResolver(orderForm), + defaultValues: { + username: initialValues?.name, + comment: initialValues?.comment, + lat: '41.311081', + long: '69.240562', + phone: initialValues?.contact_number, + }, + }); + + const [orderSuccess, setOrderSuccess] = useState(false); + const [paymentMethod, setPaymentMethod] = useState<'CASH' | 'ACCOUNT_NUMBER'>( + 'CASH', + ); + const [deliveryMethod, setDeliveryMethod] = useState< + 'YandexGo' | 'DELIVERY_COURIES' | 'PICKUP' + >('DELIVERY_COURIES'); + + useEffect(() => { + if (initialValues) { + setPaymentMethod(initialValues.payment_type); + setDeliveryMethod(initialValues.delivery_type); + } + }, [initialValues]); + + const subtotal = cartItems + ? cartItems.reduce( + (sum, item) => sum + item.product_price * item.quantity, + 0, + ) + : 0; + + const deliveryFee = + deliveryMethod === 'DELIVERY_COURIES' + ? subtotal > 50000 + ? 0 + : 15000 + : deliveryMethod === 'YandexGo' + ? 25000 + : 0; + + const total = subtotal + deliveryFee; + + const { mutate, isPending } = useMutation({ + mutationFn: (body: OrderCreateBody) => cart_api.createOrder(body), + onSuccess: () => { + setOrderSuccess(true); + queryClient.refetchQueries({ queryKey: ['cart_items'] }); + }, + onError: () => { + toast.error('Xatolik yuz berdi', { + richColors: true, + position: 'top-center', + }); + }, + }); + + const [coords, setCoords] = useState({ + latitude: 41.311081, + longitude: 69.240562, + zoom: 12, + }); + const [polygonCoords, setPolygonCoords] = useState< + [number, number][][] | null + >(null); + + const getCoords = async (name: string): Promise => { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent( + name, + )}&format=json&polygon_geojson=1&limit=1`, + ); + const data = await res.json(); + if (data.length > 0 && data[0].geojson) { + const lat = parseFloat(data[0].lat); + const lon = parseFloat(data[0].lon); + let polygon: [number, number][][] = []; + if (data[0].geojson.type === 'Polygon') { + polygon = data[0].geojson.coordinates.map((ring: [number, number][]) => + ring.map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } else if (data[0].geojson.type === 'MultiPolygon') { + polygon = data[0].geojson.coordinates.map( + (poly: [number, number][][]) => + poly[0].map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } + return { lat, lon, polygon }; + } + return null; + }; + + const handleMapClick = ( + e: ymaps.IEvent, + ) => { + const [lat, lon] = e.get('coords'); + setCoords({ latitude: lat, longitude: lon, zoom: 18 }); + form.setValue('lat', lat.toString(), { shouldDirty: true }); + form.setValue('long', lon.toString(), { shouldDirty: true }); + }; + + const handleShowMyLocation = () => { + if (!navigator.geolocation) { + alert('Sizning brauzeringiz geolokatsiyani qo‘llab-quvvatlamaydi'); + return; + } + navigator.geolocation.getCurrentPosition( + (position) => { + const lat = position.coords.latitude; + const lon = position.coords.longitude; + setCoords({ latitude: lat, longitude: lon, zoom: 14 }); + form.setValue('lat', lat.toString()); + form.setValue('long', lon.toString()); + }, + (error) => { + alert('Joylashuv aniqlanmadi: ' + error.message); + }, + ); + }; + + const cityValue = form.watch('city'); + + useEffect(() => { + if (!cityValue || cityValue.length < 3) return; + const timeout = setTimeout(async () => { + const result = await getCoords(cityValue); + if (!result) return; + setCoords({ + latitude: result.lat, + longitude: result.lon, + zoom: 12, + }); + setPolygonCoords(result.polygon); + form.setValue('lat', result.lat.toString(), { shouldDirty: true }); + form.setValue('long', result.lon.toString(), { shouldDirty: true }); + }, 700); + return () => clearTimeout(timeout); + }, [cityValue]); + + const onSubmit = (value: z.infer) => { + if (initialValues === null) { + toast.error('Savatcha bo‘sh', { + richColors: true, + position: 'top-center', + }); + return; + } + + const items = initialValues.items.map((item) => ({ + product_id: item.product.id, + quantity: item.quantity, + })); + + mutate({ + comment: value.comment, + contact_number: onlyNumber(value.phone), + delivery_type: deliveryMethod, + name: value.username, + payment_type: paymentMethod, + items: items, + long: Number(value.long), + lat: Number(value.lat), + }); + }; + + if (orderSuccess) { + return ( +
+
+
+ +
+

+ {t('Buyurtma qabul qilindi!')} +

+

+ {t('Buyurtmangiz muvaffaqiyatli qabul qilindi')} +

+ +
+
+ ); + } + + return ( +
+ <> + {/* Header */} +
+

+ {t('Buyurtmani rasmiylashtirish')} +

+

{t("Ma'lumotlaringizni to'ldiring")}

+
+
+ +
+ {/* Left Column - Forms */} +
+ {/* Contact Information */} +
+
+ +

+ {t("Shaxsiy ma'lumotlar")} +

+
+
+ ( + + + + + + + + )} + /> + + ( + + + + field.onChange(e.target.value)} + type="tel" + className="w-full h-12 border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500" + placeholder="+998 90 123 45 67" + /> + + + + )} + /> +
+ ( + + + +