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",
|
||||
"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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue