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

@@ -0,0 +1,122 @@
import type {
GetAllHelpPage,
GetAllOfferta,
GetDetailHelpPage,
GetDetailOfferta,
} from "@/pages/site-page/lib/types";
import httpClient from "@/shared/config/api/httpClient";
import { HELP_PAGE, OFFERTA } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
const createOfferta = async ({
body,
}: {
body: {
title: string;
content: string;
person_type: "individual" | "legal_entity";
is_active: boolean;
};
}) => {
const res = await httpClient.post(OFFERTA, body);
return res;
};
const updateOfferta = async ({
body,
id,
}: {
id: number;
body: {
title?: string;
content?: string;
person_type?: "individual" | "legal_entity";
is_active?: boolean;
};
}) => {
const res = await httpClient.patch(`${OFFERTA}${id}/`, body);
return res;
};
const getAllOfferta = async (params: {
page: number;
page_size: number;
}): Promise<AxiosResponse<GetAllOfferta>> => {
const res = await httpClient.get(OFFERTA, { params });
return res;
};
const getOneOfferta = async (
id: number,
): Promise<AxiosResponse<GetDetailOfferta>> => {
const res = await httpClient.get(`${OFFERTA}${id}/`);
return res;
};
const deleteOfferta = async (id: number) => {
const res = await httpClient.delete(`${OFFERTA}${id}/`);
return res;
};
const createHelpPage = async ({
body,
}: {
body: {
title: string;
content: string;
page_type: "privacy_policy" | "user_agreement";
is_active: boolean;
};
}) => {
const res = await httpClient.post(HELP_PAGE, body);
return res;
};
const getAllHelpPage = async (params: {
page: number;
page_size: number;
}): Promise<AxiosResponse<GetAllHelpPage>> => {
const res = await httpClient.get(HELP_PAGE, { params });
return res;
};
const getDetailHelpPage = async (
id: number,
): Promise<AxiosResponse<GetDetailHelpPage>> => {
const res = await httpClient.get(`${HELP_PAGE}${id}/`);
return res;
};
const updateHelpPage = async ({
body,
id,
}: {
id: number;
body: {
title?: string;
content?: string;
page_type?: "privacy_policy" | "user_agreement";
is_active?: boolean;
};
}) => {
const res = await httpClient.patch(`${HELP_PAGE}${id}/`, body);
return res;
};
const deleteHelpPage = async (id: number) => {
const res = await httpClient.delete(`${HELP_PAGE}${id}/`);
return res;
};
export {
createHelpPage,
createOfferta,
deleteHelpPage,
deleteOfferta,
getAllHelpPage,
getAllOfferta,
getDetailHelpPage,
getOneOfferta,
updateHelpPage,
updateOfferta,
};

View File

