diff --git a/apps/infoalloggi/src/components/area-riservata/dashboard.tsx b/apps/infoalloggi/src/components/area-riservata/dashboard.tsx index 5bf8d98..5e4b12f 100644 --- a/apps/infoalloggi/src/components/area-riservata/dashboard.tsx +++ b/apps/infoalloggi/src/components/area-riservata/dashboard.tsx @@ -4,7 +4,15 @@ import type { UsersId } from "~/schemas/public/Users"; import { ServizioContainer } from "~/components/servizio/main"; import { AccordionComp } from "~/components/accordionComp"; import { useTranslation } from "~/providers/I18nProvider"; -import AnalyticsChart from "~/components/analyticsChart"; +import TimeserieBarChart from "~/components/timeserieBarChart"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import TimeserieBarChartStacked from "~/components/timeserieBarChartStacked"; export const AdminDashboard = () => { const { data: stats, isLoading } = api.stats.adminDashStats.useQuery(); @@ -21,25 +29,35 @@ export const AdminDashboard = () => { {!stats ? (
Errore caricamento statistiche
) : ( -
-
- -
-
- -
-
- -
+
+ + + Utenti + + Numero totale di utenti registrati + + + + + {stats.userCount} + + + + + + +
)}
diff --git a/apps/infoalloggi/src/components/analyticsChart.tsx b/apps/infoalloggi/src/components/timeserieBarChart.tsx similarity index 90% rename from apps/infoalloggi/src/components/analyticsChart.tsx rename to apps/infoalloggi/src/components/timeserieBarChart.tsx index 2956792..b2d26d4 100644 --- a/apps/infoalloggi/src/components/analyticsChart.tsx +++ b/apps/infoalloggi/src/components/timeserieBarChart.tsx @@ -32,21 +32,21 @@ import { useTranslation } from "~/providers/I18nProvider"; const chartConfig = { value: { - label: "value", - color: "var(--color-primary)", + label: "Valore", + color: "var(--chart-2)", }, } satisfies ChartConfig; -type AnalyticsChartProps = { +type TimeserieBarChartProps = { title: string; description?: string; timeserie: { date: Date; value: number }[]; }; -export default function AnalyticsChart({ +export default function TimeserieBarChart({ timeserie, title, description, -}: AnalyticsChartProps) { +}: TimeserieBarChartProps) { const { locale } = useTranslation(); const startDate = timeserie[0] @@ -86,7 +86,7 @@ export default function AnalyticsChart({ {range?.from && range?.to ? `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}` - : "June 2025"} + : "Seleziona un intervallo di date"} @@ -137,15 +137,7 @@ export default function AnalyticsChart({ }); }} /> - "Utenti"} - /> - } - /> + } /> )} diff --git a/apps/infoalloggi/src/components/timeserieBarChartStacked.tsx b/apps/infoalloggi/src/components/timeserieBarChartStacked.tsx new file mode 100644 index 0000000..0f4f74d --- /dev/null +++ b/apps/infoalloggi/src/components/timeserieBarChartStacked.tsx @@ -0,0 +1,178 @@ +"use client"; +import * as React from "react"; +import { CalendarIcon } from "lucide-react"; +import { type DateRange } from "react-day-picker"; +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; + +import { Button } from "~/components/ui/button"; +import { Calendar } from "~/components/ui/calendar"; +import { + Card, + CardAction, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "~/components/ui/chart"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "~/components/ui/popover"; +import { cn } from "~/lib/utils"; +import { enUS, it } from "date-fns/locale"; +import { useTranslation } from "~/providers/I18nProvider"; + +const chartConfig = { + valueA: { + label: "valueA", + color: "var(--chart-2)", + }, + valueB: { + label: "valueB", + color: "var(--chart-3)", + }, +} satisfies ChartConfig; + +export type StackedTimeserie = { date: Date; valueA: number; valueB: number }; + +type AnalyticsChartProps = { + title: string; + description?: string; + valueALabel: string; + valueBLabel: string; + timeserie: StackedTimeserie[]; +}; +export default function TimeserieBarChartStacked({ + timeserie, + title, + valueALabel, + valueBLabel, + description, +}: AnalyticsChartProps) { + const { locale } = useTranslation(); + + const configs = chartConfig; + configs.valueA.label = valueALabel; + configs.valueB.label = valueBLabel; + + const startDate = timeserie[0] + ? new Date(timeserie[0].date) + : new Date(2025, 1, 1); + const endDate = timeserie[timeserie.length - 1] + ? new Date(timeserie[timeserie.length - 1]!.date) + : new Date(); + + const [range, setRange] = React.useState({ + from: startDate, + to: endDate, + }); + const filteredData = React.useMemo(() => { + if (!range?.from && !range?.to) { + return timeserie; + } + + return timeserie.filter((item) => { + return item.date >= range.from! && item.date <= range.to!; + }); + }, [range]); + + const DatePickerLocale = locale === "it" ? it : enUS; + + return ( + + + {title} + + {description || "Analytics for the selected period."} + + + + + + + + + + + + + + + {filteredData.length === 0 ? ( +
+ Nessun dato disponibile per il periodo selezionato. +
+ ) : ( + + + { + const date = new Date(String(value)); + return date.toLocaleDateString("it-IT", { + day: "numeric", + month: "short", + }); + }} + /> + } /> + + + + )} +
+
+ + + Totale: {filteredData.reduce((acc) => acc + 1, 0)} + + +
+ ); +} diff --git a/apps/infoalloggi/src/server/api/routers/stats.ts b/apps/infoalloggi/src/server/api/routers/stats.ts index 6124653..f7d9db3 100644 --- a/apps/infoalloggi/src/server/api/routers/stats.ts +++ b/apps/infoalloggi/src/server/api/routers/stats.ts @@ -1,6 +1,8 @@ import { adminProcedure, createTRPCRouter } from "~/server/api/trpc"; import { db } from "~/server/db"; import { sql } from "kysely"; +import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; +import type { StackedTimeserie } from "~/components/timeserieBarChartStacked"; export const statsRouter = createTRPCRouter({ adminDashStats: adminProcedure.query(async () => { @@ -19,20 +21,42 @@ export const statsRouter = createTRPCRouter({ value: parseInt(String(item.value)), })); - const orderTimeserie = ( - await db - .selectFrom("ordini") - .select((eb) => [ - sql`date_trunc('day', created_at)`.as("date"), - eb.fn.count("ordine_id").as("value"), - ]) - .groupBy("date") - .orderBy("date") - .execute() - ).map((item) => ({ - date: item.date, - value: parseInt(String(item.value)), - })); + const serviziTimeserie = { + valueALabel: "Transitorio", + valueBLabel: "Stabile", + data: Object.values( + ( + await db + .selectFrom("servizio") + .select((eb) => [ + sql`date_trunc('day', created_at)`.as("date"), + eb.fn.count("servizio_id").as("value"), + "tipologia", + ]) + .groupBy(["date", "tipologia"]) + .orderBy("date") + .execute() + ).reduce( + (acc, item) => { + const dateStr = item.date.toISOString().slice(0, 10); + if (!acc[dateStr]) { + acc[dateStr] = { + date: item.date, + valueA: 0, + valueB: 0, + }; + } + if (item.tipologia === TipologiaPosizioneEnum.Transitorio) { + acc[dateStr].valueA = parseInt(String(item.value)); + } else if (item.tipologia === TipologiaPosizioneEnum.Stabile) { + acc[dateStr].valueB = parseInt(String(item.value)); + } + return acc; + }, + {} as Record, + ), + ), + }; const contattiTimeserie = ( await db @@ -52,10 +76,16 @@ export const statsRouter = createTRPCRouter({ value: parseInt(String(item.value)), })); + const userCount = await db + .selectFrom("users") + .select((eb) => [eb.fn.count("id").as("count")]) + .executeTakeFirstOrThrow(); + return { userTimeserie, - orderTimeserie, + userCount: userCount.count, contattiTimeserie, + serviziTimeserie, }; }), });