163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
import { SyncManager } from '@/lib/sync-manager';
|
|
import { indexedDBService } from '@/lib/indexeddb';
|
|
import { encrypt, importKey } from '@/lib/crypto';
|
|
import { getStoredKey } from '@/lib/key-storage';
|
|
|
|
export interface Transaction {
|
|
id: number;
|
|
user_id: number;
|
|
account_id: number;
|
|
category_id: number | null;
|
|
description: string;
|
|
description_iv: string;
|
|
transaction_date: string;
|
|
amount: string;
|
|
currency_code: string;
|
|
notes: string | null;
|
|
notes_iv: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
class TransactionSyncService {
|
|
private syncManager: SyncManager;
|
|
|
|
constructor() {
|
|
this.syncManager = new SyncManager({
|
|
storeName: 'transactions',
|
|
endpoint: '/api/sync/transactions',
|
|
});
|
|
}
|
|
|
|
async sync() {
|
|
return await this.syncManager.sync();
|
|
}
|
|
|
|
async getAll(): Promise<Transaction[]> {
|
|
return await this.syncManager.getAll<Transaction>();
|
|
}
|
|
|
|
async getById(id: number): Promise<Transaction | null> {
|
|
return await this.syncManager.getById<Transaction>(id);
|
|
}
|
|
|
|
async getByAccountId(accountId: number): Promise<Transaction[]> {
|
|
try {
|
|
const allTransactions = await this.getAll();
|
|
return allTransactions.filter(t => t.account_id === accountId);
|
|
} catch (error) {
|
|
console.warn('Failed to get transactions from IndexedDB:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async create(data: Omit<Transaction, 'id' | 'created_at' | 'updated_at'>): Promise<Transaction> {
|
|
return await this.syncManager.createLocal<Transaction>(data as Omit<Transaction, 'id' | 'created_at' | 'updated_at'> & { id?: number; created_at?: string; updated_at?: string });
|
|
}
|
|
|
|
async createMany(transactions: Omit<Transaction, 'id' | 'created_at' | 'updated_at'>[]): Promise<Transaction[]> {
|
|
try {
|
|
const timestamp = new Date().toISOString();
|
|
const created: Transaction[] = [];
|
|
|
|
for (const data of transactions) {
|
|
const tempId = Date.now() + created.length;
|
|
const record = {
|
|
...data,
|
|
id: tempId,
|
|
created_at: timestamp,
|
|
updated_at: timestamp,
|
|
} as Transaction;
|
|
|
|
await indexedDBService.put('transactions', record);
|
|
await indexedDBService.addPendingChange(
|
|
'transactions',
|
|
'create',
|
|
record,
|
|
);
|
|
|
|
created.push(record);
|
|
}
|
|
|
|
return created;
|
|
} catch (error) {
|
|
console.error('Failed to create transactions in IndexedDB:', error);
|
|
throw new Error('Failed to save transactions locally. Please refresh the page and try again.');
|
|
}
|
|
}
|
|
|
|
async update(id: number, data: Partial<Transaction>): Promise<void> {
|
|
await this.syncManager.updateLocal<Transaction>(id, data);
|
|
}
|
|
|
|
async delete(id: number): Promise<void> {
|
|
await this.syncManager.deleteLocal(id);
|
|
}
|
|
|
|
async isDuplicate(
|
|
accountId: number,
|
|
transactionDate: string,
|
|
amount: number,
|
|
description: string
|
|
): Promise<boolean> {
|
|
try {
|
|
const existingTransactions = await this.getByAccountId(accountId);
|
|
|
|
const keyString = getStoredKey();
|
|
if (!keyString) {
|
|
return false;
|
|
}
|
|
|
|
const key = await importKey(keyString);
|
|
|
|
for (const existing of existingTransactions) {
|
|
if (
|
|
existing.transaction_date === transactionDate &&
|
|
parseFloat(existing.amount) === amount
|
|
) {
|
|
try {
|
|
const { decrypt } = await import('@/lib/crypto');
|
|
const decryptedExisting = await decrypt(
|
|
existing.description,
|
|
key,
|
|
existing.description_iv
|
|
);
|
|
|
|
if (decryptedExisting.toLowerCase() === description.toLowerCase()) {
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to decrypt description for duplicate check:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch (error) {
|
|
console.warn('Duplicate check failed, assuming no duplicates:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async encryptDescription(description: string): Promise<{ encrypted: string; iv: string }> {
|
|
const keyString = getStoredKey();
|
|
if (!keyString) {
|
|
throw new Error('Encryption key not set');
|
|
}
|
|
|
|
const key = await importKey(keyString);
|
|
return await encrypt(description, key);
|
|
}
|
|
|
|
async getLastSyncTime(): Promise<string | null> {
|
|
return await this.syncManager.getLastSyncTime();
|
|
}
|
|
|
|
isSyncing(): boolean {
|
|
return this.syncManager.isSyncing();
|
|
}
|
|
}
|
|
|
|
export const transactionSyncService = new TransactionSyncService();
|
|
|