fix estratti conto

This commit is contained in:
Marco Pedone 2026-05-13 14:26:18 +02:00
parent 9efb6d605f
commit ae4068fb85
4 changed files with 168 additions and 160 deletions

View file

@ -8,6 +8,7 @@
"name": "infoalloggi",
"version": "0.1.0",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@ -626,6 +627,12 @@
"node": ">=20.19.0"
}
},
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"license": "MIT",

View file

@ -23,6 +23,7 @@
},
"version": "0.1.0",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@ -160,4 +161,4 @@
"axios": "^1.16.0"
}
}
}
}

View file

@ -1,3 +1,4 @@
import { TZDate } from "@date-fns/tz";
import {
add,
differenceInMonths,
@ -5,13 +6,15 @@ import {
format,
fromUnixTime,
getUnixTime,
isBefore,
} from "date-fns";
import { it } from "date-fns/locale";
import { RefreshCcw } from "lucide-react";
import type { GetServerSideProps } from "next";
import Head from "next/head";
import { useMemo } from "react";
import toast from "react-hot-toast";
import type Stripe from "stripe";
import LoadingButton from "~/components/custom_ui/loading-button";
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { Button } from "~/components/ui/button";
@ -40,67 +43,50 @@ const reportStatusTitles: Record<string, string> = {
failed: "Fallito",
};
const START_DATE = new Date("2026-01-01");
const getMonthKey = (unix: number) => {
const date = new Date(unix * 1000); // Unix is usually in seconds, JS needs ms
return `${date.getFullYear()}-${date.getMonth() + 1}`;
};
const START_DATE = new TZDate("2026-01-01", "UTC");
/**
* Pagina di gestione reports stripe per admin: /area-riservata/admin/stripe-reports
*/
const StripeReports: NextPageWithLayout = () => {
const { data, isLoading, refetch } =
api.stripe_reports.listReports.useQuery();
const utils = api.useUtils();
const { mutate: setupEstrattoConto } =
api.stripe_reports.setupEstrattoConto.useMutation({
onSuccess: async () => {
toast.success("Report richiesto con successo");
await utils.stripe_reports.listReports.invalidate();
},
onError: (error) => {
toast.error(`Errore nella richiesta del report: ${error.message}`);
},
});
const { mutate: generateEstrattoConto } =
api.stripe_reports.generateEstrattoConto.useMutation({
onMutate: () => {
toast.loading("Generazione estratto conto in corso...", {
id: "generate-pdf",
});
},
onSuccess: async (res) => {
if (!res.data) {
toast.error("PDF non disponibile al momento. Riprova più tardi.", {
id: "generate-pdf",
});
return;
}
toast.success("PDF generato con successo!", {
id: "generate-pdf",
});
const binaryString = atob(res.data.pdf);
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
const blob = new Blob([bytes], {
type: "application/pdf",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = res.data.title;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.dismiss("generate-pdf");
},
onError: (error) => {
toast.error(`Errore nella generazione del report: ${error.message}`, {
id: "generate-pdf",
});
},
});
const now = new Date();
const monthsToShow = differenceInMonths(now, START_DATE) + 1;
const mappings = Array.from({ length: monthsToShow }, (_, i) => {
const m = add(START_DATE, { months: i });
const em = endOfMonth(m);
const um = getUnixTime(m);
const uem = getUnixTime(em);
return {
title: format(m, "MMMM yyyy", { locale: it }),
start: um,
end: uem,
};
});
const reportsByMonth = useMemo(() => {
if (!data || data.length === 0) return {};
const map: Record<string, Stripe.Reporting.ReportRun[]> = {};
data.forEach((report) => {
const start = report.parameters?.interval_start;
if (start) {
const key = getMonthKey(start);
if (!map[key]) map[key] = [];
map[key].push(report);
}
});
return map;
}, [data]);
if (isLoading) return <LoadingPage />;
if (!data) return <Status500 />;
return (
@ -136,110 +122,13 @@ const StripeReports: NextPageWithLayout = () => {
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: monthsToShow }, (_, i) => {
const m = add(START_DATE, { months: i });
const em = endOfMonth(m);
const hasReports = data.filter((report) => {
if (
!report.parameters?.interval_start ||
!report.parameters?.interval_end
) {
return false;
}
const um = getUnixTime(m);
const uem = getUnixTime(em);
// controlla se il report è allineato al mese in esame
return (
um >= report.parameters.interval_start &&
um <= report.parameters.interval_end &&
uem >= report.parameters.interval_start &&
uem <= report.parameters.interval_end
);
});
const hasBudgetReport = hasReports
.sort((a, b) => b.created - a.created)
.filter((report) =>
report.report_type.startsWith(
"balance_change_from_activity.itemized",
),
);
const hasValidBudgetReport = hasBudgetReport.find(
(report) =>
report.status === "succeeded" &&
isBefore(
now,
fromUnixTime(report.result?.expires_at ?? 0),
),
);
const hasSummaryReport = hasReports
.sort((a, b) => b.created - a.created)
.filter((report) =>
report.report_type.startsWith("balance.summary"),
);
const hasValidSummaryReport = hasSummaryReport.find(
(report) =>
report.status === "succeeded" &&
isBefore(
now,
fromUnixTime(report.result?.expires_at ?? 0),
),
);
const hasReport =
hasBudgetReport.length > 0 && hasSummaryReport.length > 0;
const hasReportValid =
hasValidBudgetReport && hasValidSummaryReport;
return (
<TableRow className="grid grid-cols-2" key={m.getTime()}>
<TableCell>
{format(m, "MMMM yyyy", { locale: it })}
</TableCell>
<TableCell>
{!hasReportValid ? (
<Button
onClick={() => setupEstrattoConto({ start: m })}
size="sm"
variant={hasReport ? "destructive" : "default"}
>
{hasReport ? "Scaduto - Rigenera" : "Genera Report"}
</Button>
) : (
<div className="flex items-center gap-2">
<Button
onClick={() => {
if (
hasValidBudgetReport.result?.url &&
hasValidSummaryReport.result?.url
) {
generateEstrattoConto({
movimentiUrl:
hasValidBudgetReport.result.url,
saldoUrl: hasValidSummaryReport.result.url,
});
}
}}
size="sm"
variant="success"
>
Disponibile - Apri Estratto Conto
</Button>
<Button
onClick={() => setupEstrattoConto({ start: m })}
size="sm"
variant="outline"
>
Rigenera Stripe Report
</Button>
</div>
)}
</TableCell>
</TableRow>
);
})}
{mappings.map((d) => (
<MonthRow
{...d}
key={d.start}
reports={reportsByMonth[getMonthKey(d.start)] || []}
/>
))}
</TableBody>
</Table>
</div>
@ -301,6 +190,117 @@ const StripeReports: NextPageWithLayout = () => {
);
};
const MonthRow = ({
title,
start,
end,
reports,
}: {
title: string;
start: number;
end: number;
reports: Stripe.Reporting.ReportRun[];
}) => {
const utils = api.useUtils();
const { mutate: setupEstrattoConto } =
api.stripe_reports.setupEstrattoConto.useMutation({
onSuccess: async () => {
toast.success("Report richiesto con successo");
await utils.stripe_reports.listReports.invalidate();
},
onError: (error) => {
toast.error(`Errore nella richiesta del report: ${error.message}`);
},
});
const { mutate: generateEstrattoConto } =
api.stripe_reports.generateEstrattoConto.useMutation({
onMutate: () => {
toast.loading("Generazione estratto conto in corso...", {
id: "generate-pdf",
});
},
onSuccess: async (res) => {
if (!res.data) {
toast.error("PDF non disponibile al momento. Riprova più tardi.", {
id: "generate-pdf",
});
return;
}
toast.success("PDF generato con successo!", {
id: "generate-pdf",
});
const binaryString = atob(res.data.pdf);
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
const blob = new Blob([bytes], {
type: "application/pdf",
});
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = res.data.title;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
toast.dismiss("generate-pdf");
},
onError: (error) => {
toast.error(`Errore nella generazione del report: ${error.message}`, {
id: "generate-pdf",
});
},
});
const isGenerating = reports.some((r) => r.status === "pending");
const hasValidBudgetReport = reports.find(
(r) =>
r.report_type.startsWith("balance_change_from_activity.itemized") &&
r.status === "succeeded",
);
const hasValidSummaryReport = reports.find(
(r) =>
r.report_type.startsWith("balance.summary") && r.status === "succeeded",
);
return (
<TableRow className="grid grid-cols-2">
<TableCell>{title}</TableCell>
<TableCell className="flex items-center gap-2">
<LoadingButton
loading={isGenerating}
onClick={() => setupEstrattoConto({ start, end })}
size="sm"
variant={isGenerating ? "info" : "default"}
>
{isGenerating ? "Generazione in corso" : "Genera Report"}
</LoadingButton>
{hasValidBudgetReport && hasValidSummaryReport && (
<Button
onClick={() => {
if (
hasValidBudgetReport.result?.url &&
hasValidSummaryReport.result?.url
) {
generateEstrattoConto({
movimentiUrl: hasValidBudgetReport.result.url,
saldoUrl: hasValidSummaryReport.result.url,
});
}
}}
size="sm"
variant="success"
>
Disponibile - Apri Estratto Conto
</Button>
)}
</TableCell>
</TableRow>
);
};
export const getServerSideProps = (async (context) => {
const access_token = context.req.cookies.access_token;

View file

@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server";
import { endOfMonth, format, getUnixTime } from "date-fns";
import { format } from "date-fns";
import { it } from "date-fns/locale";
import Papa from "papaparse";
import { z } from "zod/v4";
@ -23,12 +23,12 @@ export const stripeReportsRouter = createTRPCRouter({
setupEstrattoConto: adminProcedure
.input(
z.object({
start: z.date(),
start: z.number(),
end: z.number(),
}),
)
.mutation(async ({ input }) => {
const start = getUnixTime(input.start);
const end = getUnixTime(endOfMonth(input.start));
const { start, end } = input;
try {
await stripe.reporting.reportRuns.create({