423 lines
15 KiB
TypeScript
423 lines
15 KiB
TypeScript
import type { MetaFunction } from "@remix-run/node";
|
||
import { ArrowRight, Check, X, Loader2 } from "lucide-react";
|
||
import { formatDate, formatPrice, formatTime, safeGoBack } from "../utils/helpers";
|
||
import { useNavigate } from "@remix-run/react";
|
||
import { useState } from "react";
|
||
import { Button } from "../components/ui/button";
|
||
import {
|
||
Drawer,
|
||
DrawerContent,
|
||
DrawerHeader,
|
||
DrawerTitle,
|
||
} from "../components/ui/drawer";
|
||
import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group";
|
||
import PersianDatePicker from "../components/PersianDatePicker";
|
||
import { useSearchParams } from "react-router-dom";
|
||
import { useServiceGetPayoutRequests } from "../requestHandler/use-wallet-hooks";
|
||
import jMoment from "moment-jalaali";
|
||
import filterIcon from "~/assets/icons/filter.png";
|
||
import filterIconDark from "~/assets/icons/filter-dark.png";
|
||
import { useRootData } from "~/hooks/use-root-data";
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "لیست تراکنشها - ویترون" },
|
||
{
|
||
name: "description",
|
||
content:
|
||
"مشاهده و پیگیری تمام تراکنشهای مالی فروشگاه شما در ویترون. فیلتر زمانی و جزئیات تراکنشها.",
|
||
},
|
||
{
|
||
name: "keywords",
|
||
content: "تراکنش، تراکنش مالی، برداشت وجه، تاریخچه مالی، فروشگاه، ویترون",
|
||
},
|
||
{ name: "robots", content: "noindex, follow" },
|
||
{ property: "og:title", content: "لیست تراکنشها - ویترون" },
|
||
{
|
||
property: "og:description",
|
||
content:
|
||
"مشاهده و پیگیری تمام تراکنشهای مالی فروشگاه شما در ویترون. فیلتر زمانی و جزئیات تراکنشها.",
|
||
},
|
||
{ property: "og:type", content: "website" },
|
||
{ name: "twitter:card", content: "summary" },
|
||
{ name: "twitter:title", content: "لیست تراکنشها - ویترون" },
|
||
{
|
||
name: "twitter:description",
|
||
content:
|
||
"مشاهده و پیگیری تمام تراکنشهای مالی فروشگاه شما در ویترون. فیلتر زمانی و جزئیات تراکنشها.",
|
||
},
|
||
];
|
||
};
|
||
|
||
export default function Transactions() {
|
||
const navigate = useNavigate();
|
||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||
const [searchParams] = useSearchParams();
|
||
const { theme } = useRootData();
|
||
// Convert Persian date to Gregorian ISO string
|
||
const convertPersianToGregorian = (persianDate: string): string => {
|
||
// Persian date format is expected to be YYYY/MM/DD
|
||
const [year, month, day] = persianDate.split("/");
|
||
const jMomentDate = jMoment(`${year}/${month}/${day}`, "jYYYY/jM/jD");
|
||
return jMomentDate.format("YYYY-MM-DD");
|
||
};
|
||
|
||
// Map URL params to API filters
|
||
const getFilters = () => {
|
||
const order = searchParams.get("order") as
|
||
| "all"
|
||
| "today"
|
||
| "lastWeek"
|
||
| "lastMonth"
|
||
| "other";
|
||
const fromDate = searchParams.get("fromDate");
|
||
const toDate = searchParams.get("toDate");
|
||
|
||
if (!order || order === "all") {
|
||
return undefined;
|
||
}
|
||
|
||
// Map URL params to API params
|
||
const apiFilters: {
|
||
dateFilter?: "today" | "last_7" | "last_30" | "custom";
|
||
fromDate?: string;
|
||
toDate?: string;
|
||
} = {};
|
||
|
||
switch (order) {
|
||
case "today":
|
||
apiFilters.dateFilter = "today";
|
||
break;
|
||
case "lastWeek":
|
||
apiFilters.dateFilter = "last_7";
|
||
break;
|
||
case "lastMonth":
|
||
apiFilters.dateFilter = "last_30";
|
||
break;
|
||
case "other":
|
||
// Allow custom filter even with only one date (fromDate OR toDate)
|
||
if (fromDate || toDate) {
|
||
apiFilters.dateFilter = "custom";
|
||
if (fromDate) {
|
||
try {
|
||
apiFilters.fromDate = convertPersianToGregorian(fromDate);
|
||
} catch (error) {
|
||
console.error("Error converting fromDate:", error);
|
||
apiFilters.fromDate = fromDate; // fallback to original
|
||
}
|
||
}
|
||
if (toDate) {
|
||
try {
|
||
apiFilters.toDate = convertPersianToGregorian(toDate);
|
||
} catch (error) {
|
||
console.error("Error converting toDate:", error);
|
||
apiFilters.toDate = toDate; // fallback to original
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
return Object.keys(apiFilters).length > 0 ? apiFilters : undefined;
|
||
};
|
||
|
||
const { data: payoutRequests, isLoading } =
|
||
useServiceGetPayoutRequests(getFilters());
|
||
|
||
const handleBack = () => {
|
||
safeGoBack(navigate);
|
||
};
|
||
const filterIconClicked = () => {
|
||
setIsFilterDrawerOpen(true);
|
||
};
|
||
return (
|
||
<div className="bg-WHITE">
|
||
{/* Header */}
|
||
<div className="flex flex-row h-[65px] sticky top-0 z-30 bg-WHITE w-full items-center justify-center border-b border-inner-border">
|
||
<ArrowRight onClick={handleBack} className="absolute top-5 right-5" />
|
||
<p className={"text-B16 font-bold"}>لیست تراکنشها</p>
|
||
<div
|
||
onClick={() => {
|
||
filterIconClicked();
|
||
}}
|
||
role="button"
|
||
tabIndex={0}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
filterIconClicked();
|
||
}
|
||
}}
|
||
className="absolute top-4 h-8 w-8 left-5 rounded-full bg-WHITE shadow-xl flex items-center justify-center"
|
||
>
|
||
<img
|
||
src={theme === "dark" ? filterIconDark : filterIcon}
|
||
alt="filter"
|
||
className="w-6 h-6"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col w-full">
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center py-8">
|
||
<Loader2 className="w-8 h-8 animate-spin" />
|
||
</div>
|
||
) : payoutRequests && payoutRequests.length > 0 ? (
|
||
payoutRequests.map((request) => (
|
||
<div key={request.id} className="flex flex-col gap-2 p-4 border-b">
|
||
<div className="flex justify-between">
|
||
<div className="flex flex-row gap-2">
|
||
<p className="text-B12 font-bold">
|
||
{request.requestedAt
|
||
? formatDate(request.requestedAt)
|
||
: "تاریخ نامشخص"}
|
||
</p>
|
||
<p className="text-B12 font-bold">
|
||
{request.requestedAt ? formatTime(request.requestedAt) : ""}
|
||
</p>
|
||
</div>
|
||
{request.status === "completed" ? (
|
||
<div className="flex flex-row gap-1 text-green-500">
|
||
<Check size={16} />
|
||
<p className="text-B12 font-bold">موفق</p>
|
||
</div>
|
||
) : request.status === "rejected" ? (
|
||
<div className="flex flex-row gap-1 text-red-500">
|
||
<X size={16} />
|
||
<p className="text-B12 font-bold">ناموفق</p>
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-row gap-1 text-yellow-500">
|
||
<Loader2 size={16} className="animate-spin" />
|
||
<p className="text-B12 font-bold">در انتظار</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-row gap-2">
|
||
<p className="text-B12 font-bold">مبلغ</p>
|
||
<p className="text-R12">{formatPrice(request.amount)}</p>
|
||
</div>
|
||
{request.bankAccountCardNumber && (
|
||
<div className="flex flex-row gap-2">
|
||
<p className="text-B12 font-bold">شماره کارت:</p>
|
||
<p className="text-R12">{request.bankAccountCardNumber}</p>
|
||
</div>
|
||
)}
|
||
{request.bankAccountIban && (
|
||
<div className="flex flex-row gap-2">
|
||
<p className="text-B12 font-bold">شماره شبا:</p>
|
||
<p className="text-R12">{request.bankAccountIban}</p>
|
||
</div>
|
||
)}
|
||
{request.shomareMarja && (
|
||
<div className="flex flex-row gap-2 text-GRAY">
|
||
<p className="text-B12 font-bold">شماره مرجع بانک:</p>
|
||
<p className="text-R12">{request.shomareMarja}</p>
|
||
</div>
|
||
)}
|
||
{request.shomareErja && (
|
||
<div className="flex flex-row gap-2 text-GRAY">
|
||
<p className="text-B12 font-bold">شماره ارجاع:</p>
|
||
<p className="text-R12">{request.shomareErja}</p>
|
||
</div>
|
||
)}
|
||
{request.processedAt && (
|
||
<div className="flex flex-row gap-2 text-GRAY">
|
||
<p className="text-B12 font-bold">تاریخ پردازش:</p>
|
||
<p className="text-R12">
|
||
{formatDate(request.processedAt)}{" "}
|
||
{formatTime(request.processedAt)}
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))
|
||
) : (
|
||
<div className="flex flex-col items-center justify-center py-8 text-GRAY">
|
||
<p className="text-B16">هیچ تراکنشی یافت نشد</p>
|
||
<p className="text-R12 mt-2">هنوز درخواست برداشتی ثبت نکردهاید</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<FilterDrawer
|
||
isOpen={isFilterDrawerOpen}
|
||
onClose={() => setIsFilterDrawerOpen(false)}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
type dateRangeTypes = "all" | "today" | "lastWeek" | "lastMonth" | "other";
|
||
|
||
const FilterDrawer = ({
|
||
isOpen,
|
||
onClose,
|
||
}: {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
}) => {
|
||
const [searchParams, setSearchParams] = useSearchParams();
|
||
// Date states for custom range
|
||
const [order, setOrder] = useState<dateRangeTypes>(
|
||
(searchParams.get("order") as dateRangeTypes) || "all"
|
||
);
|
||
const [fromDate, setFromDate] = useState<string>(
|
||
searchParams.get("fromDate") || ""
|
||
);
|
||
const [toDate, setToDate] = useState<string>(
|
||
searchParams.get("toDate") || ""
|
||
);
|
||
|
||
const filterTransactions = () => {
|
||
const params: Record<string, string> = {};
|
||
|
||
if (order !== "all") {
|
||
params.order = order;
|
||
}
|
||
|
||
// Only include date params if order is "other" (custom)
|
||
if (order === "other") {
|
||
if (fromDate) {
|
||
params.fromDate = fromDate;
|
||
}
|
||
if (toDate) {
|
||
params.toDate = toDate;
|
||
}
|
||
}
|
||
// For non-custom filters, fromDate and toDate are intentionally not included
|
||
// This will remove them from the URL params
|
||
|
||
setSearchParams(params, { replace: true });
|
||
onClose();
|
||
};
|
||
|
||
return (
|
||
<Drawer open={isOpen} onOpenChange={onClose}>
|
||
<DrawerContent className="w-full pb-6">
|
||
<DrawerHeader className="flex flex-col py-3">
|
||
<div className="flex relative pt-2 text-B12 font-bold items-center justify-center w-full">
|
||
<X
|
||
className="absolute right-0 cursor-pointer"
|
||
size={24}
|
||
onClick={() => onClose()}
|
||
/>
|
||
<DrawerTitle className="text-B12 font-bold">
|
||
فیلتر نتایج
|
||
</DrawerTitle>
|
||
</div>
|
||
</DrawerHeader>
|
||
<div className="w-full p-4">
|
||
<div className="flex flex-col gap-4 w-full pb-4">
|
||
<RadioGroup
|
||
className="flex flex-col gap-4 w-full"
|
||
onValueChange={(value) => setOrder(value as dateRangeTypes)}
|
||
value={order}
|
||
>
|
||
<div className="flex w-full justify-between">
|
||
<RadioGroupItem
|
||
circleClass="h-4 w-4"
|
||
className="h-6 w-6"
|
||
value={"all"}
|
||
id="all"
|
||
/>
|
||
<label htmlFor="all" className="text-R16 w-full text-right">
|
||
همه نتایج
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex w-full justify-between">
|
||
<RadioGroupItem
|
||
circleClass="h-4 w-4"
|
||
className="h-6 w-6"
|
||
value={"today"}
|
||
id="today"
|
||
/>
|
||
<label htmlFor="today" className="text-R16 w-full text-right">
|
||
امروز
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex w-full justify-between">
|
||
<RadioGroupItem
|
||
circleClass="h-4 w-4"
|
||
className="h-6 w-6"
|
||
value={"lastWeek"}
|
||
id="lastWeek"
|
||
/>
|
||
<label
|
||
htmlFor="lastWeek"
|
||
className="text-R16 w-full text-right"
|
||
>
|
||
هفته اخیر
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex w-full justify-between">
|
||
<RadioGroupItem
|
||
circleClass="h-4 w-4"
|
||
className="h-6 w-6"
|
||
value={"lastMonth"}
|
||
id="lastMonth"
|
||
/>
|
||
<label
|
||
htmlFor="lastMonth"
|
||
className="text-R16 w-full text-right"
|
||
>
|
||
ماه اخیر
|
||
</label>
|
||
</div>
|
||
|
||
<div className="flex w-full justify-between">
|
||
<RadioGroupItem
|
||
circleClass="h-4 w-4"
|
||
className="h-6 w-6"
|
||
value={"other"}
|
||
id="other"
|
||
/>
|
||
<label htmlFor="other" className="text-R16 w-full text-right">
|
||
بازه دلخواه
|
||
</label>
|
||
</div>
|
||
</RadioGroup>
|
||
</div>
|
||
|
||
{/* Custom Date Range Picker */}
|
||
{order === "other" && (
|
||
<div className="flex gap-2 w-full pb-4">
|
||
{/* From Date */}
|
||
<div className="w-full">
|
||
<PersianDatePicker
|
||
value={fromDate}
|
||
onChange={setFromDate}
|
||
placeholder="از تاریخ"
|
||
label=""
|
||
/>
|
||
</div>
|
||
|
||
{/* To Date */}
|
||
<div className="w-full">
|
||
<PersianDatePicker
|
||
value={toDate}
|
||
onChange={setToDate}
|
||
placeholder="تا تاریخ"
|
||
label=""
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-row gap-6 w-full px-4 pt-6 pb-2">
|
||
<Button
|
||
onClick={() => filterTransactions()}
|
||
className="w-[100%]"
|
||
size={"xl"}
|
||
variant="dark"
|
||
disabled={!order || (order === "other" && !fromDate && !toDate)}
|
||
>
|
||
مشاهده نتایج
|
||
</Button>
|
||
</div>
|
||
</DrawerContent>
|
||
</Drawer>
|
||
);
|
||
};
|