+
-
{
- setCustomStartDate(date);
- if (date && customEndDate && date > customEndDate) {
- setCustomEndDate(undefined);
- }
- }}
- locale={fr}
- />
+
+ {
+ setCustomStartDate(date);
+ if (date && customEndDate && date > customEndDate) {
+ setCustomEndDate(undefined);
+ }
+ }}
+ locale={fr}
+ />
+
-
{
- if (
- date &&
- customStartDate &&
- date < customStartDate
- ) {
- return;
- }
- setCustomEndDate(date);
- if (date && customStartDate) {
- setIsCustomDatePickerOpen(false);
- }
- }}
- disabled={(date) => {
- if (!customStartDate) return true;
- return date < customStartDate;
- }}
- locale={fr}
- />
+
+ {
+ if (
+ date &&
+ customStartDate &&
+ date < customStartDate
+ ) {
+ return;
+ }
+ setCustomEndDate(date);
+ if (date && customStartDate) {
+ setIsCustomDatePickerOpen(false);
+ }
+ }}
+ disabled={(date) => {
+ if (!customStartDate) return true;
+ return date < customStartDate;
+ }}
+ locale={fr}
+ />
+
- {customStartDate && customEndDate && (
-
+
+ {customStartDate && customEndDate && (
+
)}
-
)}
diff --git a/components/settings/index.ts b/components/settings/index.ts
index cc77a68..2c58ec6 100644
--- a/components/settings/index.ts
+++ b/components/settings/index.ts
@@ -3,3 +3,4 @@ export { DangerZoneCard } from "./danger-zone-card";
export { OFXInfoCard } from "./ofx-info-card";
export { BackupCard } from "./backup-card";
export { PasswordCard } from "./password-card";
+export { ReconcileDateRangeCard } from "./reconcile-date-range-card";
diff --git a/components/settings/reconcile-date-range-card.tsx b/components/settings/reconcile-date-range-card.tsx
new file mode 100644
index 0000000..b5e6d3a
--- /dev/null
+++ b/components/settings/reconcile-date-range-card.tsx
@@ -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
(undefined);
+ const [endDate, setEndDate] = useState(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 (
+
+
+
+
+ Pointer les opérations par date
+
+
+ Marquer toutes les opérations pointées dans une plage de dates
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ setStartDate(date);
+ if (date && endDate && date > endDate) {
+ setEndDate(undefined);
+ }
+ }}
+ locale={fr}
+ />
+
+ {startDate && (
+
+ )}
+
+
+
+
+ {
+ if (date && startDate && date < startDate) {
+ return;
+ }
+ setEndDate(date);
+ if (date && startDate) {
+ setIsDatePickerOpen(false);
+ }
+ }}
+ disabled={(date) => {
+ if (startDate) {
+ return date < startDate;
+ }
+ return false;
+ }}
+ locale={fr}
+ />
+
+
+
+ {endDate && (
+
+ {startDate ? (
+ <>
+ {format(startDate, "PPP", { locale: fr })} -{" "}
+ {format(endDate, "PPP", { locale: fr })}
+ >
+ ) : (
+ <>Jusqu'au {format(endDate, "PPP", { locale: fr })}>
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Pointer toutes les opérations ?
+
+
+ {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.
+ >
+ )}
+
+
+
+
+ Annuler
+
+
+ {isReconciling ? "Pointage..." : "Pointer"}
+
+
+
+
+
+
+ );
+}
+
diff --git a/components/transactions/transaction-filters.tsx b/components/transactions/transaction-filters.tsx
index 4e2fb4b..3249cb0 100644
--- a/components/transactions/transaction-filters.tsx
+++ b/components/transactions/transaction-filters.tsx
@@ -202,44 +202,49 @@ export function TransactionFilters({
-
+
-
{
- onCustomStartDateChange(date);
- if (date && customEndDate && date > customEndDate) {
- onCustomEndDateChange(undefined);
- }
- }}
- locale={fr}
- />
+
+ {
+ onCustomStartDateChange(date);
+ if (date && customEndDate && date > customEndDate) {
+ onCustomEndDateChange(undefined);
+ }
+ }}
+ locale={fr}
+ />
+
-
{
- if (date && customStartDate && date < customStartDate) {
- return;
- }
- onCustomEndDateChange(date);
- if (date && customStartDate) {
- onCustomDatePickerOpenChange(false);
- }
- }}
- disabled={(date) => {
- if (!customStartDate) return true;
- return date < customStartDate;
- }}
- locale={fr}
- />
+
+ {
+ if (date && customStartDate && date < customStartDate) {
+ return;
+ }
+ onCustomEndDateChange(date);
+ if (date && customStartDate) {
+ onCustomDatePickerOpenChange(false);
+ }
+ }}
+ disabled={(date) => {
+ if (!customStartDate) return true;
+ return date < customStartDate;
+ }}
+ locale={fr}
+ />
+
- {customStartDate && customEndDate && (
-
+
+ {customStartDate && customEndDate && (
+
)}
-
)}
diff --git a/services/transaction.service.ts b/services/transaction.service.ts
index 327857e..2105087 100644
--- a/services/transaction.service.ts
+++ b/services/transaction.service.ts
@@ -245,4 +245,32 @@ export const transactionService = {
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 };
+ },
};