436 lines
14 KiB
TypeScript
436 lines
14 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { PageLayout, PageHeader } from "@/components/layout";
|
|
import {
|
|
RefreshCw,
|
|
Maximize2,
|
|
Minimize2,
|
|
Receipt,
|
|
Euro,
|
|
ChevronDown,
|
|
} from "lucide-react";
|
|
import {
|
|
TransactionFilters,
|
|
TransactionBulkActions,
|
|
TransactionTable,
|
|
TransactionPagination,
|
|
formatCurrency,
|
|
formatDate,
|
|
} from "@/components/transactions";
|
|
import { RuleCreateDialog } from "@/components/rules";
|
|
import { OFXImportDialog } from "@/components/import/ofx-import-dialog";
|
|
import { MonthlyChart } from "@/components/statistics";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from "@/components/ui/collapsible";
|
|
import { Upload } from "lucide-react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { useTransactionsPage } from "@/hooks/use-transactions-page";
|
|
import { useTransactionMutations } from "@/hooks/use-transaction-mutations";
|
|
import { useTransactionRules } from "@/hooks/use-transaction-rules";
|
|
import { useTransactionsChartData } from "@/hooks/use-transactions-chart-data";
|
|
|
|
export default function TransactionsPage() {
|
|
const queryClient = useQueryClient();
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
|
|
// Main page state and logic
|
|
const {
|
|
metadata,
|
|
isLoadingMetadata,
|
|
searchQuery,
|
|
setSearchQuery,
|
|
selectedAccounts,
|
|
onAccountsChange,
|
|
selectedCategories,
|
|
onCategoriesChange,
|
|
showReconciled,
|
|
onReconciledChange,
|
|
period,
|
|
onPeriodChange,
|
|
customStartDate,
|
|
customEndDate,
|
|
onCustomStartDateChange,
|
|
onCustomEndDateChange,
|
|
isCustomDatePickerOpen,
|
|
onCustomDatePickerOpenChange,
|
|
showDuplicates,
|
|
onShowDuplicatesChange,
|
|
page,
|
|
pageSize,
|
|
onPageChange,
|
|
sortField,
|
|
sortOrder,
|
|
onSortChange,
|
|
selectedTransactions,
|
|
onToggleSelectAll,
|
|
onToggleSelectTransaction,
|
|
clearSelection,
|
|
transactionsData,
|
|
isLoadingTransactions,
|
|
invalidateTransactions,
|
|
duplicateIds,
|
|
transactionParams,
|
|
} = useTransactionsPage();
|
|
|
|
// Transaction mutations
|
|
const {
|
|
toggleReconciled,
|
|
markReconciled,
|
|
setCategory,
|
|
deleteTransaction,
|
|
bulkReconcile: handleBulkReconcile,
|
|
bulkSetCategory: handleBulkSetCategory,
|
|
updatingTransactionIds,
|
|
} = useTransactionMutations({
|
|
transactionParams,
|
|
transactionsData,
|
|
});
|
|
|
|
// Transaction rules
|
|
const {
|
|
ruleDialogOpen,
|
|
setRuleDialogOpen,
|
|
ruleGroup,
|
|
handleCreateRule,
|
|
handleSaveRule,
|
|
} = useTransactionRules({
|
|
transactionsData,
|
|
metadata,
|
|
});
|
|
|
|
// Chart data
|
|
const {
|
|
monthlyData,
|
|
isLoading: isLoadingChart,
|
|
totalAmount: chartTotalAmount,
|
|
totalCount: chartTotalCount,
|
|
} = useTransactionsChartData({
|
|
selectedAccounts,
|
|
selectedCategories,
|
|
period,
|
|
customStartDate,
|
|
customEndDate,
|
|
showReconciled,
|
|
searchQuery,
|
|
});
|
|
|
|
const invalidateAll = useCallback(() => {
|
|
invalidateTransactions();
|
|
queryClient.invalidateQueries({ queryKey: ["banking-metadata"] });
|
|
}, [invalidateTransactions, queryClient]);
|
|
|
|
const handleBulkReconcileWithClear = useCallback(
|
|
(reconciled: boolean) => {
|
|
handleBulkReconcile(reconciled, selectedTransactions);
|
|
clearSelection();
|
|
},
|
|
[handleBulkReconcile, selectedTransactions, clearSelection]
|
|
);
|
|
|
|
const handleBulkSetCategoryWithClear = useCallback(
|
|
(categoryId: string | null) => {
|
|
handleBulkSetCategory(categoryId, selectedTransactions);
|
|
clearSelection();
|
|
},
|
|
[handleBulkSetCategory, selectedTransactions, clearSelection]
|
|
);
|
|
|
|
const filteredTransactions = transactionsData?.transactions || [];
|
|
const totalTransactions = transactionsData?.total || 0;
|
|
const hasMore = transactionsData?.hasMore || false;
|
|
const uncategorizedCount = transactionsData?.uncategorizedCount || 0;
|
|
const uncategorizedPercent =
|
|
totalTransactions > 0
|
|
? Math.round((uncategorizedCount / totalTransactions) * 100)
|
|
: 0;
|
|
|
|
// Use total from chart data (all filtered transactions) or fallback to paginated data
|
|
const totalAmount = chartTotalAmount ?? 0;
|
|
const displayTotalCount = chartTotalCount ?? totalTransactions;
|
|
|
|
// For filter comboboxes, we'll use empty arrays for now
|
|
// They can be enhanced later with separate queries if needed
|
|
const transactionsForAccountFilter: never[] = [];
|
|
const transactionsForCategoryFilter: never[] = [];
|
|
|
|
// Early return for loading state - prevents sidebar flash
|
|
if (isLoadingMetadata || !metadata) {
|
|
return (
|
|
<div className="flex h-screen bg-background">
|
|
<main className="flex-1 flex items-center justify-center">
|
|
<RefreshCw className="w-8 h-8 animate-spin text-muted-foreground" />
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageLayout>
|
|
<PageHeader
|
|
title="Transactions"
|
|
description={
|
|
totalTransactions > 0
|
|
? `${totalTransactions} transaction${totalTransactions > 1 ? "s" : ""} • ${uncategorizedPercent}% non catégorisées`
|
|
: `${totalTransactions} transaction${totalTransactions > 1 ? "s" : ""}`
|
|
}
|
|
actions={
|
|
<OFXImportDialog onImportComplete={invalidateAll}>
|
|
<Button className="md:h-10 md:px-5">
|
|
<Upload className="w-4 h-4 md:mr-2" />
|
|
<span className="hidden md:inline">Importer OFX</span>
|
|
</Button>
|
|
</OFXImportDialog>
|
|
}
|
|
/>
|
|
|
|
<TransactionFilters
|
|
searchQuery={searchQuery}
|
|
onSearchChange={setSearchQuery}
|
|
selectedAccounts={selectedAccounts}
|
|
onAccountsChange={onAccountsChange}
|
|
selectedCategories={selectedCategories}
|
|
onCategoriesChange={onCategoriesChange}
|
|
showReconciled={showReconciled}
|
|
onReconciledChange={onReconciledChange}
|
|
period={period}
|
|
onPeriodChange={onPeriodChange}
|
|
customStartDate={customStartDate}
|
|
customEndDate={customEndDate}
|
|
onCustomStartDateChange={onCustomStartDateChange}
|
|
onCustomEndDateChange={onCustomEndDateChange}
|
|
isCustomDatePickerOpen={isCustomDatePickerOpen}
|
|
onCustomDatePickerOpenChange={onCustomDatePickerOpenChange}
|
|
showDuplicates={showDuplicates}
|
|
onShowDuplicatesChange={onShowDuplicatesChange}
|
|
accounts={metadata.accounts}
|
|
folders={metadata.folders}
|
|
categories={metadata.categories}
|
|
transactionsForAccountFilter={transactionsForAccountFilter}
|
|
transactionsForCategoryFilter={transactionsForCategoryFilter}
|
|
/>
|
|
|
|
{(!isLoadingChart || !isLoadingTransactions) && (
|
|
<Card className="mb-6">
|
|
<Collapsible defaultOpen={true}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 py-3 px-6">
|
|
<CardTitle className="text-base font-semibold">
|
|
Statistiques
|
|
</CardTitle>
|
|
<CollapsibleTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8">
|
|
<ChevronDown className="w-4 h-4 mr-1" />
|
|
Réduire
|
|
</Button>
|
|
</CollapsibleTrigger>
|
|
</CardHeader>
|
|
<CollapsibleContent>
|
|
<CardContent className="pt-0">
|
|
{/* Summary cards */}
|
|
{!isLoadingTransactions && (
|
|
<div className="grid gap-4 grid-cols-2 mb-6">
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Nombre de transactions
|
|
</p>
|
|
<p className="text-2xl font-bold mt-1">
|
|
{displayTotalCount}
|
|
</p>
|
|
</div>
|
|
<Receipt className="w-8 h-8 text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="pt-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium text-muted-foreground">
|
|
Total
|
|
</p>
|
|
<p
|
|
className={`text-2xl font-bold mt-1 ${
|
|
totalAmount >= 0
|
|
? "text-emerald-600"
|
|
: "text-red-600"
|
|
}`}
|
|
>
|
|
{formatCurrency(totalAmount)}
|
|
</p>
|
|
</div>
|
|
<Euro className="w-8 h-8 text-muted-foreground" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Chart */}
|
|
{!isLoadingChart && monthlyData.length > 0 && (
|
|
<MonthlyChart
|
|
data={monthlyData}
|
|
formatCurrency={formatCurrency}
|
|
collapsible={false}
|
|
showDots={
|
|
period !== "all" &&
|
|
(period === "12months" || monthlyData.length <= 12)
|
|
}
|
|
/>
|
|
)}
|
|
</CardContent>
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
</Card>
|
|
)}
|
|
|
|
<TransactionBulkActions
|
|
selectedCount={selectedTransactions.size}
|
|
categories={metadata.categories}
|
|
onReconcile={handleBulkReconcileWithClear}
|
|
onSetCategory={handleBulkSetCategoryWithClear}
|
|
/>
|
|
|
|
{isLoadingTransactions ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<RefreshCw className="w-8 h-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex-1" />
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setIsFullscreen(true)}
|
|
className="gap-2"
|
|
>
|
|
<Maximize2 className="w-4 h-4" />
|
|
Plein écran
|
|
</Button>
|
|
</div>
|
|
|
|
<TransactionTable
|
|
transactions={filteredTransactions}
|
|
accounts={metadata.accounts}
|
|
categories={metadata.categories}
|
|
selectedTransactions={selectedTransactions}
|
|
sortField={sortField}
|
|
sortOrder={sortOrder}
|
|
onSortChange={onSortChange}
|
|
onToggleSelectAll={onToggleSelectAll}
|
|
onToggleSelectTransaction={onToggleSelectTransaction}
|
|
onToggleReconciled={toggleReconciled}
|
|
onMarkReconciled={markReconciled}
|
|
onSetCategory={setCategory}
|
|
onCreateRule={handleCreateRule}
|
|
onDelete={deleteTransaction}
|
|
formatCurrency={formatCurrency}
|
|
formatDate={formatDate}
|
|
updatingTransactionIds={updatingTransactionIds}
|
|
duplicateIds={duplicateIds}
|
|
highlightDuplicates={showDuplicates}
|
|
/>
|
|
|
|
<TransactionPagination
|
|
page={page}
|
|
pageSize={pageSize}
|
|
total={totalTransactions}
|
|
hasMore={hasMore}
|
|
onPageChange={onPageChange}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
|
|
<DialogContent
|
|
className="!max-w-none !max-h-none !w-full !h-full !p-0 flex flex-col !rounded-none !translate-x-0 !translate-y-0 !top-0 !left-0 !right-0 !bottom-0 !m-0"
|
|
showCloseButton={false}
|
|
style={{
|
|
width: "100vw",
|
|
height: "100vh",
|
|
maxWidth: "100vw",
|
|
maxHeight: "100vh",
|
|
}}
|
|
>
|
|
<DialogHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
|
<div className="flex items-center justify-between">
|
|
<DialogTitle>Transactions</DialogTitle>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setIsFullscreen(false)}
|
|
className="gap-2"
|
|
>
|
|
<Minimize2 className="w-4 h-4" />
|
|
Réduire
|
|
</Button>
|
|
</div>
|
|
</DialogHeader>
|
|
<div className="flex-1 overflow-auto px-6 pb-6 min-h-0">
|
|
<div className="mb-4">
|
|
<TransactionBulkActions
|
|
selectedCount={selectedTransactions.size}
|
|
categories={metadata.categories}
|
|
onReconcile={handleBulkReconcileWithClear}
|
|
onSetCategory={handleBulkSetCategoryWithClear}
|
|
/>
|
|
</div>
|
|
<TransactionTable
|
|
transactions={filteredTransactions}
|
|
accounts={metadata.accounts}
|
|
categories={metadata.categories}
|
|
selectedTransactions={selectedTransactions}
|
|
sortField={sortField}
|
|
sortOrder={sortOrder}
|
|
onSortChange={onSortChange}
|
|
onToggleSelectAll={onToggleSelectAll}
|
|
onToggleSelectTransaction={onToggleSelectTransaction}
|
|
onToggleReconciled={toggleReconciled}
|
|
onMarkReconciled={markReconciled}
|
|
onSetCategory={setCategory}
|
|
onCreateRule={handleCreateRule}
|
|
onDelete={deleteTransaction}
|
|
formatCurrency={formatCurrency}
|
|
formatDate={formatDate}
|
|
updatingTransactionIds={updatingTransactionIds}
|
|
duplicateIds={duplicateIds}
|
|
highlightDuplicates={showDuplicates}
|
|
/>
|
|
<div className="mt-4">
|
|
<TransactionPagination
|
|
page={page}
|
|
pageSize={pageSize}
|
|
total={totalTransactions}
|
|
hasMore={hasMore}
|
|
onPageChange={onPageChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<RuleCreateDialog
|
|
open={ruleDialogOpen}
|
|
onOpenChange={setRuleDialogOpen}
|
|
group={ruleGroup}
|
|
categories={metadata.categories}
|
|
onSave={handleSaveRule}
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
}
|