fix estratti conto
This commit is contained in:
parent
9efb6d605f
commit
ae4068fb85
4 changed files with 168 additions and 160 deletions
7
apps/infoalloggi/package-lock.json
generated
7
apps/infoalloggi/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "infoalloggi",
|
"name": "infoalloggi",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@date-fns/tz": "^1.4.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|
@ -626,6 +627,12 @@
|
||||||
"node": ">=20.19.0"
|
"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": {
|
"node_modules/@discoveryjs/json-ext": {
|
||||||
"version": "0.5.7",
|
"version": "0.5.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
},
|
},
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@date-fns/tz": "^1.4.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
|
@ -160,4 +161,4 @@
|
||||||
"axios": "^1.16.0"
|
"axios": "^1.16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { TZDate } from "@date-fns/tz";
|
||||||
import {
|
import {
|
||||||
add,
|
add,
|
||||||
differenceInMonths,
|
differenceInMonths,
|
||||||
|
|
@ -5,13 +6,15 @@ import {
|
||||||
format,
|
format,
|
||||||
fromUnixTime,
|
fromUnixTime,
|
||||||
getUnixTime,
|
getUnixTime,
|
||||||
isBefore,
|
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { useMemo } from "react";
|
||||||
import toast from "react-hot-toast";
|
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 { LoadingPage } from "~/components/loading";
|
||||||
import { Status500 } from "~/components/status-page";
|
import { Status500 } from "~/components/status-page";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
|
@ -40,67 +43,50 @@ const reportStatusTitles: Record<string, string> = {
|
||||||
failed: "Fallito",
|
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
|
* Pagina di gestione reports stripe per admin: /area-riservata/admin/stripe-reports
|
||||||
*/
|
*/
|
||||||
const StripeReports: NextPageWithLayout = () => {
|
const StripeReports: NextPageWithLayout = () => {
|
||||||
const { data, isLoading, refetch } =
|
const { data, isLoading, refetch } =
|
||||||
api.stripe_reports.listReports.useQuery();
|
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 now = new Date();
|
||||||
const monthsToShow = differenceInMonths(now, START_DATE) + 1;
|
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 (isLoading) return <LoadingPage />;
|
||||||
if (!data) return <Status500 />;
|
if (!data) return <Status500 />;
|
||||||
return (
|
return (
|
||||||
|
|
@ -136,110 +122,13 @@ const StripeReports: NextPageWithLayout = () => {
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{Array.from({ length: monthsToShow }, (_, i) => {
|
{mappings.map((d) => (
|
||||||
const m = add(START_DATE, { months: i });
|
<MonthRow
|
||||||
const em = endOfMonth(m);
|
{...d}
|
||||||
const hasReports = data.filter((report) => {
|
key={d.start}
|
||||||
if (
|
reports={reportsByMonth[getMonthKey(d.start)] || []}
|
||||||
!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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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) => {
|
export const getServerSideProps = (async (context) => {
|
||||||
const access_token = context.req.cookies.access_token;
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { endOfMonth, format, getUnixTime } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { it } from "date-fns/locale";
|
import { it } from "date-fns/locale";
|
||||||
import Papa from "papaparse";
|
import Papa from "papaparse";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
@ -23,12 +23,12 @@ export const stripeReportsRouter = createTRPCRouter({
|
||||||
setupEstrattoConto: adminProcedure
|
setupEstrattoConto: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
start: z.date(),
|
start: z.number(),
|
||||||
|
end: z.number(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const start = getUnixTime(input.start);
|
const { start, end } = input;
|
||||||
const end = getUnixTime(endOfMonth(input.start));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await stripe.reporting.reportRuns.create({
|
await stripe.reporting.reportRuns.create({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue