This commit is contained in:
Samandar Turgunboyev
2025-10-30 10:29:45 +05:00
parent 0154fa8950
commit 352efd6391
4 changed files with 320 additions and 161 deletions

View File

@@ -28,6 +28,12 @@ export interface UserOrderData {
tour_name: string;
}[];
total_income: string;
awaiting_payments: string;
awaiting_payments_count: string;
confirmed_order: string;
pending_confirmation: string;
completed_order: string;
cancelled_order: string;
};
};
}

View File

@@ -8,32 +8,177 @@ import { Button } from "@/shared/ui/button";
import { useQuery } from "@tanstack/react-query";
import {
AlertTriangle,
CheckCircle,
ChevronLeft,
ChevronRight,
Clock,
CreditCard,
DollarSign,
Eye,
Hotel,
Loader2,
MapPin,
Plane,
TrendingUp,
Users,
XCircle,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
type Purchase = {
id: number;
userName: string;
userPhone: string;
tourName: string;
tourId: number;
agencyName: string;
agencyId: number;
destination: string;
travelDate: string;
amount: number;
paymentStatus: "paid" | "pending" | "cancelled" | "refunded";
purchaseDate: string;
};
const mockPurchases: Purchase[] = [
{
id: 1,
userName: "Aziza Karimova",
userPhone: "+998 90 123 45 67",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-10",
amount: 1500000,
paymentStatus: "paid",
purchaseDate: "2025-10-10",
},
{
id: 2,
userName: "Sardor Rahimov",
userPhone: "+998 91 234 56 78",
tourName: "Bali Adventure Package",
tourId: 2,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Bali, Indonesia",
travelDate: "2025-11-15",
amount: 1800000,
paymentStatus: "paid",
purchaseDate: "2025-10-12",
},
{
id: 3,
userName: "Nilufar Toshmatova",
userPhone: "+998 93 345 67 89",
tourName: "Dubai Luxury Tour",
tourId: 1,
agencyName: "Silk Road Travel",
agencyId: 1,
destination: "Dubai, UAE",
travelDate: "2025-11-20",
amount: 1500000,
paymentStatus: "pending",
purchaseDate: "2025-10-14",
},
{
id: 4,
userName: "Jamshid Alimov",
userPhone: "+998 94 456 78 90",
tourName: "Istanbul Express Tour",
tourId: 3,
agencyName: "Orient Express",
agencyId: 3,
destination: "Istanbul, Turkey",
travelDate: "2025-11-05",
amount: 1200000,
paymentStatus: "cancelled",
purchaseDate: "2025-10-08",
},
{
id: 5,
userName: "Madina Yusupova",
userPhone: "+998 97 567 89 01",
tourName: "Paris Romantic Getaway",
tourId: 4,
agencyName: "Euro Travels",
agencyId: 2,
destination: "Paris, France",
travelDate: "2025-12-01",
amount: 2200000,
paymentStatus: "paid",
purchaseDate: "2025-10-16",
},
];
export default function FinancePage() {
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [tab, setTab] = useState<"bookings" | "agencies">("bookings");
const [filterStatus, setFilterStatus] = useState<
"all" | "paid" | "pending" | "cancelled" | "refunded"
>("all");
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["list_order_user"],
queryFn: () => getAllOrder({ page: 1, page_size: 10 }),
queryKey: ["list_order_user", currentPage],
queryFn: () => getAllOrder({ page: currentPage, page_size: 10 }),
});
const stats = [
{
title: t("Jami daromad"),
value: data?.data.data.results.total_income ?? "0",
description: t("Yakunlangan bandlovlardan"),
icon: <DollarSign className="text-green-400 w-6 h-6" />,
color: "text-green-400",
},
{
title: t("Kutilayotgan tolovlar"),
value: data?.data.data.results.awaiting_payments ?? "0",
description: t("Tasdiqlash kutilmoqda"),
icon: <TrendingUp className="text-yellow-400 w-6 h-6" />,
color: "text-yellow-400",
},
{
title: t("Kutilayotgan tolovlar soni"),
value: data?.data.data.results.awaiting_payments_count ?? "0",
description: t("Tolov kutilayotgan buyurtmalar soni"),
icon: <Clock className="text-orange-400 w-6 h-6" />,
color: "text-orange-400",
},
{
title: t("Tasdiqlangan buyurtmalar"),
value: data?.data.data.results.confirmed_order ?? "0",
description: t("Tasdiqlangan bandlovlar soni"),
icon: <CreditCard className="text-blue-400 w-6 h-6" />,
color: "text-blue-400",
},
{
title: t("Kutilayotgan tasdiqlar"),
value: data?.data.data.results.pending_confirmation ?? "0",
description: t("Hali tasdiqlanmagan bandlovlar"),
icon: <Clock className="text-purple-400 w-6 h-6" />,
color: "text-purple-400",
},
{
title: t("Yakunlangan buyurtmalar"),
value: data?.data.data.results.completed_order ?? "0",
description: t("Muvaffaqiyatli yakunlangan buyurtmalar"),
icon: <CheckCircle className="text-emerald-400 w-6 h-6" />,
color: "text-emerald-400",
},
{
title: t("Bekor qilingan buyurtmalar"),
value: data?.data.data.results.cancelled_order ?? "0",
description: t("Bekor qilingan bandlovlar soni"),
icon: <XCircle className="text-red-400 w-6 h-6" />,
color: "text-red-400",
},
];
const getStatusBadge = (status: OrderStatus["order_status"]) => {
const base =
"px-3 py-1 rounded-full text-sm font-medium inline-flex items-center gap-2";
@@ -120,6 +265,26 @@ export default function FinancePage() {
);
}
const agencies = Array.from(
new Set(mockPurchases.map((p) => p.agencyId)),
).map((id) => {
const agencyPurchases = mockPurchases.filter((p) => p.agencyId === id);
return {
id,
name: agencyPurchases[0].agencyName,
totalPaid: agencyPurchases
.filter((p) => p.paymentStatus === "paid")
.reduce((sum, p) => sum + p.amount, 0),
pending: agencyPurchases
.filter((p) => p.paymentStatus === "pending")
.reduce((sum, p) => sum + p.amount, 0),
purchaseCount: agencyPurchases.length,
destinations: Array.from(
new Set(agencyPurchases.map((p) => p.destination)),
),
};
});
return (
<div className="min-h-screen w-full bg-gray-900 text-gray-100">
<div className="w-[90%] mx-auto py-6">
@@ -201,70 +366,24 @@ export default function FinancePage() {
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">
{t("Jami daromad")}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-6 mb-8">
{stats.map((item, index) => (
<div
key={index}
className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between transition-all hover:scale-[1.02]"
>
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">{item.title}</p>
{item.icon}
</div>
<p className={`text-2xl font-bold mt-3 ${item.color}`}>
{Number(item.value).toLocaleString()}
</p>
<DollarSign className="text-green-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-green-400 mt-3">
{/* {formatPrice(totalRevenue, true)} */}
</p>
<p className="text-sm text-gray-500 mt-1">
{t("Yakunlangan bandlovlardan")}
</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">
{t("Kutilayotgan tolovlar")}
<p className="text-sm text-gray-500 mt-1">
{item.description}
</p>
<TrendingUp className="text-yellow-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-yellow-400 mt-3">
{/* {formatPrice(pendingRevenue, true)} */}
</p>
<p className="text-sm text-gray-500 mt-1">
{t("Tasdiqlash kutilmoqda")}
</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">
{t("Tasdiqlangan bandlovlar")}
</p>
<CreditCard className="text-blue-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-blue-400 mt-3">
{/* {
filteredPurchases.filter((p) => p.paymentStatus === "paid")
.length
} */}
</p>
<p className="text-sm text-gray-500 mt-1">
{t("Tasdiqlangan bandlovlar")}
</p>
</div>
<div className="bg-gray-800 p-6 rounded-xl shadow flex flex-col justify-between">
<div className="flex items-center justify-between">
<p className="text-gray-400 font-medium">
{t("Kutilayotgan bandlovlar")}
</p>
<Hotel className="text-purple-400 w-6 h-6" />
</div>
<p className="text-2xl font-bold text-purple-400 mt-3">
{/* {
filteredPurchases.filter(
(p) => p.paymentStatus === "pending",
).length
} */}
</p>
<p className="text-sm text-gray-500 mt-1">
{t("Kutilayotgan tolovlar")}
</p>
</div>
))}
</div>
{/* Booking Cards */}
@@ -321,10 +440,45 @@ export default function FinancePage() {
</div>
))}
</div>
<div className="flex justify-end gap-2 mt-5">
<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.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>
</>
)}
{/* {tab === "agencies" && (
{tab === "agencies" && (
<>
<h2 className="text-xl font-bold mb-6">Partner Agencies</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@@ -380,7 +534,7 @@ export default function FinancePage() {
))}
</div>
</>
)} */}
)}
</div>
</div>
);

