refresh order update
This commit is contained in:
@@ -187,7 +187,7 @@ const ProductDetail = () => {
|
|||||||
|
|
||||||
/* ---------------- HANDLERS ---------------- */
|
/* ---------------- HANDLERS ---------------- */
|
||||||
const handleAddToCart = () => {
|
const handleAddToCart = () => {
|
||||||
if (user == null) {
|
if (user === null) {
|
||||||
router.push('/auth');
|
router.push('/auth');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@ const ProductDetail = () => {
|
|||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (user == null) {
|
if (user === null) {
|
||||||
router.push('/auth');
|
router.push('/auth');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,54 +7,56 @@ export interface OrderList {
|
|||||||
user: number;
|
user: number;
|
||||||
comment: string;
|
comment: string;
|
||||||
delivery_date: string;
|
delivery_date: string;
|
||||||
items: {
|
items: OrderItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default interface OrderItem {
|
||||||
|
id: number;
|
||||||
|
quantity: number;
|
||||||
|
price: string;
|
||||||
|
product: {
|
||||||
id: number;
|
id: number;
|
||||||
quantity: number;
|
images: { id: number; images: string | null }[];
|
||||||
price: string;
|
liked: false;
|
||||||
product: {
|
meansurement: null | string;
|
||||||
|
inventory_id: null | string;
|
||||||
|
product_id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
short_name: string;
|
||||||
|
weight_netto: null | number;
|
||||||
|
weight_brutto: null | number;
|
||||||
|
litr: null | number;
|
||||||
|
box_type_code: null | number;
|
||||||
|
box_quant: null | number;
|
||||||
|
groups: {
|
||||||
id: number;
|
id: number;
|
||||||
images: { id: number; images: string | null }[];
|
|
||||||
liked: false;
|
|
||||||
meansurement: null | string;
|
|
||||||
inventory_id: null | string;
|
|
||||||
product_id: string;
|
|
||||||
code: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
short_name: string;
|
}[];
|
||||||
weight_netto: null | number;
|
state: 'A' | 'P';
|
||||||
weight_brutto: null | number;
|
barcodes: string;
|
||||||
litr: null | number;
|
article_code: null | string;
|
||||||
box_type_code: null | number;
|
marketing_group_code: null | string;
|
||||||
box_quant: null | number;
|
inventory_kinds: {
|
||||||
groups: {
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
sector_codes: [];
|
||||||
|
prices: {
|
||||||
|
id: number;
|
||||||
|
price: string;
|
||||||
|
price_type: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
code: string;
|
||||||
state: 'A' | 'P';
|
};
|
||||||
barcodes: string;
|
}[];
|
||||||
article_code: null | string;
|
|
||||||
marketing_group_code: null | string;
|
|
||||||
inventory_kinds: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
sector_codes: [];
|
payment_type: null | string;
|
||||||
prices: {
|
balance: number;
|
||||||
id: number;
|
updated_at: string;
|
||||||
price: string;
|
};
|
||||||
price_type: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
|
|
||||||
payment_type: null | string;
|
|
||||||
balance: number;
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderListRes {
|
export interface OrderListRes {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/shared/ui/select';
|
} from '@/shared/ui/select';
|
||||||
import { Textarea } from '@/shared/ui/textarea';
|
import { Textarea } from '@/shared/ui/textarea';
|
||||||
|
import { userStore } from '@/widgets/welcome/lib/hook';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import {
|
import {
|
||||||
Map,
|
Map,
|
||||||
@@ -42,18 +43,21 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
LocateFixed,
|
LocateFixed,
|
||||||
MapPin,
|
MapPin,
|
||||||
|
Minus,
|
||||||
Package,
|
Package,
|
||||||
|
Plus,
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
|
Trash2,
|
||||||
User,
|
User,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { order_api } from '../lib/api';
|
import OrderItem, { order_api } from '../lib/api';
|
||||||
|
|
||||||
const deliveryTimeSlots = [
|
const deliveryTimeSlots = [
|
||||||
{ id: 1, label: '10:00 - 12:00', start: '10:00', end: '12:00' },
|
{ id: 1, label: '10:00 - 12:00', start: '10:00', end: '12:00' },
|
||||||
@@ -61,6 +65,7 @@ const deliveryTimeSlots = [
|
|||||||
{ id: 3, label: '14:00 - 16:00', start: '14:00', end: '16:00' },
|
{ id: 3, label: '14:00 - 16:00', start: '14:00', end: '16:00' },
|
||||||
{ id: 4, label: '16:00 - 18:00', start: '16:00', end: '18:00' },
|
{ id: 4, label: '16:00 - 18:00', start: '16:00', end: '18:00' },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface CoordsData {
|
interface CoordsData {
|
||||||
lat: number;
|
lat: number;
|
||||||
lon: number;
|
lon: number;
|
||||||
@@ -70,9 +75,16 @@ interface CoordsData {
|
|||||||
const RefreshOrder = () => {
|
const RefreshOrder = () => {
|
||||||
const [deliveryDate, setDeliveryDate] = useState<Date>();
|
const [deliveryDate, setDeliveryDate] = useState<Date>();
|
||||||
const [selectedTimeSlot, setSelectedTimeSlot] = useState<string>('');
|
const [selectedTimeSlot, setSelectedTimeSlot] = useState<string>('');
|
||||||
|
const [orderItems, setOrderItems] = useState<OrderItem[]>([]);
|
||||||
|
const { user } = userStore();
|
||||||
|
const [quantityInputs, setQuantityInputs] = useState<Record<number, string>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
const id = searchParams.get('id');
|
const id = searchParams.get('id');
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
@@ -83,6 +95,18 @@ const RefreshOrder = () => {
|
|||||||
|
|
||||||
const initialValues = data?.find((e) => e.id === Number(id));
|
const initialValues = data?.find((e) => e.id === Number(id));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialValues?.items) {
|
||||||
|
const items = initialValues.items.map((item: OrderItem) => ({ ...item }));
|
||||||
|
setOrderItems(items);
|
||||||
|
const inputs: Record<number, string> = {};
|
||||||
|
items.forEach((item: OrderItem) => {
|
||||||
|
inputs[item.id] = String(item.quantity);
|
||||||
|
});
|
||||||
|
setQuantityInputs(inputs);
|
||||||
|
}
|
||||||
|
}, [initialValues]);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof orderForm>>({
|
const form = useForm<z.infer<typeof orderForm>>({
|
||||||
resolver: zodResolver(orderForm),
|
resolver: zodResolver(orderForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -92,7 +116,6 @@ const RefreshOrder = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update form when initialValues loads
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues?.comment) {
|
if (initialValues?.comment) {
|
||||||
form.setValue('comment', initialValues.comment);
|
form.setValue('comment', initialValues.comment);
|
||||||
@@ -125,6 +148,66 @@ const RefreshOrder = () => {
|
|||||||
[number, number][][] | null
|
[number, number][][] | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
// Input o'zgarishi — foydalanuvchi "0.5", "1.75" yoza oladi
|
||||||
|
const handleQuantityInput = (itemId: number, value: string) => {
|
||||||
|
// Faqat raqam va nuqtaga ruxsat
|
||||||
|
if (!/^(\d+\.?\d*)?$/.test(value)) return;
|
||||||
|
setQuantityInputs((prev) => ({ ...prev, [itemId]: value }));
|
||||||
|
|
||||||
|
const parsed = parseFloat(value);
|
||||||
|
if (!isNaN(parsed) && parsed > 0) {
|
||||||
|
setOrderItems((prev) =>
|
||||||
|
prev.map((item) =>
|
||||||
|
item.id === itemId ? { ...item, quantity: parsed } : item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Blur — bo'sh yoki 0 qiymatni 1 ga qaytarish
|
||||||
|
const handleQuantityBlur = (itemId: number) => {
|
||||||
|
const val = parseFloat(quantityInputs[itemId]);
|
||||||
|
if (!val || val <= 0) {
|
||||||
|
setQuantityInputs((prev) => ({ ...prev, [itemId]: '1' }));
|
||||||
|
setOrderItems((prev) =>
|
||||||
|
prev.map((item) =>
|
||||||
|
item.id === itemId ? { ...item, quantity: 1 } : item,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ± tugmalar — 0.5 qadamda o'zgartirish
|
||||||
|
const updateQuantity = (itemId: number, delta: number) => {
|
||||||
|
setOrderItems((prev) =>
|
||||||
|
prev.map((item) => {
|
||||||
|
if (item.id !== itemId) return item;
|
||||||
|
const newQty = Math.max(
|
||||||
|
0.5,
|
||||||
|
Math.round((item.quantity + delta) * 100) / 100,
|
||||||
|
);
|
||||||
|
setQuantityInputs((inputs) => ({
|
||||||
|
...inputs,
|
||||||
|
[itemId]: String(newQty),
|
||||||
|
}));
|
||||||
|
return { ...item, quantity: newQty };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Item o'chirish — agar 0 ta qolsa profile sahifaga yo'naltiradi
|
||||||
|
const deleteItem = (itemId: number) => {
|
||||||
|
const updated = orderItems.filter((item) => item.id !== itemId);
|
||||||
|
setOrderItems(updated);
|
||||||
|
if (updated.length === 0) {
|
||||||
|
toast.info(t('Buyurtmada mahsulot qolmadi'), {
|
||||||
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
router.push('/profile');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
|
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
|
||||||
@@ -186,11 +269,7 @@ const RefreshOrder = () => {
|
|||||||
const timeout = setTimeout(async () => {
|
const timeout = setTimeout(async () => {
|
||||||
const result = await getCoords(cityValue);
|
const result = await getCoords(cityValue);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
setCoords({
|
setCoords({ latitude: result.lat, longitude: result.lon, zoom: 12 });
|
||||||
latitude: result.lat,
|
|
||||||
longitude: result.lon,
|
|
||||||
zoom: 12,
|
|
||||||
});
|
|
||||||
setPolygonCoords(result.polygon);
|
setPolygonCoords(result.polygon);
|
||||||
form.setValue('lat', result.lat.toString(), { shouldDirty: true });
|
form.setValue('lat', result.lat.toString(), { shouldDirty: true });
|
||||||
form.setValue('long', result.lon.toString(), { shouldDirty: true });
|
form.setValue('long', result.lon.toString(), { shouldDirty: true });
|
||||||
@@ -223,7 +302,7 @@ const RefreshOrder = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const order_products = initialValues.items
|
const order_products = orderItems
|
||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.product.prices &&
|
item.product.prices &&
|
||||||
@@ -237,42 +316,44 @@ const RefreshOrder = () => {
|
|||||||
on_balance: 'Y',
|
on_balance: 'Y',
|
||||||
order_quant: item.quantity,
|
order_quant: item.quantity,
|
||||||
price_type_code: item.product.prices![0].price_type.code,
|
price_type_code: item.product.prices![0].price_type.code,
|
||||||
product_price: item.product.prices![0].price,
|
product_price: item.price,
|
||||||
warehouse_code: 'wh1',
|
warehouse_code: process.env.NEXT_PUBLIC_WARHOUSES_CODE!,
|
||||||
}));
|
}));
|
||||||
|
if (user) {
|
||||||
|
const dealTime = formatDate.format(deliveryDate, 'DD.MM.YYYY');
|
||||||
|
|
||||||
mutate({
|
mutate({
|
||||||
order: [
|
order: [
|
||||||
{
|
{
|
||||||
filial_code: 'dodge',
|
filial_code: process.env.NEXT_PUBLIC_FILIAL_CODE!,
|
||||||
delivery_date: formatDate.format(deliveryDate, 'DD.MM.YYYY'),
|
delivery_date: `${dealTime}`,
|
||||||
room_code: '100',
|
room_code: process.env.NEXT_PUBLIC_ROOM_CODE!,
|
||||||
deal_time:
|
deal_time: formatDate.format(new Date(), 'DD.MM.YYYY'),
|
||||||
formatDate.format(deliveryDate, 'DD.MM.YYYY') +
|
robot_code: process.env.NEXT_PUBLIC_ROBOT_CODE!,
|
||||||
' ' +
|
status: 'D',
|
||||||
selectedTimeSlot,
|
sales_manager_code: process.env.NEXT_PUBLIC_SALES_MANAGER_CODE!,
|
||||||
robot_code: 'r2',
|
person_code: user?.username,
|
||||||
status: 'B#N',
|
currency_code: '860',
|
||||||
sales_manager_code: '1',
|
owner_person_code: user?.username,
|
||||||
person_code: '12345678',
|
note: value.comment,
|
||||||
currency_code: '860',
|
order_products: order_products,
|
||||||
owner_person_code: '1234567',
|
},
|
||||||
note: value.comment,
|
],
|
||||||
order_products: order_products,
|
});
|
||||||
},
|
} else {
|
||||||
],
|
toast.error(t('Xatolik yuz berdi'), {
|
||||||
});
|
richColors: true,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate total price
|
const totalPrice = orderItems.reduce(
|
||||||
const totalPrice =
|
(sum, item) => sum + Number(item.price) * item.quantity,
|
||||||
initialValues?.items.reduce(
|
0,
|
||||||
(sum, item) => sum + Number(item.price) * item.quantity,
|
);
|
||||||
0,
|
|
||||||
) || 0;
|
|
||||||
|
|
||||||
const totalItems =
|
const totalItems = orderItems.reduce((sum, item) => sum + item.quantity, 0);
|
||||||
initialValues?.items.reduce((sum, item) => sum + item.quantity, 0) || 0;
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -558,7 +639,7 @@ const RefreshOrder = () => {
|
|||||||
|
|
||||||
{/* Cart Items */}
|
{/* Cart Items */}
|
||||||
<div className="space-y-3 mb-4 max-h-96 overflow-y-auto">
|
<div className="space-y-3 mb-4 max-h-96 overflow-y-auto">
|
||||||
{initialValues.items.map((item) => {
|
{orderItems.map((item) => {
|
||||||
const productImage = item.product.images?.[0]?.images
|
const productImage = item.product.images?.[0]?.images
|
||||||
? item.product.images[0].images.includes(BASE_URL)
|
? item.product.images[0].images.includes(BASE_URL)
|
||||||
? item.product.images[0].images
|
? item.product.images[0].images
|
||||||
@@ -581,19 +662,54 @@ const RefreshOrder = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h4 className="font-semibold text-sm text-gray-900 truncate">
|
<div className="flex items-start justify-between gap-1">
|
||||||
{item.product.name}
|
<h4 className="font-semibold text-sm text-gray-900 truncate">
|
||||||
</h4>
|
{item.product.name}
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
</h4>
|
||||||
{item.quantity} ×{' '}
|
{/* O'chirish tugmasi */}
|
||||||
{formatPrice(Number(item.price), true)}
|
<button
|
||||||
</p>
|
type="button"
|
||||||
|
onClick={() => deleteItem(item.id)}
|
||||||
|
className="flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full text-red-400 hover:text-red-600 hover:bg-red-50 transition"
|
||||||
|
title={t("O'chirish")}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p className="text-sm font-bold text-blue-600 mt-1">
|
<p className="text-sm font-bold text-blue-600 mt-1">
|
||||||
{formatPrice(
|
{formatPrice(
|
||||||
Number(item.price) * item.quantity,
|
Number(item.price) * item.quantity,
|
||||||
true,
|
true,
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Quantity — input + ± tugmalar */}
|
||||||
|
<div className="flex items-center gap-1 mt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateQuantity(item.id, -1)}
|
||||||
|
className="w-7 h-7 flex items-center justify-center rounded-full border border-gray-300 bg-white hover:bg-gray-100 transition"
|
||||||
|
>
|
||||||
|
<Minus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={quantityInputs[item.id] ?? item.quantity}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleQuantityInput(item.id, e.target.value)
|
||||||
|
}
|
||||||
|
onBlur={() => handleQuantityBlur(item.id)}
|
||||||
|
className="w-14 h-7 text-center text-sm font-semibold border border-gray-300 rounded-md focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateQuantity(item.id, 1)}
|
||||||
|
className="w-7 h-7 flex items-center justify-center rounded-full border border-gray-300 bg-white hover:bg-gray-100 transition"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export function ProductCard({
|
|||||||
<Button
|
<Button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (user == null) {
|
if (user === null) {
|
||||||
router.push('/auth');
|
router.push('/auth');
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user