418 lines
13 KiB
TypeScript
418 lines
13 KiB
TypeScript
import {
|
|
createOfferta,
|
|
deleteOfferta,
|
|
getAllOfferta,
|
|
getOneOfferta,
|
|
updateOfferta,
|
|
} from "@/pages/site-page/lib/api";
|
|
import { Button } from "@/shared/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/shared/ui/dialog";
|
|
import { Input } from "@/shared/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/shared/ui/select";
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Edit2, Trash2 } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import ReactQuill from "react-quill-new";
|
|
import "react-quill-new/dist/quill.snow.css";
|
|
import { toast } from "sonner";
|
|
|
|
export default function OmmaviyOfertaCRUD() {
|
|
const { t } = useTranslation();
|
|
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
|
const queryClient = useQueryClient();
|
|
const [editing, setEditing] = useState<number | null>(null);
|
|
const [form, setForm] = useState<{
|
|
title: string;
|
|
content: string;
|
|
audience: string;
|
|
active: boolean;
|
|
}>({
|
|
title: "",
|
|
audience: "Jismoniy shaxslar",
|
|
content: "",
|
|
active: true,
|
|
});
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
function resetForm() {
|
|
setForm({
|
|
title: "",
|
|
audience: "Jismoniy shaxslar",
|
|
content: "",
|
|
active: true,
|
|
});
|
|
setErrors({});
|
|
setEditing(null);
|
|
}
|
|
|
|
const { mutate: create } = useMutation({
|
|
mutationFn: (body: {
|
|
title: string;
|
|
content: string;
|
|
person_type: "individual" | "legal_entity";
|
|
is_active: boolean;
|
|
}) => createOfferta({ body }),
|
|
onSuccess: () => {
|
|
resetForm();
|
|
queryClient.refetchQueries({ queryKey: ["all_offerta"] });
|
|
queryClient.refetchQueries({ queryKey: ["detail_offerta"] });
|
|
toast.success(t("Muvaffaqiyatli yaratildi"), {
|
|
position: "top-center",
|
|
richColors: true,
|
|
});
|
|
},
|
|
onError: () => {
|
|
toast.error(t("Xatolik yuz berdi"), {
|
|
position: "top-center",
|
|
richColors: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
const { mutate: update } = useMutation({
|
|
mutationFn: ({
|
|
body,
|
|
id,
|
|
}: {
|
|
body: {
|
|
title?: string;
|
|
content?: string;
|
|
person_type?: "individual" | "legal_entity";
|
|
is_active?: boolean;
|
|
};
|
|
id: number;
|
|
}) => updateOfferta({ body, id }),
|
|
onSuccess: () => {
|
|
resetForm();
|
|
setEditing(null);
|
|
queryClient.refetchQueries({ queryKey: ["all_offerta"] });
|
|
queryClient.refetchQueries({ queryKey: ["detail_offerta"] });
|
|
toast.success(t("Muvaffaqiyatli yangilandi"), {
|
|
position: "top-center",
|
|
richColors: true,
|
|
});
|
|
},
|
|
onError: () => {
|
|
toast.error(t("Xatolik yuz berdi"), {
|
|
position: "top-center",
|
|
richColors: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
const { mutate: removeOfferta } = useMutation({
|
|
mutationFn: ({ id }: { id: number }) => deleteOfferta(id),
|
|
onSuccess: () => {
|
|
resetForm();
|
|
queryClient.refetchQueries({ queryKey: ["all_offerta"] });
|
|
queryClient.refetchQueries({ queryKey: ["detail_offerta"] });
|
|
toast.success(t("Muvaffaqiyatli o'chirildi"), {
|
|
position: "top-center",
|
|
richColors: true,
|
|
});
|
|
},
|
|
onError: () => {
|
|
toast.error(t("Xatolik yuz berdi"), {
|
|
position: "top-center",
|
|
richColors: true,
|
|
});
|
|
},
|
|
});
|
|
|
|
const { data: allOfferta } = useQuery({
|
|
queryKey: ["all_offerta"],
|
|
queryFn: () => {
|
|
return getAllOfferta({ page: 1, page_size: 99 });
|
|
},
|
|
select(data) {
|
|
return data.data.data;
|
|
},
|
|
});
|
|
|
|
const { data: detailOfferta } = useQuery({
|
|
queryKey: ["detail_offerta", editing],
|
|
queryFn: () => {
|
|
return getOneOfferta(editing!);
|
|
},
|
|
select(data) {
|
|
return data.data.data;
|
|
},
|
|
enabled: !!editing,
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (editing && detailOfferta) {
|
|
setForm({
|
|
active: detailOfferta.is_active,
|
|
audience:
|
|
detailOfferta.person_type === "individual"
|
|
? "Jismoniy shaxslar"
|
|
: "Yuridik shaxslar",
|
|
content: detailOfferta.content,
|
|
title: detailOfferta.title,
|
|
});
|
|
}
|
|
}, [detailOfferta, editing]);
|
|
|
|
function handleCreateOrUpdate() {
|
|
const newErrors: Record<string, string> = {};
|
|
if (!form.title.trim()) {
|
|
newErrors.title = "Sarlavha kiritish majburiy";
|
|
} else if (form.title.trim().length < 3) {
|
|
newErrors.title = "Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak";
|
|
}
|
|
|
|
const plainText = form.content.replace(/<[^>]+>/g, "").trim();
|
|
if (!plainText) {
|
|
newErrors.content = "Kontent kiritish majburiy";
|
|
} else if (plainText.length < 10) {
|
|
newErrors.content = "Kontent kamida 10 ta belgidan iborat bo'lishi kerak";
|
|
}
|
|
|
|
if (!form.audience) {
|
|
newErrors.audience = "Kimlar uchun degan maydonni tanlang";
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
if (Object.keys(newErrors).length > 0) {
|
|
toast.error(t("Iltimos, barcha majburiy maydonlarni to'ldiring"), {
|
|
position: "top-center",
|
|
});
|
|
return;
|
|
}
|
|
if (editing === null) {
|
|
create({
|
|
content: form.content,
|
|
is_active: form.active,
|
|
person_type:
|
|
form.audience === "Jismoniy shaxslar" ? "individual" : "legal_entity",
|
|
title: form.title,
|
|
});
|
|
} else if (editing) {
|
|
update({
|
|
body: {
|
|
content: form.content,
|
|
is_active: form.active,
|
|
person_type:
|
|
form.audience === "Jismoniy shaxslar"
|
|
? "individual"
|
|
: "legal_entity",
|
|
title: form.title,
|
|
},
|
|
id: editing,
|
|
});
|
|
}
|
|
}
|
|
|
|
function startEdit(item: number) {
|
|
setEditing(item);
|
|
}
|
|
|
|
function removeItem(id: number) {
|
|
removeOfferta({ id });
|
|
}
|
|
|
|
function toggleActive(id: number, currentStatus: boolean) {
|
|
update({
|
|
id: id,
|
|
body: {
|
|
is_active: !currentStatus,
|
|
},
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen w-full p-6 bg-gray-900">
|
|
<div className="max-w-[90%] mx-auto space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<h1 className="text-3xl font-bold">{t("Ommaviy oferta")}</h1>
|
|
</div>
|
|
|
|
<Card className="bg-gray-900">
|
|
<CardHeader>
|
|
<CardTitle>
|
|
{editing ? t("Tahrirlash") : t("Yangi oferta yaratish")}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div>
|
|
<label className="text-sm font-medium">{t("Sarlavha")}</label>
|
|
<Input
|
|
value={form.title || ""}
|
|
onChange={(e) =>
|
|
setForm((s) => ({ ...s, title: e.target.value }))
|
|
}
|
|
placeholder={t("Ommaviy oferta sarlavhasi")}
|
|
className="mt-1"
|
|
/>
|
|
{errors.title && (
|
|
<p className="text-destructive text-sm mt-1">
|
|
{t(errors.title)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="h-[280px] w-[100%]">
|
|
<label className="text-sm font-medium">{t("Kontent")}</label>
|
|
<div className="mt-1">
|
|
<ReactQuill
|
|
value={form.content || ""}
|
|
onChange={(value) =>
|
|
setForm((s) => ({ ...s, content: value }))
|
|
}
|
|
className="bg-gray-900 h-48"
|
|
placeholder={t("Oferta matnini kiriting...")}
|
|
/>
|
|
</div>
|
|
{errors.content && (
|
|
<p className="text-destructive text-sm mt-12">
|
|
{t(errors.content)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-sm font-medium">{t("Kimlar uchun")}</label>
|
|
<Select
|
|
value={form.audience || t("Barcha")}
|
|
onValueChange={(value) =>
|
|
setForm((s) => ({ ...s, audience: value }))
|
|
}
|
|
>
|
|
<SelectTrigger className="mt-1 w-full !h-12">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Jismoniy shaxslar">
|
|
{t("Jismoniy shaxslar uchun")}
|
|
</SelectItem>
|
|
<SelectItem value="Yuridik shaxslar">
|
|
{t("Yuridik shaxslar uchun")}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.audience && (
|
|
<p className="text-destructive text-sm mt-1">
|
|
{t(errors.audience)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-2 pt-4">
|
|
<Button
|
|
onClick={handleCreateOrUpdate}
|
|
className="bg-blue-600 hover:bg-blue-700"
|
|
>
|
|
{editing ? t("Saqlash") : t("Qo'shish")}
|
|
</Button>
|
|
<Button variant="outline" onClick={resetForm}>
|
|
{t("Bekor qilish")}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="space-y-3">
|
|
{allOfferta && allOfferta?.results.length === 0 && (
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<p className="text-muted-foreground text-center">
|
|
{t("Natija topilmadi.")}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{allOfferta?.results.map((it) => (
|
|
<Card key={it.id} className="overflow-hidden">
|
|
<CardContent className="pt-6">
|
|
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
|
|
<div className="flex-1">
|
|
<h3 className="font-semibold text-lg">{it.title}</h3>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
{it.person_type == "individual"
|
|
? t("Jismoniy shaxslar uchun")
|
|
: t("Yuridik shaxslar uchun")}
|
|
</p>
|
|
<p className="mt-3 text-sm line-clamp-3">{it.content}</p>
|
|
</div>
|
|
|
|
<div className="flex gap-2 items-center flex-wrap md:flex-col md:flex-nowrap">
|
|
<Button
|
|
onClick={() => startEdit(it.id)}
|
|
variant="outline"
|
|
size="sm"
|
|
>
|
|
<Edit2 className="w-4 h-4 mr-1" />
|
|
{t("Tahrirlash")}
|
|
</Button>
|
|
|
|
<Button
|
|
onClick={() => toggleActive(it.id, it.is_active)}
|
|
variant={it.is_active ? "default" : "outline"}
|
|
size="sm"
|
|
className={
|
|
it.is_active
|
|
? "w-full bg-green-600 hover:bg-green-700"
|
|
: "w-full"
|
|
}
|
|
>
|
|
{it.is_active ? t("Faol") : t("Faol emas")}
|
|
</Button>
|
|
|
|
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
className="w-full"
|
|
onClick={() => setDeleteOpen(true)}
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-1" />
|
|
{t("O'chirish")}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogTitle>{t("O'chirish tasdiqlash")}</DialogTitle>
|
|
<DialogDescription>
|
|
{t(
|
|
"Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni bekor qilib bo'lmaydi.",
|
|
)}
|
|
</DialogDescription>
|
|
<div className="flex gap-3 justify-end pt-4">
|
|
<Button onClick={() => setDeleteOpen(false)}>
|
|
{t("Bekor qilish")}
|
|
</Button>
|
|
<Button
|
|
variant={"destructive"}
|
|
onClick={() => removeItem(it.id)}
|
|
>
|
|
{t("O'chirish")}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|