Migrate ID's to UUIDv7

This commit is contained in:
Víctor Falcón 2025-11-15 21:25:33 +01:00
parent dcb78d2252
commit 9d57804dc5
31 changed files with 422 additions and 89 deletions

View File

@ -34,7 +34,7 @@ class StoreAutomationRuleRequest extends FormRequest
}],
'action_category_id' => [
'nullable',
'integer',
'string',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', auth()->id());
}),

View File

@ -34,7 +34,7 @@ class UpdateAutomationRuleRequest extends FormRequest
}],
'action_category_id' => [
'nullable',
'integer',
'string',
Rule::exists('categories', 'id')->where(function ($query) {
$query->where('user_id', auth()->id());
}),

View File

@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\AccountType;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -12,7 +13,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Account extends Model
{
/** @use HasFactory<\Database\Factories\AccountFactory> */
use HasFactory, SoftDeletes;
use HasFactory, HasUuids, SoftDeletes;
protected $fillable = [
'user_id',

View File

@ -29,9 +29,4 @@ class AccountBalance extends Model
{
return $this->belongsTo(Account::class);
}
public function newUniqueId(): string
{
return (string) \Illuminate\Support\Str::uuid7();
}
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class AutomationRule extends Model
{
/** @use HasFactory<\Database\Factories\AutomationRuleFactory> */
use HasFactory, SoftDeletes;
use HasFactory, HasUuids, SoftDeletes;
protected $fillable = [
'user_id',

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Bank extends Model
{
/** @use HasFactory<\Database\Factories\BankFactory> */
use HasFactory, SoftDeletes;
use HasFactory, HasUuids, SoftDeletes;
protected $fillable = [
'name',

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
class Category extends Model
{
/** @use HasFactory<\Database\Factories\CategoryFactory> */
use HasFactory, SoftDeletes;
use HasFactory, HasUuids, SoftDeletes;
protected $fillable = [
'name',

View File

@ -2,11 +2,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EncryptedMessage extends Model
{
use HasUuids;
protected $fillable = [
'user_id',
'encrypted_content',

View File

@ -48,9 +48,4 @@ class Transaction extends Model
{
return $this->belongsTo(Category::class);
}
public function newUniqueId(): string
{
return (string) \Illuminate\Support\Str::uuid7();
}
}

View File

@ -3,6 +3,7 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
@ -13,7 +14,7 @@ use Laravel\Fortify\TwoFactorAuthenticatable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, TwoFactorAuthenticatable;
use HasFactory, HasUuids, Notifiable, TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.

View File

@ -0,0 +1,329 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
DB::statement('SET FOREIGN_KEY_CHECKS=0');
Schema::table('sessions', function (Blueprint $table) {
$table->dropColumn('user_id');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('banks', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('accounts', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['bank_id']);
});
Schema::table('categories', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('transactions', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['account_id']);
$table->dropForeign(['category_id']);
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['action_category_id']);
});
Schema::table('account_balances', function (Blueprint $table) {
$table->dropForeign(['account_id']);
});
DB::statement('DELETE FROM users');
DB::statement('DELETE FROM banks');
DB::statement('DELETE FROM accounts');
DB::statement('DELETE FROM categories');
DB::statement('DELETE FROM transactions');
DB::statement('DELETE FROM automation_rules');
DB::statement('DELETE FROM encrypted_messages');
DB::statement('DELETE FROM account_balances');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('id');
});
Schema::table('users', function (Blueprint $table) {
$table->uuid('id')->primary()->first();
});
Schema::table('banks', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
});
Schema::table('banks', function (Blueprint $table) {
$table->uuid('id')->primary()->first();
$table->uuid('user_id')->nullable()->after('id');
});
Schema::table('accounts', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
$table->dropColumn('bank_id');
});
Schema::table('accounts', function (Blueprint $table) {
$table->uuid('id')->primary()->first();
$table->uuid('user_id')->after('id');
$table->uuid('bank_id')->after('name_iv');
});
Schema::table('categories', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
});
Schema::table('categories', function (Blueprint $table) {
$table->uuid('id')->primary()->first();
$table->uuid('user_id')->after('color');
});
Schema::table('transactions', function (Blueprint $table) {
$table->dropColumn('user_id');
$table->dropColumn('account_id');
$table->dropColumn('category_id');
});
Schema::table('transactions', function (Blueprint $table) {
$table->uuid('user_id')->after('id');
$table->uuid('account_id')->after('user_id');
$table->uuid('category_id')->nullable()->after('account_id');
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
$table->dropColumn('action_category_id');
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->uuid('id')->primary()->first();
$table->uuid('user_id')->after('id');
$table->uuid('action_category_id')->nullable()->after('rules_json');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->uuid('id')->primary()->first();
$table->uuid('user_id')->after('id');
});
Schema::table('account_balances', function (Blueprint $table) {
$table->dropColumn('account_id');
});
Schema::table('account_balances', function (Blueprint $table) {
$table->uuid('account_id')->after('id');
});
Schema::table('sessions', function (Blueprint $table) {
$table->uuid('user_id')->nullable()->after('id')->index();
});
Schema::table('banks', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('accounts', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('bank_id')->references('id')->on('banks')->onDelete('cascade');
});
Schema::table('categories', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('transactions', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('action_category_id')->references('id')->on('categories')->onDelete('set null');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('account_balances', function (Blueprint $table) {
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
});
DB::statement('SET FOREIGN_KEY_CHECKS=1');
}
public function down(): void
{
DB::statement('SET FOREIGN_KEY_CHECKS=0');
Schema::table('sessions', function (Blueprint $table) {
$table->dropColumn('user_id');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('accounts', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['bank_id']);
});
Schema::table('categories', function (Blueprint $table) {
$table->dropForeign(['user_id']);
});
Schema::table('transactions', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['account_id']);
$table->dropForeign(['category_id']);
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->dropForeign(['user_id']);
$table->dropForeign(['action_category_id']);
});
Schema::table('account_balances', function (Blueprint $table) {
$table->dropForeign(['account_id']);
});
DB::statement('DELETE FROM users');
DB::statement('DELETE FROM banks');
DB::statement('DELETE FROM accounts');
DB::statement('DELETE FROM categories');
DB::statement('DELETE FROM transactions');
DB::statement('DELETE FROM automation_rules');
DB::statement('DELETE FROM encrypted_messages');
DB::statement('DELETE FROM account_balances');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('id');
});
Schema::table('users', function (Blueprint $table) {
$table->id()->first();
});
Schema::table('banks', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
});
Schema::table('banks', function (Blueprint $table) {
$table->id()->first();
$table->foreignId('user_id')->nullable()->after('id');
});
Schema::table('accounts', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
$table->dropColumn('bank_id');
});
Schema::table('accounts', function (Blueprint $table) {
$table->id()->first();
$table->foreignId('user_id')->after('id');
$table->foreignId('bank_id')->after('name_iv');
});
Schema::table('categories', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
});
Schema::table('categories', function (Blueprint $table) {
$table->id()->first();
$table->foreignId('user_id')->after('color');
});
Schema::table('transactions', function (Blueprint $table) {
$table->dropColumn('user_id');
$table->dropColumn('account_id');
$table->dropColumn('category_id');
});
Schema::table('transactions', function (Blueprint $table) {
$table->foreignId('user_id')->after('id');
$table->foreignId('account_id')->after('user_id');
$table->foreignId('category_id')->nullable()->after('account_id');
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
$table->dropColumn('action_category_id');
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->id()->first();
$table->foreignId('user_id')->after('id');
$table->foreignId('action_category_id')->nullable()->after('rules_json');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->dropColumn('id');
$table->dropColumn('user_id');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->id()->first();
$table->foreignId('user_id')->after('id');
});
Schema::table('account_balances', function (Blueprint $table) {
$table->dropColumn('account_id');
});
Schema::table('account_balances', function (Blueprint $table) {
$table->foreignId('account_id')->after('id');
});
Schema::table('sessions', function (Blueprint $table) {
$table->foreignId('user_id')->nullable()->after('id')->index();
});
Schema::table('banks', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('accounts', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('bank_id')->references('id')->on('banks')->onDelete('cascade');
});
Schema::table('categories', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('transactions', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
});
Schema::table('automation_rules', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('action_category_id')->references('id')->on('categories')->onDelete('set null');
});
Schema::table('encrypted_messages', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::table('account_balances', function (Blueprint $table) {
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
});
DB::statement('SET FOREIGN_KEY_CHECKS=1');
}
};

