api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-29 18:41:59 +05:00
parent a9e99f9755
commit 2d0285dafc
64 changed files with 6319 additions and 2352 deletions

View File

@@ -1,6 +1,12 @@
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 { Checkbox } from "@/shared/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -16,47 +22,25 @@ import {
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";
type Offer = {
id: string;
title: string;
audience: "Jismoniy shaxslar" | "Yuridik shaxslar";
content: string;
active: boolean;
createdAt: string;
};
const FAKE_DATA: Offer[] = [
{
id: "of-1",
title: "Ommaviy oferta - Standart shartlar",
audience: "Jismoniy shaxslar",
content:
"Bu hujjat kompaniya va xizmatlardan foydalanish bo'yicha umumiy shartlarni o'z ichiga oladi.",
active: true,
createdAt: new Date().toISOString(),
},
{
id: "of-2",
title: "Yuridik shaxslar uchun oferta",
audience: "Yuridik shaxslar",
content: "Yuridik shaxslar uchun maxsus shartlar va kafolatlar.",
active: false,
createdAt: new Date(Date.now() - 86400000).toISOString(),
},
];
const STORAGE_KEY = "ommaviy_oferta_v1";
import { toast } from "sonner";
export default function OmmaviyOfertaCRUD() {
const [items, setItems] = useState<Offer[]>([]);
const [query, setQuery] = useState("");
const [editing, setEditing] = useState<Offer | null>(null);
const [form, setForm] = useState<Partial<Offer>>({
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: "",
@@ -64,24 +48,6 @@ export default function OmmaviyOfertaCRUD() {
});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
const parsed = JSON.parse(raw) as Offer[];
setItems(parsed);
} catch {
setItems(FAKE_DATA);
}
} else {
setItems(FAKE_DATA);
}
}, []);
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}, [items]);
function resetForm() {
setForm({
title: "",
@@ -93,100 +59,214 @@ export default function OmmaviyOfertaCRUD() {
setEditing(null);
}
function validate(f: Partial<Offer>) {
const e: Record<string, string> = {};
if (!f.title || f.title.trim().length < 3)
e.title = "Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak";
if (!f.content || f.content.trim().length < 10)
e.content = "Kontent kamida 10 ta belgidan iborat bo'lishi kerak";
return e;
}
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 validation = validate(form);
if (Object.keys(validation).length) {
setErrors(validation);
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) {
setItems((prev) =>
prev.map((it) =>
it.id === editing.id ? { ...it, ...(form as Offer) } : it,
),
);
resetForm();
} else {
const newItem: Offer = {
id: `of-${Date.now()}`,
title: (form.title || "Untitled").trim(),
audience: (form.audience as Offer["audience"]) || "Barcha",
content: (form.content || "").trim(),
active: form.active ?? true,
createdAt: new Date().toISOString(),
};
setItems((prev) => [newItem, ...prev]);
resetForm();
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: Offer) {
function startEdit(item: number) {
setEditing(item);
setForm({ ...item });
setErrors({});
window.scrollTo({ top: 0, behavior: "smooth" });
}
function removeItem(id: string) {
setItems((prev) => prev.filter((p) => p.id !== id));
function removeItem(id: number) {
removeOfferta({ id });
}
function toggleActive(id: string) {
setItems((prev) =>
prev.map((p) => (p.id === id ? { ...p, active: !p.active } : p)),
);
function toggleActive(id: number, currentStatus: boolean) {
update({
id: id,
body: {
is_active: !currentStatus,
},
});
}
const filtered = items.filter((it) => {
const q = query.trim().toLowerCase();
if (!q) return true;
return (
it.title.toLowerCase().includes(q) ||
it.content.toLowerCase().includes(q) ||
it.audience.toLowerCase().includes(q)
);
});
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">Ommaviy oferta</h1>
<h1 className="text-3xl font-bold">{t("Ommaviy oferta")}</h1>
</div>
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>
{editing ? "Tahrirish" : "Yangi oferta yaratish"}
{editing ? t("Tahrirlash") : t("Yangi oferta yaratish")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Sarlavha</label>
<label className="text-sm font-medium">{t("Sarlavha")}</label>
<Input
value={form.title || ""}
onChange={(e) =>
setForm((s) => ({ ...s, title: e.target.value }))
}
placeholder="Ommaviy oferta sarlavhasi"
placeholder={t("Ommaviy oferta sarlavhasi")}
className="mt-1"
/>
{errors.title && (
<p className="text-destructive text-sm mt-1">{errors.title}</p>
<p className="text-destructive text-sm mt-1">
{t(errors.title)}
</p>
)}
</div>
<div className="h-full w-[100%]">
<label className="text-sm font-medium">Kontent</label>
<div className="h-[280px] w-[100%]">
<label className="text-sm font-medium">{t("Kontent")}</label>
<div className="mt-1">
<ReactQuill
value={form.content || ""}
@@ -194,54 +274,41 @@ export default function OmmaviyOfertaCRUD() {
setForm((s) => ({ ...s, content: value }))
}
className="bg-gray-900 h-48"
placeholder="Oferta matnini kiriting..."
placeholder={t("Oferta matnini kiriting...")}
/>
</div>
{errors.content && (
<p className="text-destructive text-sm mt-1">
{errors.content}
<p className="text-destructive text-sm mt-12">
{t(errors.content)}
</p>
)}
</div>
<div className="grid grid-cols-1 gap-4 mt-24">
<div>
<label className="text-sm font-medium">Kimlar uchun</label>
<Select
value={form.audience || "Barcha"}
onValueChange={(value) =>
setForm((s) => ({
...s,
audience: value as Offer["audience"],
}))
}
>
<SelectTrigger className="mt-1 w-full !h-12">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Barcha">Barcha</SelectItem>
<SelectItem value="Jismoniy shaxslar">
Jismoniy shaxslar uchun
</SelectItem>
<SelectItem value="Yuridik shaxslar">
Yuridik shaxslar uchun
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={!!form.active}
onCheckedChange={(checked) =>
setForm((s) => ({ ...s, active: checked ? true : false }))
}
/>
<span>Faol</span>
</label>
</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">
@@ -249,92 +316,91 @@ export default function OmmaviyOfertaCRUD() {
onClick={handleCreateOrUpdate}
className="bg-blue-600 hover:bg-blue-700"
>
{editing ? "Saqlash" : "Yaratish"}
{editing ? t("Saqlash") : t("Qo'shish")}
</Button>
<Button variant="outline" onClick={resetForm}>
Bekor qilish
{t("Bekor qilish")}
</Button>
</div>
</CardContent>
</Card>
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Qidirish sarlavha, kontent yoki auditoriya bo'yicha..."
className="max-w-sm"
/>
<div className="flex gap-4 text-sm text-muted-foreground">
<span>Natija: {filtered.length}</span>
<span>Barcha: {items.length}</span>
</div>
</div>
<div className="space-y-3">
{filtered.length === 0 && (
{allOfferta && allOfferta?.results.length === 0 && (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground text-center">
Natija topilmadi.
{t("Natija topilmadi.")}
</p>
</CardContent>
</Card>
)}
{filtered.map((it) => (
{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.audience} {new Date(it.createdAt).toLocaleString()}
{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)}
onClick={() => startEdit(it.id)}
variant="outline"
size="sm"
>
<Edit2 className="w-4 h-4 mr-1" />
Tahrirlash
{t("Tahrirlash")}
</Button>
<Button
onClick={() => toggleActive(it.id)}
variant={it.active ? "default" : "outline"}
onClick={() => toggleActive(it.id, it.is_active)}
variant={it.is_active ? "default" : "outline"}
size="sm"
className={
it.active ? "bg-green-600 hover:bg-green-700" : ""
it.is_active
? "w-full bg-green-600 hover:bg-green-700"
: "w-full"
}
>
{it.active ? "Faol" : "Faol emas"}
{it.is_active ? t("Faol") : t("Faol emas")}
</Button>
<Dialog>
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">
<Button
variant="destructive"
size="sm"
className="w-full"
onClick={() => setDeleteOpen(true)}
>
<Trash2 className="w-4 h-4 mr-1" />
O'chirish
{t("O'chirish")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>O'chirish tasdiqlash</DialogTitle>
<DialogTitle>{t("O'chirish tasdiqlash")}</DialogTitle>
<DialogDescription>
Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni
bekor qilib bo'lmaydi.
{t(
"Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni bekor qilib bo'lmaydi.",
)}
</DialogDescription>
<div className="flex gap-3 justify-end pt-4">
<Button>Bekor qilish</Button>
<Button onClick={() => setDeleteOpen(false)}>
{t("Bekor qilish")}
</Button>
<Button
variant={"destructive"}
onClick={() => removeItem(it.id)}
>
O'chirish
{t("O'chirish")}
</Button>
</div>
</DialogContent>