feat: add period quick-select filter to transactions page
Add period buttons (This month, 3 months, 6 months, This year, All) above the transaction filters. "This year" is selected by default so the page no longer shows all transactions since the beginning of time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
13989dc0b8
commit
f9c6fabc13
4 changed files with 67 additions and 2 deletions
|
|
@ -1,9 +1,44 @@
|
||||||
import { useMemo } from "react";
|
import { useMemo, useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import type { TransactionFilters, Category, ImportSource } from "../../shared/types";
|
import type { TransactionFilters, Category, ImportSource } from "../../shared/types";
|
||||||
import CategoryCombobox from "../shared/CategoryCombobox";
|
import CategoryCombobox from "../shared/CategoryCombobox";
|
||||||
|
|
||||||
|
type QuickPeriod = "month" | "3months" | "6months" | "year" | "all";
|
||||||
|
const PERIODS: QuickPeriod[] = ["month", "3months", "6months", "year", "all"];
|
||||||
|
|
||||||
|
function computePeriodDates(period: QuickPeriod): { dateFrom: string | null; dateTo: string | null } {
|
||||||
|
if (period === "all") return { dateFrom: null, dateTo: null };
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = now.getMonth();
|
||||||
|
if (period === "year") return { dateFrom: `${year}-01-01`, dateTo: null };
|
||||||
|
let from: Date;
|
||||||
|
switch (period) {
|
||||||
|
case "month":
|
||||||
|
from = new Date(year, month, 1);
|
||||||
|
break;
|
||||||
|
case "3months":
|
||||||
|
from = new Date(year, month - 2, 1);
|
||||||
|
break;
|
||||||
|
case "6months":
|
||||||
|
from = new Date(year, month - 5, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const dateFrom = `${from.getFullYear()}-${String(from.getMonth() + 1).padStart(2, "0")}-${String(from.getDate()).padStart(2, "0")}`;
|
||||||
|
return { dateFrom, dateTo: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectActivePeriod(filters: TransactionFilters): QuickPeriod | null {
|
||||||
|
if (filters.dateTo) return null;
|
||||||
|
if (!filters.dateFrom) return "all";
|
||||||
|
for (const p of PERIODS) {
|
||||||
|
const { dateFrom } = computePeriodDates(p);
|
||||||
|
if (dateFrom === filters.dateFrom) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
interface TransactionFilterBarProps {
|
interface TransactionFilterBarProps {
|
||||||
filters: TransactionFilters;
|
filters: TransactionFilters;
|
||||||
categories: Category[];
|
categories: Category[];
|
||||||
|
|
@ -19,6 +54,17 @@ export default function TransactionFilterBar({
|
||||||
}: TransactionFilterBarProps) {
|
}: TransactionFilterBarProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const activePeriod = detectActivePeriod(filters);
|
||||||
|
|
||||||
|
const handlePeriodChange = useCallback(
|
||||||
|
(period: QuickPeriod) => {
|
||||||
|
const { dateFrom, dateTo } = computePeriodDates(period);
|
||||||
|
onFilterChange("dateFrom", dateFrom);
|
||||||
|
onFilterChange("dateTo", dateTo);
|
||||||
|
},
|
||||||
|
[onFilterChange]
|
||||||
|
);
|
||||||
|
|
||||||
const categoryExtras = useMemo(
|
const categoryExtras = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: "", label: t("transactions.filters.allCategories") },
|
{ value: "", label: t("transactions.filters.allCategories") },
|
||||||
|
|
@ -37,6 +83,23 @@ export default function TransactionFilterBar({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[var(--card)] rounded-xl p-4 border border-[var(--border)] mb-4">
|
<div className="bg-[var(--card)] rounded-xl p-4 border border-[var(--border)] mb-4">
|
||||||
|
{/* Period quick-select */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{PERIODS.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
onClick={() => handlePeriodChange(p)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
p === activePeriod
|
||||||
|
? "bg-[var(--primary)] text-white"
|
||||||
|
: "bg-[var(--background)] border border-[var(--border)] text-[var(--foreground)] hover:bg-[var(--muted)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t(`dashboard.period.${p}`)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="relative flex-1 min-w-[200px]">
|
<div className="relative flex-1 min-w-[200px]">
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ const initialFilters: TransactionFilters = {
|
||||||
search: "",
|
search: "",
|
||||||
categoryId: null,
|
categoryId: null,
|
||||||
sourceId: null,
|
sourceId: null,
|
||||||
dateFrom: null,
|
dateFrom: `${new Date().getFullYear()}-01-01`,
|
||||||
dateTo: null,
|
dateTo: null,
|
||||||
uncategorizedOnly: false,
|
uncategorizedOnly: false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"3months": "3 months",
|
"3months": "3 months",
|
||||||
"6months": "6 months",
|
"6months": "6 months",
|
||||||
"12months": "12 months",
|
"12months": "12 months",
|
||||||
|
"year": "This year",
|
||||||
"all": "All"
|
"all": "All"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"3months": "3 mois",
|
"3months": "3 mois",
|
||||||
"6months": "6 mois",
|
"6months": "6 mois",
|
||||||
"12months": "12 mois",
|
"12months": "12 mois",
|
||||||
|
"year": "Cette année",
|
||||||
"all": "Tout"
|
"all": "Tout"
|
||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue