order page and user page update

This commit is contained in:
Samandar Turgunboyev
2025-12-26 17:17:45 +05:00
parent 768ba9585d
commit 388c8bcfd1
9 changed files with 209 additions and 108 deletions

View File

@@ -38,6 +38,8 @@ export interface Order {
items: Item[]; items: Item[];
created_at: string; created_at: string;
long: number; long: number;
delivery_date: string;
delivery_time: string;
lat: number; lat: number;
} }

View File

@@ -4,6 +4,7 @@ import formatPrice from "@/shared/lib/formatPrice";
import { Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; import { Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
import { import {
Calendar, Calendar,
Clock3,
MapIcon, MapIcon,
MessageSquare, MessageSquare,
Package, Package,
@@ -126,6 +127,26 @@ const OrderDetail = ({ detail, setDetail, order }: Props) => {
</div> </div>
)} )}
{order.delivery_date && (
<div className="border rounded-lg p-4">
<h3 className="font-semibold text-lg mb-3 flex items-center">
<Calendar className="w-5 h-5 mr-2" />
Yetkazish sanasi
</h3>
<p className="text-lg text-gray-700">{order.delivery_date}</p>
</div>
)}
{order.delivery_time && (
<div className="border rounded-lg p-4">
<h3 className="font-semibold text-lg mb-3 flex items-center">
<Clock3 className="w-5 h-5 mr-2" />
Yetkazish vaqti
</h3>
<p className="text-lg text-gray-700">{order.delivery_time}</p>
</div>
)}
<div className="border rounded-lg p-4"> <div className="border rounded-lg p-4">
<h3 className="font-semibold text-lg mb-4 flex items-center"> <h3 className="font-semibold text-lg mb-4 flex items-center">
<Package className="w-5 h-5 mr-2" /> <Package className="w-5 h-5 mr-2" />

View File

@@ -109,6 +109,8 @@ const OrderTable = ({
<TableHead>Foydalanuvchi</TableHead> <TableHead>Foydalanuvchi</TableHead>
<TableHead>Umumiy narx</TableHead> <TableHead>Umumiy narx</TableHead>
<TableHead>Izoh</TableHead> <TableHead>Izoh</TableHead>
<TableHead>Yetkazish sanasi</TableHead>
<TableHead>Yetkazish soati</TableHead>
<TableHead>Holat</TableHead> <TableHead>Holat</TableHead>
<TableHead className="text-right">Harakatlar</TableHead> <TableHead className="text-right">Harakatlar</TableHead>
</TableRow> </TableRow>
@@ -121,6 +123,8 @@ const OrderTable = ({
<TableCell>{order.user.username}</TableCell> <TableCell>{order.user.username}</TableCell>
<TableCell>{formatPrice(order.total_price, true)}</TableCell> <TableCell>{formatPrice(order.total_price, true)}</TableCell>
<TableCell>{order.comment || "-"}</TableCell> <TableCell>{order.comment || "-"}</TableCell>
<TableCell>{order?.delivery_date || "-"}</TableCell>
<TableCell>{order?.delivery_time || "-"}</TableCell>
<TableCell> <TableCell>
<Select <Select
value={order.status} value={order.status}

View File

@@ -26,4 +26,9 @@ export const user_api = {
const res = await httpClient.delete(`${API_URLS.UserDelete(id)}`); const res = await httpClient.delete(`${API_URLS.UserDelete(id)}`);
return res; return res;
}, },
async import_user() {
const res = await httpClient.post(API_URLS.Import_User);
return res;
},
}; };

View File

@@ -1,10 +1,13 @@
export interface UserData { export interface UserData {
id: string; id: 3;
username: string; username: string;
last_login: null | string; first_name: string;
date_joined: string; last_name: string;
is_superuser: boolean; middle_name: null | string;
role: "admin" | "user" | "moderator"; gender: "M" | "F" | null;
region: null | string;
address: null | string;
created_at: string;
} }
export interface UserListRes { export interface UserListRes {
@@ -18,7 +21,13 @@ export interface UserListRes {
} }
export interface UserCreateReq { export interface UserCreateReq {
id: 3;
username: string; username: string;
password: string; first_name: string;
is_superuser: boolean; last_name: string;
middle_name: null | string;
gender: "M" | "F" | null;
region: null | string;
address: null | string;
created_at: string;
} }

View File

@@ -3,88 +3,120 @@
import type { UserData } from "@/features/users/lib/data"; import type { UserData } from "@/features/users/lib/data";
import { Avatar, AvatarFallback } from "@/shared/ui/avatar"; import { Avatar, AvatarFallback } from "@/shared/ui/avatar";
import { Badge } from "@/shared/ui/badge"; import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card, CardContent, CardHeader } from "@/shared/ui/card"; import { Card, CardContent, CardHeader } from "@/shared/ui/card";
import { Clock, Edit, Trash2 } from "lucide-react"; import { Calendar, MapPin } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react"; import { type Dispatch, type SetStateAction } from "react";
export function UserCard({ export function UserCard({
user, user,
setEditingUser, // setEditingUser,
setDialogOpen, // setDialogOpen,
setOpenDelete, // setOpenDelete,
setUserDelete, // setUserDelete,
}: { }: {
user: UserData; user: UserData;
setEditingUser: (user: UserData) => void; setEditingUser?: (user: UserData) => void;
setDialogOpen: (open: boolean) => void; setDialogOpen?: (open: boolean) => void;
setOpenDelete: Dispatch<SetStateAction<boolean>>; setOpenDelete?: Dispatch<SetStateAction<boolean>>;
setUserDelete: Dispatch<SetStateAction<UserData | null>>; setUserDelete?: Dispatch<SetStateAction<UserData | null>>;
}) { }) {
const getRoleColor = (role: UserData["role"]) => { const getGenderColor = (gender: "M" | "F" | null) => {
switch (role) { switch (gender) {
case "admin": case "M":
return "bg-red-100 text-red-800";
case "moderator":
return "bg-blue-100 text-blue-800"; return "bg-blue-100 text-blue-800";
case "user": case "F":
return "bg-green-100 text-green-800"; return "bg-pink-100 text-pink-800";
default: default:
return "bg-gray-100 text-gray-800"; return "bg-gray-100 text-gray-800";
} }
}; };
const getGenderLabel = (gender: "M" | "F" | null) => {
switch (gender) {
case "M":
return "Erkak";
case "F":
return "Ayol";
default:
return "Ko'rsatilmagan";
}
};
const getInitials = () => {
if (user.first_name && user.last_name) {
return `${user.first_name[0]}${user.last_name[0]}`.toUpperCase();
}
return user.username.substring(0, 2).toUpperCase();
};
const getFullName = () => {
const parts = [user.first_name, user.middle_name, user.last_name].filter(
Boolean,
);
return parts.length > 0 ? parts.join(" ") : user.username;
};
return ( return (
<>
<Card className="group hover:shadow-md transition-shadow"> <Card className="group hover:shadow-md transition-shadow">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar> <Avatar>
<AvatarFallback className="bg-green-100 text-green-700"> <AvatarFallback className="bg-gradient-to-br from-purple-100 to-blue-100 text-purple-700">
{user.username {getInitials()}
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<h3 className="font-semibold">{user.username}</h3> <h3 className="font-semibold">{getFullName()}</h3>
<p className="text-sm text-muted-foreground">@{user.username}</p>
</div> </div>
</div> </div>
<Badge className={getRoleColor(user.role)}> <Badge className={getGenderColor(user.gender)}>
{user.role === "admin" {getGenderLabel(user.gender)}
? "Admin"
: user.role === "user"
? "Foydalanuvchi"
: user.role}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span> <span>
{"Kirgan vaqt"} {new Date(user.date_joined).toLocaleDateString()} Ro'yxatdan o'tgan:{" "}
{new Date(user.created_at).toLocaleDateString("uz-UZ", {
year: "numeric",
month: "long",
day: "numeric",
})}
</span> </span>
</div> </div>
{user.last_login && ( {user.region && (
<div className="text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
{"Ohirgi marta kirgan"}:{" "} <MapPin className="h-4 w-4" />
{new Date(user.last_login).toLocaleDateString()} <span>Hudud: {user.region}</span>
</div> </div>
)} )}
<div className="flex items-center gap-2"> {user.address && (
<div className="flex items-start gap-2 text-sm text-muted-foreground">
<MapPin className="h-4 w-4 mt-0.5" />
<span>Manzil: {user.address}</span>
</div>
)}
{/* ID ma'lumoti */}
<div className="pt-2 border-t text-xs text-muted-foreground">
ID: {user.id}
</div>
{/* Uncommnet qilish uchun tugmalar
<div className="flex items-center gap-2 pt-2">
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setEditingUser(user); setEditingUser(user);
setDialogOpen(true); setDialogOpen?.(true);
}} }}
className="flex-1" className="flex-1"
> >
@@ -96,17 +128,15 @@ export function UserCard({
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setOpenDelete(true); setOpenDelete(true);
if (setUserDelete) {
setUserDelete(user); setUserDelete(user);
}
}} }}
className="text-red-600 hover:text-red-700" className="text-red-600 hover:text-red-700"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
*/}
</CardContent> </CardContent>
</Card> </Card>
</>
); );
} }

View File

@@ -7,17 +7,16 @@ interface Props {
data: UserData[] | undefined; data: UserData[] | undefined;
isLoading: boolean; isLoading: boolean;
isError: boolean; isError: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen?: Dispatch<SetStateAction<boolean>>;
setEditingUser: Dispatch<SetStateAction<UserData | null>>; setEditingUser?: Dispatch<SetStateAction<UserData | null>>;
setOpenDelete: Dispatch<SetStateAction<boolean>>; setOpenDelete?: Dispatch<SetStateAction<boolean>>;
setUserDelete: Dispatch<SetStateAction<UserData | null>>; setUserDelete?: Dispatch<SetStateAction<UserData | null>>;
} }
const UserTable = ({ const UserTable = ({
data, data,
isLoading, isLoading,
isError, isError,
setDialogOpen,
setEditingUser, setEditingUser,
setOpenDelete, setOpenDelete,
setUserDelete, setUserDelete,
@@ -46,9 +45,9 @@ const UserTable = ({
setOpenDelete={setOpenDelete} setOpenDelete={setOpenDelete}
setUserDelete={setUserDelete} setUserDelete={setUserDelete}
key={user.id} key={user.id}
setDialogOpen={setDialogOpen} // setDialogOpen={setDialogOpen}
setEditingUser={setEditingUser} setEditingUser={setEditingUser}
user={{ ...user, role: user.is_superuser ? "admin" : "user" }} user={{ ...user }}
/> />
))} ))}
</div> </div>

View File

@@ -1,24 +1,27 @@
import { user_api } from "@/features/users/lib/api"; import { user_api } from "@/features/users/lib/api";
import type { UserData, UserListRes } from "@/features/users/lib/data"; import type { UserData } from "@/features/users/lib/data";
import DeleteUser from "@/features/users/ui/DeleteUser"; import DeleteUser from "@/features/users/ui/DeleteUser";
import Filter from "@/features/users/ui/Filter";
import UserTable from "@/features/users/ui/UserTable"; import UserTable from "@/features/users/ui/UserTable";
import { Button } from "@/shared/ui/button";
import Pagination from "@/shared/ui/pagination"; import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Loader2 } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
const UsersList = () => { const UsersList = () => {
const [editingUser, setEditingUser] = useState<UserData | null>(null); // const [editingUser, setEditingUser] = useState<UserData | null>(null);
const queryClient = useQueryClient();
const [opneDelete, setOpenDelete] = useState(false); const [opneDelete, setOpenDelete] = useState(false);
const [userDelete, setUserDelete] = useState<UserData | null>(null); const [userDelete, setUserDelete] = useState<UserData | null>(null);
const [regionValue, setRegionValue] = useState<UserListRes | null>(null); // const [regionValue, setRegionValue] = useState<UserListRes | null>(null);
const limit = 20; const limit = 20;
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading, isError } = useQuery({ const { data, isLoading, isError } = useQuery({
queryKey: ["user_list", currentPage, regionValue], queryKey: ["user_list", currentPage],
queryFn: () => { queryFn: () => {
return user_api.list({ return user_api.list({
page: currentPage, page: currentPage,
@@ -30,28 +33,55 @@ const UsersList = () => {
}, },
}); });
const [dialogOpen, setDialogOpen] = useState(false); const { mutate, isPending } = useMutation({
mutationFn: () => user_api.import_user(),
onSuccess: () => {
toast.success("Foydalanuvchilar import qilindi", {
richColors: true,
position: "top-center",
});
queryClient.refetchQueries({ queryKey: ["user_list"] });
},
onError: () => {
toast.error("Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
// const [dialogOpen, setDialogOpen] = useState(false);
return ( return (
<div className="flex flex-col h-full p-10 w-full"> <div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Foydalanuvchilar ro'yxati</h1> <h1 className="text-2xl font-bold">Foydalanuvchilar ro'yxati</h1>
<Button
className="bg-blue-500 h-12 text-md hover:bg-blue-500 cursor-pointer"
onClick={() => mutate()}
>
{isPending ? (
<Loader2 className="animate-spin" />
) : (
"Foydalanuvchini import qilish"
)}
</Button>
<Filter {/* <Filter
dialogOpen={dialogOpen} dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
editingUser={editingUser} editingUser={editingUser}
setEditingUser={setEditingUser} setEditingUser={setEditingUser}
setRegionValue={setRegionValue} setRegionValue={setRegionValue}
/> /> */}
</div> </div>
<UserTable <UserTable
data={data?.results} data={data?.results}
isLoading={isLoading} isLoading={isLoading}
setEditingUser={setEditingUser} // setEditingUser={setEditingUser}
isError={isError} isError={isError}
setDialogOpen={setDialogOpen} // setDialogOpen={setDialogOpen}
setOpenDelete={setOpenDelete} setOpenDelete={setOpenDelete}
setUserDelete={setUserDelete} setUserDelete={setUserDelete}
/> />

View File

@@ -32,4 +32,5 @@ export const API_URLS = {
UpdateProduct: (id: string) => `${API_V}admin/product/${id}/update/`, UpdateProduct: (id: string) => `${API_V}admin/product/${id}/update/`,
DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`, DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`,
QuestionnaireList: `${API_V}admin/questionnaire/list/`, QuestionnaireList: `${API_V}admin/questionnaire/list/`,
Import_User: `${API_V}admin/user/import_users/`,
}; };