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,
};
}),
});