@@ -0,0 +1,63 @@
export interface GetAllOfferta {
status: boolean;
data: {
links: {
previous: string;
next: string;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: {
id: number;
title: string;
content: string;
person_type: "individual" | "legal_entity";
is_active: boolean;
}[];
};
}
export interface GetDetailOfferta {
status: boolean;
data: {
id: number;
title: string;
content: string;
person_type: "individual" | "legal_entity";
is_active: boolean;
};
}
export interface GetAllHelpPage {
status: boolean;
data: {
links: {
previous: string;
next: string;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: {
id: number;
title: string;
content: string;
page_type: "privacy_policy" | "user_agreement";
is_active: boolean;
}[];
};
}
export interface GetDetailHelpPage {
status: true;
data: {
id: number;
title: string;
content: string;
page_type: "privacy_policy" | "user_agreement";
is_active: true;
};
}

View File

@@ -1,3 +1,10 @@
import {
createHelpPage,
deleteHelpPage,
getAllHelpPage,
getDetailHelpPage,
updateHelpPage,
} 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";
@@ -16,84 +23,150 @@ 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: "Foydalanuvchi qollanmasi" | "Maxfiylik siyosati";
content: string;
active: boolean;
createdAt: string;
};
const FAKE_DATA: Offer[] = [
{
id: "of-1",
title: "Ommaviy oferta - Standart shartlar",
audience: "Foydalanuvchi qollanmasi",
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: "Foydalanuvchi qollanmasi uchun oferta",
audience: "Foydalanuvchi qollanmasi",
content: "Foydalanuvchi qollanmasi 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 PolicyCrud() {
const [items, setItems] = useState<Offer[]>([]);
const [query, setQuery] = useState("");
const [editing, setEditing] = useState<Offer | null>(null);
const [form, setForm] = useState<Partial<Offer>>({
title: "",
audience: "Foydalanuvchi qollanmasi",
content: "",
active: true,
const { t } = useTranslation();
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const queryClient = useQueryClient();
const { data: items } = useQuery({
queryKey: ["help_page"],
queryFn: () => {
return getAllHelpPage({ page: 1, page_size: 99 });
},
select(data) {
return data.data.data.results;
},
});
const [editing, setEditing] = useState<number | null>(null);
const { data: detail } = useQuery({
queryKey: ["help_page_detail", editing],
queryFn: () => {
return getDetailHelpPage(editing!);
},
select(data) {
return data.data.data;
},
enabled: !!editing,
});
const [form, setForm] = useState<{
title: string;
content: string;
page_type: "privacy_policy" | "user_agreement";
is_active: boolean;
}>({
title: "",
content: "",
page_type: "privacy_policy",
is_active: true,
});
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);
}
}, []);
const { mutate: create } = useMutation({
mutationFn: ({
body,
}: {
body: {
title: string;
content: string;
page_type: "privacy_policy" | "user_agreement";
is_active: boolean;
};
}) => createHelpPage({ body }),
onSuccess: () => {
resetForm();
queryClient.refetchQueries({ queryKey: ["help_page"] });
queryClient.refetchQueries({ queryKey: ["help_page_detail"] });
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}, [items]);
const { mutate: update } = useMutation({
mutationFn: ({
body,
id,
}: {
id: number;
body: {
title?: string;
content?: string;
page_type?: "privacy_policy" | "user_agreement";
is_active?: boolean;
};
}) => updateHelpPage({ body, id }),
onSuccess: () => {
resetForm();
queryClient.refetchQueries({ queryKey: ["help_page"] });
queryClient.refetchQueries({ queryKey: ["help_page_detail"] });
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const { mutate: deleteHelp } = useMutation({
mutationFn: (id: number) => deleteHelpPage(id),
onSuccess: () => {
resetForm();
queryClient.refetchQueries({ queryKey: ["help_page"] });
queryClient.refetchQueries({ queryKey: ["help_page_detail"] });
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
function resetForm() {
setForm({
title: "",
audience: "Foydalanuvchi qollanmasi",
content: "",
active: true,
is_active: true,
page_type: "privacy_policy",
});
setErrors({});
setEditing(null);
}
function validate(f: Partial<Offer>) {
useEffect(() => {
if (detail && editing) {
setForm({
content: detail.content,
is_active: detail.is_active,
page_type: detail.page_type,
title: detail.title,
});
}
}, [detail, editing]);
function validate(
f: Partial<{
title: string;
content: string;
page_type: "privacy_policy" | "user_agreement";
is_active: boolean;
}>,
) {
const e: Record<string, string> = {};
if (!f.title || f.title.trim().length < 3)
e.title = "Sarlavha kamida 3 ta belgidan iborat bo'lishi kerak";
@@ -110,83 +183,78 @@ export default function PolicyCrud() {
}
if (editing) {
setItems((prev) =>
prev.map((it) =>
it.id === editing.id ? { ...it, ...(form as Offer) } : it,
),
);
resetForm();
update({
body: {
content: form.content,
is_active: form.is_active,
page_type: form.page_type,
title: form.title,
},
id: editing,
});
} 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();
create({
body: {
content: form.content,
is_active: form.is_active,
page_type: form.page_type,
title: form.title,
},
});
}
}
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) {
deleteHelp(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("Yordam sahifalari boshqaruvi")}
</h1>
</div>
<Card className="bg-gray-900">
<CardHeader>
<CardTitle>
{editing ? "Tahrirish" : "Yangi oferta yaratish"}
{editing ? t("Tahrirlash") : t("Yangi yordam sahifasi 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("Yordam sahifasi 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,37 +262,39 @@ export default function PolicyCrud() {
setForm((s) => ({ ...s, content: value }))
}
className="bg-gray-900 h-48"
placeholder="Oferta matnini kiriting..."
placeholder={t("Yordam 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 className="grid grid-cols-1 gap-4 mt-5">
<div>
<label className="text-sm font-medium">Kimlar uchun</label>
<label className="text-sm font-medium">
{t("Sahifa turi")}
</label>
<Select
value={form.audience || "Barcha"}
value={form.page_type}
onValueChange={(value) =>
setForm((s) => ({
...s,
audience: value as Offer["audience"],
page_type: value as "privacy_policy" | "user_agreement",
}))
}
>
<SelectTrigger className="mt-1 w-full !h-12">
<SelectValue />
<SelectValue placeholder={t("Sahifa turini tanlang")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="Foydalanuvchi qollanmasi">
Foydalanuvchi qollanmasi
<SelectItem value="user_agreement">
{t("Qollanma")}
</SelectItem>
<SelectItem value="Maxfiylik siyosati">
Maxfiylik siyosati
<SelectItem value="privacy_policy">
{t("Maxfiylik siyosati")}
</SelectItem>
</SelectContent>
</Select>
@@ -233,12 +303,12 @@ export default function PolicyCrud() {
<div className="flex items-end">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={!!form.active}
checked={!!form.is_active}
onCheckedChange={(checked) =>
setForm((s) => ({ ...s, active: checked ? true : false }))
}
/>
<span>Faol</span>
<span>{t("Faol")}</span>
</label>
</div>
</div>
@@ -248,92 +318,87 @@ export default function PolicyCrud() {
onClick={handleCreateOrUpdate}
className="bg-blue-600 hover:bg-blue-700"
>
{editing ? "Saqlash" : "Yaratish"}
{editing ? t("Saqlash") : t("Yaratish")}
</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 && (
{items?.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) => (
{items?.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()}
</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"
className="w-full"
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
? "bg-green-600 hover:bg-green-700 w-full"
: "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("Ochirishni tasdiqlash")}</DialogTitle>
<DialogDescription>
Haqiqatan ham bu ofertani o'chirmoqchimisiz? Bu amalni
bekor qilib bo'lmaydi.
{t(
"Haqiqatan ham bu yordam sahifasini ochirmoqchimisiz? Bu amalni bekor qilib bolmaydi.",
)}
</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>

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>