All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
261 lines
8.5 KiB
TypeScript
261 lines
8.5 KiB
TypeScript
"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>
|
|
);
|
|
}
|
|
|