View File

@ -107,9 +107,7 @@ export function CreateAutomationRuleDialog({
title: title.trim(),
priority: parseInt(priority, 10),
rules_json: JSON.stringify(jsonLogic),
action_category_id: categoryId
? parseInt(categoryId, 10)
: null,
action_category_id: categoryId || null,
action_note: encryptedNote,
action_note_iv: noteIv,
},

View File

@ -145,9 +145,7 @@ export function EditAutomationRuleDialog({
title: title.trim(),
priority: parseInt(priority, 10),
rules_json: JSON.stringify(jsonLogic),
action_category_id: categoryId
? parseInt(categoryId, 10)
: null,
action_category_id: categoryId || null,
action_note: encryptedNote,
action_note_iv: noteIv,
},

View File

@ -45,7 +45,7 @@ export function CategoryCombobox({
const selectedCategory =
value && value !== 'null'
? categories.find((c) => c.id === parseInt(value))
? categories.find((c) => c.id === value)
: null;
const sortedCategories = [...categories].sort((a, b) =>

View File

@ -17,7 +17,7 @@ export function BulkCategorySelect({
function handleChange(newValue: string) {
setValue(newValue);
const categoryId = newValue === 'null' ? null : parseInt(newValue);
const categoryId = newValue === 'null' ? null : newValue;
onCategoryChange(categoryId);
}

View File

@ -35,12 +35,12 @@ export function CategoryCell({
return;
}
const categoryId = value === 'null' ? null : parseInt(value);
const categoryId = value === 'null' ? null : value;
setIsUpdating(true);
try {
const updateData: {
category_id: number | null;
category_id: string | null;
notes?: string;
notes_iv?: string;
} = {

View File

@ -60,7 +60,7 @@ export function EditTransactionDialog({
const [notes, setNotes] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [decryptedAccountNames, setDecryptedAccountNames] = useState<
Map<number, string>
Map<string, string>
>(new Map());
useEffect(() => {
@ -68,19 +68,15 @@ export function EditTransactionDialog({
setTransactionDate(transaction.transaction_date);
setDescription(transaction.decryptedDescription);
setAmount(transaction.amount);
setAccountId(String(transaction.account_id));
setCategoryId(
transaction.category_id
? String(transaction.category_id)
: 'null',
);
setAccountId(transaction.account_id);
setCategoryId(transaction.category_id || 'null');
setNotes(transaction.decryptedNotes || '');
} else if (mode === 'create' && open) {
const today = new Date().toISOString().split('T')[0];
setTransactionDate(today);
setDescription('');
setAmount(0);
setAccountId(accounts.length > 0 ? String(accounts[0].id) : '');
setAccountId(accounts.length > 0 ? accounts[0].id : '');
setCategoryId('null');
setNotes('');
}
@ -97,7 +93,7 @@ export function EditTransactionDialog({
try {
const key = await importKey(keyString);
const decryptedNames = new Map<number, string>();
const decryptedNames = new Map<string, string>();
await Promise.all(
accounts.map(async (account) => {
@ -160,7 +156,7 @@ export function EditTransactionDialog({
setIsSubmitting(true);
try {
const selectedCategoryId =
categoryId === 'null' ? null : parseInt(categoryId, 10);
categoryId === 'null' ? null : categoryId;
const trimmedNotes = notes.trim();
const trimmedDescription = description.trim();
@ -186,15 +182,15 @@ export function EditTransactionDialog({
);
const selectedAccount = accounts.find(
(acc) => acc.id === parseInt(accountId, 10),
(acc) => acc.id === accountId,
);
if (!selectedAccount) {
throw new Error('Selected account not found');
}
const createdTransaction = await transactionSyncService.create({
user_id: 0,
account_id: parseInt(accountId, 10),
user_id: '00000000-0000-0000-0000-000000000000',
account_id: accountId,
category_id: selectedCategoryId,
description: encryptedDescription.encrypted,
description_iv: encryptedDescription.iv,
@ -271,7 +267,7 @@ export function EditTransactionDialog({
}
const selectedAccount = accounts.find(
(acc) => acc.id === parseInt(accountId, 10),
(acc) => acc.id === accountId,
);
return (

View File

@ -4,12 +4,13 @@ import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { accountSyncService } from '@/services/account-sync';
import { type Account } from '@/types/account';
import type { UUID } from '@/types/uuid';
import { Building2 } from 'lucide-react';
import { useEffect, useState } from 'react';
interface ImportStepAccountProps {
selectedAccountId: number | null;
onAccountSelect: (accountId: number) => void;
selectedAccountId: UUID | null;
onAccountSelect: (accountId: UUID) => void;
onNext: () => void;
}
@ -59,8 +60,8 @@ export function ImportStepAccount({
return (
<div className="flex flex-col gap-6">
<RadioGroup
value={selectedAccountId?.toString()}
onValueChange={(value) => onAccountSelect(Number(value))}
value={selectedAccountId}
onValueChange={(value) => onAccountSelect(value)}
>
<div className="space-y-3">
{accounts.map((account) => (
@ -70,7 +71,7 @@ export function ImportStepAccount({
className="flex items-center space-x-3 rounded-lg border p-4 hover:bg-accent"
>
<RadioGroupItem
value={account.id.toString()}
value={account.id}
id={`account-${account.id}`}
/>
{account.bank.logo ? (

View File

@ -28,7 +28,7 @@ const db = new Dexie('whisper_money') as Dexie & {
pending_changes: EntityTable<PendingChange, 'id'>;
};
db.version(4).stores({
db.version(5).stores({
transactions: 'id, user_id, account_id, updated_at',
accounts: 'id, user_id, bank_id, updated_at',
categories: 'id, user_id, updated_at',

View File

@ -1,4 +1,5 @@
import axios from 'axios';
import type { UUID } from '@/types/uuid';
import { uuidv7 } from 'uuidv7';
import { db } from './dexie-db';
@ -10,8 +11,8 @@ export type StoreName =
| 'transactions';
export interface IndexedDBRecord {
id: number | string;
user_id?: number | null;
id: UUID;
user_id?: UUID | null;
created_at: string;
updated_at: string;
[key: string]: any;
@ -180,10 +181,7 @@ export class SyncManager {
data: Omit<T, 'id' | 'created_at' | 'updated_at'>,
): Promise<T> {
const timestamp = new Date().toISOString();
// Use UUID v7 for transactions, numeric IDs for other entities
const id =
this.options.storeName === 'transactions' ? uuidv7() : Date.now();
const id = uuidv7();
const record = {
...data,
@ -205,7 +203,7 @@ export class SyncManager {
}
async updateLocal<T extends IndexedDBRecord>(
id: number,
id: UUID,
data: Partial<T>,
): Promise<void> {
const table = db[this.options.storeName];
@ -233,7 +231,7 @@ export class SyncManager {
});
}
async deleteLocal(id: number): Promise<void> {
async deleteLocal(id: UUID): Promise<void> {
const timestamp = new Date().toISOString();
const table = db[this.options.storeName];
await table.delete(id);
@ -250,7 +248,7 @@ export class SyncManager {
return (await table.toArray()) as T[];
}
async getById<T extends IndexedDBRecord>(id: number): Promise<T | null> {
async getById<T extends IndexedDBRecord>(id: UUID): Promise<T | null> {
const table = db[this.options.storeName];
return ((await table.get(id)) as T) || null;
}

View File

@ -1,5 +1,6 @@
import { SyncManager } from '@/lib/sync-manager';
import type { Account } from '@/types/account';
import type { UUID } from '@/types/uuid';
class AccountSyncService {
private syncManager: SyncManager;
@ -19,7 +20,7 @@ class AccountSyncService {
return await this.syncManager.getAll<Account>();
}
async getById(id: number): Promise<Account | null> {
async getById(id: UUID): Promise<Account | null> {
return await this.syncManager.getById<Account>(id);
}
@ -27,11 +28,11 @@ class AccountSyncService {
return await this.syncManager.createLocal<Account>(data as any);
}
async update(id: number, data: Partial<Account>): Promise<void> {
async update(id: UUID, data: Partial<Account>): Promise<void> {
await this.syncManager.updateLocal<Account>(id, data);
}
async delete(id: number): Promise<void> {
async delete(id: UUID): Promise<void> {
await this.syncManager.deleteLocal(id);
}

View File

@ -1,5 +1,6 @@
import { SyncManager } from '@/lib/sync-manager';
import type { AutomationRule } from '@/types/automation-rule';
import type { UUID } from '@/types/uuid';
class AutomationRuleSyncService {
private syncManager: SyncManager;
@ -26,7 +27,7 @@ class AutomationRuleSyncService {
}));
}
async getById(id: number): Promise<AutomationRule | null> {
async getById(id: UUID): Promise<AutomationRule | null> {
const rule = await this.syncManager.getById<AutomationRule>(id);
if (!rule) {
return null;
@ -43,18 +44,18 @@ class AutomationRuleSyncService {
async create(data: Omit<AutomationRule, 'id'>): Promise<AutomationRule> {
return await this.syncManager.createLocal<AutomationRule>(
data as Omit<AutomationRule, 'id'> & {
id?: number;
id?: UUID;
created_at?: string;
updated_at?: string;
},
);
}
async update(id: number, data: Partial<AutomationRule>): Promise<void> {
async update(id: UUID, data: Partial<AutomationRule>): Promise<void> {
await this.syncManager.updateLocal<AutomationRule>(id, data);
}
async delete(id: number): Promise<void> {
async delete(id: UUID): Promise<void> {
await this.syncManager.deleteLocal(id);
}

View File

@ -1,5 +1,6 @@
import { SyncManager } from '@/lib/sync-manager';
import type { Bank } from '@/types/account';
import type { UUID } from '@/types/uuid';
class BankSyncService {
private syncManager: SyncManager;
@ -19,7 +20,7 @@ class BankSyncService {
return await this.syncManager.getAll<Bank>();
}
async getById(id: number): Promise<Bank | null> {
async getById(id: UUID): Promise<Bank | null> {
return await this.syncManager.getById<Bank>(id);
}

View File

@ -1,5 +1,6 @@
import { SyncManager } from '@/lib/sync-manager';
import type { Category } from '@/types/category';
import type { UUID } from '@/types/uuid';
class CategorySyncService {
private syncManager: SyncManager;
@ -19,7 +20,7 @@ class CategorySyncService {
return await this.syncManager.getAll<Category>();
}
async getById(id: number): Promise<Category | null> {
async getById(id: UUID): Promise<Category | null> {
return await this.syncManager.getById<Category>(id);
}
@ -27,11 +28,11 @@ class CategorySyncService {
return await this.syncManager.createLocal<Category>(data as any);
}
async update(id: number, data: Partial<Category>): Promise<void> {
async update(id: UUID, data: Partial<Category>): Promise<void> {
await this.syncManager.updateLocal<Category>(id, data);
}
async delete(id: number): Promise<void> {
async delete(id: UUID): Promise<void> {
await this.syncManager.deleteLocal(id);
}

View File

@ -2,13 +2,14 @@ import { encrypt, importKey } from '@/lib/crypto';
import { db } from '@/lib/dexie-db';
import { getStoredKey } from '@/lib/key-storage';
import { SyncManager } from '@/lib/sync-manager';
import type { UUID } from '@/types/uuid';
import { uuidv7 } from 'uuidv7';
export interface Transaction {
id: string;
user_id: number;
account_id: number;
category_id: number | null;
id: UUID;
user_id: UUID;
account_id: UUID;
category_id: UUID | null;
description: string;
description_iv: string;
transaction_date: string;
@ -38,11 +39,11 @@ class TransactionSyncService {
return await this.syncManager.getAll<Transaction>();
}
async getById(id: string): Promise<Transaction | null> {
async getById(id: UUID): Promise<Transaction | null> {
return (await db.transactions.get(id)) || null;
}
async getByAccountId(accountId: number): Promise<Transaction[]> {
async getByAccountId(accountId: UUID): Promise<Transaction[]> {
try {
const allTransactions = await this.getAll();
return allTransactions.filter((t) => t.account_id === accountId);

View File

@ -1,3 +1,5 @@
import { UUID } from './uuid';
export const ACCOUNT_TYPES = [
'checking',
'credit_card',
@ -24,13 +26,14 @@ export const CURRENCY_OPTIONS = [
export type CurrencyCode = (typeof CURRENCY_OPTIONS)[number];
export interface Bank {
id: number;
id: UUID;
user_id: UUID | null;
name: string;
logo: string | null;
}
export interface Account {
id: number;
id: UUID;
name: string;
name_iv: string;
bank: Bank;
@ -39,8 +42,8 @@ export interface Account {
}
export interface AccountBalance {
id: string;
account_id: number;
id: UUID;
account_id: UUID;
balance_date: string;
balance: number;
created_at: string;

View File

@ -1,12 +1,13 @@
import type { Category } from './category';
import { UUID } from './uuid';
export interface AutomationRule {
id: number;
user_id: number;
id: UUID;
user_id: UUID;
title: string;
priority: number;
rules_json: Record<string, any>;
action_category_id: number | null;
action_category_id: UUID | null;
action_note: string | null;
action_note_iv: string | null;
category?: Category;

View File

@ -1,3 +1,5 @@
import { UUID } from './uuid';
export const CATEGORY_ICONS = [
'AlertCircle',
'AlertTriangle',
@ -71,7 +73,7 @@ export const CATEGORY_COLORS = [
export type CategoryColor = (typeof CATEGORY_COLORS)[number];
export interface Category {
id: number;
id: UUID;
name: string;
icon: CategoryIcon;
color: CategoryColor;

View File

@ -1,5 +1,6 @@
import { InertiaLinkProps } from '@inertiajs/react';
import { LucideIcon } from 'lucide-react';
import { UUID } from './uuid';
export interface Auth {
user: User;
@ -31,7 +32,7 @@ export interface SharedData {
}
export interface User {
id: number;
id: UUID;
name: string;
email: string;
avatar?: string;
@ -39,5 +40,5 @@ export interface User {
two_factor_enabled?: boolean;
created_at: string;
updated_at: string;
[key: string]: unknown; // This allows for additional properties...
[key: string]: unknown;
}

View File

@ -1,11 +1,12 @@
import { type Account, type Bank } from './account';
import { type Category } from './category';
import { UUID } from './uuid';
export interface Transaction {
id: string;
user_id: number;
account_id: number;
category_id: number | null;
id: UUID;
user_id: UUID;
account_id: UUID;
category_id: UUID | null;
description: string;
description_iv: string;
transaction_date: string;
@ -30,7 +31,7 @@ export interface TransactionFilters {
dateTo: Date | null;
amountMin: number | null;
amountMax: number | null;
categoryIds: number[];
accountIds: number[];
categoryIds: UUID[];
accountIds: UUID[];
searchText: string;
}

View File

@ -0,0 +1,2 @@
export type UUID = string;