Add transactions page with fuzzy search and success dialog for expenses

Features:
- Created TransactionsPage with mobile-optimized layout
  - Card-based transaction items with status indicators
  - Fuzzy search by description, payee, reference, username, and tags
  - Day filter options (5, 30, 60, 90 days)
  - Pagination support
  - Responsive design for mobile and desktop
- Added getUserTransactions API method to ExpensesAPI
  - Supports filtering by days, user ID, and account type
  - Returns paginated transaction data
- Updated AddExpense component with success confirmation
  - Shows success message in same dialog after submission
  - Provides option to navigate to transactions page
  - Clean single-dialog approach
- Added "My Transactions" link to navbar menu
- Added Transaction and TransactionListResponse types
- Added permission management types and API methods (grantPermission, listPermissions, revokePermission)
- Installed alert-dialog component for UI consistency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2025-11-13 09:57:28 +01:00
parent 78fba2a637
commit be00c61c77
20 changed files with 914 additions and 74 deletions

View file

@ -8,7 +8,10 @@ import type {
ExpenseEntryRequest,
ExpenseEntry,
AccountNode,
UserInfo
UserInfo,
AccountPermission,
GrantPermissionRequest,
TransactionListResponse
} from '../types'
import { appConfig } from '@/app.config'
@ -302,4 +305,136 @@ export class ExpensesAPI extends BaseService {
}
}
}
/**
* List all account permissions (admin only)
*
* @param adminKey - Admin key for authentication
*/
async listPermissions(adminKey: string): Promise<AccountPermission[]> {
try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/permissions`, {
method: 'GET',
headers: this.getHeaders(adminKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})
if (!response.ok) {
throw new Error(`Failed to list permissions: ${response.statusText}`)
}
const permissions = await response.json()
return permissions as AccountPermission[]
} catch (error) {
console.error('[ExpensesAPI] Error listing permissions:', error)
throw error
}
}
/**
* Grant account permission to a user (admin only)
*
* @param adminKey - Admin key for authentication
* @param request - Permission grant request
*/
async grantPermission(
adminKey: string,
request: GrantPermissionRequest
): Promise<AccountPermission> {
try {
const response = await fetch(`${this.baseUrl}/castle/api/v1/permissions`, {
method: 'POST',
headers: this.getHeaders(adminKey),
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const errorMessage =
errorData.detail || `Failed to grant permission: ${response.statusText}`
throw new Error(errorMessage)
}
const permission = await response.json()
return permission as AccountPermission
} catch (error) {
console.error('[ExpensesAPI] Error granting permission:', error)
throw error
}
}
/**
* Revoke account permission (admin only)
*
* @param adminKey - Admin key for authentication
* @param permissionId - ID of the permission to revoke
*/
async revokePermission(adminKey: string, permissionId: string): Promise<void> {
try {
const response = await fetch(
`${this.baseUrl}/castle/api/v1/permissions/${permissionId}`,
{
method: 'DELETE',
headers: this.getHeaders(adminKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
const errorMessage =
errorData.detail || `Failed to revoke permission: ${response.statusText}`
throw new Error(errorMessage)
}
} catch (error) {
console.error('[ExpensesAPI] Error revoking permission:', error)
throw error
}
}
/**
* Get user's transactions from journal
*
* @param walletKey - Wallet key for authentication (invoice key)
* @param options - Query options for filtering and pagination
*/
async getUserTransactions(
walletKey: string,
options?: {
limit?: number
offset?: number
days?: number // 5, 30, 60, or 90
filter_user_id?: string
filter_account_type?: string
}
): Promise<TransactionListResponse> {
try {
const url = new URL(`${this.baseUrl}/castle/api/v1/entries/user`)
// Add query parameters
if (options?.limit) url.searchParams.set('limit', String(options.limit))
if (options?.offset) url.searchParams.set('offset', String(options.offset))
if (options?.days) url.searchParams.set('days', String(options.days))
if (options?.filter_user_id)
url.searchParams.set('filter_user_id', options.filter_user_id)
if (options?.filter_account_type)
url.searchParams.set('filter_account_type', options.filter_account_type)
const response = await fetch(url.toString(), {
method: 'GET',
headers: this.getHeaders(walletKey),
signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
})
if (!response.ok) {
throw new Error(`Failed to fetch transactions: ${response.statusText}`)
}
return await response.json()
} catch (error) {
console.error('[ExpensesAPI] Error fetching transactions:', error)
throw error
}
}
}