2025-08-04 17:45:44 +02:00
|
|
|
"use client";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { enUS, it } from "date-fns/locale";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { CalendarIcon } from "lucide-react";
|
2025-08-28 18:27:07 +02:00
|
|
|
import * as React from "react";
|
|
|
|
|
import type { DateRange } from "react-day-picker";
|
2025-08-04 17:45:44 +02:00
|
|
|
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
|
|
|
|
import { Button } from "~/components/ui/button";
|
|
|
|
|
import { Calendar } from "~/components/ui/calendar";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
Card,
|
|
|
|
|
CardAction,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardFooter,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/components/ui/card";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
type ChartConfig,
|
|
|
|
|
ChartContainer,
|
|
|
|
|
ChartTooltip,
|
|
|
|
|
ChartTooltipContent,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/components/ui/chart";
|
|
|
|
|
import {
|
2025-08-28 18:27:07 +02:00
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "~/components/ui/popover";
|
|
|
|
|
import { cn } from "~/lib/utils";
|
|
|
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
|
|
|
|
|
|
|
|
|
const chartConfig = {
|
2025-08-28 18:27:07 +02:00
|
|
|
value: {
|
|
|
|
|
color: "var(--chart-2)",
|
2025-08-29 16:18:32 +02:00
|
|
|
label: "Valore",
|
2025-08-28 18:27:07 +02:00
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
} satisfies ChartConfig;
|
|
|
|
|
|
2025-08-05 19:23:55 +02:00
|
|
|
type TimeserieBarChartProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
title: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
timeserie: { date: Date; value: number }[];
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
2025-08-05 19:23:55 +02:00
|
|
|
export default function TimeserieBarChart({
|
2025-08-28 18:27:07 +02:00
|
|
|
timeserie,
|
|
|
|
|
title,
|
|
|
|
|
description,
|
2025-08-05 19:23:55 +02:00
|
|
|
}: TimeserieBarChartProps) {
|
2025-08-28 18:27:07 +02:00
|
|
|
const { locale } = useTranslation();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const startDate = timeserie[0]
|
|
|
|
|
? new Date(timeserie[0].date)
|
|
|
|
|
: new Date(2025, 1, 1);
|
|
|
|
|
const endDate = timeserie[timeserie.length - 1]
|
|
|
|
|
? // biome-ignore lint/style/noNonNullAssertion: <exists>
|
|
|
|
|
new Date(timeserie[timeserie.length - 1]!.date)
|
|
|
|
|
: new Date();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const [range, setRange] = React.useState<DateRange | undefined>({
|
|
|
|
|
from: startDate,
|
|
|
|
|
to: endDate,
|
|
|
|
|
});
|
|
|
|
|
const filteredData = React.useMemo(() => {
|
|
|
|
|
if (!range?.from && !range?.to) {
|
|
|
|
|
return timeserie;
|
|
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return timeserie.filter((item) => {
|
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: <exists>
|
|
|
|
|
return item.date >= range.from! && item.date <= range.to!;
|
|
|
|
|
});
|
|
|
|
|
}, [range]);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const DatePickerLocale = locale === "it" ? it : enUS;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<Card className="@container/card w-full pb-4">
|
2025-10-10 16:18:43 +02:00
|
|
|
<CardHeader className="flex @md/card:grid flex-col border-b">
|
2025-08-28 18:27:07 +02:00
|
|
|
<CardTitle>{title}</CardTitle>
|
|
|
|
|
<CardDescription className={cn(!description && "sr-only")}>
|
|
|
|
|
{description || "Analytics for the selected period."}
|
|
|
|
|
</CardDescription>
|
2025-10-10 16:18:43 +02:00
|
|
|
<CardAction className="@md/card:mt-0 mt-2">
|
2025-08-28 18:27:07 +02:00
|
|
|
<Popover>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button variant="outline">
|
|
|
|
|
<CalendarIcon />
|
|
|
|
|
{range?.from && range?.to
|
|
|
|
|
? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`
|
|
|
|
|
: "Seleziona un intervallo di date"}
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
2025-08-29 16:18:32 +02:00
|
|
|
<PopoverContent align="end" className="w-auto overflow-hidden p-0">
|
2025-08-28 18:27:07 +02:00
|
|
|
<Calendar
|
|
|
|
|
className="w-full"
|
2025-08-29 16:18:32 +02:00
|
|
|
defaultMonth={range?.from}
|
|
|
|
|
fixedWeeks
|
2025-08-28 18:27:07 +02:00
|
|
|
locale={DatePickerLocale}
|
|
|
|
|
mode="range"
|
|
|
|
|
onSelect={setRange}
|
2025-08-29 16:18:32 +02:00
|
|
|
selected={range}
|
2025-08-28 18:27:07 +02:00
|
|
|
showOutsideDays
|
|
|
|
|
/>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</CardAction>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="px-4">
|
|
|
|
|
<ChartContainer
|
|
|
|
|
className="aspect-auto h-[250px] w-full"
|
2025-08-29 16:18:32 +02:00
|
|
|
config={chartConfig}
|
2025-08-28 18:27:07 +02:00
|
|
|
>
|
|
|
|
|
{filteredData.length === 0 ? (
|
2025-10-10 16:18:43 +02:00
|
|
|
<div className="flex h-full items-center justify-center text-base text-muted-foreground">
|
2025-08-28 18:27:07 +02:00
|
|
|
Nessun dato disponibile per il periodo selezionato.
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<BarChart
|
|
|
|
|
accessibilityLayer
|
|
|
|
|
data={filteredData}
|
|
|
|
|
margin={{
|
|
|
|
|
left: 12,
|
|
|
|
|
right: 12,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CartesianGrid vertical={false} />
|
|
|
|
|
<XAxis
|
|
|
|
|
axisLine={false}
|
2025-08-29 16:18:32 +02:00
|
|
|
dataKey="date"
|
2025-08-28 18:27:07 +02:00
|
|
|
minTickGap={20}
|
|
|
|
|
tickFormatter={(value) => {
|
|
|
|
|
const date = new Date(String(value));
|
|
|
|
|
return date.toLocaleDateString("it-IT", {
|
|
|
|
|
day: "numeric",
|
|
|
|
|
month: "short",
|
|
|
|
|
});
|
|
|
|
|
}}
|
2025-08-29 16:18:32 +02:00
|
|
|
tickLine={false}
|
|
|
|
|
tickMargin={8}
|
2025-08-28 18:27:07 +02:00
|
|
|
/>
|
|
|
|
|
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
|
|
|
|
|
<Bar dataKey="value" fill={`var(--color-value)`} radius={4} />
|
|
|
|
|
</BarChart>
|
|
|
|
|
)}
|
|
|
|
|
</ChartContainer>
|
|
|
|
|
</CardContent>
|
2025-10-10 16:18:43 +02:00
|
|
|
<CardFooter className="!pt-4 flex justify-end border-t">
|
2025-08-28 18:27:07 +02:00
|
|
|
<span className="text-muted-foreground text-sm">
|
|
|
|
|
Totale:{" "}
|
|
|
|
|
<strong>
|
|
|
|
|
{filteredData.reduce((acc, item) => acc + item.value, 0)}
|
|
|
|
|
</strong>
|
|
|
|
|
</span>
|
|
|
|
|
</CardFooter>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|