user update price type

This commit is contained in:
Samandar Turgunboyev
2026-02-17 19:38:30 +05:00
parent c006dfd907
commit 33eed28b83
4 changed files with 259 additions and 97 deletions

View File

@@ -43,4 +43,15 @@ export const user_api = {
const res = await httpClient.post(API_URLS.PasswordSet(id), body); const res = await httpClient.post(API_URLS.PasswordSet(id), body);
return res; return res;
}, },
async price_type_set({
body,
id,
}: {
id: string | number;
body: { ids: number[] };
}) {
const res = await httpClient.post(API_URLS.PriceTypeSet(id), body);
return res;
},
}; };

View File

@@ -6,6 +6,7 @@ export interface UserData {
short_name: string; short_name: string;
created_at: string; created_at: string;
password_set: boolean; password_set: boolean;
price_types: number[];
} }
export interface UserListRes { export interface UserListRes {

View File

@@ -1,9 +1,18 @@
"use client"; "use client";
import price_type_api from "@/features/price-type/lib/api";
import { user_api } from "@/features/users/lib/api"; import { user_api } from "@/features/users/lib/api";
import type { UserData } from "@/features/users/lib/data"; import type { UserData } from "@/features/users/lib/data";
import formatDate from "@/shared/lib/formatDate"; import formatDate from "@/shared/lib/formatDate";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from "@/shared/ui/command";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -19,6 +28,7 @@ import {
} from "@/shared/ui/form"; } from "@/shared/ui/form";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label"; import { Label } from "@/shared/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { import {
Table, Table,
TableBody, TableBody,
@@ -28,10 +38,10 @@ import {
TableRow, TableRow,
} from "@/shared/ui/table"; } from "@/shared/ui/table";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { Edit } from "lucide-react"; import { Check, ChevronsUpDown, Edit } from "lucide-react";
import { 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";
@@ -49,7 +59,28 @@ interface UserListProps {
export function UserCard({ users }: UserListProps) { export function UserCard({ users }: UserListProps) {
const [edit, setEdit] = useState<number | string | null>(null); const [edit, setEdit] = useState<number | string | null>(null);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [openPriceId, setOpenPriceId] = useState<number | null>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [values, setValues] = useState<Record<string | number, string[]>>({});
const { data } = useQuery({
queryKey: ["price_type"],
queryFn: () => price_type_api.list(),
select(data) {
return data.data;
},
});
useEffect(() => {
const initialValues: Record<string | number, string[]> = {};
users.forEach((user) => {
if (user.price_types) {
initialValues[user.id] = user.price_types.map((id) => id.toString());
}
});
setValues(initialValues);
}, [users]);
const form = useForm<z.infer<typeof passwordSet>>({ const form = useForm<z.infer<typeof passwordSet>>({
resolver: zodResolver(passwordSet), resolver: zodResolver(passwordSet),
@@ -87,6 +118,52 @@ export function UserCard({ users }: UserListProps) {
}, },
}); });
const { mutate: price_type_set } = useMutation({
mutationFn: ({
id,
body,
}: {
id: number | string;
body: { ids: number[] };
}) => user_api.price_type_set({ body, id }),
onSuccess: () => {
setOpenPriceId(null);
queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success("Narx qo'yildi", {
richColors: true,
position: "top-center",
});
},
onError: (err: AxiosError) => {
const errData = (err.response?.data as { data: string }).data;
const errMessage = (err.response?.data as { message: string }).message;
toast.error(errData || errMessage || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const toggleValue = (userId: string | number, value: string) => {
let newUserValues: string[] = [];
setValues((prev) => {
const userValues = prev[userId] || [];
newUserValues = userValues.includes(value)
? userValues.filter((v) => v !== value)
: [...userValues, value];
return { ...prev, [userId]: newUserValues };
});
const currentUserValues = values[userId] || [];
const nextUserValues = currentUserValues.includes(value)
? currentUserValues.filter((v) => v !== value)
: [...currentUserValues, value];
setValues((prev) => ({ ...prev, [userId]: nextUserValues }));
price_type_set({ body: { ids: nextUserValues.map(Number) }, id: userId });
};
function onSubmit(value: z.infer<typeof passwordSet>) { function onSubmit(value: z.infer<typeof passwordSet>) {
if (edit) { if (edit) {
mutate({ body: { password: value.password }, id: edit }); mutate({ body: { password: value.password }, id: edit });
@@ -102,107 +179,178 @@ export function UserCard({ users }: UserListProps) {
<TableHead>To'liq ism</TableHead> <TableHead>To'liq ism</TableHead>
<TableHead>Username</TableHead> <TableHead>Username</TableHead>
<TableHead>Ro'yxatdan o'tgan</TableHead> <TableHead>Ro'yxatdan o'tgan</TableHead>
<TableHead>Biriktirilgan narx</TableHead>
<TableHead className="text-right">Amallar</TableHead> <TableHead className="text-right">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{users.map((user) => ( {users.map((user) => {
<TableRow key={user.id} className="hover:bg-muted/50"> const userValues = values[user.id] || [];
<TableCell className="font-medium">{user.id}</TableCell> return (
<TableCell> <TableRow key={user.id} className="hover:bg-muted/50">
<div className="flex flex-col"> <TableCell className="font-medium">{user.id}</TableCell>
<span className="font-medium">{user.name}</span> <TableCell>
</div> <div className="flex flex-col">
</TableCell> <span className="font-medium">{user.name}</span>
<TableCell> </div>
<span className="text-muted-foreground">@{user.username}</span> </TableCell>
</TableCell> <TableCell>
<span className="text-muted-foreground">
@{user.username}
</span>
</TableCell>
<TableCell> <TableCell>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{formatDate.format(user.created_at, "DD-MM-YYYY")} {formatDate.format(user.created_at, "DD-MM-YYYY")}
</span> </span>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell>
<Dialog <Popover
open={open && edit === user.id} open={openPriceId === user.id}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) =>
setOpen(isOpen); setOpenPriceId(isOpen ? user.id : null)
if (!isOpen) {
setEdit(null);
form.reset();
} }
}} >
> <PopoverTrigger asChild>
<DialogTrigger asChild> <Button
<Button variant="outline"
size="sm" role="combobox"
variant="outline" aria-expanded={openPriceId === user.id}
className="cursor-pointer" className="w-[200px] justify-between cursor-pointer"
onClick={() => { >
setOpen(true); <span className="truncate">
setEdit(user.id); {userValues.length > 0
}} ? userValues
> .map(
<Edit className="h-4 w-4 mr-2" /> (id) =>
Parol {user.password_set ? "O'zgartirish" : "Qo'yish"} data?.find(
</Button> (item) => item.id.toString() === id,
</DialogTrigger> )?.name,
<DialogContent> )
<DialogHeader className="text-xl font-semibold"> .filter(Boolean)
Parolni {user.password_set ? "o'zgartirish" : "qo'yish"} .join(", ")
</DialogHeader> : "Narxni tanlang"}
<div> </span>
<Form {...form}> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<form </Button>
className="space-y-4" </PopoverTrigger>
onSubmit={form.handleSubmit(onSubmit)}
> <PopoverContent className="w-[200px] p-0" align="start">
<FormField <Command>
name="password" <CommandList>
control={form.control} <CommandEmpty>Topilmadi.</CommandEmpty>
render={({ field }) => ( <CommandGroup>
<FormItem> {data?.map((option) => {
<Label>Yangi parol</Label> const isSelected = userValues.includes(
<FormControl> option.id.toString(),
<Input );
type="password"
placeholder="12345678" return (
className="h-12 focus-visible:ring-0" <CommandItem
{...field} key={option.id}
onSelect={() =>
toggleValue(user.id, option.id.toString())
}
className="cursor-pointer"
>
<Check
className={cn(
"mr-2 h-4 w-4",
isSelected ? "opacity-100" : "opacity-0",
)}
/> />
</FormControl> {option.name}
<FormMessage /> </CommandItem>
</FormItem> );
)} })}
/> </CommandGroup>
<div className="flex justify-end gap-2"> </CommandList>
<Button </Command>
type="button" </PopoverContent>
variant="outline" </Popover>
onClick={() => { </TableCell>
setOpen(false); <TableCell className="text-right">
setEdit(null); <Dialog
form.reset(); open={open && edit === user.id}
}} onOpenChange={(isOpen) => {
> setOpen(isOpen);
Bekor qilish if (!isOpen) {
</Button> setEdit(null);
<Button form.reset();
type="submit" }
className="bg-blue-500 hover:bg-blue-600 cursor-pointer" }}
> >
Tasdiqlash <DialogTrigger asChild>
</Button> <Button
</div> size="sm"
</form> variant="outline"
</Form> className="cursor-pointer"
</div> onClick={() => {
</DialogContent> setOpen(true);
</Dialog> setEdit(user.id);
</TableCell> }}
</TableRow> >
))} <Edit className="h-4 w-4 mr-2" />
Parol {user.password_set ? "O'zgartirish" : "Qo'yish"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="text-xl font-semibold">
Parolni {user.password_set ? "o'zgartirish" : "qo'yish"}
</DialogHeader>
<div>
<Form {...form}>
<form
className="space-y-4"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
name="password"
control={form.control}
render={({ field }) => (
<FormItem>
<Label>Yangi parol</Label>
<FormControl>
<Input
type="password"
placeholder="12345678"
className="h-12 focus-visible:ring-0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => {
setOpen(false);
setEdit(null);
form.reset();
}}
>
Bekor qilish
</Button>
<Button
type="submit"
className="bg-blue-500 hover:bg-blue-600 cursor-pointer"
>
Tasdiqlash
</Button>
</div>
</form>
</Form>
</div>
</DialogContent>
</Dialog>
</TableCell>
</TableRow>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@@ -47,4 +47,6 @@ export const API_URLS = {
Update_Pyment_Type: (id: number) => Update_Pyment_Type: (id: number) =>
`${API_V}admin/product/${id}/update_payment_type/`, `${API_V}admin/product/${id}/update_payment_type/`,
Import_Balance: `${API_V}admin/product/import/balance/`, Import_Balance: `${API_V}admin/product/import/balance/`,
PriceTypeSet: (id: number | string) =>
`${API_V}admin/user/${id}/set_price_type/`,
}; };