feat: add ReconcileDateRangeCard to settings page; enhance date picker layout in statistics and transaction filters components
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
This commit is contained in:
69
app/api/banking/transactions/reconcile-date-range/route.ts
Normal file
69
app/api/banking/transactions/reconcile-date-range/route.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { revalidatePath } from "next/cache";
|
||||||
|
import { transactionService } from "@/services/transaction.service";
|
||||||
|
import { requireAuth } from "@/lib/auth-utils";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const authError = await requireAuth();
|
||||||
|
if (authError) return authError;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { startDate, endDate, reconciled = true } = body;
|
||||||
|
|
||||||
|
if (!endDate) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "endDate is required" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date format (YYYY-MM-DD)
|
||||||
|
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
if (startDate && !dateRegex.test(startDate)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid startDate format. Expected YYYY-MM-DD" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!dateRegex.test(endDate)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid endDate format. Expected YYYY-MM-DD" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate && startDate > endDate) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "startDate must be before or equal to endDate" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await transactionService.reconcileByDateRange(
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
reconciled,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Revalider le cache des pages
|
||||||
|
revalidatePath("/transactions", "page");
|
||||||
|
revalidatePath("/statistics", "page");
|
||||||
|
revalidatePath("/dashboard", "page");
|
||||||
|
revalidatePath("/settings", "page");
|
||||||
|
|
||||||
|
return NextResponse.json(result, {
|
||||||
|
headers: {
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reconciling transactions by date range:", error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to reconcile transactions";
|
||||||
|
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
OFXInfoCard,
|
OFXInfoCard,
|
||||||
BackupCard,
|
BackupCard,
|
||||||
PasswordCard,
|
PasswordCard,
|
||||||
|
ReconcileDateRangeCard,
|
||||||
} from "@/components/settings";
|
} from "@/components/settings";
|
||||||
import { useBankingData } from "@/lib/hooks";
|
import { useBankingData } from "@/lib/hooks";
|
||||||
import type { BankingData } from "@/lib/types";
|
import type { BankingData } from "@/lib/types";
|
||||||
@@ -125,6 +126,8 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
<PasswordCard />
|
<PasswordCard />
|
||||||
|
|
||||||
|
<ReconcileDateRangeCard />
|
||||||
|
|
||||||
<DangerZoneCard
|
<DangerZoneCard
|
||||||
categorizedCount={categorizedCount}
|
categorizedCount={categorizedCount}
|
||||||
onClearCategories={clearAllCategories}
|
onClearCategories={clearAllCategories}
|
||||||
|
|||||||
@@ -950,52 +950,57 @@ export default function StatisticsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-3 flex gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">
|
<label className="text-sm font-medium">
|
||||||
Date de début
|
Date de début
|
||||||
</label>
|
</label>
|
||||||
<CalendarComponent
|
<div className="scale-90 origin-top-left">
|
||||||
mode="single"
|
<CalendarComponent
|
||||||
selected={customStartDate}
|
mode="single"
|
||||||
onSelect={(date) => {
|
selected={customStartDate}
|
||||||
setCustomStartDate(date);
|
onSelect={(date) => {
|
||||||
if (date && customEndDate && date > customEndDate) {
|
setCustomStartDate(date);
|
||||||
setCustomEndDate(undefined);
|
if (date && customEndDate && date > customEndDate) {
|
||||||
}
|
setCustomEndDate(undefined);
|
||||||
}}
|
}
|
||||||
locale={fr}
|
}}
|
||||||
/>
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">
|
<label className="text-sm font-medium">
|
||||||
Date de fin
|
Date de fin
|
||||||
</label>
|
</label>
|
||||||
<CalendarComponent
|
<div className="scale-90 origin-top-left">
|
||||||
mode="single"
|
<CalendarComponent
|
||||||
selected={customEndDate}
|
mode="single"
|
||||||
onSelect={(date) => {
|
selected={customEndDate}
|
||||||
if (
|
onSelect={(date) => {
|
||||||
date &&
|
if (
|
||||||
customStartDate &&
|
date &&
|
||||||
date < customStartDate
|
customStartDate &&
|
||||||
) {
|
date < customStartDate
|
||||||
return;
|
) {
|
||||||
}
|
return;
|
||||||
setCustomEndDate(date);
|
}
|
||||||
if (date && customStartDate) {
|
setCustomEndDate(date);
|
||||||
setIsCustomDatePickerOpen(false);
|
if (date && customStartDate) {
|
||||||
}
|
setIsCustomDatePickerOpen(false);
|
||||||
}}
|
}
|
||||||
disabled={(date) => {
|
}}
|
||||||
if (!customStartDate) return true;
|
disabled={(date) => {
|
||||||
return date < customStartDate;
|
if (!customStartDate) return true;
|
||||||
}}
|
return date < customStartDate;
|
||||||
locale={fr}
|
}}
|
||||||
/>
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{customStartDate && customEndDate && (
|
</div>
|
||||||
<div className="flex gap-2 pt-2 border-t">
|
{customStartDate && customEndDate && (
|
||||||
|
<div className="flex gap-2 pt-2 border-t px-3 pb-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1016,7 +1021,6 @@ export default function StatisticsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export { DangerZoneCard } from "./danger-zone-card";
|
|||||||
export { OFXInfoCard } from "./ofx-info-card";
|
export { OFXInfoCard } from "./ofx-info-card";
|
||||||
export { BackupCard } from "./backup-card";
|
export { BackupCard } from "./backup-card";
|
||||||
export { PasswordCard } from "./password-card";
|
export { PasswordCard } from "./password-card";
|
||||||
|
export { ReconcileDateRangeCard } from "./reconcile-date-range-card";
|
||||||
|
|||||||
260
components/settings/reconcile-date-range-card.tsx
Normal file
260
components/settings/reconcile-date-range-card.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
|
||||||
|
import { CheckCircle2, Calendar } from "lucide-react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { fr } from "date-fns/locale";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { invalidateAllTransactionQueries } from "@/lib/cache-utils";
|
||||||
|
|
||||||
|
export function ReconcileDateRangeCard() {
|
||||||
|
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
||||||
|
const [isDatePickerOpen, setIsDatePickerOpen] = useState(false);
|
||||||
|
const [isReconciling, setIsReconciling] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleReconcile = async () => {
|
||||||
|
if (!endDate) return;
|
||||||
|
|
||||||
|
setIsReconciling(true);
|
||||||
|
try {
|
||||||
|
const endDateStr = format(endDate, "yyyy-MM-dd");
|
||||||
|
const body: { endDate: string; startDate?: string; reconciled: boolean } = {
|
||||||
|
endDate: endDateStr,
|
||||||
|
reconciled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
body.startDate = format(startDate, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
"/api/banking/transactions/reconcile-date-range",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || "Erreur lors du pointage");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// Invalider toutes les requêtes de transactions pour rafraîchir les données
|
||||||
|
invalidateAllTransactionQueries(queryClient);
|
||||||
|
|
||||||
|
alert(
|
||||||
|
`${result.updatedCount} opération${result.updatedCount > 1 ? "s" : ""} pointée${result.updatedCount > 1 ? "s" : ""}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Réinitialiser les dates
|
||||||
|
setStartDate(undefined);
|
||||||
|
setEndDate(undefined);
|
||||||
|
setIsDatePickerOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
alert(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Erreur lors du pointage des opérations",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsReconciling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const canReconcile = endDate && (!startDate || startDate <= endDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
Pointer les opérations par date
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Marquer toutes les opérations pointées dans une plage de dates
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Popover open={isDatePickerOpen} onOpenChange={setIsDatePickerOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left font-normal"
|
||||||
|
>
|
||||||
|
<Calendar className="mr-2 h-4 w-4" />
|
||||||
|
{endDate ? (
|
||||||
|
<>
|
||||||
|
{startDate ? (
|
||||||
|
<>
|
||||||
|
{format(startDate, "PPP", { locale: fr})} -{" "}
|
||||||
|
{format(endDate, "PPP", { locale: fr })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Jusqu'au {format(endDate, "PPP", { locale: fr })}</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Sélectionner une date de fin</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<div className="p-3 flex gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">
|
||||||
|
Date de début <span className="text-xs text-muted-foreground">(optionnel)</span>
|
||||||
|
</label>
|
||||||
|
<div className="scale-90 origin-top-left">
|
||||||
|
<CalendarComponent
|
||||||
|
mode="single"
|
||||||
|
selected={startDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setStartDate(date);
|
||||||
|
if (date && endDate && date > endDate) {
|
||||||
|
setEndDate(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{startDate && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full text-xs"
|
||||||
|
onClick={() => setStartDate(undefined)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Date de fin</label>
|
||||||
|
<div className="scale-90 origin-top-left">
|
||||||
|
<CalendarComponent
|
||||||
|
mode="single"
|
||||||
|
selected={endDate}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date && startDate && date < startDate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setEndDate(date);
|
||||||
|
if (date && startDate) {
|
||||||
|
setIsDatePickerOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={(date) => {
|
||||||
|
if (startDate) {
|
||||||
|
return date < startDate;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{endDate && (
|
||||||
|
<div className="px-3 pb-3 text-sm text-muted-foreground">
|
||||||
|
{startDate ? (
|
||||||
|
<>
|
||||||
|
{format(startDate, "PPP", { locale: fr })} -{" "}
|
||||||
|
{format(endDate, "PPP", { locale: fr })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>Jusqu'au {format(endDate, "PPP", { locale: fr })}</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={!canReconcile || isReconciling}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||||
|
Pointer les opérations
|
||||||
|
{isReconciling && (
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground">
|
||||||
|
En cours...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Pointer toutes les opérations ?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{endDate && (
|
||||||
|
<>
|
||||||
|
Cette action va marquer toutes les opérations non pointées{" "}
|
||||||
|
{startDate ? (
|
||||||
|
<>
|
||||||
|
entre {format(startDate, "PPP", { locale: fr })} et{" "}
|
||||||
|
{format(endDate, "PPP", { locale: fr })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>jusqu'au {format(endDate, "PPP", { locale: fr })}</>
|
||||||
|
)}{" "}
|
||||||
|
comme pointées. Seules les opérations non encore pointées seront
|
||||||
|
modifiées.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={isReconciling}>
|
||||||
|
Annuler
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleReconcile}
|
||||||
|
disabled={isReconciling}
|
||||||
|
className="bg-primary hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
{isReconciling ? "Pointage..." : "Pointer"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -202,44 +202,49 @@ export function TransactionFilters({
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-3 flex gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Date de début</label>
|
<label className="text-sm font-medium">Date de début</label>
|
||||||
<CalendarComponent
|
<div className="scale-90 origin-top-left">
|
||||||
mode="single"
|
<CalendarComponent
|
||||||
selected={customStartDate}
|
mode="single"
|
||||||
onSelect={(date) => {
|
selected={customStartDate}
|
||||||
onCustomStartDateChange(date);
|
onSelect={(date) => {
|
||||||
if (date && customEndDate && date > customEndDate) {
|
onCustomStartDateChange(date);
|
||||||
onCustomEndDateChange(undefined);
|
if (date && customEndDate && date > customEndDate) {
|
||||||
}
|
onCustomEndDateChange(undefined);
|
||||||
}}
|
}
|
||||||
locale={fr}
|
}}
|
||||||
/>
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Date de fin</label>
|
<label className="text-sm font-medium">Date de fin</label>
|
||||||
<CalendarComponent
|
<div className="scale-90 origin-top-left">
|
||||||
mode="single"
|
<CalendarComponent
|
||||||
selected={customEndDate}
|
mode="single"
|
||||||
onSelect={(date) => {
|
selected={customEndDate}
|
||||||
if (date && customStartDate && date < customStartDate) {
|
onSelect={(date) => {
|
||||||
return;
|
if (date && customStartDate && date < customStartDate) {
|
||||||
}
|
return;
|
||||||
onCustomEndDateChange(date);
|
}
|
||||||
if (date && customStartDate) {
|
onCustomEndDateChange(date);
|
||||||
onCustomDatePickerOpenChange(false);
|
if (date && customStartDate) {
|
||||||
}
|
onCustomDatePickerOpenChange(false);
|
||||||
}}
|
}
|
||||||
disabled={(date) => {
|
}}
|
||||||
if (!customStartDate) return true;
|
disabled={(date) => {
|
||||||
return date < customStartDate;
|
if (!customStartDate) return true;
|
||||||
}}
|
return date < customStartDate;
|
||||||
locale={fr}
|
}}
|
||||||
/>
|
locale={fr}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{customStartDate && customEndDate && (
|
</div>
|
||||||
<div className="flex gap-2 pt-2 border-t">
|
{customStartDate && customEndDate && (
|
||||||
|
<div className="flex gap-2 pt-2 border-t px-3 pb-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -260,7 +265,6 @@ export function TransactionFilters({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -245,4 +245,32 @@ export const transactionService = {
|
|||||||
duplicatesFound: duplicatesToDelete.length,
|
duplicatesFound: duplicatesToDelete.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async reconcileByDateRange(
|
||||||
|
startDate: string | undefined,
|
||||||
|
endDate: string,
|
||||||
|
reconciled: boolean = true,
|
||||||
|
): Promise<{ updatedCount: number }> {
|
||||||
|
// Update all transactions in the date range
|
||||||
|
// If startDate is not provided, use a very old date to include all transactions up to endDate
|
||||||
|
const whereClause: {
|
||||||
|
date: { lte: string; gte?: string };
|
||||||
|
isReconciled: boolean;
|
||||||
|
} = {
|
||||||
|
date: {
|
||||||
|
lte: endDate,
|
||||||
|
...(startDate && { gte: startDate }),
|
||||||
|
},
|
||||||
|
isReconciled: !reconciled, // Only update transactions that don't already have the target state
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await prisma.transaction.updateMany({
|
||||||
|
where: whereClause,
|
||||||
|
data: {
|
||||||
|
isReconciled: reconciled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { updatedCount: result.count };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user