This commit is contained in:
Samandar Turgunboyev
2025-10-30 18:28:17 +05:00
parent 352efd6391
commit 39f5b8ca3c
13 changed files with 650 additions and 115 deletions

View File

@@ -44,6 +44,7 @@ import {
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { import {
ChevronLeft,
ChevronRight, ChevronRight,
Loader2, Loader2,
Pencil, Pencil,
@@ -66,6 +67,7 @@ const faqForm = z.object({
const Faq = () => { const Faq = () => {
const [activeTab, setActiveTab] = useState<string>(""); const [activeTab, setActiveTab] = useState<string>("");
const [currentPage, setCurrentPage] = useState(1);
const { t } = useTranslation(); const { t } = useTranslation();
const [openModal, setOpenModal] = useState(false); const [openModal, setOpenModal] = useState(false);
const [editFaq, setEditFaq] = useState<number | null>(null); const [editFaq, setEditFaq] = useState<number | null>(null);
@@ -94,17 +96,24 @@ const Faq = () => {
initialPageParam: 1, initialPageParam: 1,
}); });
// Barcha kategoriyalarni birlashtirib olish useEffect(() => {
setCurrentPage(1);
}, [activeTab]);
const category = const category =
categoryData?.pages.flatMap((page) => page.data.data.results) ?? []; categoryData?.pages.flatMap((page) => page.data.data.results) ?? [];
const { data: faq } = useQuery({ const { data: faq } = useQuery({
queryKey: ["all_faq", activeTab], queryKey: ["all_faq", activeTab, currentPage],
queryFn: () => { queryFn: () => {
return getAllFaq({ page: 1, page_size: 10, category: Number(activeTab) }); return getAllFaq({
page: currentPage,
page_size: 10,
category: Number(activeTab),
});
}, },
select(data) { select(data) {
return data.data.data.results; return data.data.data;
}, },
enabled: !!activeTab, enabled: !!activeTab,
}); });
@@ -323,7 +332,7 @@ const Faq = () => {
{/* Tabs content */} {/* Tabs content */}
{category.map((cat) => ( {category.map((cat) => (
<TabsContent key={cat.id} value={String(cat.id)}> <TabsContent key={cat.id} value={String(cat.id)}>
{faq && faq?.length > 0 ? ( {faq && faq?.results.length > 0 ? (
<div className="border rounded-md overflow-hidden shadow-sm"> <div className="border rounded-md overflow-hidden shadow-sm">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -337,7 +346,7 @@ const Faq = () => {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{faq.map((faq, index) => ( {faq.results.map((faq, index) => (
<TableRow key={faq.id}> <TableRow key={faq.id}>
<TableCell className="text-center font-medium"> <TableCell className="text-center font-medium">
{index + 1} {index + 1}
@@ -380,6 +389,40 @@ const Faq = () => {
))} ))}
</Tabs> </Tabs>
<div className="flex justify-end gap-2">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(faq?.total_pages)].map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
currentPage === i + 1
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
}`}
>
{i + 1}
</button>
))}
<button
disabled={currentPage === faq?.total_pages}
onClick={() =>
setCurrentPage((p) => Math.min(p + 1, faq ? faq.total_pages : 0))
}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
<Dialog open={openModal} onOpenChange={setOpenModal}> <Dialog open={openModal} onOpenChange={setOpenModal}>
<DialogContent className="sm:max-w-[500px] h-[80vh] overflow-y-scroll bg-gray-900"> <DialogContent className="sm:max-w-[500px] h-[80vh] overflow-y-scroll bg-gray-900">
<DialogHeader> <DialogHeader>

View File

@@ -9,6 +9,13 @@ import type { AxiosResponse } from "axios";
const getAllOrder = async (params: { const getAllOrder = async (params: {
page: number; page: number;
page_size: number; page_size: number;
order_status:
| "pending_payment"
| "pending_confirmation"
| "cancelled"
| "confirmed"
| "completed"
| "";
}): Promise<AxiosResponse<UserOrderData>> => { }): Promise<AxiosResponse<UserOrderData>> => {
const res = await httpClient.get(USER_ORDERS, { params }); const res = await httpClient.get(USER_ORDERS, { params });
return res; return res;

View File

@@ -22,7 +22,7 @@ import {
Users, Users,
XCircle, XCircle,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -119,12 +119,26 @@ export default function FinancePage() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [tab, setTab] = useState<"bookings" | "agencies">("bookings"); const [tab, setTab] = useState<"bookings" | "agencies">("bookings");
const [filterStatus, setFilterStatus] = useState< const [filterStatus, setFilterStatus] = useState<
"all" | "paid" | "pending" | "cancelled" | "refunded" | ""
>("all"); | "pending_payment"
| "pending_confirmation"
| "confirmed"
| "completed"
| "cancelled"
>("");
useEffect(() => {
setCurrentPage(1);
}, [filterStatus]);
const { data, isLoading, isError, refetch } = useQuery({ const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["list_order_user", currentPage], queryKey: ["list_order_user", currentPage, filterStatus],
queryFn: () => getAllOrder({ page: currentPage, page_size: 10 }), queryFn: () =>
getAllOrder({
page: currentPage,
page_size: 10,
order_status: filterStatus,
}),
}); });
const stats = [ const stats = [
@@ -333,7 +347,14 @@ export default function FinancePage() {
<> <>
{/* Filter */} {/* Filter */}
<div className="flex gap-2 mb-6 flex-wrap"> <div className="flex gap-2 mb-6 flex-wrap">
{["all", "paid", "pending", "cancelled", "refunded"].map((s) => ( {[
"",
"pending_payment",
"pending_confirmation",
"confirmed",
"completed",
"cancelled",
].map((s) => (
<button <button
key={s} key={s}
className={`px-4 py-2 rounded-lg transition-all ${ className={`px-4 py-2 rounded-lg transition-all ${
@@ -344,23 +365,26 @@ export default function FinancePage() {
onClick={() => onClick={() =>
setFilterStatus( setFilterStatus(
s as s as
| "all" | ""
| "paid" | "pending_payment"
| "pending" | "pending_confirmation"
| "cancelled" | "confirmed"
| "refunded", | "completed"
| "cancelled",
) )
} }
> >
{s === "all" {s === ""
? t("Barcha bandlovlar") ? t("Barcha bandlovlar")
: s === "paid" : s === "pending_payment"
? t("To'langan") ? t("Pending Payment")
: s === "pending" : s === "pending_confirmation"
? t("Kutilmoqda") ? t("Pending Confirmation")
: s === "cancelled" : s === "confirmed"
? t("Bekor qilindi") ? t("Confirmed")
: t("Qaytarilgan")} : s === "completed"
? t("Completed")
: t("Cancelled")}
</button> </button>
))} ))}
</div> </div>

View File

@@ -9,8 +9,7 @@ export interface AllSeoData {
total_pages: number; total_pages: number;
page_size: number; page_size: number;
current_page: number; current_page: number;
results: [ results: {
{
id: number; id: number;
title: string; title: string;
description: string; description: string;
@@ -18,8 +17,7 @@ export interface AllSeoData {
og_title: string; og_title: string;
og_description: string; og_description: string;
og_image: string; og_image: string;
}, }[];
];
}; };
} }
@@ -28,10 +26,20 @@ export interface DetailSeoData {
data: { data: {
id: number; id: number;
title: string; title: string;
title_uz: string;
title_ru: string;
description: string; description: string;
description_uz: string;
description_ru: string;
keywords: string; keywords: string;
keywords_uz: string;
keywords_ru: string;
og_title: string; og_title: string;
og_title_uz: string;
og_title_ru: string;
og_description: string; og_description: string;
og_description_uz: string;
og_description_ru: string;
og_image: string; og_image: string;
}; };
} }

View File

@@ -24,20 +24,30 @@ import { toast } from "sonner";
type SeoData = { type SeoData = {
title: string; title: string;
title_ru: string;
description: string; description: string;
description_ru: string;
keywords: string; keywords: string;
keywords_ru: string;
ogTitle: string; ogTitle: string;
ogTitle_ru: string;
ogDescription: string; ogDescription: string;
ogDescription_ru: string;
ogImage: File | null | string; ogImage: File | null | string;
}; };
export default function Seo() { export default function Seo() {
const [formData, setFormData] = useState<SeoData>({ const [formData, setFormData] = useState<SeoData>({
title: "", title: "",
title_ru: "",
description: "", description: "",
description_ru: "",
keywords: "", keywords: "",
keywords_ru: "",
ogTitle: "", ogTitle: "",
ogTitle_ru: "",
ogDescription: "", ogDescription: "",
ogDescription_ru: "",
ogImage: null, ogImage: null,
}); });
const { t } = useTranslation(); const { t } = useTranslation();
@@ -52,6 +62,11 @@ export default function Seo() {
}); });
setFormData({ setFormData({
description: "", description: "",
description_ru: "",
keywords_ru: "",
ogDescription_ru: "",
ogTitle_ru: "",
title_ru: "",
keywords: "", keywords: "",
ogDescription: "", ogDescription: "",
ogImage: null, ogImage: null,
@@ -80,6 +95,11 @@ export default function Seo() {
setEdit(null); setEdit(null);
setFormData({ setFormData({
description: "", description: "",
description_ru: "",
keywords_ru: "",
ogDescription_ru: "",
ogTitle_ru: "",
title_ru: "",
keywords: "", keywords: "",
ogDescription: "", ogDescription: "",
ogImage: null, ogImage: null,
@@ -104,6 +124,11 @@ export default function Seo() {
setEdit(null); setEdit(null);
setFormData({ setFormData({
description: "", description: "",
description_ru: "",
keywords_ru: "",
ogDescription_ru: "",
ogTitle_ru: "",
title_ru: "",
keywords: "", keywords: "",
ogDescription: "", ogDescription: "",
ogImage: null, ogImage: null,
@@ -170,12 +195,17 @@ export default function Seo() {
useEffect(() => { useEffect(() => {
if (detailSeo) { if (detailSeo) {
setFormData({ setFormData({
description: detailSeo.description, description: detailSeo.description_uz,
keywords: detailSeo.keywords, keywords: detailSeo.keywords_uz,
ogDescription: detailSeo.og_description, ogDescription: detailSeo.og_description_uz,
ogImage: detailSeo.og_image, ogImage: detailSeo.og_image,
ogTitle: detailSeo.og_title, ogTitle: detailSeo.og_title_uz,
title: detailSeo.title, title: detailSeo.title_uz,
description_ru: detailSeo.description_ru,
keywords_ru: detailSeo.keywords_ru,
ogDescription_ru: detailSeo.og_description_ru,
ogTitle_ru: detailSeo.og_title_ru,
title_ru: detailSeo.title_ru,
}); });
setImagePreview(detailSeo.og_image || null); setImagePreview(detailSeo.og_image || null);
} }
@@ -184,10 +214,15 @@ export default function Seo() {
const handleSave = () => { const handleSave = () => {
const form = new FormData(); const form = new FormData();
form.append("title", formData.title); form.append("title", formData.title);
form.append("title_ru", formData.title_ru);
form.append("description", formData.description); form.append("description", formData.description);
form.append("description_ru", formData.description_ru);
form.append("keywords", formData.keywords); form.append("keywords", formData.keywords);
form.append("keywords_ru", formData.keywords_ru);
form.append("og_title", formData.ogTitle); form.append("og_title", formData.ogTitle);
form.append("og_title_ru", formData.ogTitle_ru);
form.append("og_description", formData.ogDescription); form.append("og_description", formData.ogDescription);
form.append("og_description_ru", formData.ogDescription_ru);
// faqat File bolsa qoshamiz // faqat File bolsa qoshamiz
if (formData.ogImage instanceof File) { if (formData.ogImage instanceof File) {
@@ -250,6 +285,29 @@ export default function Seo() {
</div> </div>
</div> </div>
<div>
<label className="block text-sm font-semibold text-white mb-2">
<FileText className="inline w-4 h-4 mr-1" /> {t("Page Title")}{" "}
(ru)
</label>
<input
type="text"
name="title_ru"
value={formData.title_ru}
onChange={handleChange}
placeholder={t("Sahifa sarlavhasi (3060 belgi)") + " ru"}
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<div className="flex items-center justify-between mt-2">
{isValidTitle && (
<CheckCircle className="w-5 h-5 text-green-400" />
)}
{getTitleLength() > 0 && !isValidTitle && (
<AlertCircle className="w-5 h-5 text-yellow-400" />
)}
</div>
</div>
{/* Description */} {/* Description */}
<div> <div>
<label className="block text-sm font-semibold text-white mb-2"> <label className="block text-sm font-semibold text-white mb-2">
@@ -275,6 +333,30 @@ export default function Seo() {
</div> </div>
</div> </div>
<div>
<label className="block text-sm font-semibold text-white mb-2">
{t("Meta Description")} (ru)
</label>
<textarea
name="description_ru"
value={formData.description_ru}
onChange={handleChange}
placeholder={t("Sahifa tavsifi (120160 belgi)") + " (ru)"}
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
<div className="flex items-center justify-between mt-2">
<span className="text-sm text-slate-400">
{getDescriptionLength()} / 160
</span>
{isValidDescription && (
<CheckCircle className="w-5 h-5 text-green-400" />
)}
{getDescriptionLength() > 0 && !isValidDescription && (
<AlertCircle className="w-5 h-5 text-yellow-400" />
)}
</div>
</div>
{/* Keywords */} {/* Keywords */}
<div> <div>
<label className="block text-sm font-semibold text-white mb-2"> <label className="block text-sm font-semibold text-white mb-2">
@@ -293,6 +375,25 @@ export default function Seo() {
</p> </p>
</div> </div>
<div>
<label className="block text-sm font-semibold text-white mb-2">
{t("Keywords")} (ru)
</label>
<input
type="text"
name="keywords_ru"
value={formData.keywords_ru}
onChange={handleChange}
placeholder={
t("Kalit so'zlar (vergul bilan ajratilgan)") + " (ru)"
}
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-slate-400 mt-1">
{t("Masalan: Python, Web Development, Coding")}
</p>
</div>
{/* OG Tags */} {/* OG Tags */}
<div className="border-t border-slate-700 pt-6"> <div className="border-t border-slate-700 pt-6">
<h3 className="text-sm font-semibold text-white mb-4"> <h3 className="text-sm font-semibold text-white mb-4">
@@ -314,6 +415,20 @@ export default function Seo() {
/> />
</div> </div>
<div>
<label className="block text-sm text-slate-300 mb-2">
{t("OG Title")} (ru)
</label>
<input
type="text"
name="ogTitle_ru"
value={formData.ogTitle_ru}
onChange={handleChange}
placeholder={t("Ijtimoiy tarmoqdagi sarlavha") + " (ru)"}
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div> <div>
<label className="block text-sm text-slate-300 mb-2"> <label className="block text-sm text-slate-300 mb-2">
{t("OG Description")} {t("OG Description")}
@@ -327,6 +442,19 @@ export default function Seo() {
/> />
</div> </div>
<div>
<label className="block text-sm text-slate-300 mb-2">
{t("OG Description")} (ru)
</label>
<textarea
name="ogDescription_ru"
value={formData.ogDescription_ru}
onChange={handleChange}
placeholder={t("Ijtimoiy tarmoqdagi tavsif") + " (ru)"}
className="w-full bg-slate-700 text-white px-4 py-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
</div>
<div> <div>
<label className="block text-sm text-slate-300 mb-2"> <label className="block text-sm text-slate-300 mb-2">
<ImageIcon className="inline w-4 h-4 mr-1" />{" "} <ImageIcon className="inline w-4 h-4 mr-1" />{" "}
@@ -368,7 +496,7 @@ export default function Seo() {
<button <button
onClick={handleSave} onClick={handleSave}
disabled={isPending} disabled={allSeo && allSeo.length !== 0}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors disabled:opacity-50" className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors disabled:opacity-50"
> >
{isPending || editPending ? ( {isPending || editPending ? (

View File

@@ -13,7 +13,9 @@ const createOfferta = async ({
}: { }: {
body: { body: {
title: string; title: string;
title_ru: string;
content: string; content: string;
content_ru: string;
person_type: "individual" | "legal_entity"; person_type: "individual" | "legal_entity";
is_active: boolean; is_active: boolean;
}; };
@@ -28,8 +30,10 @@ const updateOfferta = async ({
}: { }: {
id: number; id: number;
body: { body: {
title?: string; title: string;
content?: string; title_ru: string;
content: string;
content_ru: string;
person_type?: "individual" | "legal_entity"; person_type?: "individual" | "legal_entity";
is_active?: boolean; is_active?: boolean;
}; };
@@ -41,6 +45,7 @@ const updateOfferta = async ({
const getAllOfferta = async (params: { const getAllOfferta = async (params: {
page: number; page: number;
page_size: number; page_size: number;
person_type?: "individual" | "legal_entity";
}): Promise<AxiosResponse<GetAllOfferta>> => { }): Promise<AxiosResponse<GetAllOfferta>> => {
const res = await httpClient.get(OFFERTA, { params }); const res = await httpClient.get(OFFERTA, { params });
return res; return res;
@@ -63,6 +68,8 @@ const createHelpPage = async ({
}: { }: {
body: { body: {
title: string; title: string;
title_ru: string;
content_ru: string;
content: string; content: string;
page_type: "privacy_policy" | "user_agreement"; page_type: "privacy_policy" | "user_agreement";
is_active: boolean; is_active: boolean;
@@ -75,6 +82,7 @@ const createHelpPage = async ({
const getAllHelpPage = async (params: { const getAllHelpPage = async (params: {
page: number; page: number;
page_size: number; page_size: number;
page_type?: "privacy_policy" | "user_agreement";
}): Promise<AxiosResponse<GetAllHelpPage>> => { }): Promise<AxiosResponse<GetAllHelpPage>> => {
const res = await httpClient.get(HELP_PAGE, { params }); const res = await httpClient.get(HELP_PAGE, { params });
return res; return res;
@@ -93,8 +101,10 @@ const updateHelpPage = async ({
}: { }: {
id: number; id: number;
body: { body: {
title?: string; title: string;
content?: string; title_ru: string;
content_ru: string;
content: string;
page_type?: "privacy_policy" | "user_agreement"; page_type?: "privacy_policy" | "user_agreement";
is_active?: boolean; is_active?: boolean;
}; };

View File

@@ -25,6 +25,8 @@ export interface GetDetailOfferta {
id: number; id: number;
title: string; title: string;
content: string; content: string;
title_ru: string;
content_ru: string;
person_type: "individual" | "legal_entity"; person_type: "individual" | "legal_entity";
is_active: boolean; is_active: boolean;
}; };
@@ -56,7 +58,9 @@ export interface GetDetailHelpPage {
data: { data: {
id: number; id: number;
title: string; title: string;
title_ru: string;
content: string; content: string;
content_ru: string;
page_type: "privacy_policy" | "user_agreement"; page_type: "privacy_policy" | "user_agreement";
is_active: true; is_active: true;
}; };

View File

@@ -45,6 +45,34 @@ export default function PolicyCrud() {
}, },
}); });
const { data: PrivacyPolicy } = useQuery({
queryKey: ["privacy_policy"],
queryFn: () => {
return getAllHelpPage({
page: 1,
page_size: 99,
page_type: "privacy_policy",
});
},
select(data) {
return data.data.data;
},
});
const { data: UserAgreement } = useQuery({
queryKey: ["user_agreement"],
queryFn: () => {
return getAllHelpPage({
page: 1,
page_size: 99,
page_type: "user_agreement",
});
},
select(data) {
return data.data.data;
},
});
const [editing, setEditing] = useState<number | null>(null); const [editing, setEditing] = useState<number | null>(null);
const { data: detail } = useQuery({ const { data: detail } = useQuery({
@@ -60,11 +88,15 @@ export default function PolicyCrud() {
const [form, setForm] = useState<{ const [form, setForm] = useState<{
title: string; title: string;
title_ru: string;
content_ru: string;
content: string; content: string;
page_type: "privacy_policy" | "user_agreement"; page_type: "privacy_policy" | "user_agreement";
is_active: boolean; is_active: boolean;
}>({ }>({
title: "", title: "",
content_ru: "",
title_ru: "",
content: "", content: "",
page_type: "privacy_policy", page_type: "privacy_policy",
is_active: true, is_active: true,
@@ -78,6 +110,8 @@ export default function PolicyCrud() {
}: { }: {
body: { body: {
title: string; title: string;
title_ru: string;
content_ru: string;
content: string; content: string;
page_type: "privacy_policy" | "user_agreement"; page_type: "privacy_policy" | "user_agreement";
is_active: boolean; is_active: boolean;
@@ -87,6 +121,8 @@ export default function PolicyCrud() {
resetForm(); resetForm();
queryClient.refetchQueries({ queryKey: ["help_page"] }); queryClient.refetchQueries({ queryKey: ["help_page"] });
queryClient.refetchQueries({ queryKey: ["help_page_detail"] }); queryClient.refetchQueries({ queryKey: ["help_page_detail"] });
queryClient.refetchQueries({ queryKey: ["privacy_policy"] });
queryClient.refetchQueries({ queryKey: ["user_agreement"] });
}, },
onError: () => { onError: () => {
toast.error(t("Xatolik yuz berdi"), { toast.error(t("Xatolik yuz berdi"), {
@@ -103,8 +139,10 @@ export default function PolicyCrud() {
}: { }: {
id: number; id: number;
body: { body: {
title?: string; title: string;
content?: string; title_ru: string;
content_ru: string;
content: string;
page_type?: "privacy_policy" | "user_agreement"; page_type?: "privacy_policy" | "user_agreement";
is_active?: boolean; is_active?: boolean;
}; };
@@ -113,6 +151,8 @@ export default function PolicyCrud() {
resetForm(); resetForm();
queryClient.refetchQueries({ queryKey: ["help_page"] }); queryClient.refetchQueries({ queryKey: ["help_page"] });
queryClient.refetchQueries({ queryKey: ["help_page_detail"] }); queryClient.refetchQueries({ queryKey: ["help_page_detail"] });
queryClient.refetchQueries({ queryKey: ["privacy_policy"] });
queryClient.refetchQueries({ queryKey: ["user_agreement"] });
}, },
onError: () => { onError: () => {
toast.error(t("Xatolik yuz berdi"), { toast.error(t("Xatolik yuz berdi"), {
@@ -128,6 +168,8 @@ export default function PolicyCrud() {
resetForm(); resetForm();
queryClient.refetchQueries({ queryKey: ["help_page"] }); queryClient.refetchQueries({ queryKey: ["help_page"] });
queryClient.refetchQueries({ queryKey: ["help_page_detail"] }); queryClient.refetchQueries({ queryKey: ["help_page_detail"] });
queryClient.refetchQueries({ queryKey: ["privacy_policy"] });
queryClient.refetchQueries({ queryKey: ["user_agreement"] });
}, },
onError: () => { onError: () => {
toast.error(t("Xatolik yuz berdi"), { toast.error(t("Xatolik yuz berdi"), {
@@ -140,6 +182,8 @@ export default function PolicyCrud() {
function resetForm() { function resetForm() {
setForm({ setForm({
title: "", title: "",
title_ru: "",
content_ru: "",
content: "", content: "",
is_active: true, is_active: true,
page_type: "privacy_policy", page_type: "privacy_policy",
@@ -155,6 +199,8 @@ export default function PolicyCrud() {
is_active: detail.is_active, is_active: detail.is_active,
page_type: detail.page_type, page_type: detail.page_type,
title: detail.title, title: detail.title,
content_ru: detail.content_ru,
title_ru: detail.title_ru,
}); });
} }
}, [detail, editing]); }, [detail, editing]);
@@ -189,6 +235,8 @@ export default function PolicyCrud() {
is_active: form.is_active, is_active: form.is_active,
page_type: form.page_type, page_type: form.page_type,
title: form.title, title: form.title,
content_ru: form.content_ru,
title_ru: form.title_ru,
}, },
id: editing, id: editing,
}); });
@@ -199,6 +247,8 @@ export default function PolicyCrud() {
is_active: form.is_active, is_active: form.is_active,
page_type: form.page_type, page_type: form.page_type,
title: form.title, title: form.title,
content_ru: form.content_ru,
title_ru: form.title_ru,
}, },
}); });
} }
@@ -212,14 +262,13 @@ export default function PolicyCrud() {
deleteHelp(id); deleteHelp(id);
} }
function toggleActive(id: number, currentStatus: boolean) { const individualExists = !!PrivacyPolicy?.results?.length;
update({ const legalExists = !!UserAgreement?.results?.length;
id: id,
body: { const isAddDisabled =
is_active: !currentStatus, !editing &&
}, ((form.page_type === "privacy_policy" && individualExists) ||
}); (form.page_type === "user_agreement" && legalExists));
}
return ( return (
<div className="min-h-screen w-full p-6 bg-gray-900"> <div className="min-h-screen w-full p-6 bg-gray-900">
@@ -253,6 +302,26 @@ export default function PolicyCrud() {
</p> </p>
)} )}
</div> </div>
<div>
<label className="text-sm font-medium">
{t("Sarlavha")} (ru)
</label>
<Input
value={form.title_ru || ""}
onChange={(e) =>
setForm((s) => ({ ...s, title_ru: e.target.value }))
}
placeholder={t("Yordam sahifasi sarlavhasi") + " (ru)"}
className="mt-1"
/>
{errors.title && (
<p className="text-destructive text-sm mt-1">
{t(errors.title)}
</p>
)}
</div>
<div className="h-[280px] w-[100%]"> <div className="h-[280px] w-[100%]">
<label className="text-sm font-medium">{t("Kontent")}</label> <label className="text-sm font-medium">{t("Kontent")}</label>
<div className="mt-1"> <div className="mt-1">
@@ -272,6 +341,25 @@ export default function PolicyCrud() {
)} )}
</div> </div>
<div className="h-[280px] w-[100%]">
<label className="text-sm font-medium">{t("Kontent")} (ru)</label>
<div className="mt-1">
<ReactQuill
value={form.content_ru || ""}
onChange={(value) =>
setForm((s) => ({ ...s, content_ru: value }))
}
className="bg-gray-900 h-48"
placeholder={t("Yordam matnini kiriting...") + " (ru)"}
/>
</div>
{errors.content && (
<p className="text-destructive text-sm mt-12">
{t(errors.content)}
</p>
)}
</div>
<div className="grid grid-cols-1 gap-4 mt-5"> <div className="grid grid-cols-1 gap-4 mt-5">
<div> <div>
<label className="text-sm font-medium"> <label className="text-sm font-medium">
@@ -285,15 +373,19 @@ export default function PolicyCrud() {
page_type: value as "privacy_policy" | "user_agreement", page_type: value as "privacy_policy" | "user_agreement",
})) }))
} }
disabled={editing !== null}
> >
<SelectTrigger className="mt-1 w-full !h-12"> <SelectTrigger className="mt-1 w-full !h-12">
<SelectValue placeholder={t("Sahifa turini tanlang")} /> <SelectValue placeholder={t("Sahifa turini tanlang")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="user_agreement"> <SelectItem value="user_agreement" disabled={legalExists}>
{t("Qollanma")} {t("Qollanma")}
</SelectItem> </SelectItem>
<SelectItem value="privacy_policy"> <SelectItem
value="privacy_policy"
disabled={individualExists}
>
{t("Maxfiylik siyosati")} {t("Maxfiylik siyosati")}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@@ -316,6 +408,7 @@ export default function PolicyCrud() {
<div className="flex gap-2 pt-4"> <div className="flex gap-2 pt-4">
<Button <Button
onClick={handleCreateOrUpdate} onClick={handleCreateOrUpdate}
disabled={isAddDisabled}
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
> >
{editing ? t("Saqlash") : t("Yaratish")} {editing ? t("Saqlash") : t("Yaratish")}
@@ -358,19 +451,6 @@ export default function PolicyCrud() {
{t("Tahrirlash")} {t("Tahrirlash")}
</Button> </Button>
<Button
onClick={() => toggleActive(it.id, it.is_active)}
variant={it.is_active ? "default" : "outline"}
size="sm"
className={
it.is_active
? "bg-green-600 hover:bg-green-700 w-full"
: "w-full"
}
>
{it.is_active ? t("Faol") : t("Faol emas")}
</Button>
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}> <Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button

View File

@@ -33,15 +33,20 @@ import { toast } from "sonner";
export default function OmmaviyOfertaCRUD() { export default function OmmaviyOfertaCRUD() {
const { t } = useTranslation(); const { t } = useTranslation();
const [deleteOpen, setDeleteOpen] = useState<boolean>(false); const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedId, setSelectedId] = useState<number | null>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [editing, setEditing] = useState<number | null>(null); const [editing, setEditing] = useState<number | null>(null);
const [form, setForm] = useState<{ const [form, setForm] = useState<{
title: string; title: string;
title_ru: string;
content: string; content: string;
audience: string; audience: string;
active: boolean; active: boolean;
content_ru: string;
}>({ }>({
title: "", title: "",
content_ru: "",
title_ru: "",
audience: "Jismoniy shaxslar", audience: "Jismoniy shaxslar",
content: "", content: "",
active: true, active: true,
@@ -51,6 +56,8 @@ export default function OmmaviyOfertaCRUD() {
function resetForm() { function resetForm() {
setForm({ setForm({
title: "", title: "",
content_ru: "",
title_ru: "",
audience: "Jismoniy shaxslar", audience: "Jismoniy shaxslar",
content: "", content: "",
active: true, active: true,
@@ -62,7 +69,9 @@ export default function OmmaviyOfertaCRUD() {
const { mutate: create } = useMutation({ const { mutate: create } = useMutation({
mutationFn: (body: { mutationFn: (body: {
title: string; title: string;
title_ru: string;
content: string; content: string;
content_ru: string;
person_type: "individual" | "legal_entity"; person_type: "individual" | "legal_entity";
is_active: boolean; is_active: boolean;
}) => createOfferta({ body }), }) => createOfferta({ body }),
@@ -70,6 +79,8 @@ export default function OmmaviyOfertaCRUD() {
resetForm(); resetForm();
queryClient.refetchQueries({ queryKey: ["all_offerta"] }); queryClient.refetchQueries({ queryKey: ["all_offerta"] });
queryClient.refetchQueries({ queryKey: ["detail_offerta"] }); queryClient.refetchQueries({ queryKey: ["detail_offerta"] });
queryClient.refetchQueries({ queryKey: ["individual_offerta"] });
queryClient.refetchQueries({ queryKey: ["legal_entity"] });
toast.success(t("Muvaffaqiyatli yaratildi"), { toast.success(t("Muvaffaqiyatli yaratildi"), {
position: "top-center", position: "top-center",
richColors: true, richColors: true,
@@ -89,8 +100,10 @@ export default function OmmaviyOfertaCRUD() {
id, id,
}: { }: {
body: { body: {
title?: string; title: string;
content?: string; title_ru: string;
content: string;
content_ru: string;
person_type?: "individual" | "legal_entity"; person_type?: "individual" | "legal_entity";
is_active?: boolean; is_active?: boolean;
}; };
@@ -101,6 +114,8 @@ export default function OmmaviyOfertaCRUD() {
setEditing(null); setEditing(null);
queryClient.refetchQueries({ queryKey: ["all_offerta"] }); queryClient.refetchQueries({ queryKey: ["all_offerta"] });
queryClient.refetchQueries({ queryKey: ["detail_offerta"] }); queryClient.refetchQueries({ queryKey: ["detail_offerta"] });
queryClient.refetchQueries({ queryKey: ["individual_offerta"] });
queryClient.refetchQueries({ queryKey: ["legal_entity"] });
toast.success(t("Muvaffaqiyatli yangilandi"), { toast.success(t("Muvaffaqiyatli yangilandi"), {
position: "top-center", position: "top-center",
richColors: true, richColors: true,
@@ -118,8 +133,11 @@ export default function OmmaviyOfertaCRUD() {
mutationFn: ({ id }: { id: number }) => deleteOfferta(id), mutationFn: ({ id }: { id: number }) => deleteOfferta(id),
onSuccess: () => { onSuccess: () => {
resetForm(); resetForm();
setDeleteOpen(false);
queryClient.refetchQueries({ queryKey: ["all_offerta"] }); queryClient.refetchQueries({ queryKey: ["all_offerta"] });
queryClient.refetchQueries({ queryKey: ["detail_offerta"] }); queryClient.refetchQueries({ queryKey: ["detail_offerta"] });
queryClient.refetchQueries({ queryKey: ["individual_offerta"] });
queryClient.refetchQueries({ queryKey: ["legal_entity"] });
toast.success(t("Muvaffaqiyatli o'chirildi"), { toast.success(t("Muvaffaqiyatli o'chirildi"), {
position: "top-center", position: "top-center",
richColors: true, richColors: true,
@@ -136,7 +154,38 @@ export default function OmmaviyOfertaCRUD() {
const { data: allOfferta } = useQuery({ const { data: allOfferta } = useQuery({
queryKey: ["all_offerta"], queryKey: ["all_offerta"],
queryFn: () => { queryFn: () => {
return getAllOfferta({ page: 1, page_size: 99 }); return getAllOfferta({
page: 1,
page_size: 99,
});
},
select(data) {
return data.data.data;
},
});
const { data: individualOfferta } = useQuery({
queryKey: ["individual_offerta"],
queryFn: () => {
return getAllOfferta({
page: 1,
page_size: 99,
person_type: "individual",
});
},
select(data) {
return data.data.data;
},
});
const { data: legalOfferta } = useQuery({
queryKey: ["legal_entity"],
queryFn: () => {
return getAllOfferta({
page: 1,
page_size: 99,
person_type: "legal_entity",
});
}, },
select(data) { select(data) {
return data.data.data; return data.data.data;
@@ -158,6 +207,8 @@ export default function OmmaviyOfertaCRUD() {
if (editing && detailOfferta) { if (editing && detailOfferta) {
setForm({ setForm({
active: detailOfferta.is_active, active: detailOfferta.is_active,
content_ru: detailOfferta.content_ru,
title_ru: detailOfferta.title_ru,
audience: audience:
detailOfferta.person_type === "individual" detailOfferta.person_type === "individual"
? "Jismoniy shaxslar" ? "Jismoniy shaxslar"
@@ -197,6 +248,8 @@ export default function OmmaviyOfertaCRUD() {
if (editing === null) { if (editing === null) {
create({ create({
content: form.content, content: form.content,
content_ru: form.content_ru,
title_ru: form.title_ru,
is_active: form.active, is_active: form.active,
person_type: person_type:
form.audience === "Jismoniy shaxslar" ? "individual" : "legal_entity", form.audience === "Jismoniy shaxslar" ? "individual" : "legal_entity",
@@ -207,6 +260,8 @@ export default function OmmaviyOfertaCRUD() {
body: { body: {
content: form.content, content: form.content,
is_active: form.active, is_active: form.active,
content_ru: form.content_ru,
title_ru: form.title_ru,
person_type: person_type:
form.audience === "Jismoniy shaxslar" form.audience === "Jismoniy shaxslar"
? "individual" ? "individual"
@@ -222,18 +277,13 @@ export default function OmmaviyOfertaCRUD() {
setEditing(item); setEditing(item);
} }
function removeItem(id: number) { const individualExists = !!individualOfferta?.results?.length;
removeOfferta({ id }); const legalExists = !!legalOfferta?.results?.length;
}
function toggleActive(id: number, currentStatus: boolean) { const isAddDisabled =
update({ !editing &&
id: id, ((form.audience === "Jismoniy shaxslar" && individualExists) ||
body: { (form.audience === "Yuridik shaxslar" && legalExists));
is_active: !currentStatus,
},
});
}
return ( return (
<div className="min-h-screen w-full p-6 bg-gray-900"> <div className="min-h-screen w-full p-6 bg-gray-900">
@@ -265,7 +315,27 @@ export default function OmmaviyOfertaCRUD() {
</p> </p>
)} )}
</div> </div>
<div className="h-[280px] w-[100%]">
<div>
<label className="text-sm font-medium">
{t("Sarlavha")} (ru)
</label>
<Input
value={form.title_ru || ""}
onChange={(e) =>
setForm((s) => ({ ...s, title_ru: e.target.value }))
}
placeholder={t("Ommaviy oferta sarlavhasi") + " (ru)"}
className="mt-1"
/>
{errors.title && (
<p className="text-destructive text-sm mt-1">
{t(errors.title_ru)}
</p>
)}
</div>
<div className="h-[280px] w-full">
<label className="text-sm font-medium">{t("Kontent")}</label> <label className="text-sm font-medium">{t("Kontent")}</label>
<div className="mt-1"> <div className="mt-1">
<ReactQuill <ReactQuill
@@ -284,22 +354,48 @@ export default function OmmaviyOfertaCRUD() {
)} )}
</div> </div>
<div className="h-[280px] w-full">
<label className="text-sm font-medium">{t("Kontent")} (ru)</label>
<div className="mt-1">
<ReactQuill
value={form.content_ru || ""}
onChange={(value) =>
setForm((s) => ({ ...s, content_ru: value }))
}
className="bg-gray-900 h-48"
placeholder={t("Oferta matnini kiriting...") + " (ru)"}
/>
</div>
{errors.content && (
<p className="text-destructive text-sm mt-12">
{t(errors.content_ru)}
</p>
)}
</div>
{/* Kimlar uchun */}
<div> <div>
<label className="text-sm font-medium">{t("Kimlar uchun")}</label> <label className="text-sm font-medium">{t("Kimlar uchun")}</label>
<Select <Select
value={form.audience || t("Barcha")} value={form.audience || ""}
onValueChange={(value) => onValueChange={(value) =>
setForm((s) => ({ ...s, audience: value })) setForm((s) => ({ ...s, audience: value }))
} }
disabled={editing !== null}
> >
<SelectTrigger className="mt-1 w-full !h-12"> <SelectTrigger className="mt-1 w-full !h-12">
<SelectValue /> <SelectValue placeholder={t("Offerta turini tanlang")} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="Jismoniy shaxslar"> <SelectItem
value="Jismoniy shaxslar"
disabled={individualExists}
>
{t("Jismoniy shaxslar uchun")} {t("Jismoniy shaxslar uchun")}
</SelectItem> </SelectItem>
<SelectItem value="Yuridik shaxslar">
<SelectItem value="Yuridik shaxslar" disabled={legalExists}>
{t("Yuridik shaxslar uchun")} {t("Yuridik shaxslar uchun")}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
@@ -311,9 +407,11 @@ export default function OmmaviyOfertaCRUD() {
)} )}
</div> </div>
{/* Tugmalar */}
<div className="flex gap-2 pt-4"> <div className="flex gap-2 pt-4">
<Button <Button
onClick={handleCreateOrUpdate} onClick={handleCreateOrUpdate}
disabled={isAddDisabled}
className="bg-blue-600 hover:bg-blue-700" className="bg-blue-600 hover:bg-blue-700"
> >
{editing ? t("Saqlash") : t("Qo'shish")} {editing ? t("Saqlash") : t("Qo'shish")}
@@ -335,7 +433,6 @@ export default function OmmaviyOfertaCRUD() {
</CardContent> </CardContent>
</Card> </Card>
)} )}
{allOfferta?.results.map((it) => ( {allOfferta?.results.map((it) => (
<Card key={it.id} className="overflow-hidden"> <Card key={it.id} className="overflow-hidden">
<CardContent className="pt-6"> <CardContent className="pt-6">
@@ -360,31 +457,30 @@ export default function OmmaviyOfertaCRUD() {
{t("Tahrirlash")} {t("Tahrirlash")}
</Button> </Button>
<Button <Dialog
onClick={() => toggleActive(it.id, it.is_active)} open={deleteOpen && selectedId === it.id}
variant={it.is_active ? "default" : "outline"} onOpenChange={(open) => {
size="sm" if (!open) {
className={ setDeleteOpen(false);
it.is_active setSelectedId(null);
? "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> <DialogTrigger asChild>
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
className="w-full" className="w-full"
onClick={() => setDeleteOpen(true)} onClick={() => {
setSelectedId(it.id);
setDeleteOpen(true);
}}
> >
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 mr-1" />
{t("O'chirish")} {t("O'chirish")}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>{t("O'chirish tasdiqlash")}</DialogTitle> <DialogTitle>{t("O'chirish tasdiqlash")}</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -393,12 +489,21 @@ export default function OmmaviyOfertaCRUD() {
)} )}
</DialogDescription> </DialogDescription>
<div className="flex gap-3 justify-end pt-4"> <div className="flex gap-3 justify-end pt-4">
<Button onClick={() => setDeleteOpen(false)}> <Button
onClick={() => {
setDeleteOpen(false);
setSelectedId(null);
}}
>
{t("Bekor qilish")} {t("Bekor qilish")}
</Button> </Button>
<Button <Button
variant={"destructive"} variant="destructive"
onClick={() => removeItem(it.id)} onClick={() => {
removeOfferta({ id: selectedId! });
setDeleteOpen(false);
setSelectedId(null);
}}
> >
{t("O'chirish")} {t("O'chirish")}
</Button> </Button>

View File

@@ -19,13 +19,22 @@ import {
DialogTitle, DialogTitle,
} from "@/shared/ui/dialog"; } from "@/shared/ui/dialog";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, Loader2, Phone, Trash2, User } from "lucide-react"; import {
import { useState } from "react"; AlertTriangle,
ChevronLeft,
ChevronRight,
Loader2,
Phone,
Trash2,
User,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "sonner"; import { toast } from "sonner";
const SupportTours = () => { const SupportTours = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [selected, setSelected] = useState<GetSupportUserRes | null>(null); const [selected, setSelected] = useState<GetSupportUserRes | null>(null);
const [selectedToDelete, setSelectedToDelete] = const [selectedToDelete, setSelectedToDelete] =
useState<GetSupportUserRes | null>(null); useState<GetSupportUserRes | null>(null);
@@ -34,10 +43,18 @@ const SupportTours = () => {
"" | "pending" | "done" | "failed" "" | "pending" | "done" | "failed"
>(""); >("");
useEffect(() => {
setCurrentPage(1);
}, [filterStatus]);
const { data, isLoading, isError, refetch } = useQuery({ const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["support_user", filterStatus], queryKey: ["support_user", filterStatus, currentPage],
queryFn: () => queryFn: () =>
getSupportUser({ page: 1, page_size: 99, status: filterStatus }), getSupportUser({
page: currentPage,
page_size: 10,
status: filterStatus,
}),
}); });
const { data: agency } = useQuery({ const { data: agency } = useQuery({
@@ -210,6 +227,45 @@ const SupportTours = () => {
</> </>
)} )}
<div className="flex justify-end gap-2">
<button
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(data?.data.data?.total_pages)].map((_, i) => (
<button
key={i}
onClick={() => setCurrentPage(i + 1)}
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
currentPage === i + 1
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
}`}
>
{i + 1}
</button>
))}
<button
disabled={currentPage === data?.data.data?.total_pages}
onClick={() =>
setCurrentPage((p) =>
Math.min(
p + 1,
data?.data.data ? data?.data.data.total_pages : 0,
),
)
}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Detail Modal */} {/* Detail Modal */}
<Dialog open={!!selected} onOpenChange={() => setSelected(null)}> <Dialog open={!!selected} onOpenChange={() => setSelected(null)}>
<DialogContent className="bg-gray-800 border border-gray-700 text-gray-100 sm:max-w-lg"> <DialogContent className="bg-gray-800 border border-gray-700 text-gray-100 sm:max-w-lg">

View File

@@ -12,6 +12,8 @@ const createSiteSetting = async (body: {
longitude: string; longitude: string;
telegram: string; telegram: string;
instagram: string; instagram: string;
telegram_chat: string;
linkedin: string;
facebook: string; facebook: string;
twitter: string; twitter: string;
email: string; email: string;
@@ -32,6 +34,8 @@ const updateSiteSetting = async ({
latitude: string; latitude: string;
longitude: string; longitude: string;
telegram: string; telegram: string;
telegram_chat: string;
linkedin: string;
instagram: string; instagram: string;
facebook: string; facebook: string;
twitter: string; twitter: string;

View File

@@ -15,6 +15,8 @@ export interface GetSiteSetting {
latitude: string; latitude: string;
longitude: string; longitude: string;
telegram: string; telegram: string;
telegram_chat: string;
linkedin: string;
instagram: string; instagram: string;
facebook: string; facebook: string;
twitter: string; twitter: string;
@@ -33,6 +35,8 @@ export interface GetDetailSiteSetting {
latitude: string; latitude: string;
longitude: string; longitude: string;
telegram: string; telegram: string;
telegram_chat: string;
linkedin: string;
instagram: string; instagram: string;
facebook: string; facebook: string;
twitter: string; twitter: string;

View File

@@ -33,6 +33,7 @@ type MapClickEvent = {
type ContactInfo = { type ContactInfo = {
telegram: string; telegram: string;
instagram: string; instagram: string;
telegram_chat: string;
facebook: string; facebook: string;
twiter: string; twiter: string;
linkedin: string; linkedin: string;
@@ -44,7 +45,7 @@ type ContactInfo = {
async function getAddressFromCoords(lat: number, lon: number) { async function getAddressFromCoords(lat: number, lon: number) {
const response = await fetch( const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json`, `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=uz`,
); );
const data = await response.json(); const data = await response.json();
return data.display_name || ""; return data.display_name || "";
@@ -57,6 +58,7 @@ export default function ContactSettings() {
const [editing, setEditing] = useState<number | null>(null); const [editing, setEditing] = useState<number | null>(null);
const [form, setForm] = useState<ContactInfo>({ const [form, setForm] = useState<ContactInfo>({
telegram: "", telegram: "",
telegram_chat: "",
instagram: "", instagram: "",
facebook: "", facebook: "",
twiter: "", twiter: "",
@@ -80,12 +82,15 @@ export default function ContactSettings() {
facebook: string; facebook: string;
twitter: string; twitter: string;
email: string; email: string;
telegram_chat: string;
linkedin: string;
main_phone: string; main_phone: string;
other_phone: string; other_phone: string;
}) => createSiteSetting(body), }) => createSiteSetting(body),
onSuccess: () => { onSuccess: () => {
setForm({ setForm({
telegram: "", telegram: "",
telegram_chat: "",
instagram: "", instagram: "",
facebook: "", facebook: "",
twiter: "", twiter: "",
@@ -121,6 +126,8 @@ export default function ContactSettings() {
telegram: string; telegram: string;
instagram: string; instagram: string;
facebook: string; facebook: string;
telegram_chat: string;
linkedin: string;
twitter: string; twitter: string;
email: string; email: string;
main_phone: string; main_phone: string;
@@ -130,6 +137,7 @@ export default function ContactSettings() {
onSuccess: () => { onSuccess: () => {
setForm({ setForm({
telegram: "", telegram: "",
telegram_chat: "",
instagram: "", instagram: "",
facebook: "", facebook: "",
twiter: "", twiter: "",
@@ -157,6 +165,7 @@ export default function ContactSettings() {
onSuccess: () => { onSuccess: () => {
setForm({ setForm({
telegram: "", telegram: "",
telegram_chat: "",
instagram: "", instagram: "",
facebook: "", facebook: "",
twiter: "", twiter: "",
@@ -207,10 +216,11 @@ export default function ContactSettings() {
email: detail.email, email: detail.email,
facebook: detail.facebook, facebook: detail.facebook,
instagram: detail.instagram, instagram: detail.instagram,
linkedin: detail.twitter, linkedin: detail.linkedin,
phonePrimary: detail.main_phone, phonePrimary: detail.main_phone,
phoneSecondary: detail.other_phone, phoneSecondary: detail.other_phone,
telegram: detail.telegram, telegram: detail.telegram,
telegram_chat: detail.telegram_chat,
twiter: detail.twitter, twiter: detail.twitter,
}); });
setCoords({ setCoords({
@@ -237,14 +247,41 @@ export default function ContactSettings() {
}; };
const saveContact = () => { const saveContact = () => {
const shortLat = Number(coords.latitude.toFixed(6)); // 6ta raqamgacha const shortLat = Number(coords.latitude.toFixed(6));
const shortLon = Number(coords.longitude.toFixed(6)); const shortLon = Number(coords.longitude.toFixed(6));
// Majburiy maydonlar tekshiruvi
if (!form.address.trim()) {
toast.error(t("Manzilni kiriting!"), {
position: "top-center",
richColors: true,
});
return;
}
if (!form.phonePrimary.trim() || form.phonePrimary.length < 9) {
toast.error(t("Asosiy telefon raqamini togri kiriting!"), {
position: "top-center",
richColors: true,
});
return;
}
if (!coords.latitude || !coords.longitude) {
toast.error(t("Xaritadan joy tanlang! (latitude/longitude majburiy)"), {
position: "top-center",
richColors: true,
});
return;
}
if (editing) { if (editing) {
update({ update({
body: { body: {
address: form.address, address: form.address,
email: form.email, email: form.email,
facebook: form.facebook, facebook: form.facebook,
linkedin: form.linkedin,
telegram_chat: form.telegram_chat,
instagram: form.instagram, instagram: form.instagram,
latitude: String(shortLat), latitude: String(shortLat),
longitude: String(shortLon), longitude: String(shortLon),
@@ -259,6 +296,8 @@ export default function ContactSettings() {
create({ create({
address: form.address, address: form.address,
email: form.email, email: form.email,
linkedin: form.linkedin,
telegram_chat: form.telegram_chat,
facebook: form.facebook, facebook: form.facebook,
instagram: form.instagram, instagram: form.instagram,
latitude: String(shortLat), latitude: String(shortLat),
@@ -277,6 +316,7 @@ export default function ContactSettings() {
instagram: "", instagram: "",
facebook: "", facebook: "",
twiter: "", twiter: "",
telegram_chat: "",
linkedin: "", linkedin: "",
address: "", address: "",
email: "", email: "",
@@ -370,12 +410,26 @@ export default function ContactSettings() {
</div> </div>
<div className="text-sm">{e.telegram || "—"}</div> <div className="text-sm">{e.telegram || "—"}</div>
</div> </div>
<div>
<div className="text-sm text-muted-foreground">
Telegram Chat
</div>
<div className="text-sm">
{e.telegram_chat || "—"}
</div>
</div>
<div> <div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Instagram Instagram
</div> </div>
<div className="text-sm">{e.instagram || "—"}</div> <div className="text-sm">{e.instagram || "—"}</div>
</div> </div>
<div>
<div className="text-xs text-muted-foreground">
Instagram
</div>
<div className="text-sm">{e.linkedin || "—"}</div>
</div>
<div> <div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
Facebook Facebook
@@ -438,6 +492,7 @@ export default function ContactSettings() {
zoom: 13, zoom: 13,
}} }}
width="100%" width="100%"
la
height="400px" height="400px"
onClick={handleMapClick} onClick={handleMapClick}
> >
@@ -460,6 +515,13 @@ export default function ContactSettings() {
onChange={(e) => handleChange("telegram", e.target.value)} onChange={(e) => handleChange("telegram", e.target.value)}
/> />
</div> </div>
<div className="flex flex-col gap-2">
<Label>Telegram Chat</Label>
<Input
value={form.telegram_chat || ""}
onChange={(e) => handleChange("telegram_chat", e.target.value)}
/>
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Instagram</Label> <Label>Instagram</Label>
<Input <Input