refactor: standardize quotation marks across all files and improve code consistency
This commit is contained in:
1286
lib/defaults.ts
1286
lib/defaults.ts
File diff suppressed because it is too large
Load Diff
67
lib/hooks.ts
67
lib/hooks.ts
@@ -1,67 +1,68 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import type { BankingData } from "./types"
|
||||
import { loadData } from "./store-db"
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { BankingData } from "./types";
|
||||
import { loadData } from "./store-db";
|
||||
|
||||
export function useBankingData() {
|
||||
const [data, setData] = useState<BankingData | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [data, setData] = useState<BankingData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const fetchedData = await loadData()
|
||||
setData(fetchedData)
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const fetchedData = await loadData();
|
||||
setData(fetchedData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("Failed to load data"))
|
||||
console.error("Error loading banking data:", err)
|
||||
setError(err instanceof Error ? err : new Error("Failed to load data"));
|
||||
console.error("Error loading banking data:", err);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
fetchData();
|
||||
}, [fetchData]);
|
||||
|
||||
const update = useCallback((newData: BankingData) => {
|
||||
// Optimistic update - the actual save happens in individual operations
|
||||
setData(newData)
|
||||
}, [])
|
||||
setData(newData);
|
||||
}, []);
|
||||
|
||||
return { data, isLoading, error, refresh, update }
|
||||
return { data, isLoading, error, refresh, update };
|
||||
}
|
||||
|
||||
export function useLocalStorage<T>(key: string, initialValue: T) {
|
||||
const [storedValue, setStoredValue] = useState<T>(initialValue)
|
||||
const [storedValue, setStoredValue] = useState<T>(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
const item = window.localStorage.getItem(key);
|
||||
if (item) {
|
||||
setStoredValue(JSON.parse(item))
|
||||
setStoredValue(JSON.parse(item));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}, [key])
|
||||
}, [key]);
|
||||
|
||||
const setValue = (value: T | ((val: T) => T)) => {
|
||||
try {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value
|
||||
setStoredValue(valueToStore)
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [storedValue, setValue] as const
|
||||
return [storedValue, setValue] as const;
|
||||
}
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
import type { OFXAccount, OFXTransaction } from "./types"
|
||||
import type { OFXAccount, OFXTransaction } from "./types";
|
||||
|
||||
export function parseOFX(content: string): OFXAccount | null {
|
||||
try {
|
||||
// Remove SGML header and clean up
|
||||
const xmlStart = content.indexOf("<OFX>")
|
||||
if (xmlStart === -1) return null
|
||||
const xmlStart = content.indexOf("<OFX>");
|
||||
if (xmlStart === -1) return null;
|
||||
|
||||
let xml = content.substring(xmlStart)
|
||||
let xml = content.substring(xmlStart);
|
||||
|
||||
// Convert SGML to XML-like format
|
||||
xml = xml.replace(/<(\w+)>([^<]+)(?=<)/g, "<$1>$2</$1>")
|
||||
xml = xml.replace(/<(\w+)>([^<]+)(?=<)/g, "<$1>$2</$1>");
|
||||
|
||||
// Extract account info
|
||||
const bankId = extractValue(xml, "BANKID") || extractValue(xml, "ORG") || "UNKNOWN"
|
||||
const accountId = extractValue(xml, "ACCTID") || "UNKNOWN"
|
||||
const accountType = extractValue(xml, "ACCTTYPE") || "CHECKING"
|
||||
const balanceStr = extractValue(xml, "BALAMT") || "0"
|
||||
const balance = Number.parseFloat(balanceStr)
|
||||
const balanceDate = extractValue(xml, "DTASOF") || new Date().toISOString()
|
||||
const currency = extractValue(xml, "CURDEF") || "EUR"
|
||||
const bankId =
|
||||
extractValue(xml, "BANKID") || extractValue(xml, "ORG") || "UNKNOWN";
|
||||
const accountId = extractValue(xml, "ACCTID") || "UNKNOWN";
|
||||
const accountType = extractValue(xml, "ACCTTYPE") || "CHECKING";
|
||||
const balanceStr = extractValue(xml, "BALAMT") || "0";
|
||||
const balance = Number.parseFloat(balanceStr);
|
||||
const balanceDate = extractValue(xml, "DTASOF") || new Date().toISOString();
|
||||
const currency = extractValue(xml, "CURDEF") || "EUR";
|
||||
|
||||
// Extract transactions
|
||||
const transactions: OFXTransaction[] = []
|
||||
const stmtTrnRegex = /<STMTTRN>([\s\S]*?)<\/STMTTRN>/gi
|
||||
let match
|
||||
const transactions: OFXTransaction[] = [];
|
||||
const stmtTrnRegex = /<STMTTRN>([\s\S]*?)<\/STMTTRN>/gi;
|
||||
let match;
|
||||
|
||||
while ((match = stmtTrnRegex.exec(xml)) !== null) {
|
||||
const trnXml = match[1]
|
||||
const trnXml = match[1];
|
||||
|
||||
const fitId = extractValue(trnXml, "FITID") || `${Date.now()}-${Math.random()}`
|
||||
const dateStr = extractValue(trnXml, "DTPOSTED") || ""
|
||||
const amountStr = extractValue(trnXml, "TRNAMT") || "0"
|
||||
const name = extractValue(trnXml, "NAME") || extractValue(trnXml, "MEMO") || "Unknown"
|
||||
const memo = extractValue(trnXml, "MEMO")
|
||||
const checkNum = extractValue(trnXml, "CHECKNUM")
|
||||
const type = extractValue(trnXml, "TRNTYPE") || "OTHER"
|
||||
const fitId =
|
||||
extractValue(trnXml, "FITID") || `${Date.now()}-${Math.random()}`;
|
||||
const dateStr = extractValue(trnXml, "DTPOSTED") || "";
|
||||
const amountStr = extractValue(trnXml, "TRNAMT") || "0";
|
||||
const name =
|
||||
extractValue(trnXml, "NAME") ||
|
||||
extractValue(trnXml, "MEMO") ||
|
||||
"Unknown";
|
||||
const memo = extractValue(trnXml, "MEMO");
|
||||
const checkNum = extractValue(trnXml, "CHECKNUM");
|
||||
const type = extractValue(trnXml, "TRNTYPE") || "OTHER";
|
||||
|
||||
transactions.push({
|
||||
fitId,
|
||||
@@ -44,7 +49,7 @@ export function parseOFX(content: string): OFXAccount | null {
|
||||
memo: memo ? cleanString(memo) : undefined,
|
||||
checkNum: checkNum ?? undefined,
|
||||
type,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -55,37 +60,39 @@ export function parseOFX(content: string): OFXAccount | null {
|
||||
balanceDate: parseOFXDate(balanceDate),
|
||||
currency,
|
||||
transactions,
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error parsing OFX:", error)
|
||||
return null
|
||||
console.error("Error parsing OFX:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractValue(xml: string, tag: string): string | null {
|
||||
const regex = new RegExp(`<${tag}>([^<]+)`, "i")
|
||||
const match = xml.match(regex)
|
||||
return match ? match[1].trim() : null
|
||||
const regex = new RegExp(`<${tag}>([^<]+)`, "i");
|
||||
const match = xml.match(regex);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function parseOFXDate(dateStr: string): string {
|
||||
if (!dateStr || dateStr.length < 8) return new Date().toISOString()
|
||||
if (!dateStr || dateStr.length < 8) return new Date().toISOString();
|
||||
|
||||
const year = dateStr.substring(0, 4)
|
||||
const month = dateStr.substring(4, 6)
|
||||
const day = dateStr.substring(6, 8)
|
||||
const year = dateStr.substring(0, 4);
|
||||
const month = dateStr.substring(4, 6);
|
||||
const day = dateStr.substring(6, 8);
|
||||
|
||||
return `${year}-${month}-${day}`
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function mapAccountType(type: string): "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER" {
|
||||
const upper = type.toUpperCase()
|
||||
if (upper.includes("CHECK") || upper.includes("CURRENT")) return "CHECKING"
|
||||
if (upper.includes("SAV")) return "SAVINGS"
|
||||
if (upper.includes("CREDIT")) return "CREDIT_CARD"
|
||||
return "OTHER"
|
||||
function mapAccountType(
|
||||
type: string,
|
||||
): "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER" {
|
||||
const upper = type.toUpperCase();
|
||||
if (upper.includes("CHECK") || upper.includes("CURRENT")) return "CHECKING";
|
||||
if (upper.includes("SAV")) return "SAVINGS";
|
||||
if (upper.includes("CREDIT")) return "CREDIT_CARD";
|
||||
return "OTHER";
|
||||
}
|
||||
|
||||
function cleanString(str: string): string {
|
||||
return str.replace(/\s+/g, " ").trim()
|
||||
return str.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
|
||||
log:
|
||||
process.env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
|
||||
118
lib/store-db.ts
118
lib/store-db.ts
@@ -1,27 +1,35 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import type { BankingData, Account, Transaction, Folder, Category } from "./types"
|
||||
import type {
|
||||
BankingData,
|
||||
Account,
|
||||
Transaction,
|
||||
Folder,
|
||||
Category,
|
||||
} from "./types";
|
||||
|
||||
const API_BASE = "/api/banking"
|
||||
const API_BASE = "/api/banking";
|
||||
|
||||
export async function loadData(): Promise<BankingData> {
|
||||
const response = await fetch(API_BASE)
|
||||
const response = await fetch(API_BASE);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load data")
|
||||
throw new Error("Failed to load data");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function addAccount(account: Omit<Account, "id">): Promise<Account> {
|
||||
export async function addAccount(
|
||||
account: Omit<Account, "id">,
|
||||
): Promise<Account> {
|
||||
const response = await fetch(`${API_BASE}/accounts`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(account),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to add account")
|
||||
throw new Error("Failed to add account");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateAccount(account: Account): Promise<Account> {
|
||||
@@ -29,44 +37,48 @@ export async function updateAccount(account: Account): Promise<Account> {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(account),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update account")
|
||||
throw new Error("Failed to update account");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteAccount(accountId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/accounts?id=${accountId}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete account")
|
||||
throw new Error("Failed to delete account");
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTransactions(transactions: Transaction[]): Promise<{ count: number; transactions: Transaction[] }> {
|
||||
export async function addTransactions(
|
||||
transactions: Transaction[],
|
||||
): Promise<{ count: number; transactions: Transaction[] }> {
|
||||
const response = await fetch(`${API_BASE}/transactions`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(transactions),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to add transactions")
|
||||
throw new Error("Failed to add transactions");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateTransaction(transaction: Transaction): Promise<Transaction> {
|
||||
export async function updateTransaction(
|
||||
transaction: Transaction,
|
||||
): Promise<Transaction> {
|
||||
const response = await fetch(`${API_BASE}/transactions`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(transaction),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update transaction")
|
||||
throw new Error("Failed to update transaction");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function addFolder(folder: Omit<Folder, "id">): Promise<Folder> {
|
||||
@@ -74,11 +86,11 @@ export async function addFolder(folder: Omit<Folder, "id">): Promise<Folder> {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(folder),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to add folder")
|
||||
throw new Error("Failed to add folder");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateFolder(folder: Folder): Promise<Folder> {
|
||||
@@ -86,32 +98,34 @@ export async function updateFolder(folder: Folder): Promise<Folder> {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(folder),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update folder")
|
||||
throw new Error("Failed to update folder");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/folders?id=${folderId}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete folder")
|
||||
throw new Error("Failed to delete folder");
|
||||
}
|
||||
}
|
||||
|
||||
export async function addCategory(category: Omit<Category, "id">): Promise<Category> {
|
||||
export async function addCategory(
|
||||
category: Omit<Category, "id">,
|
||||
): Promise<Category> {
|
||||
const response = await fetch(`${API_BASE}/categories`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(category),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to add category")
|
||||
throw new Error("Failed to add category");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateCategory(category: Category): Promise<Category> {
|
||||
@@ -119,55 +133,57 @@ export async function updateCategory(category: Category): Promise<Category> {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(category),
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to update category")
|
||||
throw new Error("Failed to update category");
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function deleteCategory(categoryId: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/categories?id=${categoryId}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete category")
|
||||
throw new Error("Failed to delete category");
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-categorize a transaction based on keywords
|
||||
export function autoCategorize(description: string, categories: Category[]): string | null {
|
||||
const lowerDesc = description.toLowerCase()
|
||||
export function autoCategorize(
|
||||
description: string,
|
||||
categories: Category[],
|
||||
): string | null {
|
||||
const lowerDesc = description.toLowerCase();
|
||||
|
||||
for (const category of categories) {
|
||||
for (const keyword of category.keywords) {
|
||||
const lowerKeyword = keyword.toLowerCase()
|
||||
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
|
||||
// Pour les keywords courts (< 6 chars), matcher uniquement des mots entiers
|
||||
// Évite les faux positifs comme "chat" dans "achat"
|
||||
if (lowerKeyword.length < 6) {
|
||||
const wordBoundary = new RegExp(`\\b${escapeRegex(lowerKeyword)}\\b`)
|
||||
const wordBoundary = new RegExp(`\\b${escapeRegex(lowerKeyword)}\\b`);
|
||||
if (wordBoundary.test(lowerDesc)) {
|
||||
return category.id
|
||||
return category.id;
|
||||
}
|
||||
} else {
|
||||
// Pour les keywords plus longs, includes() suffit
|
||||
if (lowerDesc.includes(lowerKeyword)) {
|
||||
return category.id
|
||||
return category.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// Échappe les caractères spéciaux pour les regex
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
export function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
|
||||
193
lib/store.ts
193
lib/store.ts
@@ -1,20 +1,26 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import type { BankingData, Account, Transaction, Folder, Category } from "./types"
|
||||
import { defaultCategories, defaultRootFolder } from "./defaults"
|
||||
import type {
|
||||
BankingData,
|
||||
Account,
|
||||
Transaction,
|
||||
Folder,
|
||||
Category,
|
||||
} from "./types";
|
||||
import { defaultCategories, defaultRootFolder } from "./defaults";
|
||||
|
||||
const STORAGE_KEY = "banking-app-data"
|
||||
const STORAGE_KEY = "banking-app-data";
|
||||
|
||||
// Convertir les CategoryDefinition en Category pour le localStorage
|
||||
function buildCategoriesFromDefaults(): Category[] {
|
||||
const slugToId = new Map<string, string>()
|
||||
const categories: Category[] = []
|
||||
const slugToId = new Map<string, string>();
|
||||
const categories: Category[] = [];
|
||||
|
||||
// D'abord les parents
|
||||
const parents = defaultCategories.filter((c) => c.parentSlug === null)
|
||||
const parents = defaultCategories.filter((c) => c.parentSlug === null);
|
||||
parents.forEach((cat, index) => {
|
||||
const id = `cat-${index + 1}`
|
||||
slugToId.set(cat.slug, id)
|
||||
const id = `cat-${index + 1}`;
|
||||
slugToId.set(cat.slug, id);
|
||||
categories.push({
|
||||
id,
|
||||
name: cat.name,
|
||||
@@ -22,14 +28,14 @@ function buildCategoriesFromDefaults(): Category[] {
|
||||
icon: cat.icon,
|
||||
keywords: cat.keywords,
|
||||
parentId: null,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Puis les enfants
|
||||
const children = defaultCategories.filter((c) => c.parentSlug !== null)
|
||||
const children = defaultCategories.filter((c) => c.parentSlug !== null);
|
||||
children.forEach((cat, index) => {
|
||||
const id = `cat-${parents.length + index + 1}`
|
||||
slugToId.set(cat.slug, id)
|
||||
const id = `cat-${parents.length + index + 1}`;
|
||||
slugToId.set(cat.slug, id);
|
||||
categories.push({
|
||||
id,
|
||||
name: cat.name,
|
||||
@@ -37,10 +43,10 @@ function buildCategoriesFromDefaults(): Category[] {
|
||||
icon: cat.icon,
|
||||
keywords: cat.keywords,
|
||||
parentId: cat.parentSlug ? slugToId.get(cat.parentSlug) || null : null,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return categories
|
||||
return categories;
|
||||
}
|
||||
|
||||
const defaultData: BankingData = {
|
||||
@@ -48,148 +54,163 @@ const defaultData: BankingData = {
|
||||
transactions: [],
|
||||
folders: [defaultRootFolder],
|
||||
categories: buildCategoriesFromDefaults(),
|
||||
}
|
||||
};
|
||||
|
||||
export function loadData(): BankingData {
|
||||
if (typeof window === "undefined") return defaultData
|
||||
if (typeof window === "undefined") return defaultData;
|
||||
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) {
|
||||
saveData(defaultData)
|
||||
return defaultData
|
||||
saveData(defaultData);
|
||||
return defaultData;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
return JSON.parse(stored);
|
||||
} catch {
|
||||
return defaultData
|
||||
return defaultData;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveData(data: BankingData): void {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function addAccount(account: Account): BankingData {
|
||||
const data = loadData()
|
||||
data.accounts.push(account)
|
||||
saveData(data)
|
||||
return data
|
||||
const data = loadData();
|
||||
data.accounts.push(account);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function updateAccount(account: Account): BankingData {
|
||||
const data = loadData()
|
||||
const index = data.accounts.findIndex((a) => a.id === account.id)
|
||||
const data = loadData();
|
||||
const index = data.accounts.findIndex((a) => a.id === account.id);
|
||||
if (index !== -1) {
|
||||
data.accounts[index] = account
|
||||
saveData(data)
|
||||
data.accounts[index] = account;
|
||||
saveData(data);
|
||||
}
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
export function deleteAccount(accountId: string): BankingData {
|
||||
const data = loadData()
|
||||
data.accounts = data.accounts.filter((a) => a.id !== accountId)
|
||||
data.transactions = data.transactions.filter((t) => t.accountId !== accountId)
|
||||
saveData(data)
|
||||
return data
|
||||
const data = loadData();
|
||||
data.accounts = data.accounts.filter((a) => a.id !== accountId);
|
||||
data.transactions = data.transactions.filter(
|
||||
(t) => t.accountId !== accountId,
|
||||
);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function addTransactions(transactions: Transaction[]): BankingData {
|
||||
const data = loadData()
|
||||
const data = loadData();
|
||||
|
||||
// Filter out duplicates based on fitId
|
||||
const existingFitIds = new Set(data.transactions.map((t) => `${t.accountId}-${t.fitId}`))
|
||||
const newTransactions = transactions.filter((t) => !existingFitIds.has(`${t.accountId}-${t.fitId}`))
|
||||
const existingFitIds = new Set(
|
||||
data.transactions.map((t) => `${t.accountId}-${t.fitId}`),
|
||||
);
|
||||
const newTransactions = transactions.filter(
|
||||
(t) => !existingFitIds.has(`${t.accountId}-${t.fitId}`),
|
||||
);
|
||||
|
||||
data.transactions.push(...newTransactions)
|
||||
saveData(data)
|
||||
return data
|
||||
data.transactions.push(...newTransactions);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function updateTransaction(transaction: Transaction): BankingData {
|
||||
const data = loadData()
|
||||
const index = data.transactions.findIndex((t) => t.id === transaction.id)
|
||||
const data = loadData();
|
||||
const index = data.transactions.findIndex((t) => t.id === transaction.id);
|
||||
if (index !== -1) {
|
||||
data.transactions[index] = transaction
|
||||
saveData(data)
|
||||
data.transactions[index] = transaction;
|
||||
saveData(data);
|
||||
}
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
export function addFolder(folder: Folder): BankingData {
|
||||
const data = loadData()
|
||||
data.folders.push(folder)
|
||||
saveData(data)
|
||||
return data
|
||||
const data = loadData();
|
||||
data.folders.push(folder);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function updateFolder(folder: Folder): BankingData {
|
||||
const data = loadData()
|
||||
const index = data.folders.findIndex((f) => f.id === folder.id)
|
||||
const data = loadData();
|
||||
const index = data.folders.findIndex((f) => f.id === folder.id);
|
||||
if (index !== -1) {
|
||||
data.folders[index] = folder
|
||||
saveData(data)
|
||||
data.folders[index] = folder;
|
||||
saveData(data);
|
||||
}
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
export function deleteFolder(folderId: string): BankingData {
|
||||
const data = loadData()
|
||||
const data = loadData();
|
||||
// Move accounts to root
|
||||
data.accounts = data.accounts.map((a) => (a.folderId === folderId ? { ...a, folderId: "folder-root" } : a))
|
||||
data.accounts = data.accounts.map((a) =>
|
||||
a.folderId === folderId ? { ...a, folderId: "folder-root" } : a,
|
||||
);
|
||||
// Move subfolders to parent
|
||||
const folder = data.folders.find((f) => f.id === folderId)
|
||||
const folder = data.folders.find((f) => f.id === folderId);
|
||||
if (folder) {
|
||||
data.folders = data.folders.map((f) => (f.parentId === folderId ? { ...f, parentId: folder.parentId } : f))
|
||||
data.folders = data.folders.map((f) =>
|
||||
f.parentId === folderId ? { ...f, parentId: folder.parentId } : f,
|
||||
);
|
||||
}
|
||||
data.folders = data.folders.filter((f) => f.id !== folderId)
|
||||
saveData(data)
|
||||
return data
|
||||
data.folders = data.folders.filter((f) => f.id !== folderId);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function addCategory(category: Category): BankingData {
|
||||
const data = loadData()
|
||||
data.categories.push(category)
|
||||
saveData(data)
|
||||
return data
|
||||
const data = loadData();
|
||||
data.categories.push(category);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function updateCategory(category: Category): BankingData {
|
||||
const data = loadData()
|
||||
const index = data.categories.findIndex((c) => c.id === category.id)
|
||||
const data = loadData();
|
||||
const index = data.categories.findIndex((c) => c.id === category.id);
|
||||
if (index !== -1) {
|
||||
data.categories[index] = category
|
||||
saveData(data)
|
||||
data.categories[index] = category;
|
||||
saveData(data);
|
||||
}
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
export function deleteCategory(categoryId: string): BankingData {
|
||||
const data = loadData()
|
||||
data.categories = data.categories.filter((c) => c.id !== categoryId)
|
||||
const data = loadData();
|
||||
data.categories = data.categories.filter((c) => c.id !== categoryId);
|
||||
// Remove category from transactions
|
||||
data.transactions = data.transactions.map((t) => (t.categoryId === categoryId ? { ...t, categoryId: null } : t))
|
||||
saveData(data)
|
||||
return data
|
||||
data.transactions = data.transactions.map((t) =>
|
||||
t.categoryId === categoryId ? { ...t, categoryId: null } : t,
|
||||
);
|
||||
saveData(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
// Auto-categorize a transaction based on keywords
|
||||
export function autoCategorize(description: string, categories: Category[]): string | null {
|
||||
const lowerDesc = description.toLowerCase()
|
||||
export function autoCategorize(
|
||||
description: string,
|
||||
categories: Category[],
|
||||
): string | null {
|
||||
const lowerDesc = description.toLowerCase();
|
||||
|
||||
for (const category of categories) {
|
||||
for (const keyword of category.keywords) {
|
||||
if (lowerDesc.includes(keyword.toLowerCase())) {
|
||||
return category.id
|
||||
return category.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
export function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
98
lib/types.ts
98
lib/types.ts
@@ -1,72 +1,72 @@
|
||||
// Types for the banking management application
|
||||
|
||||
export interface Transaction {
|
||||
id: string
|
||||
accountId: string
|
||||
date: string
|
||||
amount: number
|
||||
description: string
|
||||
type: "DEBIT" | "CREDIT"
|
||||
categoryId: string | null
|
||||
isReconciled: boolean
|
||||
fitId: string // OFX unique transaction ID
|
||||
memo?: string
|
||||
checkNum?: string
|
||||
id: string;
|
||||
accountId: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
description: string;
|
||||
type: "DEBIT" | "CREDIT";
|
||||
categoryId: string | null;
|
||||
isReconciled: boolean;
|
||||
fitId: string; // OFX unique transaction ID
|
||||
memo?: string;
|
||||
checkNum?: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string
|
||||
name: string
|
||||
bankId: string
|
||||
accountNumber: string
|
||||
type: "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER"
|
||||
folderId: string | null
|
||||
balance: number
|
||||
currency: string
|
||||
lastImport: string | null
|
||||
id: string;
|
||||
name: string;
|
||||
bankId: string;
|
||||
accountNumber: string;
|
||||
type: "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER";
|
||||
folderId: string | null;
|
||||
balance: number;
|
||||
currency: string;
|
||||
lastImport: string | null;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
id: string
|
||||
name: string
|
||||
parentId: string | null
|
||||
color: string
|
||||
icon: string
|
||||
id: string;
|
||||
name: string;
|
||||
parentId: string | null;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
icon: string
|
||||
keywords: string[] // For auto-categorization
|
||||
parentId: string | null
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
keywords: string[]; // For auto-categorization
|
||||
parentId: string | null;
|
||||
}
|
||||
|
||||
export interface BankingData {
|
||||
accounts: Account[]
|
||||
transactions: Transaction[]
|
||||
folders: Folder[]
|
||||
categories: Category[]
|
||||
accounts: Account[];
|
||||
transactions: Transaction[];
|
||||
folders: Folder[];
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
// OFX Parsed types
|
||||
export interface OFXTransaction {
|
||||
fitId: string
|
||||
date: string
|
||||
amount: number
|
||||
name: string
|
||||
memo?: string
|
||||
checkNum?: string
|
||||
type: string
|
||||
fitId: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
name: string;
|
||||
memo?: string;
|
||||
checkNum?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface OFXAccount {
|
||||
bankId: string
|
||||
accountId: string
|
||||
accountType: string
|
||||
balance: number
|
||||
balanceDate: string
|
||||
currency: string
|
||||
transactions: OFXTransaction[]
|
||||
bankId: string;
|
||||
accountId: string;
|
||||
accountType: string;
|
||||
balance: number;
|
||||
balanceDate: string;
|
||||
currency: string;
|
||||
transactions: OFXTransaction[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user