Compare commits
2 Commits
299a66e6ff
...
385f68bbdf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
385f68bbdf | ||
|
|
f8919b19b3 |
@@ -14,9 +14,3 @@ README.md
|
|||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
17
.gitea/workflows/deploy.yml
Normal file
17
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Deploy with Docker Compose
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main # adapte la branche que tu veux déployer
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: mac-orbstack-runner # le nom que tu as donné au runner
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy stack
|
||||||
|
run: |
|
||||||
|
docker compose up -d
|
||||||
@@ -68,7 +68,7 @@ function FolderDropZone({
|
|||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
isOver && "ring-2 ring-primary ring-offset-2 rounded-lg p-2"
|
isOver && "ring-2 ring-primary ring-offset-2 rounded-lg p-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -91,7 +91,7 @@ export default function AccountsPage() {
|
|||||||
const [editingAccount, setEditingAccount] = useState<Account | null>(null);
|
const [editingAccount, setEditingAccount] = useState<Account | null>(null);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [selectedAccounts, setSelectedAccounts] = useState<Set<string>>(
|
const [selectedAccounts, setSelectedAccounts] = useState<Set<string>>(
|
||||||
new Set()
|
new Set(),
|
||||||
);
|
);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -117,7 +117,7 @@ export default function AccountsPage() {
|
|||||||
activationConstraint: {
|
activationConstraint: {
|
||||||
distance: 8,
|
distance: 8,
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -131,7 +131,7 @@ export default function AccountsPage() {
|
|||||||
|
|
||||||
// Convert accountsWithStats to regular accounts for compatibility
|
// Convert accountsWithStats to regular accounts for compatibility
|
||||||
const accounts = accountsWithStats.map(
|
const accounts = accountsWithStats.map(
|
||||||
({ transactionCount: _transactionCount, ...account }) => account
|
({ transactionCount: _transactionCount, ...account }) => account,
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatCurrency = (amount: number) => {
|
const formatCurrency = (amount: number) => {
|
||||||
@@ -191,7 +191,7 @@ export default function AccountsPage() {
|
|||||||
const count = selectedAccounts.size;
|
const count = selectedAccounts.size;
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
`Supprimer ${count} compte${count > 1 ? "s" : ""} et toutes leurs transactions ?`
|
`Supprimer ${count} compte${count > 1 ? "s" : ""} et toutes leurs transactions ?`,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
@@ -202,7 +202,7 @@ export default function AccountsPage() {
|
|||||||
`/api/banking/accounts?ids=${ids.join(",")}`,
|
`/api/banking/accounts?ids=${ids.join(",")}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to delete accounts");
|
throw new Error("Failed to delete accounts");
|
||||||
@@ -275,7 +275,7 @@ export default function AccountsPage() {
|
|||||||
const handleDeleteFolder = async (folderId: string) => {
|
const handleDeleteFolder = async (folderId: string) => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
"Supprimer ce dossier ? Les comptes seront déplacés à la racine."
|
"Supprimer ce dossier ? Les comptes seront déplacés à la racine.",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
@@ -315,7 +315,7 @@ export default function AccountsPage() {
|
|||||||
// Déplacer vers le dossier du compte cible
|
// Déplacer vers le dossier du compte cible
|
||||||
const targetAccountId = overId.replace("account-", "");
|
const targetAccountId = overId.replace("account-", "");
|
||||||
const targetAccount = accountsWithStats.find(
|
const targetAccount = accountsWithStats.find(
|
||||||
(a) => a.id === targetAccountId
|
(a) => a.id === targetAccountId,
|
||||||
);
|
);
|
||||||
if (targetAccount) {
|
if (targetAccount) {
|
||||||
targetFolderId = targetAccount.folderId;
|
targetFolderId = targetAccount.folderId;
|
||||||
@@ -337,7 +337,7 @@ export default function AccountsPage() {
|
|||||||
(old: Array<Account & { transactionCount: number }> | undefined) => {
|
(old: Array<Account & { transactionCount: number }> | undefined) => {
|
||||||
if (!old) return old;
|
if (!old) return old;
|
||||||
return old.map((a) => (a.id === accountId ? updatedAccount : a));
|
return old.map((a) => (a.id === accountId ? updatedAccount : a));
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Faire la requête en arrière-plan
|
// Faire la requête en arrière-plan
|
||||||
@@ -362,7 +362,7 @@ export default function AccountsPage() {
|
|||||||
|
|
||||||
const totalBalance = accounts.reduce(
|
const totalBalance = accounts.reduce(
|
||||||
(sum, a) => sum + getAccountBalance(a),
|
(sum, a) => sum + getAccountBalance(a),
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Grouper les comptes par folder
|
// Grouper les comptes par folder
|
||||||
@@ -375,7 +375,7 @@ export default function AccountsPage() {
|
|||||||
acc[folderId].push(account);
|
acc[folderId].push(account);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<string, Account[]>
|
{} as Record<string, Account[]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Obtenir les folders racine (sans parent) et les trier par nom
|
// Obtenir les folders racine (sans parent) et les trier par nom
|
||||||
@@ -429,7 +429,7 @@ export default function AccountsPage() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
isMobile ? "text-xl" : "text-2xl",
|
isMobile ? "text-xl" : "text-2xl",
|
||||||
"font-bold",
|
"font-bold",
|
||||||
totalBalance >= 0 ? "text-emerald-600" : "text-red-600"
|
totalBalance >= 0 ? "text-emerald-600" : "text-red-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
@@ -486,17 +486,17 @@ export default function AccountsPage() {
|
|||||||
"text-xs sm:text-sm font-semibold tabular-nums shrink-0",
|
"text-xs sm:text-sm font-semibold tabular-nums shrink-0",
|
||||||
accountsByFolder["no-folder"].reduce(
|
accountsByFolder["no-folder"].reduce(
|
||||||
(sum, a) => sum + getAccountBalance(a),
|
(sum, a) => sum + getAccountBalance(a),
|
||||||
0
|
0,
|
||||||
) >= 0
|
) >= 0
|
||||||
? "text-emerald-600"
|
? "text-emerald-600"
|
||||||
: "text-red-600"
|
: "text-red-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
accountsByFolder["no-folder"].reduce(
|
accountsByFolder["no-folder"].reduce(
|
||||||
(sum, a) => sum + getAccountBalance(a),
|
(sum, a) => sum + getAccountBalance(a),
|
||||||
0
|
0,
|
||||||
)
|
),
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -505,7 +505,7 @@ export default function AccountsPage() {
|
|||||||
<div className="grid gap-2 sm:gap-3 md:gap-4 grid-cols-2 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-2 sm:gap-3 md:gap-4 grid-cols-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{accountsByFolder["no-folder"].map((account) => {
|
{accountsByFolder["no-folder"].map((account) => {
|
||||||
const folder = metadata.folders.find(
|
const folder = metadata.folders.find(
|
||||||
(f: FolderType) => f.id === account.folderId
|
(f: FolderType) => f.id === account.folderId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -533,7 +533,7 @@ export default function AccountsPage() {
|
|||||||
const folderAccounts = accountsByFolder[folder.id] || [];
|
const folderAccounts = accountsByFolder[folder.id] || [];
|
||||||
const folderBalance = folderAccounts.reduce(
|
const folderBalance = folderAccounts.reduce(
|
||||||
(sum, a) => sum + getAccountBalance(a),
|
(sum, a) => sum + getAccountBalance(a),
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -562,7 +562,7 @@ export default function AccountsPage() {
|
|||||||
"text-xs sm:text-sm font-semibold tabular-nums shrink-0",
|
"text-xs sm:text-sm font-semibold tabular-nums shrink-0",
|
||||||
folderBalance >= 0
|
folderBalance >= 0
|
||||||
? "text-emerald-600"
|
? "text-emerald-600"
|
||||||
: "text-red-600"
|
: "text-red-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatCurrency(folderBalance)}
|
{formatCurrency(folderBalance)}
|
||||||
@@ -622,7 +622,7 @@ export default function AccountsPage() {
|
|||||||
<div className="grid gap-2 sm:gap-3 md:gap-4 grid-cols-2 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-2 sm:gap-3 md:gap-4 grid-cols-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{folderAccounts.map((account) => {
|
{folderAccounts.map((account) => {
|
||||||
const accountFolder = metadata.folders.find(
|
const accountFolder = metadata.folders.find(
|
||||||
(f: FolderType) => f.id === account.folderId
|
(f: FolderType) => f.id === account.folderId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -666,7 +666,7 @@ export default function AccountsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
{accounts.find(
|
{accounts.find(
|
||||||
(a) => a.id === activeId.replace("account-", "")
|
(a) => a.id === activeId.replace("account-", ""),
|
||||||
)?.name || ""}
|
)?.name || ""}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function GET(request: NextRequest) {
|
|||||||
console.error("Error fetching accounts:", error);
|
console.error("Error fetching accounts:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch accounts" },
|
{ error: "Failed to fetch accounts" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ export async function POST(request: Request) {
|
|||||||
console.error("Error creating account:", error);
|
console.error("Error creating account:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to create account" },
|
{ error: "Failed to create account" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ export async function PUT(request: Request) {
|
|||||||
console.error("Error updating account:", error);
|
console.error("Error updating account:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to update account" },
|
{ error: "Failed to update account" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export async function DELETE(request: Request) {
|
|||||||
if (accountIds.length === 0) {
|
if (accountIds.length === 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "At least one account ID is required" },
|
{ error: "At least one account ID is required" },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await accountService.deleteMany(accountIds);
|
await accountService.deleteMany(accountIds);
|
||||||
@@ -116,14 +116,14 @@ export async function DELETE(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Account ID is required" },
|
{ error: "Account ID is required" },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,13 +141,13 @@ export async function DELETE(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting account:", error);
|
console.error("Error deleting account:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to delete account" },
|
{ error: "Failed to delete account" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function GET(request: NextRequest) {
|
|||||||
console.error("Error fetching category stats:", error);
|
console.error("Error fetching category stats:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch category stats" },
|
{ error: "Failed to fetch category stats" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export async function POST(request: Request) {
|
|||||||
console.error("Error creating category:", error);
|
console.error("Error creating category:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to create category" },
|
{ error: "Failed to create category" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export async function PUT(request: Request) {
|
|||||||
console.error("Error updating category:", error);
|
console.error("Error updating category:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to update category" },
|
{ error: "Failed to update category" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export async function DELETE(request: Request) {
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Category ID is required" },
|
{ error: "Category ID is required" },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,13 +112,13 @@ export async function DELETE(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting category:", error);
|
console.error("Error deleting category:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to delete category" },
|
{ error: "Failed to delete category" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,3 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export async function POST(request: Request) {
|
|||||||
console.error("Error creating folder:", error);
|
console.error("Error creating folder:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to create folder" },
|
{ error: "Failed to create folder" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ export async function PUT(request: Request) {
|
|||||||
console.error("Error updating folder:", error);
|
console.error("Error updating folder:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to update folder" },
|
{ error: "Failed to update folder" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export async function DELETE(request: Request) {
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Folder ID is required" },
|
{ error: "Folder ID is required" },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export async function DELETE(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof FolderNotFoundError) {
|
if (error instanceof FolderNotFoundError) {
|
||||||
@@ -88,7 +88,7 @@ export async function DELETE(request: Request) {
|
|||||||
console.error("Error deleting folder:", error);
|
console.error("Error deleting folder:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to delete folder" },
|
{ error: "Failed to delete folder" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export async function POST() {
|
|||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error clearing categories:", error);
|
console.error("Error clearing categories:", error);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export async function GET(request: NextRequest) {
|
|||||||
console.error("Error fetching transactions:", error);
|
console.error("Error fetching transactions:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch transactions" },
|
{ error: "Failed to fetch transactions" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ export async function POST(request: Request) {
|
|||||||
console.error("Error creating transactions:", error);
|
console.error("Error creating transactions:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to create transactions" },
|
{ error: "Failed to create transactions" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ export async function PUT(request: Request) {
|
|||||||
const transaction: Transaction = await request.json();
|
const transaction: Transaction = await request.json();
|
||||||
const updated = await transactionService.update(
|
const updated = await transactionService.update(
|
||||||
transaction.id,
|
transaction.id,
|
||||||
transaction
|
transaction,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Revalider le cache des pages
|
// Revalider le cache des pages
|
||||||
@@ -120,7 +120,7 @@ export async function PUT(request: Request) {
|
|||||||
console.error("Error updating transaction:", error);
|
console.error("Error updating transaction:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to update transaction" },
|
{ error: "Failed to update transaction" },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ export async function DELETE(request: Request) {
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Transaction ID is required" },
|
{ error: "Transaction ID is required" },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ export async function DELETE(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Cache-Control": "no-store",
|
"Cache-Control": "no-store",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting transaction:", error);
|
console.error("Error deleting transaction:", error);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function CategoriesPage() {
|
|||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
||||||
const [expandedParents, setExpandedParents] = useState<Set<string>>(
|
const [expandedParents, setExpandedParents] = useState<Set<string>>(
|
||||||
new Set()
|
new Set(),
|
||||||
);
|
);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -53,7 +53,7 @@ export default function CategoriesPage() {
|
|||||||
});
|
});
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [recatResults, setRecatResults] = useState<RecategorizationResult[]>(
|
const [recatResults, setRecatResults] = useState<RecategorizationResult[]>(
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false);
|
const [isRecatDialogOpen, setIsRecatDialogOpen] = useState(false);
|
||||||
const [isRecategorizing, setIsRecategorizing] = useState(false);
|
const [isRecategorizing, setIsRecategorizing] = useState(false);
|
||||||
@@ -69,7 +69,7 @@ export default function CategoriesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const parents = metadata.categories.filter(
|
const parents = metadata.categories.filter(
|
||||||
(c: Category) => c.parentId === null
|
(c: Category) => c.parentId === null,
|
||||||
);
|
);
|
||||||
const children: Record<string, Category[]> = {};
|
const children: Record<string, Category[]> = {};
|
||||||
const orphans: Category[] = [];
|
const orphans: Category[] = [];
|
||||||
@@ -78,7 +78,7 @@ export default function CategoriesPage() {
|
|||||||
.filter((c: Category) => c.parentId !== null)
|
.filter((c: Category) => c.parentId !== null)
|
||||||
.forEach((child: Category) => {
|
.forEach((child: Category) => {
|
||||||
const parentExists = parents.some(
|
const parentExists = parents.some(
|
||||||
(p: Category) => p.id === child.parentId
|
(p: Category) => p.id === child.parentId,
|
||||||
);
|
);
|
||||||
if (parentExists) {
|
if (parentExists) {
|
||||||
if (!children[child.parentId!]) {
|
if (!children[child.parentId!]) {
|
||||||
@@ -136,7 +136,7 @@ export default function CategoriesPage() {
|
|||||||
|
|
||||||
return { total, count };
|
return { total, count };
|
||||||
},
|
},
|
||||||
[categoryStats, childrenByParent]
|
[categoryStats, childrenByParent],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoadingMetadata || !metadata || isLoadingStats || !categoryStats) {
|
if (isLoadingMetadata || !metadata || isLoadingStats || !categoryStats) {
|
||||||
@@ -248,7 +248,7 @@ export default function CategoriesPage() {
|
|||||||
try {
|
try {
|
||||||
// Fetch uncategorized transactions
|
// Fetch uncategorized transactions
|
||||||
const uncategorizedResponse = await fetch(
|
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) {
|
if (!uncategorizedResponse.ok) {
|
||||||
throw new Error("Failed to fetch uncategorized transactions");
|
throw new Error("Failed to fetch uncategorized transactions");
|
||||||
@@ -261,11 +261,11 @@ export default function CategoriesPage() {
|
|||||||
for (const transaction of uncategorized) {
|
for (const transaction of uncategorized) {
|
||||||
const categoryId = autoCategorize(
|
const categoryId = autoCategorize(
|
||||||
transaction.description + " " + (transaction.memo || ""),
|
transaction.description + " " + (transaction.memo || ""),
|
||||||
metadata.categories
|
metadata.categories,
|
||||||
);
|
);
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
const category = metadata.categories.find(
|
const category = metadata.categories.find(
|
||||||
(c: Category) => c.id === categoryId
|
(c: Category) => c.id === categoryId,
|
||||||
);
|
);
|
||||||
if (category) {
|
if (category) {
|
||||||
results.push({ transaction, category });
|
results.push({ transaction, category });
|
||||||
@@ -299,9 +299,9 @@ export default function CategoriesPage() {
|
|||||||
return children.some(
|
return children.some(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.name.toLowerCase().includes(query) ||
|
c.name.toLowerCase().includes(query) ||
|
||||||
c.keywords.some((k) => k.toLowerCase().includes(query))
|
c.keywords.some((k) => k.toLowerCase().includes(query)),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -346,9 +346,9 @@ export default function CategoriesPage() {
|
|||||||
(c) =>
|
(c) =>
|
||||||
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
c.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
c.keywords.some((k) =>
|
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;
|
: allChildren;
|
||||||
const stats = getCategoryStats(parent.id, true);
|
const stats = getCategoryStats(parent.id, true);
|
||||||
@@ -435,7 +435,7 @@ export default function CategoriesPage() {
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
{new Date(result.transaction.date).toLocaleDateString(
|
{new Date(result.transaction.date).toLocaleDateString(
|
||||||
"fr-FR"
|
"fr-FR",
|
||||||
)}
|
)}
|
||||||
{" • "}
|
{" • "}
|
||||||
{new Intl.NumberFormat("fr-FR", {
|
{new Intl.NumberFormat("fr-FR", {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function RulesPage() {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
includeUncategorized: true,
|
includeUncategorized: true,
|
||||||
},
|
},
|
||||||
!!metadata
|
!!metadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
@@ -58,7 +58,7 @@ export default function RulesPage() {
|
|||||||
const [filterMinCount, setFilterMinCount] = useState(2);
|
const [filterMinCount, setFilterMinCount] = useState(2);
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||||
const [selectedGroup, setSelectedGroup] = useState<TransactionGroup | null>(
|
const [selectedGroup, setSelectedGroup] = useState<TransactionGroup | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [isAutoCategorizing, setIsAutoCategorizing] = useState(false);
|
const [isAutoCategorizing, setIsAutoCategorizing] = useState(false);
|
||||||
@@ -89,7 +89,7 @@ export default function RulesPage() {
|
|||||||
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
|
totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0),
|
||||||
suggestedKeyword: suggestKeyword(descriptions),
|
suggestedKeyword: suggestKeyword(descriptions),
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter by search query
|
// Filter by search query
|
||||||
@@ -100,7 +100,7 @@ export default function RulesPage() {
|
|||||||
(g) =>
|
(g) =>
|
||||||
g.displayName.toLowerCase().includes(query) ||
|
g.displayName.toLowerCase().includes(query) ||
|
||||||
g.key.includes(query) ||
|
g.key.includes(query) ||
|
||||||
g.suggestedKeyword.toLowerCase().includes(query)
|
g.suggestedKeyword.toLowerCase().includes(query),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ export default function RulesPage() {
|
|||||||
|
|
||||||
// 1. Add keyword to category
|
// 1. Add keyword to category
|
||||||
const category = metadata.categories.find(
|
const category = metadata.categories.find(
|
||||||
(c: { id: string }) => c.id === ruleData.categoryId
|
(c: { id: string }) => c.id === ruleData.categoryId,
|
||||||
);
|
);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error("Category not found");
|
throw new Error("Category not found");
|
||||||
@@ -177,7 +177,7 @@ export default function RulesPage() {
|
|||||||
|
|
||||||
// Check if keyword already exists
|
// Check if keyword already exists
|
||||||
const keywordExists = category.keywords.some(
|
const keywordExists = category.keywords.some(
|
||||||
(k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase()
|
(k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!keywordExists) {
|
if (!keywordExists) {
|
||||||
@@ -195,8 +195,8 @@ export default function RulesPage() {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id, categoryId: ruleData.categoryId }),
|
body: JSON.stringify({ id, categoryId: ruleData.categoryId }),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ export default function RulesPage() {
|
|||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
invalidateAllCategoryQueries(queryClient);
|
invalidateAllCategoryQueries(queryClient);
|
||||||
},
|
},
|
||||||
[metadata, queryClient]
|
[metadata, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAutoCategorize = useCallback(async () => {
|
const handleAutoCategorize = useCallback(async () => {
|
||||||
@@ -218,7 +218,7 @@ export default function RulesPage() {
|
|||||||
for (const transaction of uncategorized) {
|
for (const transaction of uncategorized) {
|
||||||
const categoryId = autoCategorize(
|
const categoryId = autoCategorize(
|
||||||
transaction.description + " " + (transaction.memo || ""),
|
transaction.description + " " + (transaction.memo || ""),
|
||||||
metadata.categories
|
metadata.categories,
|
||||||
);
|
);
|
||||||
if (categoryId) {
|
if (categoryId) {
|
||||||
await fetch("/api/banking/transactions", {
|
await fetch("/api/banking/transactions", {
|
||||||
@@ -234,7 +234,7 @@ export default function RulesPage() {
|
|||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
invalidateAllCategoryQueries(queryClient);
|
invalidateAllCategoryQueries(queryClient);
|
||||||
alert(
|
alert(
|
||||||
`${categorizedCount} transaction(s) catégorisée(s) automatiquement`
|
`${categorizedCount} transaction(s) catégorisée(s) automatiquement`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error auto-categorizing:", error);
|
console.error("Error auto-categorizing:", error);
|
||||||
@@ -253,8 +253,8 @@ export default function RulesPage() {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ...t, categoryId }),
|
body: JSON.stringify({ ...t, categoryId }),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
// Invalider toutes les queries liées
|
// Invalider toutes les queries liées
|
||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
@@ -264,7 +264,7 @@ export default function RulesPage() {
|
|||||||
alert("Erreur lors de la catégorisation");
|
alert("Erreur lors de la catégorisation");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[queryClient]
|
[queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default function TransactionsPage() {
|
|||||||
handleBulkReconcile(reconciled, selectedTransactions);
|
handleBulkReconcile(reconciled, selectedTransactions);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
},
|
},
|
||||||
[handleBulkReconcile, selectedTransactions, clearSelection]
|
[handleBulkReconcile, selectedTransactions, clearSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBulkSetCategoryWithClear = useCallback(
|
const handleBulkSetCategoryWithClear = useCallback(
|
||||||
@@ -106,7 +106,7 @@ export default function TransactionsPage() {
|
|||||||
handleBulkSetCategory(categoryId, selectedTransactions);
|
handleBulkSetCategory(categoryId, selectedTransactions);
|
||||||
clearSelection();
|
clearSelection();
|
||||||
},
|
},
|
||||||
[handleBulkSetCategory, selectedTransactions, clearSelection]
|
[handleBulkSetCategory, selectedTransactions, clearSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredTransactions = transactionsData?.transactions || [];
|
const filteredTransactions = transactionsData?.transactions || [];
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export function TransactionFilters({
|
|||||||
onRemoveCategory={(id) => {
|
onRemoveCategory={(id) => {
|
||||||
const newCategories = selectedCategories.filter((c) => c !== id);
|
const newCategories = selectedCategories.filter((c) => c !== id);
|
||||||
onCategoriesChange(
|
onCategoriesChange(
|
||||||
newCategories.length > 0 ? newCategories : ["all"]
|
newCategories.length > 0 ? newCategories : ["all"],
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onClearCategories={() => onCategoriesChange(["all"])}
|
onClearCategories={() => onCategoriesChange(["all"])}
|
||||||
@@ -391,7 +391,7 @@ function ActiveFilters({
|
|||||||
|
|
||||||
const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id));
|
const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id));
|
||||||
const selectedCats = categories.filter((c) =>
|
const selectedCats = categories.filter((c) =>
|
||||||
selectedCategories.includes(c.id)
|
selectedCategories.includes(c.id),
|
||||||
);
|
);
|
||||||
const isUncategorized = selectedCategories.includes("uncategorized");
|
const isUncategorized = selectedCategories.includes("uncategorized");
|
||||||
|
|
||||||
|
|||||||
@@ -48,4 +48,3 @@ export function TransactionPagination({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export function TransactionTable({
|
|||||||
setFocusedIndex(index);
|
setFocusedIndex(index);
|
||||||
onMarkReconciled(transactionId);
|
onMarkReconciled(transactionId);
|
||||||
},
|
},
|
||||||
[onMarkReconciled]
|
[onMarkReconciled],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
@@ -198,7 +198,7 @@ export function TransactionTable({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[focusedIndex, transactions, onMarkReconciled, virtualizer]
|
[focusedIndex, transactions, onMarkReconciled, virtualizer],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -215,7 +215,7 @@ export function TransactionTable({
|
|||||||
(accountId: string) => {
|
(accountId: string) => {
|
||||||
return accounts.find((a) => a.id === accountId);
|
return accounts.find((a) => a.id === accountId);
|
||||||
},
|
},
|
||||||
[accounts]
|
[accounts],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getCategory = useCallback(
|
const getCategory = useCallback(
|
||||||
@@ -223,7 +223,7 @@ export function TransactionTable({
|
|||||||
if (!categoryId) return null;
|
if (!categoryId) return null;
|
||||||
return categories.find((c) => c.id === categoryId);
|
return categories.find((c) => c.id === categoryId);
|
||||||
},
|
},
|
||||||
[categories]
|
[categories],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -281,7 +281,7 @@ export function TransactionTable({
|
|||||||
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
|
"p-4 space-y-3 hover:bg-muted/50 cursor-pointer border-b border-border",
|
||||||
transaction.isReconciled && "bg-emerald-500/5",
|
transaction.isReconciled && "bg-emerald-500/5",
|
||||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||||
isDuplicate && "shadow-sm"
|
isDuplicate && "shadow-sm",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -323,7 +323,7 @@ export function TransactionTable({
|
|||||||
"font-semibold tabular-nums text-sm md:text-base shrink-0",
|
"font-semibold tabular-nums text-sm md:text-base shrink-0",
|
||||||
transaction.amount >= 0
|
transaction.amount >= 0
|
||||||
? "text-emerald-600"
|
? "text-emerald-600"
|
||||||
: "text-red-600"
|
: "text-red-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{transaction.amount >= 0 ? "+" : ""}
|
{transaction.amount >= 0 ? "+" : ""}
|
||||||
@@ -358,7 +358,7 @@ export function TransactionTable({
|
|||||||
showBadge
|
showBadge
|
||||||
align="start"
|
align="start"
|
||||||
disabled={updatingTransactionIds.has(
|
disabled={updatingTransactionIds.has(
|
||||||
transaction.id
|
transaction.id,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +391,7 @@ export function TransactionTable({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (
|
if (
|
||||||
confirm(
|
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);
|
onDelete(transaction.id);
|
||||||
@@ -507,7 +507,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",
|
"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",
|
transaction.isReconciled && "bg-emerald-500/5",
|
||||||
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
isFocused && "bg-primary/10 ring-1 ring-primary/30",
|
||||||
isDuplicate && "shadow-sm"
|
isDuplicate && "shadow-sm",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
<div className="p-3" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -576,7 +576,7 @@ export function TransactionTable({
|
|||||||
"p-3 text-right font-semibold tabular-nums",
|
"p-3 text-right font-semibold tabular-nums",
|
||||||
transaction.amount >= 0
|
transaction.amount >= 0
|
||||||
? "text-emerald-600"
|
? "text-emerald-600"
|
||||||
: "text-red-600"
|
: "text-red-600",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{transaction.amount >= 0 ? "+" : ""}
|
{transaction.amount >= 0 ? "+" : ""}
|
||||||
@@ -643,7 +643,7 @@ export function TransactionTable({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (
|
if (
|
||||||
confirm(
|
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);
|
onDelete(transaction.id);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
banking-app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function useTransactionMutations({
|
|||||||
if (!transactionsData) return;
|
if (!transactionsData) return;
|
||||||
|
|
||||||
const transaction = transactionsData.transactions.find(
|
const transaction = transactionsData.transactions.find(
|
||||||
(t) => t.id === transactionId
|
(t) => t.id === transactionId,
|
||||||
);
|
);
|
||||||
if (!transaction) return;
|
if (!transaction) return;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export function useTransactionMutations({
|
|||||||
transactions: oldData.transactions.map((t) =>
|
transactions: oldData.transactions.map((t) =>
|
||||||
t.id === transactionId
|
t.id === transactionId
|
||||||
? { ...t, isReconciled: newReconciledState }
|
? { ...t, isReconciled: newReconciledState }
|
||||||
: t
|
: t,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -75,7 +75,7 @@ export function useTransactionMutations({
|
|||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transactionsData, transactionParams, queryClient]
|
[transactionsData, transactionParams, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const markReconciled = useCallback(
|
const markReconciled = useCallback(
|
||||||
@@ -83,7 +83,7 @@ export function useTransactionMutations({
|
|||||||
if (!transactionsData) return;
|
if (!transactionsData) return;
|
||||||
|
|
||||||
const transaction = transactionsData.transactions.find(
|
const transaction = transactionsData.transactions.find(
|
||||||
(t) => t.id === transactionId
|
(t) => t.id === transactionId,
|
||||||
);
|
);
|
||||||
if (!transaction || transaction.isReconciled) return;
|
if (!transaction || transaction.isReconciled) return;
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export function useTransactionMutations({
|
|||||||
return {
|
return {
|
||||||
...oldData,
|
...oldData,
|
||||||
transactions: oldData.transactions.map((t) =>
|
transactions: oldData.transactions.map((t) =>
|
||||||
t.id === transactionId ? { ...t, isReconciled: true } : t
|
t.id === transactionId ? { ...t, isReconciled: true } : t,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -129,7 +129,7 @@ export function useTransactionMutations({
|
|||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transactionsData, transactionParams, queryClient]
|
[transactionsData, transactionParams, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const setCategory = useCallback(
|
const setCategory = useCallback(
|
||||||
@@ -137,7 +137,7 @@ export function useTransactionMutations({
|
|||||||
if (!transactionsData) return;
|
if (!transactionsData) return;
|
||||||
|
|
||||||
const transaction = transactionsData.transactions.find(
|
const transaction = transactionsData.transactions.find(
|
||||||
(t) => t.id === transactionId
|
(t) => t.id === transactionId,
|
||||||
);
|
);
|
||||||
if (!transaction) return;
|
if (!transaction) return;
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ export function useTransactionMutations({
|
|||||||
return {
|
return {
|
||||||
...oldData,
|
...oldData,
|
||||||
transactions: oldData.transactions.map((t) =>
|
transactions: oldData.transactions.map((t) =>
|
||||||
t.id === transactionId ? { ...t, categoryId } : t
|
t.id === transactionId ? { ...t, categoryId } : t,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -187,7 +187,7 @@ export function useTransactionMutations({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transactionsData, transactionParams, queryClient]
|
[transactionsData, transactionParams, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteTransaction = useCallback(
|
const deleteTransaction = useCallback(
|
||||||
@@ -205,7 +205,7 @@ export function useTransactionMutations({
|
|||||||
return {
|
return {
|
||||||
...oldData,
|
...oldData,
|
||||||
transactions: oldData.transactions.filter(
|
transactions: oldData.transactions.filter(
|
||||||
(t) => t.id !== transactionId
|
(t) => t.id !== transactionId,
|
||||||
),
|
),
|
||||||
total: oldData.total - 1,
|
total: oldData.total - 1,
|
||||||
};
|
};
|
||||||
@@ -216,14 +216,14 @@ export function useTransactionMutations({
|
|||||||
`/api/banking/transactions?id=${transactionId}`,
|
`/api/banking/transactions?id=${transactionId}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
throw new Error(
|
throw new Error(
|
||||||
errorData.error ||
|
errorData.error ||
|
||||||
`Failed to delete transaction: ${response.status}`
|
`Failed to delete transaction: ${response.status}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ export function useTransactionMutations({
|
|||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transactionsData, transactionParams, queryClient]
|
[transactionsData, transactionParams, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const bulkReconcile = useCallback(
|
const bulkReconcile = useCallback(
|
||||||
@@ -246,7 +246,7 @@ export function useTransactionMutations({
|
|||||||
if (!transactionsData) return;
|
if (!transactionsData) return;
|
||||||
|
|
||||||
const transactionsToUpdate = transactionsData.transactions.filter((t) =>
|
const transactionsToUpdate = transactionsData.transactions.filter((t) =>
|
||||||
selectedTransactionIds.has(t.id)
|
selectedTransactionIds.has(t.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
||||||
@@ -263,7 +263,7 @@ export function useTransactionMutations({
|
|||||||
transactions: oldData.transactions.map((t) =>
|
transactions: oldData.transactions.map((t) =>
|
||||||
transactionIds.includes(t.id)
|
transactionIds.includes(t.id)
|
||||||
? { ...t, isReconciled: reconciled }
|
? { ...t, isReconciled: reconciled }
|
||||||
: t
|
: t,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -275,8 +275,8 @@ export function useTransactionMutations({
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ...t, isReconciled: reconciled }),
|
body: JSON.stringify({ ...t, isReconciled: reconciled }),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TOUJOURS revalider après succès pour garantir la cohérence
|
// TOUJOURS revalider après succès pour garantir la cohérence
|
||||||
@@ -290,7 +290,7 @@ export function useTransactionMutations({
|
|||||||
invalidateAllTransactionQueries(queryClient);
|
invalidateAllTransactionQueries(queryClient);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transactionsData, transactionParams, queryClient]
|
[transactionsData, transactionParams, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
const bulkSetCategory = useCallback(
|
const bulkSetCategory = useCallback(
|
||||||
@@ -298,7 +298,7 @@ export function useTransactionMutations({
|
|||||||
if (!transactionsData) return;
|
if (!transactionsData) return;
|
||||||
|
|
||||||
const transactionsToUpdate = transactionsData.transactions.filter((t) =>
|
const transactionsToUpdate = transactionsData.transactions.filter((t) =>
|
||||||
selectedTransactionIds.has(t.id)
|
selectedTransactionIds.has(t.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
||||||
@@ -318,7 +318,7 @@ export function useTransactionMutations({
|
|||||||
return {
|
return {
|
||||||
...oldData,
|
...oldData,
|
||||||
transactions: oldData.transactions.map((t) =>
|
transactions: oldData.transactions.map((t) =>
|
||||||
transactionIds.includes(t.id) ? { ...t, categoryId } : t
|
transactionIds.includes(t.id) ? { ...t, categoryId } : t,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -330,8 +330,8 @@ export function useTransactionMutations({
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ ...t, categoryId }),
|
body: JSON.stringify({ ...t, categoryId }),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TOUJOURS revalider après succès pour garantir la cohérence
|
// TOUJOURS revalider après succès pour garantir la cohérence
|
||||||
@@ -351,7 +351,7 @@ export function useTransactionMutations({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[transactionsData, transactionParams, queryClient]
|
[transactionsData, transactionParams, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function useTransactionRules({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
|
||||||
const [ruleTransaction, setRuleTransaction] = useState<Transaction | null>(
|
const [ruleTransaction, setRuleTransaction] = useState<Transaction | null>(
|
||||||
null
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateRule = useCallback((transaction: Transaction) => {
|
const handleCreateRule = useCallback((transaction: Transaction) => {
|
||||||
@@ -40,7 +40,7 @@ export function useTransactionRules({
|
|||||||
|
|
||||||
const normalizedDesc = normalizeDescription(ruleTransaction.description);
|
const normalizedDesc = normalizeDescription(ruleTransaction.description);
|
||||||
const similarTransactions = transactionsData.transactions.filter(
|
const similarTransactions = transactionsData.transactions.filter(
|
||||||
(t) => normalizeDescription(t.description) === normalizedDesc
|
(t) => normalizeDescription(t.description) === normalizedDesc,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (similarTransactions.length === 0) return null;
|
if (similarTransactions.length === 0) return null;
|
||||||
@@ -51,7 +51,7 @@ export function useTransactionRules({
|
|||||||
transactions: similarTransactions,
|
transactions: similarTransactions,
|
||||||
totalAmount: similarTransactions.reduce((sum, t) => sum + t.amount, 0),
|
totalAmount: similarTransactions.reduce((sum, t) => sum + t.amount, 0),
|
||||||
suggestedKeyword: suggestKeyword(
|
suggestedKeyword: suggestKeyword(
|
||||||
similarTransactions.map((t) => t.description)
|
similarTransactions.map((t) => t.description),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}, [ruleTransaction, transactionsData]);
|
}, [ruleTransaction, transactionsData]);
|
||||||
@@ -67,7 +67,7 @@ export function useTransactionRules({
|
|||||||
|
|
||||||
// Add keyword to category
|
// Add keyword to category
|
||||||
const category = metadata.categories.find(
|
const category = metadata.categories.find(
|
||||||
(c) => c.id === ruleData.categoryId
|
(c) => c.id === ruleData.categoryId,
|
||||||
);
|
);
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw new Error("Category not found");
|
throw new Error("Category not found");
|
||||||
@@ -75,7 +75,7 @@ export function useTransactionRules({
|
|||||||
|
|
||||||
// Check if keyword already exists
|
// Check if keyword already exists
|
||||||
const keywordExists = category.keywords.some(
|
const keywordExists = category.keywords.some(
|
||||||
(k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase()
|
(k: string) => k.toLowerCase() === ruleData.keyword.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!keywordExists) {
|
if (!keywordExists) {
|
||||||
@@ -93,8 +93,8 @@ export function useTransactionRules({
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id, categoryId: ruleData.categoryId }),
|
body: JSON.stringify({ id, categoryId: ruleData.categoryId }),
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ export function useTransactionRules({
|
|||||||
invalidateAllCategoryQueries(queryClient);
|
invalidateAllCategoryQueries(queryClient);
|
||||||
setRuleDialogOpen(false);
|
setRuleDialogOpen(false);
|
||||||
},
|
},
|
||||||
[metadata, queryClient]
|
[metadata, queryClient],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ export function useTransactionsPage() {
|
|||||||
const [showReconciled, setShowReconciled] = useState<string>("all");
|
const [showReconciled, setShowReconciled] = useState<string>("all");
|
||||||
const [period, setPeriod] = useState<Period>("all");
|
const [period, setPeriod] = useState<Period>("all");
|
||||||
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
|
const [customStartDate, setCustomStartDate] = useState<Date | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
const [customEndDate, setCustomEndDate] = useState<Date | undefined>(
|
const [customEndDate, setCustomEndDate] = useState<Date | undefined>(
|
||||||
undefined
|
undefined,
|
||||||
);
|
);
|
||||||
const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false);
|
const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false);
|
||||||
const [showDuplicates, setShowDuplicates] = useState(false);
|
const [showDuplicates, setShowDuplicates] = useState(false);
|
||||||
@@ -48,7 +48,7 @@ export function useTransactionsPage() {
|
|||||||
|
|
||||||
// Selection state
|
// Selection state
|
||||||
const [selectedTransactions, setSelectedTransactions] = useState<Set<string>>(
|
const [selectedTransactions, setSelectedTransactions] = useState<Set<string>>(
|
||||||
new Set()
|
new Set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Debounce search query
|
// Debounce search query
|
||||||
@@ -168,8 +168,7 @@ export function useTransactionsPage() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePeriodChange = useCallback(
|
const handlePeriodChange = useCallback((p: Period) => {
|
||||||
(p: Period) => {
|
|
||||||
setPeriod(p);
|
setPeriod(p);
|
||||||
setPage(0);
|
setPage(0);
|
||||||
if (p !== "custom") {
|
if (p !== "custom") {
|
||||||
@@ -177,9 +176,7 @@ export function useTransactionsPage() {
|
|||||||
} else {
|
} else {
|
||||||
setIsCustomDatePickerOpen(true);
|
setIsCustomDatePickerOpen(true);
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCustomStartDateChange = useCallback((date: Date | undefined) => {
|
const handleCustomStartDateChange = useCallback((date: Date | undefined) => {
|
||||||
setCustomStartDate(date);
|
setCustomStartDate(date);
|
||||||
@@ -191,7 +188,8 @@ export function useTransactionsPage() {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSortChange = useCallback((field: SortField) => {
|
const handleSortChange = useCallback(
|
||||||
|
(field: SortField) => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
|
setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
|
||||||
} else {
|
} else {
|
||||||
@@ -199,7 +197,9 @@ export function useTransactionsPage() {
|
|||||||
setSortOrder(field === "date" ? "desc" : "asc");
|
setSortOrder(field === "date" ? "desc" : "asc");
|
||||||
}
|
}
|
||||||
setPage(0);
|
setPage(0);
|
||||||
}, [sortField]);
|
},
|
||||||
|
[sortField],
|
||||||
|
);
|
||||||
|
|
||||||
const toggleSelectAll = useCallback(() => {
|
const toggleSelectAll = useCallback(() => {
|
||||||
if (!transactionsData) return;
|
if (!transactionsData) return;
|
||||||
@@ -207,7 +207,7 @@ export function useTransactionsPage() {
|
|||||||
setSelectedTransactions(new Set());
|
setSelectedTransactions(new Set());
|
||||||
} else {
|
} else {
|
||||||
setSelectedTransactions(
|
setSelectedTransactions(
|
||||||
new Set(transactionsData.transactions.map((t) => t.id))
|
new Set(transactionsData.transactions.map((t) => t.id)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [transactionsData, selectedTransactions.size]);
|
}, [transactionsData, selectedTransactions.size]);
|
||||||
@@ -283,4 +283,3 @@ export function useTransactionsPage() {
|
|||||||
transactionParams,
|
transactionParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|||||||
|
|
||||||
// Helper function to serialize transaction params into a query key
|
// Helper function to serialize transaction params into a query key
|
||||||
export function getTransactionsQueryKey(
|
export function getTransactionsQueryKey(
|
||||||
params: TransactionsPaginatedParams = {}
|
params: TransactionsPaginatedParams = {},
|
||||||
): (string | number)[] {
|
): (string | number)[] {
|
||||||
const key: (string | number)[] = ["transactions"];
|
const key: (string | number)[] = ["transactions"];
|
||||||
if (params.limit) key.push(`limit:${params.limit}`);
|
if (params.limit) key.push(`limit:${params.limit}`);
|
||||||
@@ -106,7 +106,7 @@ export function getTransactionsQueryKey(
|
|||||||
|
|
||||||
export function useTransactions(
|
export function useTransactions(
|
||||||
params: TransactionsPaginatedParams = {},
|
params: TransactionsPaginatedParams = {},
|
||||||
enabled = true
|
enabled = true,
|
||||||
) {
|
) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ export function useTransactions(
|
|||||||
if (params.isReconciled !== undefined && params.isReconciled !== "all") {
|
if (params.isReconciled !== undefined && params.isReconciled !== "all") {
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
"isReconciled",
|
"isReconciled",
|
||||||
params.isReconciled === true ? "true" : "false"
|
params.isReconciled === true ? "true" : "false",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (params.sortField) searchParams.set("sortField", params.sortField);
|
if (params.sortField) searchParams.set("sortField", params.sortField);
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ export const transactionService = {
|
|||||||
|
|
||||||
// Create sets for fast lookup
|
// Create sets for fast lookup
|
||||||
const existingFitIdSet = new Set(
|
const existingFitIdSet = new Set(
|
||||||
existingByFitId.map((t) => `${t.accountId}-${t.fitId}`)
|
existingByFitId.map((t) => `${t.accountId}-${t.fitId}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create set for duplicates by amount + date + description
|
// Create set for duplicates by amount + date + description
|
||||||
const existingCriteriaSet = new Set(
|
const existingCriteriaSet = new Set(
|
||||||
allExistingTransactions.map(
|
allExistingTransactions.map(
|
||||||
(t) => `${t.accountId}-${t.date}-${t.amount}-${t.description}`
|
(t) => `${t.accountId}-${t.date}-${t.amount}-${t.description}`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter out duplicates based on fitId OR (amount + date + description)
|
// Filter out duplicates based on fitId OR (amount + date + description)
|
||||||
@@ -85,7 +85,7 @@ export const transactionService = {
|
|||||||
|
|
||||||
async update(
|
async update(
|
||||||
id: string,
|
id: string,
|
||||||
data: Partial<Omit<Transaction, "id">>
|
data: Partial<Omit<Transaction, "id">>,
|
||||||
): Promise<Transaction> {
|
): Promise<Transaction> {
|
||||||
const updated = await prisma.transaction.update({
|
const updated = await prisma.transaction.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
Reference in New Issue
Block a user