mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-24 15:59:24 +00:00
Compare commits
6 Commits
main
...
44c1fa22e0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44c1fa22e0 | ||
|
|
7a81f9ff9c | ||
|
|
362410c29b | ||
|
|
6368b5c62c | ||
|
|
300b4e7db7 | ||
|
|
19814121de |
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -6,6 +6,9 @@ on:
|
||||
pull_request:
|
||||
branches: ["main", "dev"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-python:
|
||||
name: Test Python
|
||||
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -5,6 +5,11 @@ on:
|
||||
tags:
|
||||
- "**"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -44,6 +49,9 @@ jobs:
|
||||
|
||||
push-docker-backend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -90,6 +98,9 @@ jobs:
|
||||
|
||||
push-docker-frontend:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -137,6 +148,8 @@ jobs:
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
needs: [build, publish-to-pypi, push-docker-backend, push-docker-frontend]
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -123,6 +123,7 @@ export default function TransactionsTable() {
|
||||
search: debouncedSearchTerm || undefined,
|
||||
summaryOnly: false,
|
||||
}),
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
|
||||
const transactions = transactionsResponse?.data || [];
|
||||
@@ -355,6 +356,28 @@ export default function TransactionsTable() {
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate stats from current page transactions
|
||||
const totalIncome = transactions
|
||||
.filter((t: Transaction) => t.transaction_value > 0)
|
||||
.reduce((sum: number, t: Transaction) => sum + t.transaction_value, 0);
|
||||
|
||||
const totalExpenses = Math.abs(
|
||||
transactions
|
||||
.filter((t: Transaction) => t.transaction_value < 0)
|
||||
.reduce((sum: number, t: Transaction) => sum + t.transaction_value, 0)
|
||||
);
|
||||
|
||||
// Get currency from first transaction, fallback to EUR
|
||||
const displayCurrency = transactions.length > 0 ? transactions[0].transaction_currency : "EUR";
|
||||
|
||||
const stats = {
|
||||
totalCount: pagination?.total || 0,
|
||||
pageCount: transactions.length,
|
||||
totalIncome,
|
||||
totalExpenses,
|
||||
netChange: totalIncome - totalExpenses,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-full">
|
||||
{/* New FilterBar */}
|
||||
@@ -366,6 +389,78 @@ export default function TransactionsTable() {
|
||||
isSearchLoading={isSearchLoading}
|
||||
/>
|
||||
|
||||
{/* Transaction Statistics */}
|
||||
{transactions.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||
Showing
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-foreground mt-1">
|
||||
{stats.pageCount}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
of {stats.totalCount} total
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||
Income
|
||||
</p>
|
||||
<BlurredValue className="text-2xl font-bold text-green-600 mt-1 block">
|
||||
+{formatCurrency(stats.totalIncome, displayCurrency)}
|
||||
</BlurredValue>
|
||||
</div>
|
||||
<TrendingUp className="h-8 w-8 text-green-600 opacity-50" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||
Expenses
|
||||
</p>
|
||||
<BlurredValue className="text-2xl font-bold text-red-600 mt-1 block">
|
||||
-{formatCurrency(stats.totalExpenses, displayCurrency)}
|
||||
</BlurredValue>
|
||||
</div>
|
||||
<TrendingDown className="h-8 w-8 text-red-600 opacity-50" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground uppercase tracking-wider">
|
||||
Net Change
|
||||
</p>
|
||||
<BlurredValue
|
||||
className={`text-2xl font-bold mt-1 block ${
|
||||
stats.netChange >= 0 ? "text-green-600" : "text-red-600"
|
||||
}`}
|
||||
>
|
||||
{stats.netChange >= 0 ? "+" : ""}
|
||||
{formatCurrency(stats.netChange, displayCurrency)}
|
||||
</BlurredValue>
|
||||
</div>
|
||||
{stats.netChange >= 0 ? (
|
||||
<TrendingUp className="h-8 w-8 text-green-600 opacity-50" />
|
||||
) : (
|
||||
<TrendingDown className="h-8 w-8 text-red-600 opacity-50" />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Responsive Table/Cards */}
|
||||
<Card>
|
||||
{/* Desktop Table View (hidden on mobile) */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -30,6 +31,21 @@ export function FilterBar({
|
||||
isSearchLoading = false,
|
||||
className,
|
||||
}: FilterBarProps) {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const cursorPositionRef = useRef<number | null>(null);
|
||||
|
||||
// Maintain focus and cursor position on search input during re-renders
|
||||
useEffect(() => {
|
||||
const currentInput = searchInputRef.current;
|
||||
if (!currentInput) return;
|
||||
|
||||
// Restore focus and cursor position after data fetches complete
|
||||
if (cursorPositionRef.current !== null && document.activeElement !== currentInput) {
|
||||
currentInput.focus();
|
||||
currentInput.setSelectionRange(cursorPositionRef.current, cursorPositionRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
const hasActiveFilters =
|
||||
filterState.searchTerm ||
|
||||
filterState.selectedAccount ||
|
||||
@@ -61,9 +77,19 @@ export function FilterBar({
|
||||
<div className="relative w-[200px]">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search transactions..."
|
||||
value={filterState.searchTerm}
|
||||
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
|
||||
onChange={(e) => {
|
||||
cursorPositionRef.current = e.target.selectionStart;
|
||||
onFilterChange("searchTerm", e.target.value);
|
||||
}}
|
||||
onFocus={() => {
|
||||
cursorPositionRef.current = searchInputRef.current?.selectionStart ?? null;
|
||||
}}
|
||||
onBlur={() => {
|
||||
cursorPositionRef.current = null;
|
||||
}}
|
||||
className="pl-9 pr-8 bg-background"
|
||||
/>
|
||||
{isSearchLoading && (
|
||||
@@ -99,9 +125,19 @@ export function FilterBar({
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
placeholder="Search..."
|
||||
value={filterState.searchTerm}
|
||||
onChange={(e) => onFilterChange("searchTerm", e.target.value)}
|
||||
onChange={(e) => {
|
||||
cursorPositionRef.current = e.target.selectionStart;
|
||||
onFilterChange("searchTerm", e.target.value);
|
||||
}}
|
||||
onFocus={() => {
|
||||
cursorPositionRef.current = searchInputRef.current?.selectionStart ?? null;
|
||||
}}
|
||||
onBlur={() => {
|
||||
cursorPositionRef.current = null;
|
||||
}}
|
||||
className="pl-9 pr-8 bg-background w-full"
|
||||
/>
|
||||
{isSearchLoading && (
|
||||
|
||||
Reference in New Issue
Block a user