View File

@@ -61,11 +61,12 @@ export default function FinanceDetailUser() {
};
}) => updateDetailOrder({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_order"] });
queryClient.refetchQueries({ queryKey: ["list_order_user"] });
toast.success(t("Status muvaffaqiyatli yangilandi"), {
richColors: true,
position: "top-center",
});
queryClient.invalidateQueries({ queryKey: ["detail_order"] });
},
onError: () => {
toast.error(t("Statusni yangilashda xatolik yuz berdi"), {

View File

@@ -89,32 +89,6 @@ const SupportTours = () => {
});
};
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
<p className="text-slate-400">{t("Ma'lumotlar yuklanmoqda...")}</p>
</div>
);
}
if (isError) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
<AlertTriangle className="w-10 h-10 text-red-500" />
<p className="text-lg">
{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
</p>
<Button
onClick={() => refetch()}
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
>
{t("Qayta urinish")}
</Button>
</div>
);
}
return (
<div className="min-h-screen bg-gray-900 text-gray-100 p-6 space-y-6 w-full">
<h1 className="text-3xl font-bold tracking-tight mb-4 text-white">
@@ -141,76 +115,100 @@ const SupportTours = () => {
</button>
))}
</div>
{/* Cards */}
<div className="grid gap-5 sm:grid-cols-3 lg:grid-cols-3">
{data && data.data.data.results.length === 0 ? (
<div className="col-span-full flex flex-col items-center justify-center min-h-[50vh] w-full text-center text-white gap-4">
<p className="text-lg">{t("Natija topilmadi")}</p>
</div>
) : (
data?.data.data.results.map((req) => (
<Card
key={req.id}
className="bg-gray-800/70 border border-gray-700 shadow-md hover:shadow-lg hover:bg-gray-800 transition-all duration-200 justify-between"
>
<CardHeader className="pb-2 flex justify-between items-center">
<div className="flex gap-2">
<CardTitle className="flex items-center gap-2 text-lg font-semibold text-white">
<User className="w-5 h-5 text-blue-400" />
{req.name}
</CardTitle>
{isError ? (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
<AlertTriangle className="w-10 h-10 text-red-500" />
<p className="text-lg">
{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
</p>
<Button
onClick={() => refetch()}
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
>
{t("Qayta urinish")}
</Button>
</div>
) : (
<>
{isLoading ? (
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
<p className="text-slate-400">
{t("Ma'lumotlar yuklanmoqda...")}
</p>
</div>
) : (
<div className="grid gap-5 sm:grid-cols-3 lg:grid-cols-3">
{data && data.data.data.results.length === 0 ? (
<div className="col-span-full flex flex-col items-center justify-center min-h-[50vh] w-full text-center text-white gap-4">
<p className="text-lg">{t("Natija topilmadi")}</p>
</div>
<Badge
variant="outline"
className={`px-2 py-1 rounded-md text-xs font-medium ${
req.status === "pending"
? "bg-red-500/10 text-red-400 border-red-400/40"
: "bg-green-500/10 text-green-400 border-green-400/40"
}`}
>
{req.status === "pending" ? t("Kutilmoqda") : t("done")}
</Badge>
</CardHeader>
<CardContent className="space-y-3 mt-1">
{req.travel_agency !== null ? (
<span className="text-md text-gray-400">
{t("Agentlikka tegishli")}
</span>
) : (
<span className="text-md text-gray-400">
{t("Sayt bo'yicha")}
</span>
)}
<div className="flex items-center gap-2 mt-2 text-sm text-gray-400">
<Phone className="w-4 h-4 text-gray-400" />
{formatPhone(req.phone_number)}
</div>
<div className="grid grid-cols-2 justify-end items-end gap-2">
<Button
variant="outline"
size="sm"
className="mt-3 w-full border-gray-600 text-gray-200 hover:bg-gray-700 hover:text-white"
onClick={() => setSelected(req)}
) : (
data?.data.data.results.map((req) => (
<Card
key={req.id}
className="bg-gray-800/70 border border-gray-700 shadow-md hover:shadow-lg hover:bg-gray-800 transition-all duration-200 justify-between"
>
{t("Batafsil ko'rish")}
</Button>
<Button
size="sm"
variant="destructive"
className="flex items-center gap-1"
onClick={() => setSelectedToDelete(req)}
>
<Trash2 className="w-4 h-4" /> {t("O'chirish")}
</Button>
</div>
</CardContent>
</Card>
))
)}
</div>
<CardHeader className="pb-2 flex justify-between items-center">
<div className="flex gap-2">
<CardTitle className="flex items-center gap-2 text-lg font-semibold text-white">
<User className="w-5 h-5 text-blue-400" />
{req.name}
</CardTitle>
</div>
<Badge
variant="outline"
className={`px-2 py-1 rounded-md text-xs font-medium ${
req.status === "pending"
? "bg-red-500/10 text-red-400 border-red-400/40"
: "bg-green-500/10 text-green-400 border-green-400/40"
}`}
>
{req.status === "pending" ? t("Kutilmoqda") : t("done")}
</Badge>
</CardHeader>
<CardContent className="space-y-3 mt-1">
{req.travel_agency !== null ? (
<span className="text-md text-gray-400">
{t("Agentlikka tegishli")}
</span>
) : (
<span className="text-md text-gray-400">
{t("Sayt bo'yicha")}
</span>
)}
<div className="flex items-center gap-2 mt-2 text-sm text-gray-400">
<Phone className="w-4 h-4 text-gray-400" />
{formatPhone(req.phone_number)}
</div>
<div className="grid grid-cols-2 justify-end items-end gap-2">
<Button
variant="outline"
size="sm"
className="mt-3 w-full border-gray-600 text-gray-200 hover:bg-gray-700 hover:text-white"
onClick={() => setSelected(req)}
>
{t("Batafsil ko'rish")}
</Button>
<Button
size="sm"
variant="destructive"
className="flex items-center gap-1"
onClick={() => setSelectedToDelete(req)}
>
<Trash2 className="w-4 h-4" /> {t("O'chirish")}
</Button>
</div>
</CardContent>
</Card>
))
)}
</div>
)}
</>
)}
{/* Detail Modal */}
<Dialog open={!!selected} onOpenChange={() => setSelected(null)}>