diff --git a/.env.production b/.env.production index 12d1926..0a957dd 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ -NEXT_PUBLIC_APP_VERSION=2.2.8 +NEXT_PUBLIC_APP_VERSION=2.3.0 NEXT_PUBLIC_TOAST_AUTO_CLOSE_DURATION=2500 NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0 NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=opplens-270d1.firebaseapp.com diff --git a/src/app/(home)/_components/dashboard/daily-trend-chart.tsx b/src/app/(home)/_components/dashboard/daily-trend-chart.tsx new file mode 100644 index 0000000..32dd3c6 --- /dev/null +++ b/src/app/(home)/_components/dashboard/daily-trend-chart.tsx @@ -0,0 +1,414 @@ +"use client"; + +import gsap from "gsap"; +import { useId, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { AnimatedCounter } from "./animated-counter"; +import { PulseIcon, TrendUpIcon } from "./icons"; + +export type DailyPoint = { date: string; label: string; count: number }; + +export type DailyChartAccent = { + /** Three stops for the line gradient (left → middle → right). */ + line: [string, string, string]; + /** Solid colour used for the area fill, dots and pulse. */ + fill: string; +}; + +type Props = { + title: string; + subtitle: string; + data: DailyPoint[]; + isLoading: boolean; + accent?: DailyChartAccent; + unit?: { singular: string; plural: string }; + loadingLabel?: string; +}; + +const W = 1000; +const H = 240; +const PAD_L = 0; +const PAD_R = 0; +const PAD_T = 24; +const PAD_B = 36; + +const DEFAULT_ACCENT: DailyChartAccent = { + line: ["#5750F1", "#8155FF", "#0ABEF9"], + fill: "#5750F1", +}; + +export function DailyTrendChart({ + title, + subtitle, + data, + isLoading, + accent = DEFAULT_ACCENT, + unit = { singular: "item", plural: "items" }, + loadingLabel = "Loading…", +}: Props) { + const rootRef = useRef(null); + const svgRef = useRef(null); + const pathRef = useRef(null); + const areaRef = useRef(null); + const dotsRef = useRef(null); + + // Unique gradient/filter ids so multiple charts can live on one page. + const uid = useId().replace(/[^a-zA-Z0-9]/g, ""); + const fillId = `dtFill-${uid}`; + const lineId = `dtLine-${uid}`; + const glowId = `dtGlow-${uid}`; + + const [hoverIdx, setHoverIdx] = useState(null); + + const { linePath, areaPath, points, sumY } = useMemo(() => { + const max = Math.max(1, ...data.map((d) => d.count)); + const innerW = W - PAD_L - PAD_R; + const innerH = H - PAD_T - PAD_B; + const step = data.length > 1 ? innerW / (data.length - 1) : innerW; + + const pts = data.map((d, i) => ({ + x: PAD_L + i * step, + y: PAD_T + innerH - (d.count / max) * innerH, + d, + })); + + if (pts.length === 0) { + return { linePath: "", areaPath: "", points: pts, sumY: 0 }; + } + + const line = pts + .map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`) + .join(" "); + + const area = `${line} L ${pts[pts.length - 1].x.toFixed(2)} ${PAD_T + innerH} L ${pts[0].x.toFixed(2)} ${PAD_T + innerH} Z`; + + return { + linePath: line, + areaPath: area, + points: pts, + sumY: data.reduce((acc, p) => acc + p.count, 0), + }; + }, [data]); + + useLayoutEffect(() => { + const reduced = + typeof window !== "undefined" && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + const ctx = gsap.context(() => { + if (reduced) return; + gsap.from(rootRef.current, { + y: 24, + opacity: 0, + duration: 0.6, + ease: "power3.out", + delay: 0.2, + }); + + if (pathRef.current && linePath) { + const length = pathRef.current.getTotalLength(); + gsap.fromTo( + pathRef.current, + { strokeDasharray: length, strokeDashoffset: length }, + { + strokeDashoffset: 0, + duration: 1.4, + ease: "power2.inOut", + delay: 0.3, + }, + ); + } + if (areaRef.current) { + gsap.fromTo( + areaRef.current, + { opacity: 0, y: 12 }, + { opacity: 1, y: 0, duration: 1, delay: 0.5, ease: "power2.out" }, + ); + } + if (dotsRef.current) { + gsap.fromTo( + dotsRef.current.querySelectorAll("circle"), + { scale: 0, transformOrigin: "center" }, + { + scale: 1, + duration: 0.4, + stagger: 0.05, + ease: "back.out(2)", + delay: 0.8, + }, + ); + } + }, rootRef); + return () => ctx.revert(); + }, [linePath]); + + const last = points[points.length - 1]?.d.count ?? 0; + const prev = points[points.length - 2]?.d.count ?? 0; + const dayDelta = last - prev; + const avg = data.length ? Math.round(sumY / data.length) : 0; + + const handleMove = (e: React.MouseEvent) => { + if (!svgRef.current || !points.length) return; + const rect = svgRef.current.getBoundingClientRect(); + const px = ((e.clientX - rect.left) / rect.width) * W; + let bestI = 0; + let bestD = Infinity; + for (let i = 0; i < points.length; i++) { + const d = Math.abs(points[i].x - px); + if (d < bestD) { + bestD = d; + bestI = i; + } + } + setHoverIdx(bestI); + }; + + const hovered = hoverIdx !== null ? points[hoverIdx] : null; + const innerH = H - PAD_T - PAD_B; + const tooltipLeftPct = hovered ? (hovered.x / W) * 100 : 0; + const tooltipTopPct = hovered ? (hovered.y / H) * 100 : 0; + const flipLeft = hovered ? hovered.x > W * 0.7 : false; + const flipRight = hovered ? hovered.x < W * 0.12 : false; + const flipDown = hovered ? hovered.y < H * 0.35 : false; + const tooltipTX = flipLeft ? "-100%" : flipRight ? "0%" : "-50%"; + const tooltipTY = flipDown ? "calc(0% + 14px)" : "calc(-100% - 14px)"; + + return ( +
+
+
+

+ {title} +

+

{subtitle}

+
+
+
+
+ Today +
+
+ +
+
+
+
+ Avg/day +
+
+ +
+
+
+ + {dayDelta >= 0 ? "+" : ""} + {dayDelta} vs yesterday +
+
+
+ +
+ {isLoading ? ( +
+ {loadingLabel} +
+ ) : ( +
+ setHoverIdx(null)} + > + + + + + + + + + + + + + + + + + + + + {[0.25, 0.5, 0.75].map((p) => ( + + ))} + + {areaPath && ( + + )} + {linePath && ( + <> + + + + )} + + {hovered && ( + + )} + + + {points.map((p, i) => { + const isHover = hoverIdx === i; + const isLatest = i === points.length - 1; + return ( + + {isLatest && ( + <> + + + + + + + )} + + + ); + })} + + + {points.map((p, i) => { + const isFirst = i === 0; + const isLast = i === points.length - 1; + if (!(isFirst || isLast || i % 2 === 0)) return null; + return ( + + {p.d.label} + + ); + })} + + + {hovered && ( +
+
+ {hovered.d.label} +
+
+ + {hovered.d.count} + + + {hovered.d.count === 1 ? unit.singular : unit.plural} + +
+ {hoverIdx !== null && hoverIdx > 0 && ( +
+ {(() => { + const diff = + hovered.d.count - points[hoverIdx - 1].d.count; + const sign = diff > 0 ? "+" : ""; + return `${sign}${diff} vs previous day`; + })()} +
+ )} +
+ )} +
+ )} +
+
+ ); +} diff --git a/src/app/(home)/_components/dashboard/index.tsx b/src/app/(home)/_components/dashboard/index.tsx index a9a4a41..dfd37de 100644 --- a/src/app/(home)/_components/dashboard/index.tsx +++ b/src/app/(home)/_components/dashboard/index.tsx @@ -5,6 +5,7 @@ import { CountryDistribution } from "./country-distribution"; import { DashboardHero } from "./hero"; import { NoticeTypeBreakdown } from "./notice-types"; import { RecentTenders } from "./recent-tenders"; +import { StatisticsSection } from "./statistics-section"; import { TenderStatCards } from "./stat-cards"; import { TenderTrendsChart } from "./trends-chart"; import { useDashboardData } from "./use-dashboard-data"; @@ -66,6 +67,8 @@ export function TenderDashboard() { + + ); } diff --git a/src/app/(home)/_components/dashboard/statistics-section.tsx b/src/app/(home)/_components/dashboard/statistics-section.tsx new file mode 100644 index 0000000..fc77c96 --- /dev/null +++ b/src/app/(home)/_components/dashboard/statistics-section.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { AnimatedCounter } from "./animated-counter"; +import { DailyChartAccent, DailyTrendChart } from "./daily-trend-chart"; +import { SparkleIcon, TenderIcon } from "./icons"; +import { + STATISTICS_DAY_OPTIONS, + StatisticsDays, + useStatistics, +} from "./use-statistics"; + +const TED_ACCENT: DailyChartAccent = { + line: ["#5750F1", "#8155FF", "#0ABEF9"], + fill: "#5750F1", +}; +const DOCS_ACCENT: DailyChartAccent = { + line: ["#0ABEF9", "#3C9DF8", "#22AD5C"], + fill: "#0ABEF9", +}; +const TRANSLATE_ACCENT: DailyChartAccent = { + line: ["#F59460", "#F97316", "#EF4444"], + fill: "#F59460", +}; + +export function StatisticsSection() { + const { + days, + setDays, + isLoading, + scrapedTed, + scrapedDocuments, + translatedNotices, + totalScrapedDocuments, + totalTranslatedTenders, + } = useStatistics(); + + return ( +
+
+
+

+ Scraping & Translation +

+

+ Daily ingestion and translation activity +

+
+ +
+ +
+ + +
+ +
+ + + +
+
+ ); +} + +function DaysSelector({ + value, + onChange, +}: { + value: StatisticsDays; + onChange: (days: StatisticsDays) => void; +}) { + return ( +
+ {STATISTICS_DAY_OPTIONS.map((option) => ( + + ))} +
+ ); +} + +function TotalCard({ + label, + hint, + value, + Icon, + color, + accentGradient, + loading, +}: { + label: string; + hint: string; + value: number; + Icon: (props: React.SVGProps) => React.JSX.Element; + color: string; + accentGradient: string; + loading: boolean; +}) { + return ( +
+
+
+
+ +
+
+
+ {loading ? ( + + ) : ( + + + + )} +
+
+ {label} +
+
{hint}
+
+
+
+ ); +} diff --git a/src/app/(home)/_components/dashboard/to-labeled-points.ts b/src/app/(home)/_components/dashboard/to-labeled-points.ts new file mode 100644 index 0000000..d6452ae --- /dev/null +++ b/src/app/(home)/_components/dashboard/to-labeled-points.ts @@ -0,0 +1,22 @@ +import type { TDashboardTrendPoint } from "@/lib/api/types"; +import type { DailyPoint } from "./daily-trend-chart"; + +/** + * Convert an API daily series (`{ date: "YYYY-MM-DD", count }`) into chart + * points with a locale-formatted axis label. API dates are UTC calendar days + * and are kept verbatim in `date` for lookups. + */ +export function toLabeledPoints(series: TDashboardTrendPoint[]): DailyPoint[] { + return series.map((point) => { + const [y, m, d] = point.date.split("-").map(Number); + const date = new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1)); + return { + date: point.date, + label: date.toLocaleDateString("en", { + month: "short", + day: "numeric", + }), + count: point.count, + }; + }); +} diff --git a/src/app/(home)/_components/dashboard/trends-chart.tsx b/src/app/(home)/_components/dashboard/trends-chart.tsx index a537e50..0149dc3 100644 --- a/src/app/(home)/_components/dashboard/trends-chart.tsx +++ b/src/app/(home)/_components/dashboard/trends-chart.tsx @@ -1,382 +1,21 @@ "use client"; -import gsap from "gsap"; -import { useLayoutEffect, useMemo, useRef, useState } from "react"; -import { AnimatedCounter } from "./animated-counter"; -import { PulseIcon, TrendUpIcon } from "./icons"; - -type Point = { date: string; label: string; count: number }; +import { DailyPoint, DailyTrendChart } from "./daily-trend-chart"; type Props = { - data: Point[]; + data: DailyPoint[]; isLoading: boolean; }; -const W = 1000; -const H = 240; -const PAD_L = 0; -const PAD_R = 0; -const PAD_T = 24; -const PAD_B = 36; - export function TenderTrendsChart({ data, isLoading }: Props) { - const rootRef = useRef(null); - const svgRef = useRef(null); - const pathRef = useRef(null); - const areaRef = useRef(null); - const dotsRef = useRef(null); - - const [hoverIdx, setHoverIdx] = useState(null); - - const { linePath, areaPath, points, sumY } = useMemo(() => { - const max = Math.max(1, ...data.map((d) => d.count)); - const innerW = W - PAD_L - PAD_R; - const innerH = H - PAD_T - PAD_B; - const step = data.length > 1 ? innerW / (data.length - 1) : innerW; - - const pts = data.map((d, i) => ({ - x: PAD_L + i * step, - y: PAD_T + innerH - (d.count / max) * innerH, - d, - })); - - if (pts.length === 0) { - return { linePath: "", areaPath: "", points: pts, sumY: 0 }; - } - - const line = pts - .map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`) - .join(" "); - - const area = `${line} L ${pts[pts.length - 1].x.toFixed(2)} ${PAD_T + innerH} L ${pts[0].x.toFixed(2)} ${PAD_T + innerH} Z`; - - return { - linePath: line, - areaPath: area, - points: pts, - sumY: data.reduce((acc, p) => acc + p.count, 0), - }; - }, [data]); - - useLayoutEffect(() => { - const reduced = - typeof window !== "undefined" && - window.matchMedia("(prefers-reduced-motion: reduce)").matches; - - const ctx = gsap.context(() => { - if (reduced) return; - gsap.from(rootRef.current, { - y: 24, - opacity: 0, - duration: 0.6, - ease: "power3.out", - delay: 0.2, - }); - - if (pathRef.current && linePath) { - const length = pathRef.current.getTotalLength(); - gsap.fromTo( - pathRef.current, - { strokeDasharray: length, strokeDashoffset: length }, - { - strokeDashoffset: 0, - duration: 1.4, - ease: "power2.inOut", - delay: 0.3, - }, - ); - } - if (areaRef.current) { - gsap.fromTo( - areaRef.current, - { opacity: 0, y: 12 }, - { opacity: 1, y: 0, duration: 1, delay: 0.5, ease: "power2.out" }, - ); - } - if (dotsRef.current) { - gsap.fromTo( - dotsRef.current.querySelectorAll("circle"), - { scale: 0, transformOrigin: "center" }, - { - scale: 1, - duration: 0.4, - stagger: 0.05, - ease: "back.out(2)", - delay: 0.8, - }, - ); - } - }, rootRef); - return () => ctx.revert(); - }, [linePath]); - - const last = points[points.length - 1]?.d.count ?? 0; - const prev = points[points.length - 2]?.d.count ?? 0; - const dayDelta = last - prev; - const avg = data.length ? Math.round(sumY / data.length) : 0; - - const handleMove = (e: React.MouseEvent) => { - if (!svgRef.current || !points.length) return; - const rect = svgRef.current.getBoundingClientRect(); - const px = ((e.clientX - rect.left) / rect.width) * W; - let bestI = 0; - let bestD = Infinity; - for (let i = 0; i < points.length; i++) { - const d = Math.abs(points[i].x - px); - if (d < bestD) { - bestD = d; - bestI = i; - } - } - setHoverIdx(bestI); - }; - - const hovered = hoverIdx !== null ? points[hoverIdx] : null; - const innerH = H - PAD_T - PAD_B; - const tooltipLeftPct = hovered ? (hovered.x / W) * 100 : 0; - const tooltipTopPct = hovered ? (hovered.y / H) * 100 : 0; - const flipLeft = hovered ? hovered.x > W * 0.7 : false; - const flipRight = hovered ? hovered.x < W * 0.12 : false; - const flipDown = hovered ? hovered.y < H * 0.35 : false; - const tooltipTX = flipLeft ? "-100%" : flipRight ? "0%" : "-50%"; - const tooltipTY = flipDown ? "calc(0% + 14px)" : "calc(-100% - 14px)"; - return ( -
-
-
-

- Tender Flow — Last 14 days -

-

- New tender notices received per day -

-
-
-
-
- Today -
-
- -
-
-
-
- Avg/day -
-
- -
-
-
- - {dayDelta >= 0 ? "+" : ""} - {dayDelta} vs yesterday -
-
-
- -
- {isLoading ? ( -
- Loading flow… -
- ) : ( -
- setHoverIdx(null)} - > - - - - - - - - - - - - - - - - - - - - {[0.25, 0.5, 0.75].map((p) => ( - - ))} - - {areaPath && ( - - )} - {linePath && ( - <> - - - - )} - - {hovered && ( - - )} - - - {points.map((p, i) => { - const isHover = hoverIdx === i; - const isLatest = i === points.length - 1; - return ( - - {isLatest && ( - <> - - - - - - - )} - - - ); - })} - - - {points.map((p, i) => { - const isFirst = i === 0; - const isLast = i === points.length - 1; - if (!(isFirst || isLast || i % 2 === 0)) return null; - return ( - - {p.d.label} - - ); - })} - - - {hovered && ( -
-
- {hovered.d.label} -
-
- - {hovered.d.count} - - - {hovered.d.count === 1 ? "tender" : "tenders"} - -
- {hoverIdx !== null && hoverIdx > 0 && ( -
- {(() => { - const diff = - hovered.d.count - points[hoverIdx - 1].d.count; - const sign = diff > 0 ? "+" : ""; - return `${sign}${diff} vs previous day`; - })()} -
- )} -
- )} -
- )} -
-
+ ); } diff --git a/src/app/(home)/_components/dashboard/use-dashboard-data.ts b/src/app/(home)/_components/dashboard/use-dashboard-data.ts index 9ad5569..4bdcf14 100644 --- a/src/app/(home)/_components/dashboard/use-dashboard-data.ts +++ b/src/app/(home)/_components/dashboard/use-dashboard-data.ts @@ -9,6 +9,7 @@ import { useTendersQuery, } from "@/hooks/queries"; import { useMemo } from "react"; +import { toLabeledPoints } from "./to-labeled-points"; export type DashboardMetrics = ReturnType; @@ -61,21 +62,10 @@ export function useDashboardData() { refetchRecent(); }; - const trend = useMemo(() => { - const series = trendResponse?.series ?? []; - return series.map((point) => { - const [y, m, d] = point.date.split("-").map(Number); - const date = new Date(Date.UTC(y, (m ?? 1) - 1, d ?? 1)); - return { - date: point.date, - label: date.toLocaleDateString("en", { - month: "short", - day: "numeric", - }), - count: point.count, - }; - }); - }, [trendResponse]); + const trend = useMemo( + () => toLabeledPoints(trendResponse?.series ?? []), + [trendResponse], + ); const countries = useMemo( () => diff --git a/src/app/(home)/_components/dashboard/use-statistics.ts b/src/app/(home)/_components/dashboard/use-statistics.ts new file mode 100644 index 0000000..a861b82 --- /dev/null +++ b/src/app/(home)/_components/dashboard/use-statistics.ts @@ -0,0 +1,44 @@ +"use client"; + +import { useDashboardStatisticsQuery } from "@/hooks/queries"; +import { useMemo, useState } from "react"; +import { toLabeledPoints } from "./to-labeled-points"; + +export const STATISTICS_DAY_OPTIONS = [7, 14, 30, 90] as const; + +export type StatisticsDays = (typeof STATISTICS_DAY_OPTIONS)[number]; + +export function useStatistics() { + const [days, setDays] = useState(14); + + const query = useDashboardStatisticsQuery({ days }); + const stats = query.data?.data; + + const daily = stats?.daily; + + const scrapedTed = useMemo( + () => toLabeledPoints(daily?.scraped_ted ?? []), + [daily], + ); + const scrapedDocuments = useMemo( + () => toLabeledPoints(daily?.scraped_documents ?? []), + [daily], + ); + const translatedNotices = useMemo( + () => toLabeledPoints(daily?.translated_notices ?? []), + [daily], + ); + + return { + days, + setDays, + isLoading: query.isPending, + isError: query.isError, + refetch: query.refetch, + scrapedTed, + scrapedDocuments, + translatedNotices, + totalScrapedDocuments: stats?.totals.scraped_documents ?? 0, + totalTranslatedTenders: stats?.totals.translated_tenders ?? 0, + }; +} diff --git a/src/components/Tables/admins/useAdminsPresenter.ts b/src/components/Tables/admins/useAdminsPresenter.ts index 1c2abbc..6ab6ef9 100644 --- a/src/components/Tables/admins/useAdminsPresenter.ts +++ b/src/components/Tables/admins/useAdminsPresenter.ts @@ -218,8 +218,15 @@ export const useAdminsPresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ + ...apiDefaultParams, + offset: 0, + limit: 10, + search: "", + status: "", + }); router.push(pathName); - }, [pathName, router]); + }, [filterFormReset, pathName, router]); const columns = [ "row", diff --git a/src/components/Tables/cms/useCmsTablePresenter.ts b/src/components/Tables/cms/useCmsTablePresenter.ts index 022ec12..ede9912 100644 --- a/src/components/Tables/cms/useCmsTablePresenter.ts +++ b/src/components/Tables/cms/useCmsTablePresenter.ts @@ -61,8 +61,9 @@ const useCmsTablePresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ q: "", key: "" }); router.push(pathname); - }, [pathname, router]); + }, [pathname, router, filterFormReset]); const onConfirmDelete = () => { if (!currentCms?.id) return; diff --git a/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts b/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts index 1d06ee6..63ff37d 100644 --- a/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts +++ b/src/components/Tables/companies/company-categories/useCompanyCategoriesPresenter.ts @@ -94,8 +94,9 @@ const useCompanyCategoriesPresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ q: "", published: undefined }); router.push(pathName); - }, [pathName, router]); + }, [pathName, router, filterFormReset]); const onConfirmDelete = () => { if (!currentCategory) return; diff --git a/src/components/Tables/companies/useCompanyListPresenter.ts b/src/components/Tables/companies/useCompanyListPresenter.ts index 0cfe1bc..bdb885c 100644 --- a/src/components/Tables/companies/useCompanyListPresenter.ts +++ b/src/components/Tables/companies/useCompanyListPresenter.ts @@ -100,8 +100,15 @@ export const useCompanyListPresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ + name: "", + email: "", + phone: "", + currency: undefined, + employee_count: "", + }); router.push(pathName); - }, [pathName, router]); + }, [pathName, router, filterFormReset]); const onConfirmDelete = () => { if (currentCompany) { diff --git a/src/components/Tables/contact-us/useContactUsListPresenter.ts b/src/components/Tables/contact-us/useContactUsListPresenter.ts index 6470359..2e611ea 100644 --- a/src/components/Tables/contact-us/useContactUsListPresenter.ts +++ b/src/components/Tables/contact-us/useContactUsListPresenter.ts @@ -90,8 +90,9 @@ export const useContactUsListPresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ q: "", status: "" }); router.push(pathname); - }, [pathname, router]); + }, [pathname, router, filterFormReset]); const onConfirmDelete = () => { if (!currentContact) return; diff --git a/src/components/Tables/customers/useCustomerListPresenter.ts b/src/components/Tables/customers/useCustomerListPresenter.ts index 7987db1..d26b8f0 100644 --- a/src/components/Tables/customers/useCustomerListPresenter.ts +++ b/src/components/Tables/customers/useCustomerListPresenter.ts @@ -125,8 +125,9 @@ const useCustomerListPresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ status: undefined, role: undefined, type: undefined }); router.push(pathName); - }, [pathName, router]); + }, [pathName, router, filterFormReset]); const columns = [ "row", diff --git a/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts b/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts index d2116fe..ede635d 100644 --- a/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts +++ b/src/components/Tables/feedback-table/useFeedbackTablePresenter.ts @@ -90,8 +90,9 @@ const useFeedbackTablePresenter = ({ id, paramKey }: IProps) => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ q: "" }); router.push(pathName); - }, [pathName, router]); + }, [pathName, router, filterFormReset]); const getShowcaseSectionTitle = () => { if (paramKey === "tender_id") { diff --git a/src/components/Tables/inquiries/useInquiriesListPresenter.ts b/src/components/Tables/inquiries/useInquiriesListPresenter.ts index d9f45cd..8f3be80 100644 --- a/src/components/Tables/inquiries/useInquiriesListPresenter.ts +++ b/src/components/Tables/inquiries/useInquiriesListPresenter.ts @@ -88,8 +88,9 @@ const useInquiriesListPresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ q: "", status: "" }); router.push(pathname); - }, [pathname, router]); + }, [pathname, router, filterFormReset]); const deleteInquiry = () => { if (!currentInquiry?.id) return; diff --git a/src/components/Tables/notification-history/useNotificationHistoryTablePresenter.ts b/src/components/Tables/notification-history/useNotificationHistoryTablePresenter.ts index 102fe5c..18a8f6a 100644 --- a/src/components/Tables/notification-history/useNotificationHistoryTablePresenter.ts +++ b/src/components/Tables/notification-history/useNotificationHistoryTablePresenter.ts @@ -83,8 +83,9 @@ const useNotificationHistoryTablePresenter = () => { }; const clearAllFilters = useCallback(() => { + filterFormReset({ seen: undefined, search: "" }); router.push(pathName); - }, [pathName, router]); + }, [pathName, router, filterFormReset]); return { notificationHistory: data?.data, diff --git a/src/hooks/queries/useDashboardQueries.ts b/src/hooks/queries/useDashboardQueries.ts index 0dca227..a3319f8 100644 --- a/src/hooks/queries/useDashboardQueries.ts +++ b/src/hooks/queries/useDashboardQueries.ts @@ -29,6 +29,18 @@ export const useDashboardTrendQuery = (params?: { }); }; +export const useDashboardStatisticsQuery = (params?: { days?: number }) => { + const queryKey = useMemo( + () => [API_ENDPOINTS.DASHBOARD.STATISTICS, params], + [params], + ); + return useQuery({ + queryKey, + queryFn: () => dashboardService.statistics(params), + refetchInterval: 60_000, + }); +}; + export const useDashboardCountriesQuery = (params?: { limit?: number }) => { const queryKey = useMemo( () => [API_ENDPOINTS.DASHBOARD.COUNTRIES, params], diff --git a/src/lib/api/endpoints.ts b/src/lib/api/endpoints.ts index e7be7bd..b9a1979 100644 --- a/src/lib/api/endpoints.ts +++ b/src/lib/api/endpoints.ts @@ -86,6 +86,7 @@ export const API_ENDPOINTS = { DASHBOARD: { SUMMARY: "dashboard/summary", TREND: "dashboard/trend", + STATISTICS: "dashboard/statistics", COUNTRIES: "dashboard/countries", NOTICE_TYPES: "dashboard/notice-types", CLOSING_SOON: "dashboard/closing-soon", diff --git a/src/lib/api/services/dashboard-service.ts b/src/lib/api/services/dashboard-service.ts index a4def45..01597c7 100644 --- a/src/lib/api/services/dashboard-service.ts +++ b/src/lib/api/services/dashboard-service.ts @@ -5,6 +5,7 @@ import { TDashboardClosingSoon, TDashboardCountries, TDashboardNoticeTypes, + TDashboardStatistics, TDashboardSummary, TDashboardTrend, } from "../types"; @@ -33,6 +34,18 @@ export const dashboardService = { } }, + statistics: async (params?: { + days?: number; + }): Promise> => { + try { + return (await api.get(API_ENDPOINTS.DASHBOARD.STATISTICS, { params })) + .data; + } catch (error) { + console.error("ERROR caught in Dashboard Service => statistics:", error); + throw error; + } + }, + countries: async (params?: { limit?: number; }): Promise> => { diff --git a/src/lib/api/types/Dashboard.ts b/src/lib/api/types/Dashboard.ts index f239fd5..7e3e3a7 100644 --- a/src/lib/api/types/Dashboard.ts +++ b/src/lib/api/types/Dashboard.ts @@ -21,6 +21,24 @@ export type TDashboardTrend = { series: TDashboardTrendPoint[]; }; +export type TDashboardStatisticsDaily = { + scraped_ted: TDashboardTrendPoint[]; + scraped_documents: TDashboardTrendPoint[]; + translated_notices: TDashboardTrendPoint[]; +}; + +export type TDashboardStatisticsTotals = { + scraped_documents: number; + translated_tenders: number; +}; + +export type TDashboardStatistics = { + days: number; + generated_at: number; + daily: TDashboardStatisticsDaily; + totals: TDashboardStatisticsTotals; +}; + export type TDashboardCountryItem = { country_code: string; count: number;