refactor: standardize quotation marks in pnpm-lock.yaml and improve code formatting across various components; enhance readability and maintain consistency in code style
This commit is contained in:
@@ -134,7 +134,11 @@ export default function AccountsPage() {
|
||||
|
||||
// Convert accountsWithStats to regular accounts for compatibility
|
||||
const accounts = accountsWithStats.map(
|
||||
({ transactionCount: _transactionCount, calculatedBalance: _calculatedBalance, ...account }) => account,
|
||||
({
|
||||
transactionCount: _transactionCount,
|
||||
calculatedBalance: _calculatedBalance,
|
||||
...account
|
||||
}) => account,
|
||||
);
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
@@ -167,7 +171,8 @@ export default function AccountsPage() {
|
||||
const balance = formData.totalBalance - formData.initialBalance;
|
||||
|
||||
// Convertir "folder-root" en null
|
||||
const folderId = formData.folderId === "folder-root" ? null : formData.folderId;
|
||||
const folderId =
|
||||
formData.folderId === "folder-root" ? null : formData.folderId;
|
||||
|
||||
const updatedAccount = {
|
||||
...editingAccount,
|
||||
@@ -384,7 +389,16 @@ export default function AccountsPage() {
|
||||
// Update cache directly
|
||||
queryClient.setQueryData(
|
||||
["accounts-with-stats"],
|
||||
(old: Array<Account & { transactionCount: number; calculatedBalance: number }> | undefined) => {
|
||||
(
|
||||
old:
|
||||
| Array<
|
||||
Account & {
|
||||
transactionCount: number;
|
||||
calculatedBalance: number;
|
||||
}
|
||||
>
|
||||
| undefined,
|
||||
) => {
|
||||
if (!old) return old;
|
||||
return old.map((a) => (a.id === accountId ? updatedAccount : a));
|
||||
},
|
||||
@@ -573,7 +587,9 @@ export default function AccountsPage() {
|
||||
account={account}
|
||||
folder={folder}
|
||||
transactionCount={getTransactionCount(account.id)}
|
||||
calculatedBalance={accountWithStats?.calculatedBalance}
|
||||
calculatedBalance={
|
||||
accountWithStats?.calculatedBalance
|
||||
}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
formatCurrency={formatCurrency}
|
||||
@@ -694,7 +710,9 @@ export default function AccountsPage() {
|
||||
account={account}
|
||||
folder={accountFolder}
|
||||
transactionCount={getTransactionCount(account.id)}
|
||||
calculatedBalance={accountWithStats?.calculatedBalance}
|
||||
calculatedBalance={
|
||||
accountWithStats?.calculatedBalance
|
||||
}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
formatCurrency={formatCurrency}
|
||||
|
||||
@@ -126,4 +126,3 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -66,4 +66,3 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function CategoriesPage() {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
||||
const [expandedParents, setExpandedParents] = useState<Set<string>>(
|
||||
new Set()
|
||||
new Set(),
|
||||
);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -55,7 +55,7 @@ export default function CategoriesPage() {
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [recatResults, setRecatResults] = useState<RecategorizationResult[]>(
|
||||
[]
|
||||
[],
|
||||
);
|
||||
const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false);
|
||||
const [isRecategorizing, setIsRecategorizing] = useState(false);
|
||||
@@ -63,7 +63,7 @@ export default function CategoriesPage() {
|
||||
// Persister l'état "tout déplier" dans le localStorage
|
||||
const [expandAllByDefault, setExpandAllByDefault] = useLocalStorage(
|
||||
"categories-expand-all-by-default",
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
// Organiser les catégories par parent
|
||||
@@ -77,7 +77,7 @@ export default function CategoriesPage() {
|
||||
};
|
||||
|
||||
const parents = metadata.categories.filter(
|
||||
(c: Category) => c.parentId === null
|
||||
(c: Category) => c.parentId === null,
|
||||
);
|
||||
const children: Record<string, Category[]> = {};
|
||||
const orphans: Category[] = [];
|
||||
@@ -86,7 +86,7 @@ export default function CategoriesPage() {
|
||||
.filter((c: Category) => c.parentId !== null)
|
||||
.forEach((child: Category) => {
|
||||
const parentExists = parents.some(
|
||||
(p: Category) => p.id === child.parentId
|
||||
(p: Category) => p.id === child.parentId,
|
||||
);
|
||||
if (parentExists) {
|
||||
if (!children[child.parentId!]) {
|
||||
@@ -110,7 +110,7 @@ export default function CategoriesPage() {
|
||||
if (parentCategories.length > 0 && expandedParents.size === 0) {
|
||||
if (expandAllByDefault) {
|
||||
setExpandedParents(
|
||||
new Set(parentCategories.map((p: Category) => p.id))
|
||||
new Set(parentCategories.map((p: Category) => p.id)),
|
||||
);
|
||||
} else {
|
||||
setExpandedParents(new Set());
|
||||
@@ -150,7 +150,7 @@ export default function CategoriesPage() {
|
||||
|
||||
return { total, count };
|
||||
},
|
||||
[categoryStats, childrenByParent]
|
||||
[categoryStats, childrenByParent],
|
||||
);
|
||||
|
||||
if (isLoadingMetadata || !metadata || isLoadingStats || !categoryStats) {
|
||||
@@ -264,7 +264,7 @@ export default function CategoriesPage() {
|
||||
try {
|
||||
// Fetch uncategorized transactions
|
||||
const uncategorizedResponse = await fetch(
|
||||
"/api/banking/transactions?limit=1000&offset=0&includeUncategorized=true"
|
||||
"/api/banking/transactions?limit=1000&offset=0&includeUncategorized=true",
|
||||
);
|
||||
if (!uncategorizedResponse.ok) {
|
||||
throw new Error("Failed to fetch uncategorized transactions");
|
||||
@@ -277,11 +277,11 @@ export default function CategoriesPage() {
|
||||
for (const transaction of uncategorized) {
|
||||
const categoryId = autoCategorize(
|
||||
transaction.description + " " + (transaction.memo || ""),
|
||||
metadata.categories
|
||||
metadata.categories,
|
||||
);
|
||||
if (categoryId) {
|
||||
const category = metadata.categories.find(
|
||||
(c: Category) => c.id === categoryId
|
||||
(c: Category) => c.id === categoryId,
|
||||
);
|
||||
if (category) {
|
||||
results.push({ transaction, category });
|
||||
@@ -315,9 +315,9 @@ export default function CategoriesPage() {
|
||||
return children.some(
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(query) ||
|
||||
c.keywords.some((k) => k.toLowerCase().includes(query))
|
||||
c.keywords.some((k) => k.toLowerCase().includes(query)),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -362,9 +362,9 @@ export default function CategoriesPage() {
|
||||
(c) =>
|
||||
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.keywords.some((k) =>
|
||||
k.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
k.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
) ||
|
||||
parent.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
parent.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
: allChildren;
|
||||
const stats = getCategoryStats(parent.id, true);
|
||||
@@ -451,7 +451,7 @@ export default function CategoriesPage() {
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{new Date(result.transaction.date).toLocaleDateString(
|
||||
"fr-FR"
|
||||
"fr-FR",
|
||||
)}
|
||||
{" • "}
|
||||
{new Intl.NumberFormat("fr-FR", {
|
||||
|
||||
@@ -36,10 +36,8 @@ export default function RulesPage() {
|
||||
const { data: metadata, isLoading: isLoadingMetadata } = useBankingMetadata();
|
||||
|
||||
// Fetch uncategorized transactions only
|
||||
const {
|
||||
data: transactionsData,
|
||||
isLoading: isLoadingTransactions,
|
||||
} = useTransactions(
|
||||
const { data: transactionsData, isLoading: isLoadingTransactions } =
|
||||
useTransactions(
|
||||
{
|
||||
limit: 10000, // Large limit to get all uncategorized
|
||||
offset: 0,
|
||||
|
||||
@@ -59,15 +59,15 @@ export default function StatisticsPage() {
|
||||
// Persister les filtres dans le localStorage
|
||||
const [period, setPeriod] = useLocalStorage<Period>(
|
||||
"statistics-period",
|
||||
"6months"
|
||||
"6months",
|
||||
);
|
||||
const [selectedAccounts, setSelectedAccounts] = useLocalStorage<string[]>(
|
||||
"statistics-selected-accounts",
|
||||
["all"]
|
||||
["all"],
|
||||
);
|
||||
const [selectedCategories, setSelectedCategories] = useLocalStorage<string[]>(
|
||||
"statistics-selected-categories",
|
||||
["all"]
|
||||
["all"],
|
||||
);
|
||||
const [excludeInternalTransfers, setExcludeInternalTransfers] =
|
||||
useLocalStorage("statistics-exclude-internal-transfers", true);
|
||||
@@ -83,11 +83,11 @@ export default function StatisticsPage() {
|
||||
// Convertir les ISO strings en Date
|
||||
const customStartDate = useMemo(
|
||||
() => (customStartDateISO ? new Date(customStartDateISO) : undefined),
|
||||
[customStartDateISO]
|
||||
[customStartDateISO],
|
||||
);
|
||||
const customEndDate = useMemo(
|
||||
() => (customEndDateISO ? new Date(customEndDateISO) : undefined),
|
||||
[customEndDateISO]
|
||||
[customEndDateISO],
|
||||
);
|
||||
|
||||
// Fonctions pour mettre à jour les dates avec persistance
|
||||
@@ -106,7 +106,13 @@ export default function StatisticsPage() {
|
||||
setCustomStartDateISO(null);
|
||||
setCustomEndDateISO(null);
|
||||
}
|
||||
}, [period, customStartDateISO, customEndDateISO, setCustomStartDateISO, setCustomEndDateISO]);
|
||||
}, [
|
||||
period,
|
||||
customStartDateISO,
|
||||
customEndDateISO,
|
||||
setCustomStartDateISO,
|
||||
setCustomEndDateISO,
|
||||
]);
|
||||
|
||||
// Get start date based on period
|
||||
const startDate = useMemo(() => {
|
||||
@@ -1031,7 +1037,11 @@ export default function StatisticsPage() {
|
||||
selected={customStartDate}
|
||||
onSelect={(date) => {
|
||||
setCustomStartDate(date);
|
||||
if (date && customEndDate && date > customEndDate) {
|
||||
if (
|
||||
date &&
|
||||
customEndDate &&
|
||||
date > customEndDate
|
||||
) {
|
||||
setCustomEndDate(undefined);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function TransactionsPage() {
|
||||
handleBulkReconcile(reconciled, selectedTransactions);
|
||||
clearSelection();
|
||||
},
|
||||
[handleBulkReconcile, selectedTransactions, clearSelection]
|
||||
[handleBulkReconcile, selectedTransactions, clearSelection],
|
||||
);
|
||||
|
||||
const handleBulkSetCategoryWithClear = useCallback(
|
||||
@@ -164,13 +164,13 @@ export default function TransactionsPage() {
|
||||
handleBulkSetCategory(categoryId, selectedTransactions);
|
||||
clearSelection();
|
||||
},
|
||||
[handleBulkSetCategory, selectedTransactions, clearSelection]
|
||||
[handleBulkSetCategory, selectedTransactions, clearSelection],
|
||||
);
|
||||
|
||||
// Stabilize transactions reference to prevent unnecessary re-renders
|
||||
const filteredTransactions = useMemo(
|
||||
() => transactionsData?.transactions || [],
|
||||
[transactionsData?.transactions]
|
||||
[transactionsData?.transactions],
|
||||
);
|
||||
const totalTransactions = transactionsData?.total || 0;
|
||||
const hasMore = transactionsData?.hasMore || false;
|
||||
@@ -188,7 +188,7 @@ export default function TransactionsPage() {
|
||||
const reconciledPercent = useMemo(() => {
|
||||
if (chartTransactions.length === 0) return "0.00";
|
||||
const reconciledCount = chartTransactions.filter(
|
||||
(t) => t.isReconciled
|
||||
(t) => t.isReconciled,
|
||||
).length;
|
||||
return ((reconciledCount / chartTransactions.length) * 100).toFixed(2);
|
||||
}, [chartTransactions]);
|
||||
@@ -196,7 +196,7 @@ export default function TransactionsPage() {
|
||||
const categorizedPercent = useMemo(() => {
|
||||
if (chartTransactions.length === 0) return "0.00";
|
||||
const categorizedCount = chartTransactions.filter(
|
||||
(t) => t.categoryId !== null
|
||||
(t) => t.categoryId !== null,
|
||||
).length;
|
||||
return ((categorizedCount / chartTransactions.length) * 100).toFixed(2);
|
||||
}, [chartTransactions]);
|
||||
@@ -204,7 +204,7 @@ export default function TransactionsPage() {
|
||||
// Persist statistics collapsed state in localStorage
|
||||
const [isStatsExpanded, setIsStatsExpanded] = useLocalStorage(
|
||||
"transactions-stats-expanded",
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
// Early return for loading state - prevents sidebar flash
|
||||
|
||||
@@ -55,7 +55,8 @@ export function AccountCard({
|
||||
const isMobile = useIsMobile();
|
||||
const Icon = accountTypeIcons[account.type];
|
||||
const realBalance = getAccountBalance(account);
|
||||
const hasBalanceDifference = calculatedBalance !== undefined &&
|
||||
const hasBalanceDifference =
|
||||
calculatedBalance !== undefined &&
|
||||
Math.abs(account.balance - calculatedBalance) > 0.01;
|
||||
|
||||
const {
|
||||
@@ -200,7 +201,8 @@ export function AccountCard({
|
||||
</span>
|
||||
{hasBalanceDifference && (
|
||||
<span className="text-xs text-destructive font-semibold">
|
||||
(diff: {formatCurrency(account.balance - calculatedBalance)})
|
||||
(diff: {formatCurrency(account.balance - calculatedBalance)}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -137,7 +137,8 @@ export function AccountEditDialog({
|
||||
placeholder="0.00"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Solde de départ pour équilibrer le compte. Le balance sera calculé automatiquement (solde total - solde initial).
|
||||
Solde de départ pour équilibrer le compte. Le balance sera calculé
|
||||
automatiquement (solde total - solde initial).
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -54,7 +54,11 @@ export function AccountMergeSelectDialog({
|
||||
}
|
||||
|
||||
const handleMerge = async () => {
|
||||
if (!sourceAccountId || !targetAccountId || sourceAccountId === targetAccountId) {
|
||||
if (
|
||||
!sourceAccountId ||
|
||||
!targetAccountId ||
|
||||
sourceAccountId === targetAccountId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,13 +109,20 @@ export function AccountMergeSelectDialog({
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>Compte à conserver (destination)</Label>
|
||||
<RadioGroup value={targetAccountId} onValueChange={setTargetAccountId}>
|
||||
<RadioGroup
|
||||
value={targetAccountId}
|
||||
onValueChange={setTargetAccountId}
|
||||
>
|
||||
{selectedAccounts.map((account) => (
|
||||
<div
|
||||
key={account.id}
|
||||
className="flex items-start space-x-3 rounded-lg border p-3 hover:bg-accent"
|
||||
>
|
||||
<RadioGroupItem value={account.id} id={account.id} className="mt-1" />
|
||||
<RadioGroupItem
|
||||
value={account.id}
|
||||
id={account.id}
|
||||
className="mt-1"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={account.id}
|
||||
className="flex-1 cursor-pointer space-y-1"
|
||||
@@ -120,7 +131,9 @@ export function AccountMergeSelectDialog({
|
||||
<div className="text-xs text-muted-foreground space-y-0.5">
|
||||
<div>Numéro: {account.accountNumber}</div>
|
||||
<div>Bank ID: {account.bankId}</div>
|
||||
<div>Solde: {formatCurrency(getAccountBalance(account))}</div>
|
||||
<div>
|
||||
Solde: {formatCurrency(getAccountBalance(account))}
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -133,9 +146,9 @@ export function AccountMergeSelectDialog({
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>Attention :</strong> Toutes les transactions du compte "
|
||||
{sourceAccount.name}" seront déplacées vers "{targetAccount.name}". Le
|
||||
compte "{sourceAccount.name}" sera supprimé après la fusion. Cette
|
||||
action est irréversible.
|
||||
{sourceAccount.name}" seront déplacées vers "
|
||||
{targetAccount.name}". Le compte "{sourceAccount.name}" sera
|
||||
supprimé après la fusion. Cette action est irréversible.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -153,4 +166,3 @@ export function AccountMergeSelectDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,9 +130,7 @@ export function AccountsSummary({ data }: AccountsSummaryProps) {
|
||||
<span
|
||||
className={cn(
|
||||
"font-bold tabular-nums text-base",
|
||||
realBalance >= 0
|
||||
? "text-success"
|
||||
: "text-destructive",
|
||||
realBalance >= 0 ? "text-success" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(realBalance)}
|
||||
|
||||
@@ -19,7 +19,7 @@ interface OverviewCardsProps {
|
||||
export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
const totalBalance = data.accounts.reduce(
|
||||
(sum, acc) => sum + getAccountBalance(acc),
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
const thisMonth = new Date();
|
||||
@@ -27,7 +27,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
const thisMonthStr = thisMonth.toISOString().slice(0, 7);
|
||||
|
||||
const monthTransactions = data.transactions.filter((t) =>
|
||||
t.date.startsWith(thisMonthStr)
|
||||
t.date.startsWith(thisMonthStr),
|
||||
);
|
||||
|
||||
const income = monthTransactions
|
||||
@@ -44,7 +44,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
total > 0 ? ((reconciled / total) * 100).toFixed(2) : "0.00";
|
||||
|
||||
const categorized = data.transactions.filter(
|
||||
(t) => t.categoryId !== null
|
||||
(t) => t.categoryId !== null,
|
||||
).length;
|
||||
const categorizedPercent =
|
||||
total > 0 ? ((categorized / total) * 100).toFixed(2) : "0.00";
|
||||
@@ -61,7 +61,10 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<Card className="stat-card-gradient-1 card-hover group relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<Wallet className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-primary" strokeWidth={1} />
|
||||
<Wallet
|
||||
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-primary"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
||||
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
||||
@@ -72,9 +75,7 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<div
|
||||
className={cn(
|
||||
"text-2xl sm:text-3xl md:text-3xl lg:text-2xl xl:text-3xl font-black tracking-tight mb-3 leading-tight break-words",
|
||||
totalBalance >= 0
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
totalBalance >= 0 ? "text-success" : "text-destructive",
|
||||
)}
|
||||
>
|
||||
{formatCurrency(totalBalance)}
|
||||
@@ -88,7 +89,10 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<Card className="stat-card-gradient-2 card-hover group relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<TrendingUp className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-success" strokeWidth={1} />
|
||||
<TrendingUp
|
||||
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-success"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
||||
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
||||
@@ -111,7 +115,10 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<Card className="stat-card-gradient-3 card-hover group relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<TrendingDown className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-destructive" strokeWidth={1} />
|
||||
<TrendingDown
|
||||
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-destructive"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
||||
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
||||
@@ -134,7 +141,10 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<Card className="stat-card-gradient-4 card-hover group relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<CreditCard className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-4" strokeWidth={1} />
|
||||
<CreditCard
|
||||
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-4"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
||||
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
||||
@@ -154,7 +164,10 @@ export function OverviewCards({ data }: OverviewCardsProps) {
|
||||
<Card className="stat-card-gradient-5 card-hover group relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<Tag className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-5" strokeWidth={1} />
|
||||
<Tag
|
||||
className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28 text-chart-5"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3 px-5 pt-5 sm:px-6 sm:pt-6 relative z-10">
|
||||
<CardTitle className="text-[10px] sm:text-xs font-semibold text-muted-foreground/80 leading-tight uppercase tracking-wider flex-1 min-w-0">
|
||||
|
||||
@@ -89,7 +89,7 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
|
||||
"font-black tabular-nums text-sm md:text-base shrink-0 md:hidden",
|
||||
transaction.amount >= 0
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
: "text-destructive",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
@@ -131,7 +131,7 @@ export function RecentTransactions({ data }: RecentTransactionsProps) {
|
||||
"font-black tabular-nums text-base md:text-lg shrink-0 hidden md:block leading-tight",
|
||||
transaction.amount >= 0
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
: "text-destructive",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
|
||||
@@ -93,7 +93,7 @@ function SidebarContent({
|
||||
"w-full justify-start gap-3 h-12 rounded-2xl px-3",
|
||||
isActive &&
|
||||
"bg-gradient-to-r from-primary/15 via-primary/10 to-primary/5 border-2 border-primary/30 shadow-lg shadow-primary/10 backdrop-blur-sm",
|
||||
collapsed && "justify-center px-2 w-12 mx-auto"
|
||||
collapsed && "justify-center px-2 w-12 mx-auto",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
@@ -103,7 +103,7 @@ function SidebarContent({
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold text-sm",
|
||||
isActive && "text-primary font-bold"
|
||||
isActive && "text-primary font-bold",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
@@ -118,7 +118,7 @@ function SidebarContent({
|
||||
<div
|
||||
className={cn(
|
||||
"border-t border-border/30 pt-2",
|
||||
collapsed ? "p-2" : "p-4"
|
||||
collapsed ? "p-2" : "p-4",
|
||||
)}
|
||||
>
|
||||
<Link href="/settings" onClick={handleLinkClick} className="block mb-2">
|
||||
@@ -126,7 +126,7 @@ function SidebarContent({
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"w-full justify-start gap-3 h-12 rounded-2xl px-3",
|
||||
collapsed && "justify-center px-2 w-12 mx-auto"
|
||||
collapsed && "justify-center px-2 w-12 mx-auto",
|
||||
)}
|
||||
>
|
||||
<Settings className="w-5 h-5 shrink-0" />
|
||||
@@ -141,7 +141,7 @@ function SidebarContent({
|
||||
className={cn(
|
||||
"w-full justify-start gap-3 h-12 rounded-2xl px-3 mb-2",
|
||||
"text-destructive",
|
||||
collapsed && "justify-center px-2 w-12 mx-auto"
|
||||
collapsed && "justify-center px-2 w-12 mx-auto",
|
||||
)}
|
||||
>
|
||||
<LogOut className="w-5 h-5 shrink-0" />
|
||||
@@ -187,13 +187,13 @@ export function Sidebar({ open, onOpenChange }: SidebarProps) {
|
||||
className={cn(
|
||||
"hidden md:flex flex-col h-screen bg-sidebar text-sidebar-foreground border-r border-sidebar-border",
|
||||
"backdrop-blur-xl",
|
||||
collapsed ? "w-16" : "w-64"
|
||||
collapsed ? "w-16" : "w-64",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center border-b border-border/30",
|
||||
collapsed ? "justify-center p-4" : "justify-between p-6"
|
||||
collapsed ? "justify-center p-4" : "justify-between p-6",
|
||||
)}
|
||||
>
|
||||
{!collapsed && (
|
||||
|
||||
@@ -12,7 +12,7 @@ export function BackgroundProvider() {
|
||||
const applyBackground = () => {
|
||||
try {
|
||||
const pageBackground = document.querySelector(
|
||||
".page-background"
|
||||
".page-background",
|
||||
) as HTMLElement;
|
||||
if (!pageBackground) return;
|
||||
|
||||
@@ -30,7 +30,7 @@ export function BackgroundProvider() {
|
||||
"bg-gradient-orange",
|
||||
"bg-solid-light",
|
||||
"bg-solid-dark",
|
||||
"bg-custom-image"
|
||||
"bg-custom-image",
|
||||
);
|
||||
|
||||
const root = document.documentElement;
|
||||
@@ -39,7 +39,7 @@ export function BackgroundProvider() {
|
||||
pageBackground.classList.add("bg-custom-image");
|
||||
root.style.setProperty(
|
||||
"--custom-background-image",
|
||||
`url(${settings.customImageUrl})`
|
||||
`url(${settings.customImageUrl})`,
|
||||
);
|
||||
} else {
|
||||
pageBackground.classList.add(`bg-${settings.type || "default"}`);
|
||||
|
||||
@@ -40,43 +40,50 @@ const DEFAULT_BACKGROUNDS: Array<{
|
||||
{
|
||||
value: "default",
|
||||
label: "Neutre",
|
||||
preview: "linear-gradient(135deg, oklch(0.985 0 0) 0%, oklch(0.97 0.005 260) 50%, oklch(0.985 0 0) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(0.985 0 0) 0%, oklch(0.97 0.005 260) 50%, oklch(0.985 0 0) 100%)",
|
||||
description: "Fond neutre et élégant",
|
||||
},
|
||||
{
|
||||
value: "gradient-blue",
|
||||
label: "Océan",
|
||||
preview: "linear-gradient(135deg, oklch(0.95 0.03 230) 0%, oklch(0.88 0.08 225) 50%, oklch(0.78 0.12 220) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(0.95 0.03 230) 0%, oklch(0.88 0.08 225) 50%, oklch(0.78 0.12 220) 100%)",
|
||||
description: "Dégradé bleu apaisant",
|
||||
},
|
||||
{
|
||||
value: "gradient-purple",
|
||||
label: "Améthyste",
|
||||
preview: "linear-gradient(135deg, oklch(0.95 0.04 300) 0%, oklch(0.88 0.1 295) 50%, oklch(0.78 0.15 290) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(0.95 0.04 300) 0%, oklch(0.88 0.1 295) 50%, oklch(0.78 0.15 290) 100%)",
|
||||
description: "Dégradé violet sophistiqué",
|
||||
},
|
||||
{
|
||||
value: "gradient-green",
|
||||
label: "Forêt",
|
||||
preview: "linear-gradient(135deg, oklch(0.95 0.04 160) 0%, oklch(0.88 0.1 155) 50%, oklch(0.78 0.14 150) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(0.95 0.04 160) 0%, oklch(0.88 0.1 155) 50%, oklch(0.78 0.14 150) 100%)",
|
||||
description: "Dégradé vert naturel",
|
||||
},
|
||||
{
|
||||
value: "gradient-orange",
|
||||
label: "Aurore",
|
||||
preview: "linear-gradient(135deg, oklch(0.97 0.03 80) 0%, oklch(0.92 0.08 60) 50%, oklch(0.85 0.14 45) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(0.97 0.03 80) 0%, oklch(0.92 0.08 60) 50%, oklch(0.85 0.14 45) 100%)",
|
||||
description: "Dégradé orange chaleureux",
|
||||
},
|
||||
{
|
||||
value: "solid-light",
|
||||
label: "Lumineux",
|
||||
preview: "linear-gradient(135deg, oklch(1 0 0) 0%, oklch(0.98 0.005 260) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(1 0 0) 0%, oklch(0.98 0.005 260) 100%)",
|
||||
description: "Fond blanc épuré",
|
||||
},
|
||||
{
|
||||
value: "solid-dark",
|
||||
label: "Minuit",
|
||||
preview: "linear-gradient(135deg, oklch(0.18 0.02 260) 0%, oklch(0.08 0.015 250) 100%)",
|
||||
preview:
|
||||
"linear-gradient(135deg, oklch(0.18 0.02 260) 0%, oklch(0.08 0.015 250) 100%)",
|
||||
description: "Fond sombre immersif",
|
||||
},
|
||||
];
|
||||
@@ -89,14 +96,14 @@ export function BackgroundCard() {
|
||||
|
||||
const currentSettings = useMemo<BackgroundSettings>(
|
||||
() => backgroundSettings || { type: "default" },
|
||||
[backgroundSettings]
|
||||
[backgroundSettings],
|
||||
);
|
||||
|
||||
const [customImageUrl, setCustomImageUrl] = useState(
|
||||
currentSettings.customImageUrl || ""
|
||||
currentSettings.customImageUrl || "",
|
||||
);
|
||||
const [showCustomInput, setShowCustomInput] = useState(
|
||||
currentSettings.type === "custom-image"
|
||||
currentSettings.type === "custom-image",
|
||||
);
|
||||
|
||||
// Synchroniser customImageUrl avec les settings
|
||||
@@ -112,7 +119,7 @@ export function BackgroundCard() {
|
||||
const applyBackground = (settings: BackgroundSettings) => {
|
||||
const root = document.documentElement;
|
||||
const pageBackground = document.querySelector(
|
||||
".page-background"
|
||||
".page-background",
|
||||
) as HTMLElement;
|
||||
|
||||
if (!pageBackground) return;
|
||||
@@ -126,14 +133,14 @@ export function BackgroundCard() {
|
||||
"bg-gradient-orange",
|
||||
"bg-solid-light",
|
||||
"bg-solid-dark",
|
||||
"bg-custom-image"
|
||||
"bg-custom-image",
|
||||
);
|
||||
|
||||
if (settings.type === "custom-image" && settings.customImageUrl) {
|
||||
pageBackground.classList.add("bg-custom-image");
|
||||
root.style.setProperty(
|
||||
"--custom-background-image",
|
||||
`url(${settings.customImageUrl})`
|
||||
`url(${settings.customImageUrl})`,
|
||||
);
|
||||
} else {
|
||||
pageBackground.classList.add(`bg-${settings.type || "default"}`);
|
||||
@@ -142,7 +149,7 @@ export function BackgroundCard() {
|
||||
|
||||
// Déclencher un événement personnalisé pour notifier les autres composants
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("background-changed", { detail: settings })
|
||||
new CustomEvent("background-changed", { detail: settings }),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -222,7 +229,7 @@ export function BackgroundCard() {
|
||||
"relative flex flex-col items-center justify-center p-4 rounded-lg border-2 cursor-pointer transition-all",
|
||||
currentSettings.type === bg.value
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
: "border-border hover:border-primary/50",
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
@@ -245,7 +252,7 @@ export function BackgroundCard() {
|
||||
"relative flex flex-col items-center justify-center p-4 rounded-lg border-2 cursor-pointer transition-all",
|
||||
currentSettings.type === "custom-image"
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
: "border-border hover:border-primary/50",
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
|
||||
@@ -46,7 +46,7 @@ export function DangerZoneCard({
|
||||
const result = await onDeduplicate();
|
||||
if (result.deletedCount > 0) {
|
||||
alert(
|
||||
`${result.deletedCount} transaction${result.deletedCount > 1 ? "s" : ""} en double supprimée${result.deletedCount > 1 ? "s" : ""}`
|
||||
`${result.deletedCount} transaction${result.deletedCount > 1 ? "s" : ""} en double supprimée${result.deletedCount > 1 ? "s" : ""}`,
|
||||
);
|
||||
} else {
|
||||
alert("Aucun doublon trouvé");
|
||||
|
||||
@@ -45,7 +45,8 @@ export function ReconcileDateRangeCard() {
|
||||
setIsReconciling(true);
|
||||
try {
|
||||
const endDateStr = format(endDate, "yyyy-MM-dd");
|
||||
const body: { endDate: string; startDate?: string; reconciled: boolean } = {
|
||||
const body: { endDate: string; startDate?: string; reconciled: boolean } =
|
||||
{
|
||||
endDate: endDateStr,
|
||||
reconciled: true,
|
||||
};
|
||||
@@ -134,7 +135,10 @@ export function ReconcileDateRangeCard() {
|
||||
<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>
|
||||
Date de début{" "}
|
||||
<span className="text-xs text-muted-foreground">
|
||||
(optionnel)
|
||||
</span>
|
||||
</label>
|
||||
<div className="scale-90 origin-top-left">
|
||||
<CalendarComponent
|
||||
@@ -233,8 +237,8 @@ export function ReconcileDateRangeCard() {
|
||||
) : (
|
||||
<>jusqu'au {format(endDate, "PPP", { locale: fr })}</>
|
||||
)}{" "}
|
||||
comme pointées. Seules les opérations non encore pointées seront
|
||||
modifiées.
|
||||
comme pointées. Seules les opérations non encore pointées
|
||||
seront modifiées.
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
@@ -257,4 +261,3 @@ export function ReconcileDateRangeCard() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export function ThemeCard() {
|
||||
"relative flex flex-col items-center justify-center p-4 rounded-lg border-2 cursor-pointer transition-all",
|
||||
currentTheme === themeOption.value
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/50"
|
||||
: "border-border hover:border-primary/50",
|
||||
)}
|
||||
>
|
||||
<RadioGroupItem
|
||||
|
||||
@@ -54,7 +54,7 @@ export function MonthlyChart({
|
||||
// Formater les labels de manière plus compacte
|
||||
const formatMonthLabel = (month: string) => {
|
||||
// Format: "janv. 24" -> "janv 24" (enlever le point)
|
||||
return month.replace('.', '');
|
||||
return month.replace(".", "");
|
||||
};
|
||||
|
||||
const chartContent = (
|
||||
@@ -62,7 +62,15 @@ export function MonthlyChart({
|
||||
{data.length > 0 ? (
|
||||
<div className="h-[400px] sm:h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ left: 0, right: 10, top: 10, bottom: data.length > 6 ? 80 : 60 }}>
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: data.length > 6 ? 80 : 60,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
|
||||
@@ -24,7 +24,10 @@ export function StatsSummaryCards({
|
||||
<Card className="stat-card-textured relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<TrendingUp className="h-16 w-16 md:h-20 md:w-20 text-success" strokeWidth={1} />
|
||||
<TrendingUp
|
||||
className="h-16 w-16 md:h-20 md:w-20 text-success"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
|
||||
<CardTitle className="text-[10px] md:text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
||||
@@ -41,7 +44,10 @@ export function StatsSummaryCards({
|
||||
<Card className="stat-card-textured relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<TrendingDown className="h-16 w-16 md:h-20 md:w-20 text-destructive" strokeWidth={1} />
|
||||
<TrendingDown
|
||||
className="h-16 w-16 md:h-20 md:w-20 text-destructive"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
|
||||
<CardTitle className="text-[10px] md:text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
||||
@@ -58,7 +64,10 @@ export function StatsSummaryCards({
|
||||
<Card className="stat-card-textured relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<ArrowRight className="h-16 w-16 md:h-20 md:w-20 text-muted-foreground/40" strokeWidth={1} />
|
||||
<ArrowRight
|
||||
className="h-16 w-16 md:h-20 md:w-20 text-muted-foreground/40"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
|
||||
<CardTitle className="text-[10px] md:text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
||||
@@ -75,12 +84,12 @@ export function StatsSummaryCards({
|
||||
<Card className="stat-card-textured relative overflow-hidden">
|
||||
{/* Icône en arrière-plan */}
|
||||
<div className="absolute bottom-2 right-2 opacity-[0.04] z-0 pointer-events-none">
|
||||
<div className={cn(
|
||||
<div
|
||||
className={cn(
|
||||
"h-16 w-16 md:h-20 md:w-20 rounded-full border-2",
|
||||
savings >= 0
|
||||
? "border-success"
|
||||
: "border-destructive"
|
||||
)} />
|
||||
savings >= 0 ? "border-success" : "border-destructive",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pb-3 px-5 pt-5 relative z-10">
|
||||
<CardTitle className="text-[10px] md:text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
||||
|
||||
@@ -4,12 +4,7 @@ import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { CategoryIcon } from "@/components/ui/category-icon";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
TabsContent,
|
||||
} from "@/components/ui/tabs";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import type { Transaction, Category } from "@/lib/types";
|
||||
|
||||
@@ -111,7 +106,8 @@ export function TopExpensesList({
|
||||
"fr-FR",
|
||||
)}
|
||||
</span>
|
||||
{expense.categoryId && (() => {
|
||||
{expense.categoryId &&
|
||||
(() => {
|
||||
const expenseCategory = categories.find(
|
||||
(c) => c.id === expense.categoryId,
|
||||
);
|
||||
|
||||
@@ -169,7 +169,7 @@ export function TransactionTable({
|
||||
setFocusedIndex(index);
|
||||
onMarkReconciled(transactionId);
|
||||
},
|
||||
[onMarkReconciled]
|
||||
[onMarkReconciled],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
@@ -198,7 +198,7 @@ export function TransactionTable({
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusedIndex, transactions, onMarkReconciled, virtualizer]
|
||||
[focusedIndex, transactions, onMarkReconciled, virtualizer],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -215,7 +215,7 @@ export function TransactionTable({
|
||||
(accountId: string) => {
|
||||
return accounts.find((a) => a.id === accountId);
|
||||
},
|
||||
[accounts]
|
||||
[accounts],
|
||||
);
|
||||
|
||||
const getCategory = useCallback(
|
||||
@@ -223,7 +223,7 @@ export function TransactionTable({
|
||||
if (!categoryId) return null;
|
||||
return categories.find((c) => c.id === categoryId);
|
||||
},
|
||||
[categories]
|
||||
[categories],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -281,7 +281,7 @@ export function TransactionTable({
|
||||
"p-4 md:p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
|
||||
transaction.isReconciled && "bg-emerald-500/5",
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||
isDuplicate && "shadow-sm"
|
||||
isDuplicate && "shadow-sm",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -324,7 +324,7 @@ export function TransactionTable({
|
||||
"font-semibold tabular-nums text-base md:text-base shrink-0",
|
||||
transaction.amount >= 0
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
: "text-destructive",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
@@ -359,7 +359,7 @@ export function TransactionTable({
|
||||
showBadge
|
||||
align="start"
|
||||
disabled={updatingTransactionIds.has(
|
||||
transaction.id
|
||||
transaction.id,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -392,7 +392,7 @@ export function TransactionTable({
|
||||
e.stopPropagation();
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
|
||||
)
|
||||
) {
|
||||
onDelete(transaction.id);
|
||||
@@ -508,7 +508,7 @@ export function TransactionTable({
|
||||
"grid grid-cols-[auto_120px_2fr_150px_180px_140px_auto_auto] gap-0 border-b border-border hover:bg-muted/50 cursor-pointer",
|
||||
transaction.isReconciled && "bg-emerald-500/5",
|
||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||
isDuplicate && "shadow-sm"
|
||||
isDuplicate && "shadow-sm",
|
||||
)}
|
||||
>
|
||||
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||
@@ -577,7 +577,7 @@ export function TransactionTable({
|
||||
"p-3 text-right font-semibold tabular-nums",
|
||||
transaction.amount >= 0
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
: "text-destructive",
|
||||
)}
|
||||
>
|
||||
{transaction.amount >= 0 ? "+" : ""}
|
||||
@@ -644,7 +644,7 @@ export function TransactionTable({
|
||||
e.stopPropagation();
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`
|
||||
`Êtes-vous sûr de vouloir supprimer cette transaction ?\n\n${transaction.description}\n${formatCurrency(transaction.amount)}`,
|
||||
)
|
||||
) {
|
||||
onDelete(transaction.id);
|
||||
|
||||
@@ -22,7 +22,7 @@ const badgeVariants = cva(
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
|
||||
@@ -33,7 +33,7 @@ const buttonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
|
||||
@@ -99,10 +99,7 @@ export function CategoryFilterCombobox({
|
||||
if (isAll || isUncategorized) {
|
||||
// Start fresh with this category and its children (if parent)
|
||||
if (isParentCategory && childCategories.length > 0) {
|
||||
newSelection = [
|
||||
newValue,
|
||||
...childCategories.map((child) => child.id),
|
||||
];
|
||||
newSelection = [newValue, ...childCategories.map((child) => child.id)];
|
||||
} else {
|
||||
newSelection = [newValue];
|
||||
}
|
||||
@@ -111,7 +108,7 @@ export function CategoryFilterCombobox({
|
||||
if (isParentCategory && childCategories.length > 0) {
|
||||
const childIds = childCategories.map((child) => child.id);
|
||||
newSelection = value.filter(
|
||||
(v) => v !== newValue && !childIds.includes(v)
|
||||
(v) => v !== newValue && !childIds.includes(v),
|
||||
);
|
||||
} else {
|
||||
newSelection = value.filter((v) => v !== newValue);
|
||||
|
||||
@@ -15,7 +15,7 @@ function Checkbox({
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input bg-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/30 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -39,7 +39,7 @@ function DialogOverlay({
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -61,7 +61,7 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 flex flex-col w-full max-w-[calc(100%-2rem)] max-h-[calc(100vh-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 overflow-y-auto sm:max-w-lg",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -96,7 +96,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -16,7 +16,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
"focus-visible:border-primary/50 focus-visible:ring-primary/20 focus-visible:ring-[3px] focus-visible:shadow-md focus-visible:shadow-primary/10",
|
||||
"hover:border-primary/30 hover:shadow-sm",
|
||||
"aria-invalid:ring-destructive/30 aria-invalid:border-destructive",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||
"bg-muted w-fit text-muted-foreground pointer-events-none inline-flex h-5 min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
||||
"[&_svg:not([class*='size-'])]:size-3",
|
||||
"[[data-slot=tooltip-content]_&]:bg-background/15 [[data-slot=tooltip-content]_&]:text-background",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useState } from "react";
|
||||
*/
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T
|
||||
initialValue: T,
|
||||
): [T, (value: T | ((val: T) => T)) => void] {
|
||||
// État pour stocker la valeur
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
@@ -42,4 +42,3 @@ export function useLocalStorage<T>(
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
||||
|
||||
@@ -146,13 +146,13 @@ export function useTransactionsBalanceChart({
|
||||
// Fetch transactions before startDate for initial balance calculation
|
||||
const { data: beforeStartDateData } = useTransactions(
|
||||
beforeStartDateParams,
|
||||
!!metadata && period !== "all" && !!startDate
|
||||
!!metadata && period !== "all" && !!startDate,
|
||||
);
|
||||
|
||||
// Fetch all filtered transactions for chart
|
||||
const { data: transactionsData, isLoading } = useTransactions(
|
||||
chartParams,
|
||||
!!metadata
|
||||
!!metadata,
|
||||
);
|
||||
|
||||
// Calculate balance chart data
|
||||
@@ -169,7 +169,7 @@ export function useTransactionsBalanceChart({
|
||||
|
||||
// Sort transactions by date
|
||||
const sortedTransactions = [...transactions].sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||
);
|
||||
|
||||
// Calculate starting balance: initialBalance + transactions before startDate
|
||||
@@ -181,7 +181,7 @@ export function useTransactionsBalanceChart({
|
||||
// Start with initial balances
|
||||
runningBalance = accountsToUse.reduce(
|
||||
(sum: number, acc: Account) => sum + (acc.initialBalance || 0),
|
||||
0
|
||||
0,
|
||||
);
|
||||
|
||||
// Add transactions before startDate if we have them
|
||||
@@ -190,7 +190,7 @@ export function useTransactionsBalanceChart({
|
||||
(t) => {
|
||||
const transactionDate = new Date(t.date);
|
||||
return transactionDate < startDate;
|
||||
}
|
||||
},
|
||||
);
|
||||
beforeStartTransactions.forEach((t) => {
|
||||
runningBalance += t.amount;
|
||||
@@ -206,7 +206,7 @@ export function useTransactionsBalanceChart({
|
||||
});
|
||||
|
||||
const aggregatedBalanceData: BalanceChartDataPoint[] = Array.from(
|
||||
aggregatedBalanceByDate.entries()
|
||||
aggregatedBalanceByDate.entries(),
|
||||
).map(([date, balance]) => ({
|
||||
date: new Date(date).toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
@@ -234,7 +234,7 @@ export function useTransactionsBalanceChart({
|
||||
(t) => {
|
||||
const transactionDate = new Date(t.date);
|
||||
return transactionDate < startDate;
|
||||
}
|
||||
},
|
||||
);
|
||||
beforeStartTransactions.forEach((t) => {
|
||||
const currentBalance = accountRunningBalances.get(t.accountId) || 0;
|
||||
@@ -246,7 +246,7 @@ export function useTransactionsBalanceChart({
|
||||
const transactionsForAccounts = selectedAccounts.includes("all")
|
||||
? sortedTransactions
|
||||
: sortedTransactions.filter((t) =>
|
||||
selectedAccounts.includes(t.accountId)
|
||||
selectedAccounts.includes(t.accountId),
|
||||
);
|
||||
|
||||
transactionsForAccounts.forEach((t) => {
|
||||
@@ -276,7 +276,7 @@ export function useTransactionsBalanceChart({
|
||||
(t) => {
|
||||
const transactionDate = new Date(t.date);
|
||||
return transactionDate < startDate && t.accountId === account.id;
|
||||
}
|
||||
},
|
||||
);
|
||||
beforeStartTransactions.forEach((t) => {
|
||||
accountStartingBalance += t.amount;
|
||||
@@ -304,7 +304,7 @@ export function useTransactionsBalanceChart({
|
||||
});
|
||||
|
||||
return point;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -115,7 +115,7 @@ export function useTransactionsChartData({
|
||||
// Fetch all filtered transactions for chart
|
||||
const { data: transactionsData, isLoading } = useTransactions(
|
||||
chartParams,
|
||||
!!metadata
|
||||
!!metadata,
|
||||
);
|
||||
|
||||
// Calculate monthly chart data
|
||||
@@ -141,7 +141,7 @@ export function useTransactionsChartData({
|
||||
|
||||
// Format months with year: use short format for better readability
|
||||
const sortedMonths = Array.from(monthlyMap.entries()).sort((a, b) =>
|
||||
a[0].localeCompare(b[0])
|
||||
a[0].localeCompare(b[0]),
|
||||
);
|
||||
|
||||
const monthlyChartData: MonthlyChartData[] = sortedMonths.map(
|
||||
@@ -160,7 +160,7 @@ export function useTransactionsChartData({
|
||||
depenses: values.expenses,
|
||||
solde: values.income - values.expenses,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return monthlyChartData;
|
||||
@@ -184,10 +184,12 @@ export function useTransactionsChartData({
|
||||
});
|
||||
|
||||
const categoryChartData: CategoryChartData[] = Array.from(
|
||||
categoryTotals.entries()
|
||||
categoryTotals.entries(),
|
||||
)
|
||||
.map(([categoryId, total]) => {
|
||||
const category = metadata.categories.find((c: Category) => c.id === categoryId);
|
||||
const category = metadata.categories.find(
|
||||
(c: Category) => c.id === categoryId,
|
||||
);
|
||||
return {
|
||||
name: category?.name || "Non catégorisé",
|
||||
value: Math.round(total),
|
||||
@@ -204,10 +206,7 @@ export function useTransactionsChartData({
|
||||
// Calculate total amount and count from all filtered transactions
|
||||
const totalAmount = useMemo(() => {
|
||||
if (!transactionsData) return 0;
|
||||
return transactionsData.transactions.reduce(
|
||||
(sum, t) => sum + t.amount,
|
||||
0
|
||||
);
|
||||
return transactionsData.transactions.reduce((sum, t) => sum + t.amount, 0);
|
||||
}, [transactionsData]);
|
||||
|
||||
const totalCount = useMemo(() => {
|
||||
@@ -223,4 +222,3 @@ export function useTransactionsChartData({
|
||||
transactions: transactionsData?.transactions || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ export function useTransactionsForAccountFilter({
|
||||
// Fetch all filtered transactions (without account filter)
|
||||
const { data: transactionsData, isLoading } = useTransactions(
|
||||
filterParams,
|
||||
!!metadata
|
||||
!!metadata,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -106,4 +106,3 @@ export function useTransactionsForAccountFilter({
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ export function useTransactionsForCategoryFilter({
|
||||
// Fetch all filtered transactions (without category filter)
|
||||
const { data: transactionsData, isLoading } = useTransactions(
|
||||
filterParams,
|
||||
!!metadata
|
||||
!!metadata,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -102,4 +102,3 @@ export function useTransactionsForCategoryFilter({
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,10 @@ export function useTransactionsPage() {
|
||||
"all",
|
||||
]);
|
||||
const [showReconciled, setShowReconciled] = useState<string>("all");
|
||||
const [period, setPeriod] = useLocalStorage<Period>("transactions-period", "3months");
|
||||
const [period, setPeriod] = useLocalStorage<Period>(
|
||||
"transactions-period",
|
||||
"3months",
|
||||
);
|
||||
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
|
||||
undefined,
|
||||
);
|
||||
@@ -85,12 +88,12 @@ export function useTransactionsPage() {
|
||||
categoryIds.forEach((categoryId) => {
|
||||
// Check if this is a parent category
|
||||
const category = metadata.categories.find(
|
||||
(c: Category) => c.id === categoryId
|
||||
(c: Category) => c.id === categoryId,
|
||||
);
|
||||
if (category && category.parentId === null) {
|
||||
// Find all children of this parent
|
||||
const children = metadata.categories.filter(
|
||||
(c: Category) => c.parentId === categoryId
|
||||
(c: Category) => c.parentId === categoryId,
|
||||
);
|
||||
children.forEach((child: Category) => {
|
||||
expandedCategoryIds.add(child.id);
|
||||
@@ -205,7 +208,8 @@ export function useTransactionsPage() {
|
||||
setPage(0);
|
||||
}, []);
|
||||
|
||||
const handlePeriodChange = useCallback((p: Period) => {
|
||||
const handlePeriodChange = useCallback(
|
||||
(p: Period) => {
|
||||
setPeriod(p);
|
||||
setPage(0);
|
||||
if (p !== "custom") {
|
||||
@@ -213,7 +217,9 @@ export function useTransactionsPage() {
|
||||
} else {
|
||||
setIsCustomDatePickerOpen(true);
|
||||
}
|
||||
}, [setPeriod]);
|
||||
},
|
||||
[setPeriod],
|
||||
);
|
||||
|
||||
const handleCustomStartDateChange = useCallback((date: Date | undefined) => {
|
||||
setCustomStartDate(date);
|
||||
|
||||
6637
pnpm-lock.yaml
generated
6637
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,12 @@ async function main() {
|
||||
const accountNumbers = process.argv.slice(2);
|
||||
|
||||
if (accountNumbers.length === 0) {
|
||||
console.error("Usage: tsx scripts/fix-account-balances.ts <accountNumber1> [accountNumber2] ...");
|
||||
console.error("Exemple: tsx scripts/fix-account-balances.ts 0748461N022 7555880857A");
|
||||
console.error(
|
||||
"Usage: tsx scripts/fix-account-balances.ts <accountNumber1> [accountNumber2] ...",
|
||||
);
|
||||
console.error(
|
||||
"Exemple: tsx scripts/fix-account-balances.ts 0748461N022 7555880857A",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -47,7 +51,9 @@ async function main() {
|
||||
console.log(` Balance calculée: ${calculatedBalance}`);
|
||||
|
||||
if (Math.abs(account.balance - calculatedBalance) > 0.01) {
|
||||
console.log(`\n⚠️ Différence détectée: ${Math.abs(account.balance - calculatedBalance).toFixed(2)}`);
|
||||
console.log(
|
||||
`\n⚠️ Différence détectée: ${Math.abs(account.balance - calculatedBalance).toFixed(2)}`,
|
||||
);
|
||||
console.log(`Mise à jour du solde...`);
|
||||
|
||||
await prisma.account.update({
|
||||
@@ -68,9 +74,7 @@ async function main() {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
main().catch((e) => {
|
||||
console.error("Erreur:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ async function main() {
|
||||
const accountNumber = process.argv[2];
|
||||
|
||||
if (!accountNumber) {
|
||||
console.error("Usage: tsx scripts/merge-duplicate-accounts.ts <accountNumber>");
|
||||
console.error(
|
||||
"Usage: tsx scripts/merge-duplicate-accounts.ts <accountNumber>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -52,11 +54,19 @@ async function main() {
|
||||
|
||||
// Le compte avec bankId numérique (pas "FR") est le bon - le garder comme principal
|
||||
// Si pas de bankId numérique, garder le plus récent
|
||||
const primaryAccount = accounts.find(acc => acc.bankId !== "FR" && acc.bankId !== "") || accounts[0];
|
||||
const accountsToMerge = accounts.filter(acc => acc.id !== primaryAccount.id);
|
||||
const primaryAccount =
|
||||
accounts.find((acc) => acc.bankId !== "FR" && acc.bankId !== "") ||
|
||||
accounts[0];
|
||||
const accountsToMerge = accounts.filter(
|
||||
(acc) => acc.id !== primaryAccount.id,
|
||||
);
|
||||
|
||||
console.log(`\nCompte principal (conservé): ${primaryAccount.id} (bankId: ${primaryAccount.bankId})`);
|
||||
console.log(`Comptes à fusionner: ${accountsToMerge.map((a) => `${a.id} (bankId: ${a.bankId})`).join(", ")}\n`);
|
||||
console.log(
|
||||
`\nCompte principal (conservé): ${primaryAccount.id} (bankId: ${primaryAccount.bankId})`,
|
||||
);
|
||||
console.log(
|
||||
`Comptes à fusionner: ${accountsToMerge.map((a) => `${a.id} (bankId: ${a.bankId})`).join(", ")}\n`,
|
||||
);
|
||||
|
||||
// Calculer l'initialBalance total (somme des initialBalance)
|
||||
const totalInitialBalance = accounts.reduce(
|
||||
@@ -111,7 +121,9 @@ async function main() {
|
||||
0,
|
||||
);
|
||||
|
||||
console.log(`Balance calculée à partir des transactions: ${calculatedBalance}`);
|
||||
console.log(
|
||||
`Balance calculée à partir des transactions: ${calculatedBalance}`,
|
||||
);
|
||||
|
||||
// Mettre à jour la balance du compte principal
|
||||
// Garder le bankId du compte principal (celui qui est correct)
|
||||
@@ -125,12 +137,14 @@ async function main() {
|
||||
// Garder le bankId du compte principal (le bon)
|
||||
bankId: primaryAccount.bankId,
|
||||
// Garder le dernier import le plus récent parmi tous les comptes
|
||||
lastImport:
|
||||
accounts.reduce((latest, acc) => {
|
||||
lastImport: accounts.reduce(
|
||||
(latest, acc) => {
|
||||
if (!acc.lastImport) return latest;
|
||||
if (!latest) return acc.lastImport;
|
||||
return acc.lastImport > latest ? acc.lastImport : latest;
|
||||
}, null as string | null),
|
||||
},
|
||||
null as string | null,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -152,7 +166,9 @@ async function main() {
|
||||
|
||||
if (finalAccount) {
|
||||
console.log(`\nVérification finale:`);
|
||||
console.log(` - Transactions dans le compte: ${finalAccount.transactions.length}`);
|
||||
console.log(
|
||||
` - Transactions dans le compte: ${finalAccount.transactions.length}`,
|
||||
);
|
||||
console.log(` - Balance: ${finalAccount.balance}`);
|
||||
console.log(` - Bank ID: ${finalAccount.bankId}`);
|
||||
}
|
||||
@@ -160,9 +176,7 @@ async function main() {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
main().catch((e) => {
|
||||
console.error("Erreur:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -396,7 +396,9 @@ export const bankingService = {
|
||||
});
|
||||
|
||||
return accounts.map(
|
||||
(a): Account & { transactionCount: number; calculatedBalance: number } => ({
|
||||
(
|
||||
a,
|
||||
): Account & { transactionCount: number; calculatedBalance: number } => ({
|
||||
id: a.id,
|
||||
name: a.name,
|
||||
bankId: a.bankId,
|
||||
|
||||
Reference in New Issue
Block a user