import { useState, useCallback, useEffect, useMemo } from "react"; import { getDaysDiff, getUnitsForDayRange } from "../constants/configs"; import { DateUnit } from "../types/analytics"; import useSearchParams from "./use-search-params"; export type AnalyticsFilters = { dateFrom?: string; dateTo?: string; unit?: DateUnit; scope?: "all" | "others" | "membership"; membership?: number; page?: string; }; const isValidDate = (dateString: string): boolean => { if (!/^\d{4}-\d{2}-\d{2}$/.test(dateString)) { return false; } const date = new Date(dateString); return !isNaN(date.getTime()) && date.toISOString().startsWith(dateString); }; const parseDateParam = (value: string | null): string | null => { if (!value) return null; return isValidDate(value) ? value : null; }; const parseUnitParam = (value: string | null): DateUnit | null => { if ( value === "hour" || value === "day" || value === "week" || value === "month" || value === "year" ) { return value as DateUnit; } return null; }; export function useAnalyticsFilters() { const [params, setParams] = useSearchParams({ "date-from": { type: "string" as const }, "date-to": { type: "string" as const }, unit: { type: "string" as const }, scope: { type: "string", default: "all" }, membership: { type: "number" as const }, page: { type: "string" as const } }); const filters: AnalyticsFilters = { dateFrom: parseDateParam(params["date-from"]) || undefined, dateTo: parseDateParam(params["date-to"]) || undefined, unit: parseUnitParam(params["unit"]) || undefined, scope: (params["scope"] as "all" | "others" | "membership") || undefined, membership: params["membership"] || undefined }; const getValidUnits = useCallback((): Array => { if (!filters.dateFrom || !filters.dateTo) { return ["day", "week"]; } const dateFromObj = new Date(filters.dateFrom); const dateToObj = new Date(filters.dateTo); const daysDiff = getDaysDiff(dateFromObj, dateToObj); const allValidUnits = getUnitsForDayRange(daysDiff); const supportedUnits: Array = [ "hour", "day", "week", "month", "year" ]; const validUnits = allValidUnits.filter((u): u is DateUnit => supportedUnits.includes(u as any) ); return validUnits.length > 0 ? validUnits : ["day"]; }, [filters.dateFrom, filters.dateTo]); const validUnits = useMemo(() => getValidUnits(), [getValidUnits]); useEffect(() => { if (!filters.unit || !validUnits.includes(filters.unit)) { const newUnit = validUnits[0]; if (newUnit && newUnit !== filters.unit) { setParams((prev) => ({ ...prev, unit: newUnit })); } } }, [filters.unit, validUnits, setParams]); const isValidDateRange = useCallback( (filters: AnalyticsFilters): boolean => { if (!filters.dateFrom || !filters.dateTo) return true; return new Date(filters.dateFrom) <= new Date(filters.dateTo); }, [] ); const setFilters = useCallback( ( updates: | Partial | ((prev: AnalyticsFilters) => Partial) ) => { const currentFilters = { dateFrom: parseDateParam(params["date-from"]) || undefined, dateTo: parseDateParam(params["date-to"]) || undefined, unit: parseUnitParam(params["unit"]) || undefined, scope: (params["scope"] as "all" | "others" | "membership") || undefined, membership: params["membership"] || undefined }; const newFilters = { ...currentFilters, ...(typeof updates === "function" ? updates(currentFilters) : updates) }; if (newFilters.dateFrom && !isValidDate(newFilters.dateFrom)) { console.warn(`Invalid dateFrom: ${newFilters.dateFrom}`); return; } if (newFilters.dateTo && !isValidDate(newFilters.dateTo)) { console.warn(`Invalid dateTo: ${newFilters.dateTo}`); return; } if (!isValidDateRange(newFilters)) { console.warn( "Invalid date range: dateFrom must be before or equal to dateTo" ); return; } if ( newFilters.unit && !["hour", "day", "week", "month", "year"].includes( newFilters.unit ) ) { console.warn(`Invalid unit: ${newFilters.unit}`); return; } if ( newFilters.scope && !["all", "others", "membership"].includes(newFilters.scope) ) { console.warn(`Invalid scope: ${newFilters.scope}`); return; } let compatibleUnit = newFilters.unit; if (newFilters.dateFrom && newFilters.dateTo && newFilters.unit) { const dateFromObj = new Date(newFilters.dateFrom); const dateToObj = new Date(newFilters.dateTo); const daysDiff = getDaysDiff(dateFromObj, dateToObj); const allValidUnits = getUnitsForDayRange(daysDiff); const supportedUnits: Array = [ "hour", "day", "week", "month", "year" ]; const validUnits = allValidUnits.filter((u): u is DateUnit => supportedUnits.includes(u as any) ); if (!validUnits.includes(newFilters.unit)) { compatibleUnit = validUnits.length > 0 ? validUnits[0] : "day"; } } setParams({ "date-from": newFilters.dateFrom || null, "date-to": newFilters.dateTo || null, unit: compatibleUnit || null, scope: newFilters.scope || "all", membership: newFilters.membership || null }); }, [params, setParams, isValidDateRange] ); const clearFilters = useCallback( (keys?: Array) => { if (keys) { const updates: Partial> = {}; const keyMap = { dateFrom: "date-from", dateTo: "date-to", unit: "unit", scope: "scope", membership: "membership" }; for (const key of keys) { updates[keyMap[key as keyof typeof keyMap]] = null; } setParams(updates as any); } else { setParams({ "date-from": null, "date-to": null, unit: null, scope: "all", membership: null }); } }, [setParams] ); const replaceFilters = useCallback( (newFilters: Partial) => { if (newFilters.dateFrom && !isValidDate(newFilters.dateFrom)) { console.warn(`Invalid dateFrom: ${newFilters.dateFrom}`); return; } if (newFilters.dateTo && !isValidDate(newFilters.dateTo)) { console.warn(`Invalid dateTo: ${newFilters.dateTo}`); return; } if (!isValidDateRange(newFilters)) { console.warn( "Invalid date range: dateFrom must be before or equal to dateTo" ); return; } if ( newFilters.unit && !["hour", "day", "week", "month", "year"].includes( newFilters.unit ) ) { console.warn(`Invalid unit: ${newFilters.unit}`); return; } let compatibleUnit = newFilters.unit; if (newFilters.dateFrom && newFilters.dateTo && newFilters.unit) { const dateFromObj = new Date(newFilters.dateFrom); const dateToObj = new Date(newFilters.dateTo); const daysDiff = getDaysDiff(dateFromObj, dateToObj); const allValidUnits = getUnitsForDayRange(daysDiff); const supportedUnits: Array = [ "hour", "day", "week", "month", "year" ]; const validUnitsForRange = allValidUnits.filter( (u): u is DateUnit => supportedUnits.includes(u as any) ); if (!validUnitsForRange.includes(newFilters.unit)) { compatibleUnit = validUnitsForRange.length > 0 ? validUnitsForRange[0] : "day"; } } setParams({ "date-from": newFilters.dateFrom || null, "date-to": newFilters.dateTo || null, unit: compatibleUnit || null, scope: newFilters.scope || "all", membership: newFilters.membership || null }); }, [setParams, isValidDateRange] ); const compatibleFilters: AnalyticsFilters = { ...filters, unit: filters.unit && validUnits.includes(filters.unit) ? filters.unit : validUnits.length > 0 ? validUnits[0] : undefined }; return { dateFrom: compatibleFilters.dateFrom, dateTo: compatibleFilters.dateTo, unit: compatibleFilters.unit, scope: compatibleFilters.scope, membership: compatibleFilters.membership, filters: compatibleFilters, validUnits, isValidDateRange: isValidDateRange(compatibleFilters), setFilters, clearFilters, replaceFilters }; }