Files
gastro-bot/src/features/profile/ui/RefreshOrder.tsx
2025-12-26 17:13:34 +05:00

568 lines
21 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { cart_api, OrderCreateBody } from '@/features/cart/lib/api';
import { orderForm } from '@/features/cart/lib/form';
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 { Calendar } from '@/shared/ui/calendar';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/shared/ui/form';
import { Input } from '@/shared/ui/input';
import { Label } from '@/shared/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/shared/ui/popover';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/shared/ui/select';
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 {
Calendar as CalIcon,
CheckCircle2,
Clock,
Loader2,
LocateFixed,
MapPin,
User,
} 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';
const deliveryTimeSlots = [
{ id: 1, label: '09:00 - 11:00', start: '09:00', end: '11:00' },
{ id: 2, label: '11:00 - 13:00', start: '11:00', end: '13:00' },
{ id: 3, label: '13:00 - 15:00', start: '13:00', end: '15:00' },
{ id: 4, label: '15:00 - 17:00', start: '15:00', end: '17:00' },
{ id: 5, label: '17:00 - 19:00', start: '17:00', end: '19:00' },
{ id: 6, label: '19:00 - 21:00', start: '19:00', end: '21:00' },
];
interface CoordsData {
lat: number;
lon: number;
polygon: [number, number][][];
}
const RefreshOrder = () => {
const [deliveryDate, setDeliveryDate] = useState<Date>();
const [selectedTimeSlot, setSelectedTimeSlot] = useState<string>('');
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<z.infer<typeof orderForm>>({
resolver: zodResolver(orderForm),
defaultValues: {
comment: initialValues?.comment,
lat: '41.311081',
long: '69.240562',
},
});
const [orderSuccess, setOrderSuccess] = useState(false);
const subtotal = cartItems
? cartItems.reduce(
(sum, item) => sum + item.product_price * item.quantity,
0,
)
: 0;
const total = subtotal;
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<CoordsData | null> => {
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<MouseEvent, { coords: [number, number] }>,
) => {
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 qollab-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<typeof orderForm>) => {
if (!deliveryDate) {
toast.error('Yetkazib berish sanasini tanlang', {
richColors: true,
position: 'top-center',
});
return;
}
if (!selectedTimeSlot) {
toast.error('Yetkazib berish vaqtini tanlang', {
richColors: true,
position: 'top-center',
});
return;
}
if (initialValues === null) {
toast.error('Savatcha bosh', {
richColors: true,
position: 'top-center',
});
return;
}
const items = initialValues.items.map((item) => ({
product_id: Number(item.product.id),
quantity: item.quantity,
}));
mutate({
comment: value.comment,
items: items,
long: Number(value.long),
lat: Number(value.lat),
date: formatDate.format(deliveryDate, 'YYYY-MM-DD'),
time: selectedTimeSlot,
});
};
if (orderSuccess) {
return (
<div className="flex justify-center items-center h-screen">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full text-center">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle2 className="w-12 h-12 text-green-600" />
</div>
<h2 className="text-2xl font-bold text-gray-800 mb-2">
{t('Buyurtma qabul qilindi!')}
</h2>
<p className="text-gray-500 mb-6">
{t('Buyurtmangiz muvaffaqiyatli qabul qilindi')}
</p>
<button
onClick={() => (window.location.href = '/')}
className="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700 transition"
>
{t('Bosh sahifaga qaytish')}
</button>
</div>
</div>
);
}
return (
<div className="custom-container mb-5">
<>
{/* Header */}
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-800 mb-2">
{t('Buyurtmani rasmiylashtirish')}
</h1>
<p className="text-gray-600">{t("Ma'lumotlaringizni to'ldiring")}</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Forms */}
<div className="lg:col-span-2 space-y-6">
{/* Contact Information */}
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center gap-2 mb-4">
<User className="w-5 h-5 text-blue-600" />
<h2 className="text-xl font-semibold">
{t("Shaxsiy ma'lumotlar")}
</h2>
</div>
<FormField
control={form.control}
name="comment"
render={({ field }) => (
<FormItem>
<Label className="block mt-3 text-sm font-medium text-gray-700">
{t('Izoh')}
</Label>
<FormControl>
<Textarea
{...field}
className="w-full min-h-42 max-h-64 border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
placeholder={t('Izoh')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center gap-2 mb-4">
<MapPin className="w-5 h-5 text-blue-600" />
<h2 className="text-xl font-semibold">
{t('Yetkazib berish manzili')}
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="city"
render={({ field }) => (
<FormItem>
<Label className="block text-sm font-medium text-gray-700">
{t('Manzilni qidirish')}
</Label>
<FormControl>
<Input
{...field}
type="text"
className="w-full border h-12 border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:border-blue-500"
placeholder={t('Toshkent')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="md:col-span-2">
<div className="relative h-80 border rounded-lg overflow-hidden">
<YMaps query={{ lang: 'en_RU' }}>
<Map
key={`${coords.latitude}-${coords.longitude}`}
state={{
center: [coords.latitude, coords.longitude],
zoom: coords.zoom,
}}
width="100%"
height="100%"
onClick={handleMapClick}
>
<ZoomControl
options={{
position: { right: '10px', bottom: '70px' },
}}
/>
<Placemark
geometry={[coords.latitude, coords.longitude]}
/>
{polygonCoords && (
<Polygon
geometry={polygonCoords}
options={{
fillColor: 'rgba(0, 150, 255, 0.2)',
strokeColor: 'rgba(0, 150, 255, 0.8)',
strokeWidth: 2,
interactivityModel: 'default#transparent',
}}
/>
)}
</Map>
</YMaps>
<Button
type="button"
size="sm"
onClick={handleShowMyLocation}
className="absolute bottom-3 right-2.5 shadow-md bg-white text-black hover:bg-gray-100"
>
<LocateFixed className="w-4 h-4 mr-1" />
{t('Mening joylashuvim')}
</Button>
</div>
</div>
</div>
</div>
{/* Yetkazib berish vaqti - Yangilangan versiya */}
<div className="bg-white rounded-lg shadow-md p-6">
<div className="flex items-center gap-2 mb-4">
<Clock className="w-5 h-5 text-blue-600" />
<h2 className="text-xl font-semibold">
{t('Yetkazib berish vaqti')}
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Sana tanlash */}
<div className="space-y-2">
<Label className="block text-sm font-medium text-gray-700">
{t('Yetkazib berish sanasi')}
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
className={cn(
'w-full h-12 justify-start text-left font-normal',
!deliveryDate && 'text-muted-foreground',
)}
>
<CalIcon className="mr-2 h-4 w-4" />
{deliveryDate ? (
formatDate.format(deliveryDate, 'DD-MM-YYYY')
) : (
<span>{t('Sanani tanlang')}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={deliveryDate}
captionLayout="dropdown"
toYear={new Date().getFullYear() + 10}
onSelect={setDeliveryDate}
disabled={(date) =>
date < new Date() ||
date < new Date(new Date().setHours(0, 0, 0, 0))
}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
{/* Vaqt tanlash */}
<div className="space-y-2">
<Label className="block text-sm font-medium text-gray-700">
{t("Vaqt oralig'i")}
</Label>
<Select
value={selectedTimeSlot}
onValueChange={setSelectedTimeSlot}
>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder={t('Vaqtni tanlang')} />
</SelectTrigger>
<SelectContent>
{deliveryTimeSlots.map((slot) => (
<SelectItem key={slot.id} value={slot.label}>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-500" />
{slot.label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Tanlangan vaqt ko'rinishi */}
{deliveryDate && selectedTimeSlot && (
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0">
<CalIcon className="w-5 h-5 text-blue-600" />
</div>
<div>
<p className="font-semibold text-gray-800">
{t('Tanlangan yetkazib berish vaqti')}
</p>
<p className="text-sm text-gray-600 mt-1">
{formatDate.format(deliveryDate, 'DD-MM-YYYY')} |{' '}
{selectedTimeSlot}
</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* Right Column - Order Summary */}
<div className="lg:col-span-1">
<div className="bg-white rounded-lg shadow-md p-6 sticky top-4">
<h3 className="text-xl font-bold mb-4">{t('Mahsulotlar')}</h3>
{/* Cart Items */}
<div className="space-y-3 mb-4 max-h-60 overflow-y-auto">
{cartItems?.map((item) => (
<div key={item.id} className="flex gap-3 pb-3 border-b">
<Image
width={500}
height={500}
src={item.product_image}
alt={item.product_name}
className="w-16 h-16 object-contain bg-gray-100 rounded"
/>
<div className="flex-1">
<h4 className="font-medium text-sm">
{item.product_name}
</h4>
<p className="text-sm text-gray-500">
{item.quantity} x{' '}
{formatPrice(item.product_price, true)}
</p>
<p className="font-semibold text-sm">
{formatPrice(
item.product_price * item.quantity,
true,
)}
</p>
</div>
</div>
))}
</div>
{/* Pricing */}
<div className="space-y-2 mb-4 pt-4 border-t">
<div className="flex justify-between text-gray-600">
<span>{t('Mahsulotlar')}:</span>
<span>{subtotal && formatPrice(subtotal, true)}</span>
</div>
</div>
<div className="border-t pt-4 mb-6">
<div className="flex justify-between items-center">
<span className="text-lg font-semibold">
{t('Jami')}:
</span>
<span className="text-2xl font-bold text-blue-600">
{total && formatPrice(total, true)}
</span>
</div>
</div>
<button
type="submit"
disabled={isPending}
className="w-full bg-blue-600 text-white py-4 rounded-lg font-semibold hover:bg-blue-700 transition disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{isPending ? (
<span className="flex items-center justify-center gap-2">
<Loader2 className="animate-spin" />
</span>
) : (
t('Buyurtmani tasdiqlash')
)}
</button>
</div>
</div>
</div>
</form>
</Form>
</>
</div>
);
};
export default RefreshOrder;