├── .cursor
└── rules
│ ├── frontend-rules.mdc
│ └── rust-rules.mdc
├── .devcontainer
└── devcontainer.json
├── .eslintrc.cjs
├── .github
├── FUNDING.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .prettierrc.cjs
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── ROADMAP.md
├── app-icon.png
├── components.json
├── index.html
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── button-buy-me-a-coffee.png
├── illustration.png
├── illustration2.png
├── logo.svg
├── market-data
│ └── yahoo-finance.png
├── screenshot.png
├── wf-vector.png
└── wf-vector2.png
├── src-core
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── diesel.toml
├── migrations
│ ├── .keep
│ ├── 2023-11-08-162221_init_db
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2024-09-16-023604_portfolio_history
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2024-09-21-023605_settings_to_kv
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2024-09-22-012202_init_exchange_rates
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2024-09-28-225756_add_calculated_at
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2024-10-08-193300_contrib_limits
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2024-10-15-173026_csv_import_profiles
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2025-01-27-000001_migrate_fx_to_quotes
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2025-03-17-185736_add_start_end_dates_to_contribution_limits
│ │ ├── down.sql
│ │ └── up.sql
│ ├── 2025-03-18-222805_add_amount_field_and_use_decimal
│ │ ├── down.sql
│ │ └── up.sql
│ └── 2025-04-21-195716_create_daily_account_history
│ │ ├── down.sql
│ │ └── up.sql
└── src
│ ├── accounts
│ ├── accounts_constants.rs
│ ├── accounts_model.rs
│ ├── accounts_repository.rs
│ ├── accounts_service.rs
│ ├── accounts_traits.rs
│ └── mod.rs
│ ├── activities
│ ├── activities_constants.rs
│ ├── activities_errors.rs
│ ├── activities_model.rs
│ ├── activities_repository.rs
│ ├── activities_service.rs
│ ├── activities_traits.rs
│ └── mod.rs
│ ├── assets
│ ├── assets_constants.rs
│ ├── assets_model.rs
│ ├── assets_repository.rs
│ ├── assets_service.rs
│ ├── assets_traits.rs
│ └── mod.rs
│ ├── constants.rs
│ ├── db
│ ├── mod.rs
│ └── write_actor.rs
│ ├── errors.rs
│ ├── fx
│ ├── currency_converter.rs
│ ├── fx_errors.rs
│ ├── fx_model.rs
│ ├── fx_repository.rs
│ ├── fx_service.rs
│ ├── fx_traits.rs
│ └── mod.rs
│ ├── goals
│ ├── goals_model.rs
│ ├── goals_repository.rs
│ ├── goals_service.rs
│ ├── goals_traits.rs
│ └── mod.rs
│ ├── lib.rs
│ ├── limits
│ ├── limits_model.rs
│ ├── limits_repository.rs
│ ├── limits_service.rs
│ ├── limits_traits.rs
│ └── mod.rs
│ ├── market_data
│ ├── market_data_constants.rs
│ ├── market_data_errors.rs
│ ├── market_data_model.rs
│ ├── market_data_repository.rs
│ ├── market_data_service.rs
│ ├── market_data_traits.rs
│ ├── mod.rs
│ └── providers
│ │ ├── manual_provider.rs
│ │ ├── market_data_provider.rs
│ │ ├── mod.rs
│ │ ├── models.rs
│ │ ├── provider_registry.rs
│ │ └── yahoo_provider.rs
│ ├── portfolio
│ ├── holdings
│ │ ├── holdings_model.rs
│ │ ├── holdings_service.rs
│ │ ├── holdings_valuation_service.rs
│ │ ├── holdings_valuation_service_tests.rs
│ │ └── mod.rs
│ ├── income
│ │ ├── income_model.rs
│ │ ├── income_service.rs
│ │ └── mod.rs
│ ├── mod.rs
│ ├── performance
│ │ ├── mod.rs
│ │ ├── performance_model.rs
│ │ └── performance_service.rs
│ ├── snapshot
│ │ ├── holdings_calculator.rs
│ │ ├── holdings_calculator_tests.rs
│ │ ├── mod.rs
│ │ ├── positions_model.rs
│ │ ├── snapshot_model.rs
│ │ ├── snapshot_repository.rs
│ │ ├── snapshot_service.rs
│ │ └── snapshot_service_tests.rs
│ └── valuation
│ │ ├── mod.rs
│ │ ├── valuation_calculator.rs
│ │ ├── valuation_model.rs
│ │ ├── valuation_repository.rs
│ │ └── valuation_service.rs
│ ├── schema.rs
│ ├── settings
│ ├── mod.rs
│ ├── settings_model.rs
│ ├── settings_repository.rs
│ └── settings_service.rs
│ └── utils
│ ├── decimal_serde.rs
│ ├── mod.rs
│ └── time_utils.rs
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── capabilities
│ └── desktop.json
├── gen
│ └── schemas
│ │ ├── acl-manifests.json
│ │ ├── capabilities.json
│ │ ├── desktop-schema.json
│ │ ├── linux-schema.json
│ │ ├── macOS-schema.json
│ │ └── windows-schema.json
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── android
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ │ └── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ └── ic_launcher_round.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ └── ios
│ │ ├── AppIcon-20x20@1x.png
│ │ ├── AppIcon-20x20@2x-1.png
│ │ ├── AppIcon-20x20@2x.png
│ │ ├── AppIcon-20x20@3x.png
│ │ ├── AppIcon-29x29@1x.png
│ │ ├── AppIcon-29x29@2x-1.png
│ │ ├── AppIcon-29x29@2x.png
│ │ ├── AppIcon-29x29@3x.png
│ │ ├── AppIcon-40x40@1x.png
│ │ ├── AppIcon-40x40@2x-1.png
│ │ ├── AppIcon-40x40@2x.png
│ │ ├── AppIcon-40x40@3x.png
│ │ ├── AppIcon-512@2x.png
│ │ ├── AppIcon-60x60@2x.png
│ │ ├── AppIcon-60x60@3x.png
│ │ ├── AppIcon-76x76@1x.png
│ │ ├── AppIcon-76x76@2x.png
│ │ └── AppIcon-83.5x83.5@2x.png
├── src
│ ├── commands
│ │ ├── account.rs
│ │ ├── activity.rs
│ │ ├── asset.rs
│ │ ├── goal.rs
│ │ ├── limits.rs
│ │ ├── market_data.rs
│ │ ├── mod.rs
│ │ ├── portfolio.rs
│ │ ├── settings.rs
│ │ └── utilities.rs
│ ├── context
│ │ ├── mod.rs
│ │ ├── providers.rs
│ │ └── registry.rs
│ ├── events.rs
│ ├── listeners.rs
│ ├── main.rs
│ ├── menu.rs
│ └── updater.rs
└── tauri.conf.json
├── src
├── App.tsx
├── adapters
│ ├── index.ts
│ └── tauri.ts
├── assets
│ └── logo.png
├── commands
│ ├── README.md
│ ├── account.ts
│ ├── activity-import.ts
│ ├── activity.ts
│ ├── contribution-limits.ts
│ ├── exchange-rates.ts
│ ├── file.ts
│ ├── goal.ts
│ ├── import-listener.ts
│ ├── market-data.ts
│ ├── portfolio-listener.ts
│ ├── portfolio.ts
│ └── settings.ts
├── components
│ ├── account-selector.tsx
│ ├── alert-feedback.tsx
│ ├── amount-display.tsx
│ ├── benchmark-symbol-selector.tsx
│ ├── custom-pie-chart.tsx
│ ├── date-range-selector.tsx
│ ├── delete-confirm.tsx
│ ├── empty-placeholder.tsx
│ ├── error-boundary.tsx
│ ├── gain-amount.tsx
│ ├── gain-percent.tsx
│ ├── header.tsx
│ ├── history-chart-symbol.tsx
│ ├── history-chart.tsx
│ ├── icons.tsx
│ ├── interval-selector.tsx
│ ├── metric-display.tsx
│ ├── performance-chart.tsx
│ ├── privacy-amount.tsx
│ ├── privacy-toggle.tsx
│ ├── quantity-display.tsx
│ ├── searchable-select.tsx
│ ├── shell.tsx
│ ├── tailwind-indicator.tsx
│ ├── ticker-search.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── alert.tsx
│ │ ├── autocomplete.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── calendar-rac.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── chart.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── currency-input.tsx
│ │ ├── data-table
│ │ ├── data-table-column-header.tsx
│ │ ├── data-table-faceted-filter.tsx
│ │ ├── data-table-pagination.tsx
│ │ ├── data-table-toolbar.tsx
│ │ └── index.tsx
│ │ ├── date-picker-input.tsx
│ │ ├── date-range-picker.tsx
│ │ ├── datefield-rac.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── empty-placeholder.tsx
│ │ ├── form.tsx
│ │ ├── hover-card.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── money-input.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── quantity-input.tsx
│ │ ├── radio-group.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── tag-input.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ ├── tooltip.tsx
│ │ └── use-toast.ts
├── context
│ └── privacy-context.tsx
├── hooks
│ ├── use-accounts-simple-performance.ts
│ ├── use-accounts.ts
│ ├── use-calculate-portfolio.ts
│ ├── use-market-data-providers.ts
│ ├── use-settings-mutation.ts
│ ├── use-settings.ts
│ ├── use-sync-market-data.ts
│ └── use-valuation-history.ts
├── lib
│ ├── activity-utils.test.ts
│ ├── activity-utils.ts
│ ├── constants.ts
│ ├── currencies.ts
│ ├── export-utils.ts
│ ├── portfolio-helper.test.ts
│ ├── portfolio-helper.ts
│ ├── query-keys.ts
│ ├── schemas.ts
│ ├── settings-provider.tsx
│ ├── types.ts
│ └── utils.ts
├── main.tsx
├── pages
│ ├── account
│ │ ├── account-contribution-limit.tsx
│ │ ├── account-holdings.tsx
│ │ ├── account-metrics.tsx
│ │ ├── account-page.tsx
│ │ └── performance-grid.tsx
│ ├── activity
│ │ ├── activity-page.tsx
│ │ ├── components
│ │ │ ├── activity-delete-modal.tsx
│ │ │ ├── activity-form.tsx
│ │ │ ├── activity-operations.tsx
│ │ │ ├── activity-table.tsx
│ │ │ ├── activity-type-selector.tsx
│ │ │ ├── editable-activity-table.tsx
│ │ │ └── forms
│ │ │ │ ├── cash-form.tsx
│ │ │ │ ├── common.tsx
│ │ │ │ ├── holdings-form.tsx
│ │ │ │ ├── income-form.tsx
│ │ │ │ ├── other-form.tsx
│ │ │ │ ├── schemas.ts
│ │ │ │ └── trade-form.tsx
│ │ ├── hooks
│ │ │ └── use-activity-mutations.ts
│ │ └── import
│ │ │ ├── activity-import-page.tsx
│ │ │ ├── components
│ │ │ ├── csv-file-viewer.tsx
│ │ │ ├── file-dropzone.tsx
│ │ │ ├── help-tooltip.tsx
│ │ │ ├── import-alert.tsx
│ │ │ ├── mapping-editor.tsx
│ │ │ ├── mapping-status-card.tsx
│ │ │ ├── mapping-table-cells.tsx
│ │ │ ├── mapping-table.tsx
│ │ │ ├── progress-indicator.tsx
│ │ │ └── step-indicator.tsx
│ │ │ ├── hooks
│ │ │ ├── use-activity-import-mutations.ts
│ │ │ ├── use-csv-parser.ts
│ │ │ └── use-import-mapping.ts
│ │ │ ├── import-help.tsx
│ │ │ ├── import-preview-table.tsx
│ │ │ ├── import-validation-alert.tsx
│ │ │ ├── steps
│ │ │ ├── account-selection-step.tsx
│ │ │ ├── mapping-step.tsx
│ │ │ ├── preview-step.tsx
│ │ │ └── result-step.tsx
│ │ │ └── utils
│ │ │ └── validation-utils.ts
│ ├── asset
│ │ ├── asset-detail-card.tsx
│ │ ├── asset-history-card.tsx
│ │ ├── asset-lots-table.tsx
│ │ ├── asset-profile-page.tsx
│ │ ├── quote-history-table.tsx
│ │ ├── use-asset-profile-mutations.ts
│ │ └── use-quote-mutations.ts
│ ├── dashboard
│ │ ├── accounts-summary.tsx
│ │ ├── balance.tsx
│ │ ├── dashboard-page.tsx
│ │ ├── goals-chart.tsx
│ │ ├── goals.tsx
│ │ ├── overview.tsx
│ │ └── portfolio-update-trigger.tsx
│ ├── holdings
│ │ ├── components
│ │ │ ├── account-allocation-chart.tsx
│ │ │ ├── cash-holdings-widget.tsx
│ │ │ ├── classes-chart.tsx
│ │ │ ├── composition-chart.tsx
│ │ │ ├── country-chart.tsx
│ │ │ ├── currency-chart.tsx
│ │ │ ├── holdings-table.tsx
│ │ │ ├── index.ts
│ │ │ └── sectors-chart.tsx
│ │ └── holdings-page.tsx
│ ├── income
│ │ ├── income-history-chart.tsx
│ │ └── income-page.tsx
│ ├── layouts
│ │ ├── app-layout.tsx
│ │ └── sidebar-nav.tsx
│ ├── onboarding
│ │ ├── onboarding-page.tsx
│ │ ├── onboarding-step1.tsx
│ │ ├── onboarding-step2.tsx
│ │ └── onboarding-step3.tsx
│ ├── performance
│ │ ├── hooks
│ │ │ ├── use-performance-data.ts
│ │ │ └── use-performance-summary.ts
│ │ └── performance-page.tsx
│ └── settings
│ │ ├── accounts
│ │ ├── accounts-page.tsx
│ │ └── components
│ │ │ ├── account-edit-modal.tsx
│ │ │ ├── account-form.tsx
│ │ │ ├── account-item.tsx
│ │ │ ├── account-operations.tsx
│ │ │ └── use-account-mutations.ts
│ │ ├── appearance
│ │ ├── appearance-form.tsx
│ │ └── appearance-page.tsx
│ │ ├── contribution-limits
│ │ ├── components
│ │ │ ├── account-selection.tsx
│ │ │ ├── contribution-limit-edit-modal.tsx
│ │ │ ├── contribution-limit-form.tsx
│ │ │ ├── contribution-limit-item.tsx
│ │ │ └── contribution-limit-operations.tsx
│ │ ├── contribution-limits-page.tsx
│ │ └── use-contribution-limit-mutations.ts
│ │ ├── exports
│ │ ├── exports-form.tsx
│ │ ├── exports-page.tsx
│ │ └── use-export-data.ts
│ │ ├── general
│ │ ├── currency-settings.tsx
│ │ ├── exchange-rates
│ │ │ ├── add-exchange-rate-form.tsx
│ │ │ ├── exchange-rates-settings.tsx
│ │ │ ├── rate-cell.tsx
│ │ │ └── use-exchange-rate.ts
│ │ └── general-page.tsx
│ │ ├── goals
│ │ ├── components
│ │ │ ├── goal-allocations.tsx
│ │ │ ├── goal-edit-modal.tsx
│ │ │ ├── goal-form.tsx
│ │ │ ├── goal-item.tsx
│ │ │ └── goal-operations.tsx
│ │ ├── goals-page.tsx
│ │ └── use-goal-mutations.ts
│ │ ├── header.tsx
│ │ ├── layout.tsx
│ │ ├── market-data-settings.tsx
│ │ └── sidebar-nav.tsx
├── routes.tsx
├── styles.css
├── test
│ └── setup.ts
├── use-global-event-listener.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.test.json
├── vite.config.d.ts
├── vite.config.js
└── vite.config.ts
/.cursor/rules/frontend-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: frontend and typescript rules
3 | globs: src/, *.tsx, *.ts
4 | ---
5 | # Frontend Development Guidelines
6 |
7 | ## Tech Stack
8 | - Node.js
9 | - React
10 | - Vite
11 | - TanStack Query
12 | - TanStack Router
13 | - Tailwind CSS
14 |
15 | ## Code Style and Structure
16 |
17 | ### General Principles
18 | - Write concise, technical TypeScript code following industry best practices
19 | - Avoid code duplication; use functions and modules for reusable logic
20 | - Use functional and declarative programming patterns
21 | - Avoid classes
22 | - Use descriptive variable names with auxiliary verbs (e.g., `isLoading`, `hasError`)
23 |
24 | ### File Structure
25 | 1. Exported component
26 | 2. Subcomponents
27 | 3. Helpers
28 | 4. Static content
29 | 5. Types
30 |
31 | ### Naming Conventions
32 | - Use lowercase with dashes for directories (e.g., `components/auth-wizard`)
33 | - Favor named exports for components
34 |
35 | ### TypeScript Usage
36 | - Use TypeScript for all code
37 | - Prefer interfaces over types
38 | - Avoid enums; use maps instead
39 | - Use functional components with TypeScript interfaces
40 |
41 | ### Syntax and Formatting
42 | - Use the `function` keyword for pure functions
43 | - Use curly braces for all conditionals
44 | - Favor simplicity over cleverness
45 | - Use declarative JSX
46 |
47 | ### UI and Styling
48 | - Use Tailwind for components and styling
49 |
50 | ### Performance Optimization
51 | Focus on:
52 | - Immutable data structures
53 | - Efficient data fetching strategies
54 | - Network request optimization
55 | - Efficient data structures and algorithms
56 | - Efficient rendering strategies
57 | - Optimized state management
--------------------------------------------------------------------------------
/.cursor/rules/rust-rules.mdc:
--------------------------------------------------------------------------------
1 | ---
2 | description: Rules to apply to Rust backend of this tauri app
3 | globs: *.rs
4 | alwaysApply: false
5 | ---
6 | You are an expert in Rust, async programming.
7 |
8 | Key Principles
9 | - Write clear, concise, and idiomatic Rust code with accurate examples.
10 | - Do only the task I asked, do not try to other things
11 | - Use async programming paradigms effectively.
12 | - Prioritize modularity, clean code organization, and efficient resource management.
13 | - Use expressive variable names that convey intent (e.g., `is_ready`, `has_data`).
14 | - Adhere to Rust's naming conventions: snake_case for variables and functions, PascalCase for types and structs.
15 | - Avoid code duplication; use functions and modules to encapsulate reusable logic.
16 | - Write code with safety, concurrency, and performance in mind, embracing Rust's ownership and type system.
17 | - When refactoring, make sure to remove unused code
18 |
19 | Project Stack:
20 | - Desktop application using Tauri Framework
21 | - Diesel ORM for database access
22 | - SQLite
23 |
24 |
25 | Error Handling and Safety
26 | - Embrace Rust's Result and Option types for error handling.
27 | - Use `?` operator to propagate errors in async functions.
28 | - Implement custom error types using `thiserror` for more descriptive errors.
29 | - Handle errors and edge cases early, returning errors where appropriate.
30 | - Use `.await` responsibly, ensuring safe points for context switching.
31 |
32 | Testing
33 | - Write unit tests with `tokio::test` for async tests.
34 | - Use `tokio::time::pause` for testing time-dependent code without real delays.
35 | - Implement integration tests to validate async behavior and concurrency.
36 | - Use mocks and fakes for external dependencies in tests.
37 |
38 |
39 | Key Conventions
40 | 1. Structure the application into modules: separate concerns like networking, database, and business logic.
41 | 2. Use environment variables for configuration management (e.g., `dotenv` crate).
42 | 3. Ensure code is well-documented with inline comments and Rustdoc.
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended-type-checked',
7 | 'plugin:react-hooks/recommended',
8 | 'plugin:react/recommended',
9 | 'plugin:react/jsx-runtime',
10 | 'plugin:@tanstack/eslint-plugin-query/recommended',
11 | 'eslint-config-prettier',
12 | 'plugin:@typescript-eslint/stylistic-type-checked'
13 | ],
14 | ignorePatterns: ['dist', '.eslintrc.cjs'],
15 | parser: '@typescript-eslint/parser',
16 | plugins: ['react-refresh', '@tanstack/query'],
17 | parserOptions: {
18 | ecmaVersion: 'latest',
19 | sourceType: 'module',
20 | project: ['./tsconfig.json', './tsconfig.node.json'],
21 | tsconfigRootDir: __dirname,
22 | },
23 | settings: {
24 | react: {
25 | version: 'detect',
26 | },
27 | },
28 | rules: {
29 | "@typescript-eslint/unbound-method": "off",
30 | "react/prop-types": "off"
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | buy_me_a_coffee: afadil
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | # Dependencies
11 | node_modules
12 | .pnp
13 | .pnp.js
14 | .pnpm-store/
15 |
16 | # Build outputs
17 | dist
18 | dist-ssr
19 | *.local
20 | build
21 | out
22 |
23 | # Testing
24 | coverage
25 | .nyc_output
26 | src-core/tests/output/
27 |
28 | # Environment files
29 | .env
30 | .env.*
31 | !.env.example
32 |
33 | # Editor directories and files
34 | .vscode/*
35 | !.vscode/extensions.json
36 | !.vscode/settings.json
37 | !.vscode/tasks.json
38 | !.vscode/launch.json
39 | .idea
40 | .DS_Store
41 | *.suo
42 | *.ntvs*
43 | *.njsproj
44 | *.sln
45 | *.sw?
46 |
47 | # TypeScript
48 | *.tsbuildinfo
49 | tsconfig.node.tsbuildinfo
50 |
51 | # Cache and temp files
52 | .cache
53 | .temp
54 | .tmp
55 | .eslintcache
56 | .stylelintcache
57 |
58 | # Database
59 | db/*
60 |
61 | # Rust/Cargo (since you have Rust files)
62 | target/
63 | **/*.rs.bk
64 | Cargo.lock
65 |
66 | # OS generated files
67 | .DS_Store
68 | .DS_Store?
69 | ._*
70 | .Spotlight-V100
71 | .Trashes
72 | ehthumbs.db
73 | Thumbs.db
74 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | arrowParens: 'always',
3 | bracketSameLine: false,
4 | bracketSpacing: true,
5 | embeddedLanguageFormatting: 'auto',
6 | endOfLine: 'lf',
7 | htmlWhitespaceSensitivity: 'css',
8 | insertPragma: false,
9 | jsxSingleQuote: false,
10 | printWidth: 100,
11 | proseWrap: 'always',
12 | quoteProps: 'as-needed',
13 | requirePragma: false,
14 | semi: true,
15 | singleAttributePerLine: false,
16 | singleQuote: true,
17 | trailingComma: 'all',
18 | useTabs: false,
19 | //tabWidth: 1,
20 | overrides: [
21 | {
22 | files: ['**/*.json'],
23 | options: {
24 | useTabs: false,
25 | },
26 | },
27 | ],
28 | plugins: ['prettier-plugin-tailwindcss'],
29 | };
30 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
3 | }
4 |
--------------------------------------------------------------------------------
/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/app-icon.png
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/styles.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Wealthfolio
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/button-buy-me-a-coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/button-buy-me-a-coffee.png
--------------------------------------------------------------------------------
/public/illustration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/illustration.png
--------------------------------------------------------------------------------
/public/illustration2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/illustration2.png
--------------------------------------------------------------------------------
/public/market-data/yahoo-finance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/market-data/yahoo-finance.png
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/screenshot.png
--------------------------------------------------------------------------------
/public/wf-vector.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/wf-vector.png
--------------------------------------------------------------------------------
/public/wf-vector2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/public/wf-vector2.png
--------------------------------------------------------------------------------
/src-core/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 |
--------------------------------------------------------------------------------
/src-core/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wealthfolio_core"
3 | version = "1.1.3"
4 | description = "An Open Source Desktop Portfolio tracker"
5 | authors = ["Aziz Fadil"]
6 | license = "AGPL-3.0"
7 | repository = "https://github.com/afadil/wealthfolio"
8 | edition = "2021"
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [dependencies]
13 | serde = { version = "1.0", features = ["derive"] }
14 | serde_json = "1.0"
15 | diesel = { version = "2.2", features = ["sqlite", "chrono", "r2d2", "numeric", "returning_clauses_for_sqlite_3_35"] }
16 | chrono = { version = "0.4", features = ["serde"] }
17 | uuid = { version = "1.10", features = ["v4"] }
18 | rusqlite = { version = "0.34", features = ["bundled"] }
19 | csv = "1.3"
20 | yahoo_finance_api = "4.0"
21 | regex = "1.10"
22 | reqwest = { version = "0.12", features = ["json", "cookies" ] }
23 | thiserror = "1.0"
24 | lazy_static = "1.5"
25 | diesel_migrations = { version = "2.2", features = ["sqlite" ] }
26 | r2d2 = "0.8"
27 | dashmap = "6.1"
28 | async-trait = "0.1"
29 | rust_decimal = { version = "1.37", features = ["maths","serde-float"] }
30 | num-traits = "0.2"
31 | rust_decimal_macros = "1.37"
32 | log = "0.4"
33 | futures = "0.3"
34 | serde_with = "3.4"
35 | tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
36 |
37 | [dev-dependencies]
38 |
--------------------------------------------------------------------------------
/src-core/diesel.toml:
--------------------------------------------------------------------------------
1 | # For documentation on how to configure this file,
2 | # see https://diesel.rs/guides/configuring-diesel-cli
3 |
4 | [print_schema]
5 | file = "src/schema.rs"
6 | custom_type_derives = ["diesel::query_builder::QueryId"]
7 |
8 | [migrations_directory]
9 | dir = "migrations"
10 |
--------------------------------------------------------------------------------
/src-core/migrations/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-core/migrations/.keep
--------------------------------------------------------------------------------
/src-core/migrations/2023-11-08-162221_init_db/down.sql:
--------------------------------------------------------------------------------
1 |
2 | DROP TABLE IF EXISTS goals_allocation;
3 | DROP TABLE IF EXISTS goals;
4 | DROP TABLE IF EXISTS settings;
5 | DROP TABLE IF EXISTS quotes;
6 | DROP TABLE IF EXISTS activities;
7 | DROP TABLE IF EXISTS assets;
8 | DROP TABLE IF EXISTS accounts;
9 | DROP TABLE IF EXISTS platforms;
10 |
11 | DROP INDEX IF EXISTS market_data_data_source_date_symbol_key;
12 | DROP INDEX IF EXISTS market_data_symbol_idx;
13 | DROP INDEX IF EXISTS assets_data_source_symbol_key;
14 |
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-16-023604_portfolio_history/down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS portfolio_history;
2 | DROP INDEX IF EXISTS idx_portfolio_history_account_date;
3 |
4 | DROP TABLE IF EXISTS exchange_rates;
5 | DROP INDEX IF EXISTS idx_exchange_rates_currencies;
6 |
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-16-023604_portfolio_history/up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE portfolio_history (
2 | id TEXT NOT NULL PRIMARY KEY,
3 | account_id TEXT NOT NULL,
4 | date DATE NOT NULL,
5 | total_value NUMERIC NOT NULL DEFAULT 0,
6 | market_value NUMERIC NOT NULL DEFAULT 0,
7 | book_cost NUMERIC NOT NULL DEFAULT 0,
8 | available_cash NUMERIC NOT NULL DEFAULT 0,
9 | net_deposit NUMERIC NOT NULL DEFAULT 0,
10 | currency TEXT NOT NULL,
11 | base_currency TEXT NOT NULL,
12 | total_gain_value NUMERIC NOT NULL DEFAULT 0,
13 | total_gain_percentage NUMERIC NOT NULL DEFAULT 0,
14 | day_gain_percentage NUMERIC NOT NULL DEFAULT 0,
15 | day_gain_value NUMERIC NOT NULL DEFAULT 0,
16 | allocation_percentage NUMERIC NOT NULL DEFAULT 0,
17 | exchange_rate NUMERIC NOT NULL DEFAULT 0,
18 | holdings TEXT,
19 | UNIQUE(account_id, date)
20 | );
21 | CREATE INDEX idx_portfolio_history_account_date ON portfolio_history(account_id, date);
22 |
23 | -- change goals table column types
24 | ALTER TABLE "goals" ADD COLUMN "target_amount_new" NUMERIC NOT NULL DEFAULT 0;
25 | UPDATE "goals" SET "target_amount_new" = "target_amount";
26 | ALTER TABLE "goals" DROP COLUMN "target_amount";
27 | ALTER TABLE "goals" RENAME COLUMN "target_amount_new" TO "target_amount";
28 | ALTER TABLE "goals" ADD COLUMN "is_achieved_new" BOOLEAN NOT NULL DEFAULT false;
29 | UPDATE "goals" SET "is_achieved_new" = COALESCE("is_achieved", false);
30 | ALTER TABLE "goals" DROP COLUMN "is_achieved";
31 | ALTER TABLE "goals" RENAME COLUMN "is_achieved_new" TO "is_achieved";
32 |
33 | CREATE TABLE exchange_rates (
34 | id TEXT NOT NULL PRIMARY KEY,
35 | from_currency TEXT NOT NULL,
36 | to_currency TEXT NOT NULL,
37 | rate NUMERIC NOT NULL,
38 | source TEXT NOT NULL,
39 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
40 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
41 | UNIQUE(from_currency, to_currency)
42 | );
43 |
44 | CREATE INDEX idx_exchange_rates_currencies ON exchange_rates(from_currency, to_currency);
45 |
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-21-023605_settings_to_kv/down.sql:
--------------------------------------------------------------------------------
1 | -- Create a temporary table with the original structure
2 | CREATE TABLE "settings" (
3 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4 | theme TEXT NOT NULL DEFAULT 'light',
5 | font TEXT NOT NULL,
6 | base_currency TEXT NOT NULL
7 | );
8 |
9 | -- Migrate data back from app_settings to settings
10 | INSERT INTO settings (theme, font, base_currency)
11 | SELECT
12 | (SELECT setting_value FROM app_settings WHERE setting_key = 'theme'),
13 | (SELECT setting_value FROM app_settings WHERE setting_key = 'font'),
14 | (SELECT setting_value FROM app_settings WHERE setting_key = 'base_currency');
15 |
16 | -- Drop the new app_settings table
17 | DROP TABLE "app_settings";
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-21-023605_settings_to_kv/up.sql:
--------------------------------------------------------------------------------
1 | -- Create the new app_settings table with key-value structure
2 | CREATE TABLE "app_settings" (
3 | "setting_key" TEXT NOT NULL PRIMARY KEY,
4 | "setting_value" TEXT NOT NULL
5 | );
6 |
7 | -- Migrate existing settings to the new table
8 | INSERT INTO "app_settings" ("setting_key", "setting_value")
9 | SELECT 'theme', theme FROM settings
10 | UNION ALL
11 | SELECT 'font', font FROM settings
12 | UNION ALL
13 | SELECT 'base_currency', base_currency FROM settings;
14 |
15 | -- Drop the old settings table
16 | DROP TABLE "settings";
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-22-012202_init_exchange_rates/down.sql:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-core/migrations/2024-09-22-012202_init_exchange_rates/down.sql
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-22-012202_init_exchange_rates/up.sql:
--------------------------------------------------------------------------------
1 | -- Get the base currency from app_setting
2 | WITH base_currency AS (
3 | SELECT setting_value AS currency
4 | FROM app_settings
5 | WHERE setting_key = 'base_currency'
6 | )
7 |
8 | -- Insert exchange rates for accounts
9 | INSERT OR IGNORE INTO exchange_rates (id, from_currency, to_currency, rate, source)
10 | SELECT
11 | base_currency.currency || accounts.currency || '=X' AS id,
12 | base_currency.currency,
13 | accounts.currency,
14 | 1.0, -- Default rate, to be updated later
15 | 'MANUAL'
16 | FROM accounts
17 | CROSS JOIN base_currency
18 | WHERE accounts.currency != base_currency.currency
19 |
20 | UNION
21 |
22 | -- Insert exchange rates for activities
23 | SELECT DISTINCT
24 | accounts.currency || activities.currency || '=X' AS id,
25 | accounts.currency,
26 | activities.currency,
27 | 1.0, -- Default rate, to be updated later
28 | 'MANUAL'
29 | FROM activities
30 | JOIN accounts ON activities.account_id = accounts.id
31 | WHERE activities.currency != accounts.currency
32 |
33 | UNION
34 |
35 | -- Insert exchange rates from base currency to activity currency
36 | SELECT DISTINCT
37 | base_currency.currency || activities.currency || '=X' AS id,
38 | base_currency.currency,
39 | activities.currency,
40 | 1.0, -- Default rate, to be updated later
41 | 'MANUAL'
42 | FROM activities
43 | CROSS JOIN base_currency
44 | WHERE activities.currency != base_currency.currency;
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-28-225756_add_calculated_at/down.sql:
--------------------------------------------------------------------------------
1 | --- Down migration
2 | ALTER TABLE portfolio_history DROP COLUMN calculated_at;
3 |
--------------------------------------------------------------------------------
/src-core/migrations/2024-09-28-225756_add_calculated_at/up.sql:
--------------------------------------------------------------------------------
1 | -- Your SQL goes here
2 | -- Up migration
3 | ALTER TABLE portfolio_history ADD COLUMN calculated_at TIMESTAMP NOT NULL DEFAULT '2024-09-28 12:00:00';
4 |
5 | CREATE INDEX idx_activities_account_id ON activities(account_id);
--------------------------------------------------------------------------------
/src-core/migrations/2024-10-08-193300_contrib_limits/down.sql:
--------------------------------------------------------------------------------
1 | -- Drop contribution_limits table
2 | DROP TABLE IF EXISTS contribution_limits;
3 |
4 | -- Remove instance_id from app_settings
5 | DELETE FROM app_settings WHERE setting_key = 'instance_id';
--------------------------------------------------------------------------------
/src-core/migrations/2024-10-08-193300_contrib_limits/up.sql:
--------------------------------------------------------------------------------
1 |
2 | CREATE TABLE contribution_limits (
3 | id TEXT PRIMARY KEY NOT NULL,
4 | group_name TEXT NOT NULL,
5 | contribution_year INTEGER NOT NULL,
6 | limit_amount NUMERIC NOT NULL,
7 | account_ids TEXT,
8 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
10 | );
11 |
12 | -- Add instance_id to app_settings
13 | INSERT INTO app_settings (setting_key, setting_value) VALUES ('instance_id', (SELECT hex(randomblob(16))));
14 |
--------------------------------------------------------------------------------
/src-core/migrations/2024-10-15-173026_csv_import_profiles/down.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS activity_import_profiles;
--------------------------------------------------------------------------------
/src-core/migrations/2024-10-15-173026_csv_import_profiles/up.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS activity_import_profiles (
2 | account_id TEXT PRIMARY KEY NOT NULL,
3 | field_mappings TEXT NOT NULL,
4 | activity_mappings TEXT NOT NULL,
5 | symbol_mappings TEXT NOT NULL,
6 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
8 | );
9 |
10 |
--------------------------------------------------------------------------------
/src-core/migrations/2025-01-27-000001_migrate_fx_to_quotes/down.sql:
--------------------------------------------------------------------------------
1 | -- First recreate the exchange_rates table
2 | CREATE TABLE exchange_rates (
3 | id TEXT NOT NULL PRIMARY KEY,
4 | from_currency TEXT NOT NULL,
5 | to_currency TEXT NOT NULL,
6 | rate NUMERIC NOT NULL,
7 | source TEXT NOT NULL,
8 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
9 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
10 | UNIQUE(from_currency, to_currency)
11 | );
12 |
13 | -- Migrate data back from quotes to exchange_rates
14 | INSERT INTO exchange_rates (id, from_currency, to_currency, rate, source, created_at, updated_at)
15 | SELECT
16 | quotes.symbol as id,
17 | substr(quotes.symbol, 1, 3) as from_currency,
18 | substr(quotes.symbol, 4, 3) as to_currency,
19 | quotes.close as rate,
20 | quotes.data_source as source,
21 | quotes.created_at,
22 | quotes.date as updated_at
23 | FROM quotes
24 | JOIN assets ON quotes.symbol = assets.symbol
25 | WHERE assets.asset_type = 'FOREX'
26 | AND quotes.date = (
27 | SELECT MAX(date)
28 | FROM quotes AS q2
29 | WHERE q2.symbol = quotes.symbol
30 | );
31 |
32 | -- Clean up the quotes that were inserted for exchange rates
33 | DELETE FROM quotes
34 | WHERE symbol IN (
35 | SELECT symbol
36 | FROM assets
37 | WHERE asset_type = 'FOREX'
38 | );
39 |
40 | -- Clean up the currency assets
41 | DELETE FROM assets
42 | WHERE asset_type = 'FOREX';
43 |
44 | -- Drop the indexes created in the up migration
45 | DROP INDEX IF EXISTS idx_quotes_symbol_date;
46 | DROP INDEX IF EXISTS idx_quotes_date;
47 | DROP INDEX IF EXISTS idx_assets_type_currency;
48 |
49 |
50 | -- Revert the countries JSON field back from "name" to "code"
51 | UPDATE assets
52 | SET countries = REPLACE(countries, '"name":', '"code":')
53 | WHERE countries IS NOT NULL;
54 |
55 | -- Revert the capitalization of asset types and data sources
56 | UPDATE assets
57 | SET asset_type = CASE
58 | WHEN asset_type = 'EQUITY' THEN 'Equity'
59 | WHEN asset_type = 'CRYPTOCURRENCY' THEN 'Cryptocurrency'
60 | WHEN asset_type = 'CURRENCY' THEN 'Currency'
61 | WHEN asset_type = 'FOREX' THEN 'Currency'
62 | ELSE LOWER(asset_type)
63 | END
64 | WHERE asset_type != 'CASH';
65 |
66 |
67 |
--------------------------------------------------------------------------------
/src-core/migrations/2025-03-17-185736_add_start_end_dates_to_contribution_limits/down.sql:
--------------------------------------------------------------------------------
1 | -- This file should undo anything in `up.sql`
2 | -- Remove start_date and end_date columns from contribution_limits table
3 | ALTER TABLE contribution_limits DROP COLUMN start_date;
4 | ALTER TABLE contribution_limits DROP COLUMN end_date;
5 |
--------------------------------------------------------------------------------
/src-core/migrations/2025-03-17-185736_add_start_end_dates_to_contribution_limits/up.sql:
--------------------------------------------------------------------------------
1 | -- Your SQL goes here
2 |
3 | -- Add start_date and end_date columns to contribution_limits table
4 | ALTER TABLE contribution_limits ADD COLUMN start_date TIMESTAMP NULL;
5 | ALTER TABLE contribution_limits ADD COLUMN end_date TIMESTAMP NULL;
6 |
--------------------------------------------------------------------------------
/src-core/migrations/2025-04-21-195716_create_daily_account_history/down.sql:
--------------------------------------------------------------------------------
1 | -- This file should undo anything in `up.sql`
2 | DROP TABLE IF EXISTS daily_account_valuation;
3 | DROP TABLE IF EXISTS holdings_snapshots; -- This will also drop associated indexes implicitly, but good practice to be explicit for new ones.
4 |
5 |
6 | -- Recreate the original portfolio_history table
7 | CREATE TABLE portfolio_history (
8 | id TEXT NOT NULL PRIMARY KEY,
9 | account_id TEXT NOT NULL,
10 | date DATE NOT NULL,
11 | total_value NUMERIC NOT NULL DEFAULT 0,
12 | market_value NUMERIC NOT NULL DEFAULT 0,
13 | book_cost NUMERIC NOT NULL DEFAULT 0,
14 | available_cash NUMERIC NOT NULL DEFAULT 0,
15 | net_deposit NUMERIC NOT NULL DEFAULT 0,
16 | currency TEXT NOT NULL,
17 | base_currency TEXT NOT NULL,
18 | total_gain_value NUMERIC NOT NULL DEFAULT 0,
19 | total_gain_percentage NUMERIC NOT NULL DEFAULT 0,
20 | day_gain_percentage NUMERIC NOT NULL DEFAULT 0,
21 | day_gain_value NUMERIC NOT NULL DEFAULT 0,
22 | allocation_percentage NUMERIC NOT NULL DEFAULT 0,
23 | exchange_rate NUMERIC NOT NULL DEFAULT 0,
24 | holdings TEXT,
25 | calculated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Added default value
26 | UNIQUE(account_id, date)
27 | );
28 | CREATE INDEX idx_portfolio_history_account_date ON portfolio_history(account_id, date);
29 |
--------------------------------------------------------------------------------
/src-core/migrations/2025-04-21-195716_create_daily_account_history/up.sql:
--------------------------------------------------------------------------------
1 | -- Your SQL goes here
2 | -- Drop the existing table (Warning: Deletes all data!)
3 | DROP TABLE IF EXISTS portfolio_history;
4 |
5 | -- Recreate the table with the new snapshot structure
6 | CREATE TABLE holdings_snapshots (
7 | id TEXT PRIMARY KEY NOT NULL, -- PK: e.g., "ACCOUNTID_YYYY-MM-DD"
8 | account_id TEXT NOT NULL,
9 | snapshot_date DATE NOT NULL, -- Format: YYYY-MM-DD
10 | currency TEXT NOT NULL,
11 |
12 | -- Store complex data as JSON strings
13 | positions TEXT NOT NULL DEFAULT '{}', -- JSON HashMap
14 | cash_balances TEXT NOT NULL DEFAULT '{}', -- JSON HashMap
15 |
16 | -- Store Decimals as TEXT
17 | cost_basis TEXT NOT NULL DEFAULT '0.0',
18 | net_contribution TEXT NOT NULL DEFAULT '0.0',
19 |
20 | -- Store timestamp
21 | calculated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) -- Store as ISO 8601 string
22 | );
23 |
24 |
25 | -- Add indexes for common query patterns
26 | CREATE INDEX IF NOT EXISTS idx_holdings_snapshots_account_date ON holdings_snapshots (account_id, snapshot_date);
27 | CREATE INDEX IF NOT EXISTS idx_holdings_snapshots_date ON holdings_snapshots (snapshot_date);
28 | CREATE INDEX IF NOT EXISTS idx_holdings_snapshots_account_id ON holdings_snapshots (account_id);
29 |
30 |
31 | -- table to store daily account history valuation metrics
32 | CREATE TABLE daily_account_valuation (
33 | id TEXT PRIMARY KEY NOT NULL,
34 | account_id TEXT NOT NULL,
35 | valuation_date DATE NOT NULL, -- Assuming NaiveDate maps to DATE
36 | account_currency TEXT NOT NULL,
37 | base_currency TEXT NOT NULL,
38 | fx_rate_to_base TEXT NOT NULL, -- Storing Decimal as TEXT
39 | cash_balance TEXT NOT NULL,
40 | investment_market_value TEXT NOT NULL,
41 | total_value TEXT NOT NULL,
42 | cost_basis TEXT NOT NULL,
43 | net_contribution TEXT NOT NULL,
44 | calculated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) -- Store as ISO 8601 string with default
45 | );
46 |
47 | -- Add index for faster lookups by account_id and date
48 | CREATE INDEX idx_daily_account_valuation_account_date ON daily_account_valuation(account_id, valuation_date);
49 |
--------------------------------------------------------------------------------
/src-core/src/accounts/accounts_constants.rs:
--------------------------------------------------------------------------------
1 | /// Default account type for new accounts
2 | pub const DEFAULT_ACCOUNT_TYPE: &str = "SECURITIES";
3 |
4 |
--------------------------------------------------------------------------------
/src-core/src/accounts/accounts_traits.rs:
--------------------------------------------------------------------------------
1 | use diesel::sqlite::SqliteConnection;
2 | use async_trait::async_trait;
3 |
4 | use super::accounts_model::{Account, AccountUpdate, NewAccount};
5 | use crate::errors::Result;
6 |
7 | /// Trait defining the contract for Account repository operations.
8 | #[async_trait]
9 | pub trait AccountRepositoryTrait: Send + Sync {
10 | fn create_in_transaction(
11 | &self,
12 | new_account: NewAccount,
13 | conn: &mut SqliteConnection
14 | ) -> Result;
15 | async fn update(&self, account_update: AccountUpdate) -> Result;
16 | async fn delete(&self, account_id: &str) -> Result;
17 | fn get_by_id(&self, account_id: &str) -> Result;
18 | fn list(
19 | &self,
20 | is_active_filter: Option,
21 | account_ids: Option<&[String]>,
22 | ) -> Result>;
23 | }
24 |
25 | /// Trait defining the contract for Account service operations.
26 | #[async_trait]
27 | pub trait AccountServiceTrait: Send + Sync {
28 | async fn create_account(&self, new_account: NewAccount) -> Result;
29 | async fn update_account(&self, account_update: AccountUpdate) -> Result;
30 | async fn delete_account(&self, account_id: &str) -> Result<()>;
31 | fn get_account(&self, account_id: &str) -> Result;
32 | fn list_accounts(
33 | &self,
34 | is_active_filter: Option,
35 | account_ids: Option<&[String]>,
36 | ) -> Result>;
37 | fn get_all_accounts(&self) -> Result>;
38 | fn get_active_accounts(&self) -> Result>;
39 | fn get_accounts_by_ids(&self, account_ids: &[String]) -> Result>;
40 | }
--------------------------------------------------------------------------------
/src-core/src/accounts/mod.rs:
--------------------------------------------------------------------------------
1 | // Module declarations
2 | pub(crate) mod accounts_constants;
3 | pub(crate) mod accounts_model;
4 | pub(crate) mod accounts_repository;
5 | pub(crate) mod accounts_service;
6 | pub(crate) mod accounts_traits;
7 |
8 | // Re-export the public interface
9 | pub use accounts_constants::*;
10 | // pub use accounts_errors::*;
11 | pub use accounts_model::{Account, AccountDB, AccountUpdate, NewAccount};
12 | pub use accounts_repository::AccountRepository;
13 | pub use accounts_service::AccountService;
14 | pub use accounts_traits::{AccountRepositoryTrait, AccountServiceTrait};
15 |
16 |
--------------------------------------------------------------------------------
/src-core/src/activities/activities_constants.rs:
--------------------------------------------------------------------------------
1 | /// Activity types
2 | pub const ACTIVITY_TYPE_BUY: &str = "BUY";
3 | pub const ACTIVITY_TYPE_SELL: &str = "SELL";
4 | pub const ACTIVITY_TYPE_DIVIDEND: &str = "DIVIDEND";
5 | pub const ACTIVITY_TYPE_INTEREST: &str = "INTEREST";
6 | pub const ACTIVITY_TYPE_DEPOSIT: &str = "DEPOSIT";
7 | pub const ACTIVITY_TYPE_WITHDRAWAL: &str = "WITHDRAWAL";
8 | pub const ACTIVITY_TYPE_TRANSFER_IN: &str = "TRANSFER_IN";
9 | pub const ACTIVITY_TYPE_TRANSFER_OUT: &str = "TRANSFER_OUT";
10 | pub const ACTIVITY_TYPE_FEE: &str = "FEE";
11 | pub const ACTIVITY_TYPE_TAX: &str = "TAX";
12 | pub const ACTIVITY_TYPE_SPLIT: &str = "SPLIT";
13 | pub const ACTIVITY_TYPE_ADD_HOLDING: &str = "ADD_HOLDING";
14 | pub const ACTIVITY_TYPE_REMOVE_HOLDING: &str = "REMOVE_HOLDING";
15 |
16 | /// Trading activity types
17 | pub const TRADING_ACTIVITY_TYPES: [&str; 5] = [
18 | ACTIVITY_TYPE_BUY,
19 | ACTIVITY_TYPE_SELL,
20 | ACTIVITY_TYPE_SPLIT,
21 | ACTIVITY_TYPE_ADD_HOLDING,
22 | ACTIVITY_TYPE_REMOVE_HOLDING,
23 | ];
24 |
25 | /// Income activity types
26 | pub const INCOME_ACTIVITY_TYPES: [&str; 2] = [
27 | ACTIVITY_TYPE_DIVIDEND,
28 | ACTIVITY_TYPE_INTEREST,
29 | ];
--------------------------------------------------------------------------------
/src-core/src/activities/activities_errors.rs:
--------------------------------------------------------------------------------
1 | use diesel::result::Error as DieselError;
2 | use thiserror::Error;
3 |
4 | /// Custom error type for activity-related operations
5 | #[derive(Debug, Error)]
6 | pub enum ActivityError {
7 | #[error("Database error: {0}")]
8 | DatabaseError(String),
9 | #[error("Not found: {0}")]
10 | NotFound(String),
11 | #[error("Invalid data: {0}")]
12 | InvalidData(String),
13 | #[error("Asset error: {0}")]
14 | AssetError(String),
15 | #[error("Currency exchange error: {0}")]
16 | CurrencyExchangeError(String),
17 | }
18 |
19 | impl From for ActivityError {
20 | fn from(err: DieselError) -> Self {
21 | match err {
22 | DieselError::NotFound => ActivityError::NotFound("Record not found".to_string()),
23 | _ => ActivityError::DatabaseError(err.to_string()),
24 | }
25 | }
26 | }
27 |
28 | impl From for String {
29 | fn from(error: ActivityError) -> Self {
30 | error.to_string()
31 | }
32 | }
33 |
34 | impl From for diesel::result::Error {
35 | fn from(err: ActivityError) -> Self {
36 | // Convert ActivityError to a diesel error
37 | // Using DatabaseError as it's the most appropriate for general errors
38 | diesel::result::Error::DatabaseError(
39 | diesel::result::DatabaseErrorKind::SerializationFailure,
40 | Box::new(format!("{}", err))
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src-core/src/activities/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod activities_constants;
2 | pub(crate) mod activities_errors;
3 | pub(crate) mod activities_model;
4 | pub(crate) mod activities_repository;
5 | pub(crate) mod activities_service;
6 | pub(crate) mod activities_traits;
7 |
8 | pub use activities_constants::*;
9 | pub use activities_errors::ActivityError;
10 | pub use activities_model::{Activity, ActivityType, ActivityDB, ActivityDetails, ActivityImport, ActivitySearchResponse, ActivitySearchResponseMeta, ActivityUpdate, ImportMapping, ImportMappingData, NewActivity, Sort};
11 | pub use activities_repository::ActivityRepository;
12 | pub use activities_service::ActivityService;
13 | pub use activities_traits::{ActivityRepositoryTrait, ActivityServiceTrait};
--------------------------------------------------------------------------------
/src-core/src/assets/assets_constants.rs:
--------------------------------------------------------------------------------
1 |
2 |
3 | /// Default asset type for cash assets
4 | pub const CASH_ASSET_TYPE: &str = "CASH";
5 |
6 | /// Default asset type for currency assets
7 | pub const FOREX_ASSET_TYPE: &str = "FOREX";
8 |
9 | /// Default asset class for cash and currency assets
10 | pub const CASH_ASSET_CLASS: &str = "CASH";
--------------------------------------------------------------------------------
/src-core/src/assets/assets_traits.rs:
--------------------------------------------------------------------------------
1 | use super::assets_model::{Asset, AssetData, NewAsset, UpdateAssetProfile};
2 | use crate::errors::Result;
3 |
4 | /// Trait defining the contract for Asset service operations.
5 | #[async_trait::async_trait]
6 | pub trait AssetServiceTrait: Send + Sync {
7 | fn get_assets(&self) -> Result>;
8 | fn get_asset_by_id(&self, asset_id: &str) -> Result;
9 | async fn get_asset_data(&self, asset_id: &str) -> Result;
10 | async fn update_asset_profile(&self, asset_id: &str, payload: UpdateAssetProfile) -> Result;
11 | fn load_cash_assets(&self, base_currency: &str) -> Result>;
12 | async fn create_cash_asset(&self, currency: &str) -> Result;
13 | async fn get_or_create_asset(&self, asset_id: &str, context_currency: Option) -> Result;
14 | async fn update_asset_data_source(&self, asset_id: &str, data_source: String) -> Result;
15 | async fn get_assets_by_symbols(&self, symbols: &Vec) -> Result>;
16 | }
17 |
18 | /// Trait defining the contract for Asset repository operations.
19 | #[async_trait::async_trait]
20 | pub trait AssetRepositoryTrait: Send + Sync {
21 | async fn create(&self, new_asset: NewAsset) -> Result;
22 | async fn update_profile(&self, asset_id: &str, payload: UpdateAssetProfile) -> Result;
23 | async fn update_data_source(&self, asset_id: &str, data_source: String) -> Result;
24 | fn get_by_id(&self, asset_id: &str) -> Result;
25 | fn list(&self) -> Result>;
26 | fn list_cash_assets(&self, base_currency: &str) -> Result>;
27 | fn list_by_symbols(&self, symbols: &Vec) -> Result>;
28 | }
--------------------------------------------------------------------------------
/src-core/src/assets/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod assets_constants;
2 | // pub(crate) mod assets_errors;
3 | pub(crate) mod assets_model;
4 | pub(crate) mod assets_repository;
5 | pub(crate) mod assets_service;
6 | pub(crate) mod assets_traits;
7 |
8 | // Re-export the public interface
9 | pub use assets_constants::*;
10 | pub use assets_model::{Asset, NewAsset, UpdateAssetProfile, AssetData};
11 | pub use assets_repository::AssetRepository;
12 | pub use assets_service::AssetService;
13 | pub use assets_traits::{AssetServiceTrait, AssetRepositoryTrait};
14 |
15 | // Re-export error types for convenience
16 | // pub use assets_errors::{AssetError, Result};
17 |
--------------------------------------------------------------------------------
/src-core/src/constants.rs:
--------------------------------------------------------------------------------
1 |
2 | /// Total account ID
3 | pub const PORTFOLIO_TOTAL_ACCOUNT_ID: &str = "TOTAL";
4 |
5 | /// Decimal precision for valuation calculations
6 | pub const DECIMAL_PRECISION: u32 = 6;
7 |
8 | /// Decimal precision for display
9 | pub const DISPLAY_DECIMAL_PRECISION: u32 = 2;
10 |
11 | /// Quantity threshold for significant positions
12 | pub const QUANTITY_THRESHOLD: &str = "0.00000001";
--------------------------------------------------------------------------------
/src-core/src/fx/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod currency_converter;
2 | pub mod fx_errors;
3 | pub mod fx_model;
4 | pub mod fx_repository;
5 | pub mod fx_service;
6 | pub mod fx_traits;
7 |
8 | pub use fx_errors::FxError;
9 | pub use fx_model::{ExchangeRate, NewExchangeRate};
10 | pub use fx_service::FxService;
11 | pub use fx_repository::FxRepository;
12 | pub use currency_converter::CurrencyConverter;
13 | pub use fx_traits::{FxRepositoryTrait, FxServiceTrait};
14 |
--------------------------------------------------------------------------------
/src-core/src/goals/goals_model.rs:
--------------------------------------------------------------------------------
1 | use diesel::prelude::*;
2 | use diesel::Queryable;
3 | use diesel::Selectable;
4 | use serde::{Deserialize, Serialize};
5 | use crate::accounts::Account;
6 |
7 | #[derive(
8 | Queryable,
9 | Identifiable,
10 | AsChangeset,
11 | Selectable,
12 | PartialEq,
13 | Serialize,
14 | Deserialize,
15 | Debug,
16 | Clone,
17 | )]
18 | #[diesel(table_name = crate::schema::goals)]
19 | #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
20 | #[serde(rename_all = "camelCase")]
21 | pub struct Goal {
22 | pub id: String,
23 | pub title: String,
24 | pub description: Option,
25 | pub target_amount: f64,
26 | pub is_achieved: bool,
27 | }
28 |
29 | #[derive(Insertable, Serialize, Deserialize, Debug, Clone)]
30 | #[diesel(table_name = crate::schema::goals)]
31 | #[serde(rename_all = "camelCase")]
32 | pub struct NewGoal {
33 | pub id: Option,
34 | pub title: String,
35 | pub description: Option,
36 | pub target_amount: f64,
37 | pub is_achieved: bool,
38 | }
39 |
40 | #[derive(
41 | Insertable,
42 | Queryable,
43 | Identifiable,
44 | Associations,
45 | AsChangeset,
46 | Selectable,
47 | PartialEq,
48 | Serialize,
49 | Deserialize,
50 | Debug,
51 | Clone,
52 | )]
53 | #[diesel(belongs_to(Goal))]
54 | #[diesel(belongs_to(Account))]
55 | #[diesel(table_name = crate::schema::goals_allocation)]
56 | #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
57 | #[serde(rename_all = "camelCase")]
58 | pub struct GoalsAllocation {
59 | pub id: String,
60 | pub goal_id: String,
61 | pub account_id: String,
62 | pub percent_allocation: i32,
63 | }
64 |
--------------------------------------------------------------------------------
/src-core/src/goals/goals_service.rs:
--------------------------------------------------------------------------------
1 | use crate::goals::goals_model::{Goal, GoalsAllocation, NewGoal};
2 | use crate::errors::Result;
3 | use crate::goals::goals_traits::{GoalRepositoryTrait, GoalServiceTrait};
4 | use async_trait::async_trait;
5 | use std::sync::Arc;
6 |
7 | pub struct GoalService {
8 | goal_repo: Arc,
9 | }
10 |
11 | impl GoalService {
12 | pub fn new(goal_repo: Arc) -> Self {
13 | GoalService {
14 | goal_repo,
15 | }
16 | }
17 | }
18 |
19 | #[async_trait]
20 | impl GoalServiceTrait for GoalService {
21 | fn get_goals(&self) -> Result> {
22 | self.goal_repo.load_goals()
23 | }
24 |
25 | async fn create_goal(
26 | &self,
27 | new_goal: NewGoal,
28 | ) -> Result {
29 | self.goal_repo.insert_new_goal(new_goal).await
30 | }
31 |
32 | async fn update_goal(
33 | &self,
34 | updated_goal_data: Goal,
35 | ) -> Result {
36 | self.goal_repo.update_goal(updated_goal_data).await
37 | }
38 |
39 | async fn delete_goal(
40 | &self,
41 | goal_id_to_delete: String,
42 | ) -> Result {
43 | self.goal_repo.delete_goal(goal_id_to_delete).await
44 | }
45 |
46 | async fn upsert_goal_allocations(
47 | &self,
48 | allocations: Vec,
49 | ) -> Result {
50 | self.goal_repo.upsert_goal_allocations(allocations).await
51 | }
52 |
53 | fn load_goals_allocations(&self) -> Result> {
54 | self.goal_repo.load_allocations_for_non_achieved_goals()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src-core/src/goals/goals_traits.rs:
--------------------------------------------------------------------------------
1 | use crate::goals::goals_model::{Goal, GoalsAllocation, NewGoal};
2 | use crate::errors::Result;
3 | use async_trait::async_trait;
4 |
5 | /// Trait for goal repository operations
6 | #[async_trait]
7 | pub trait GoalRepositoryTrait: Send + Sync {
8 | fn load_goals(&self) -> Result>;
9 | async fn insert_new_goal(&self, new_goal: NewGoal) -> Result;
10 | async fn update_goal(&self, goal_update: Goal) -> Result;
11 | async fn delete_goal(&self, goal_id_to_delete: String) -> Result;
12 | fn load_allocations_for_non_achieved_goals(&self) -> Result>;
13 | async fn upsert_goal_allocations(&self, allocations: Vec) -> Result;
14 | }
15 |
16 | /// Trait for goal service operations
17 | #[async_trait]
18 | pub trait GoalServiceTrait: Send + Sync {
19 | fn get_goals(&self) -> Result>;
20 | async fn create_goal(&self, new_goal: NewGoal) -> Result;
21 | async fn update_goal(&self, updated_goal_data: Goal) -> Result;
22 | async fn delete_goal(&self, goal_id_to_delete: String) -> Result;
23 | async fn upsert_goal_allocations(&self, allocations: Vec) -> Result;
24 | fn load_goals_allocations(&self) -> Result>;
25 | }
--------------------------------------------------------------------------------
/src-core/src/goals/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod goals_repository;
2 | pub mod goals_service;
3 | pub mod goals_model;
4 | pub mod goals_traits;
5 |
6 | pub use goals_service::GoalService;
7 | pub use goals_repository::GoalRepository;
8 | pub use goals_traits::{GoalRepositoryTrait, GoalServiceTrait};
9 |
--------------------------------------------------------------------------------
/src-core/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod db;
2 | pub mod constants;
3 | pub mod accounts;
4 | pub mod activities;
5 | pub mod assets;
6 |
7 | pub mod errors;
8 | pub mod fx;
9 | pub mod goals;
10 | pub mod limits;
11 | pub mod market_data;
12 | pub mod portfolio;
13 | pub mod schema;
14 | pub mod settings;
15 | pub mod utils;
16 | pub use portfolio::*;
17 | pub use assets::*;
18 |
19 | pub use errors::Error;
20 | pub use errors::Result;
21 |
22 |
--------------------------------------------------------------------------------
/src-core/src/limits/limits_model.rs:
--------------------------------------------------------------------------------
1 | use chrono::NaiveDateTime;
2 | use diesel::prelude::*;
3 | use rust_decimal::Decimal;
4 | use serde::{Deserialize, Serialize};
5 | use std::collections::HashMap;
6 |
7 | #[derive(Queryable, Insertable, Identifiable, Serialize, Deserialize, Debug, Clone)]
8 | #[diesel(table_name = crate::schema::contribution_limits)]
9 | #[serde(rename_all = "camelCase")]
10 | pub struct ContributionLimit {
11 | pub id: String,
12 | pub group_name: String,
13 | pub contribution_year: i32,
14 | pub limit_amount: f64,
15 | pub account_ids: Option,
16 | pub created_at: NaiveDateTime,
17 | pub updated_at: NaiveDateTime,
18 | pub start_date: Option,
19 | pub end_date: Option,
20 | }
21 |
22 | #[derive(Insertable, AsChangeset, Serialize, Deserialize, Debug, Clone)]
23 | #[diesel(table_name = crate::schema::contribution_limits)]
24 | #[serde(rename_all = "camelCase")]
25 | pub struct NewContributionLimit {
26 | pub id: Option,
27 | pub group_name: String,
28 | pub contribution_year: i32,
29 | pub limit_amount: f64,
30 | pub account_ids: Option,
31 | pub start_date: Option,
32 | pub end_date: Option,
33 | }
34 |
35 | #[derive(Serialize, Debug)]
36 | #[serde(rename_all = "camelCase")]
37 | pub struct AccountDeposit {
38 | pub amount: Decimal,
39 | pub currency: String,
40 | pub converted_amount: Decimal,
41 | }
42 |
43 | #[derive(Serialize, Debug)]
44 | #[serde(rename_all = "camelCase")]
45 | pub struct DepositsCalculation {
46 | pub total: Decimal,
47 | pub base_currency: String,
48 | pub by_account: HashMap,
49 | }
50 |
--------------------------------------------------------------------------------
/src-core/src/limits/limits_traits.rs:
--------------------------------------------------------------------------------
1 | use super::limits_model::{ ContributionLimit, DepositsCalculation, NewContributionLimit};
2 | use crate::errors::Result;
3 | use async_trait::async_trait;
4 |
5 | /// Trait defining the contract for Contribution Limit repository operations.
6 | #[async_trait]
7 | pub trait ContributionLimitRepositoryTrait: Send + Sync {
8 | fn get_contribution_limit(&self, id: &str) -> Result;
9 | fn get_contribution_limits(&self) -> Result>;
10 | async fn create_contribution_limit(&self, new_limit: NewContributionLimit) -> Result;
11 | async fn update_contribution_limit(
12 | &self,
13 | id: &str,
14 | updated_limit: NewContributionLimit,
15 | ) -> Result;
16 | async fn delete_contribution_limit(&self, id: &str) -> Result<()>;
17 | }
18 |
19 | /// Trait defining the contract for Contribution Limit service operations.
20 | #[async_trait]
21 | pub trait ContributionLimitServiceTrait: Send + Sync {
22 | fn get_contribution_limits(&self) -> Result>;
23 | async fn create_contribution_limit(&self, new_limit: NewContributionLimit) -> Result;
24 | async fn update_contribution_limit(
25 | &self,
26 | id: &str,
27 | updated_limit: NewContributionLimit,
28 | ) -> Result;
29 | async fn delete_contribution_limit(&self, id: &str) -> Result<()>;
30 | fn calculate_deposits_for_contribution_limit(
31 | &self,
32 | limit_id: &str,
33 | base_currency: &str,
34 | ) -> Result;
35 | // Note: calculate_deposits_by_period might be better as a private helper or part of the trait if needed elsewhere
36 | }
--------------------------------------------------------------------------------
/src-core/src/limits/mod.rs:
--------------------------------------------------------------------------------
1 | mod limits_model;
2 | mod limits_repository;
3 | mod limits_service;
4 | mod limits_traits;
5 |
6 | pub use limits_model::{AccountDeposit, ContributionLimit, DepositsCalculation, NewContributionLimit};
7 | pub use limits_service::ContributionLimitService;
8 | pub use limits_repository::ContributionLimitRepository;
9 | pub use limits_traits::ContributionLimitServiceTrait;
10 |
11 |
--------------------------------------------------------------------------------
/src-core/src/market_data/market_data_constants.rs:
--------------------------------------------------------------------------------
1 | /// Data source identifiers
2 | pub const DATA_SOURCE_YAHOO: &str = "YAHOO";
3 | pub const DATA_SOURCE_MANUAL: &str = "MANUAL";
4 | pub const DATA_SOURCE_CALCULATED: &str = "CALCULATED";
5 |
6 | /// Default values
7 | pub const DEFAULT_QUOTE_BATCH_SIZE: usize = 1000;
8 | pub const DEFAULT_HISTORY_DAYS: i64 = 3650; // 10 years
9 |
10 | /// Time constants
11 | pub const MARKET_DATA_QUOTE_TIME: (u32, u32, u32) = (16, 0, 0); // 4:00 PM
--------------------------------------------------------------------------------
/src-core/src/market_data/market_data_errors.rs:
--------------------------------------------------------------------------------
1 | use thiserror::Error;
2 |
3 | use crate::errors::DatabaseError;
4 | use yahoo_finance_api::YahooError;
5 |
6 | #[derive(Error, Debug)]
7 | pub enum MarketDataError {
8 | #[error("Database error: {0}")]
9 | DatabaseError(#[from] diesel::result::Error),
10 |
11 | #[error("Database error: {0}")]
12 | DatabaseConnectionError(#[from] DatabaseError),
13 |
14 | #[error("Provider error: {0}")]
15 | ProviderError(String),
16 |
17 | #[error("Network error: {0}")]
18 | NetworkError(#[from] reqwest::Error),
19 |
20 | #[error("Parsing error: {0}")]
21 | ParsingError(String),
22 |
23 | #[error("Not found: {0}")]
24 | NotFound(String),
25 |
26 | #[error("Unauthorized: {0}")]
27 | Unauthorized(String),
28 |
29 | #[error("Rate limit exceeded")]
30 | RateLimitExceeded,
31 |
32 | #[error("Invalid data: {0}")]
33 | InvalidData(String),
34 |
35 | #[error("Unknown error: {0}")]
36 | Unknown(String),
37 | }
38 |
39 | impl From for MarketDataError {
40 | fn from(error: YahooError) -> Self {
41 | match error {
42 | YahooError::FetchFailed(e) => MarketDataError::ProviderError(e),
43 | YahooError::NoQuotes => MarketDataError::NotFound("No quotes found".to_string()),
44 | YahooError::NoResult => MarketDataError::NotFound("No data found".to_string()),
45 | _ => MarketDataError::Unknown(error.to_string()),
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src-core/src/market_data/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod market_data_constants;
2 | pub(crate) mod market_data_errors;
3 | pub(crate) mod market_data_model;
4 | pub(crate) mod market_data_repository;
5 | pub(crate) mod market_data_service;
6 | pub(crate) mod market_data_traits;
7 | pub(crate) mod providers;
8 |
9 | // Re-export the public interface
10 | pub use market_data_constants::*;
11 | pub use market_data_model::{Quote, QuoteSummary, QuoteRequest, DataSource, MarketDataProviderInfo};
12 | pub use market_data_repository::MarketDataRepository;
13 | pub use market_data_service::MarketDataService;
14 | pub use market_data_traits::MarketDataServiceTrait;
15 |
16 | // Re-export provider types
17 | pub use providers::market_data_provider::{MarketDataProvider, AssetProfiler};
18 |
19 | // Re-export error types for convenience
20 | pub use market_data_errors::MarketDataError;
21 |
--------------------------------------------------------------------------------
/src-core/src/market_data/providers/manual_provider.rs:
--------------------------------------------------------------------------------
1 |
2 | use crate::market_data::market_data_model::DataSource;
3 | use crate::market_data::providers::market_data_provider::AssetProfiler;
4 | use crate::market_data::market_data_errors::MarketDataError;
5 |
6 | use super::models::AssetProfile;
7 | pub struct ManualProvider;
8 |
9 | impl ManualProvider {
10 | pub fn new() -> Result {
11 | Ok(ManualProvider)
12 | }
13 | }
14 |
15 | #[async_trait::async_trait]
16 | impl AssetProfiler for ManualProvider {
17 | async fn get_asset_profile(&self, symbol: &str) -> Result {
18 | if symbol.starts_with("$CASH-") {
19 | Ok(AssetProfile {
20 | id: Some(symbol.to_string()),
21 | isin: None,
22 | name: Some(symbol.to_string()),
23 | asset_type: Some("CASH".to_string()),
24 | asset_class: Some("CASH".to_string()),
25 | asset_sub_class: Some("CASH".to_string()),
26 | symbol: symbol.to_string(),
27 | data_source: DataSource::Manual.as_str().to_string(),
28 | currency: symbol[6..].to_string(),
29 | ..Default::default()
30 | })
31 | } else {
32 | Ok(AssetProfile {
33 | id: Some(symbol.to_string()),
34 | isin: None,
35 | name: Some(symbol.to_string()),
36 | asset_type: Some("EQUITY".to_string()),
37 | symbol: symbol.to_string(),
38 | data_source: DataSource::Manual.as_str().to_string(),
39 | ..Default::default()
40 | })
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src-core/src/market_data/providers/market_data_provider.rs:
--------------------------------------------------------------------------------
1 | use crate::market_data::{MarketDataError, Quote as ModelQuote, QuoteSummary};
2 | use async_trait::async_trait;
3 | use std::time::SystemTime;
4 |
5 | use super::models::AssetProfile;
6 |
7 |
8 |
9 | #[async_trait]
10 | pub trait MarketDataProvider: Send + Sync {
11 | async fn search_ticker(&self, query: &str) -> Result, MarketDataError>;
12 | async fn get_latest_quote(&self, symbol: &str, fallback_currency: String) -> Result;
13 | async fn get_historical_quotes(
14 | &self,
15 | symbol: &str,
16 | start: SystemTime,
17 | end: SystemTime,
18 | fallback_currency: String,
19 | ) -> Result, MarketDataError>;
20 |
21 | /// Fetch historical quotes for multiple symbols in parallel
22 | async fn get_historical_quotes_bulk(
23 | &self,
24 | symbols_with_currencies: &[(String, String)],
25 | start: SystemTime,
26 | end: SystemTime,
27 | ) -> Result<(Vec, Vec<(String, String)>), MarketDataError>;
28 | }
29 |
30 | #[async_trait]
31 | pub trait AssetProfiler: Send + Sync {
32 | async fn get_asset_profile(&self, symbol: &str) -> Result;
33 | }
34 |
--------------------------------------------------------------------------------
/src-core/src/market_data/providers/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod manual_provider;
2 | pub mod models;
3 | pub mod provider_registry;
4 | pub mod yahoo_provider;
5 | pub mod market_data_provider;
6 | pub use provider_registry::ProviderRegistry;
7 |
--------------------------------------------------------------------------------
/src-core/src/market_data/providers/provider_registry.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use crate::market_data::providers::market_data_provider::{MarketDataProvider, AssetProfiler};
4 | use crate::market_data::market_data_errors::MarketDataError;
5 | use crate::market_data::market_data_model::DataSource;
6 | use super::{yahoo_provider::YahooProvider, manual_provider::ManualProvider};
7 |
8 | pub struct ProviderRegistry {
9 | yahoo: Arc,
10 | manual: Arc,
11 | }
12 |
13 | impl ProviderRegistry {
14 | pub async fn new() -> Result {
15 | Ok(Self {
16 | yahoo: Arc::new(YahooProvider::new().await?),
17 | manual: Arc::new(ManualProvider::new()?),
18 | })
19 | }
20 |
21 | pub fn get_provider(&self, source: DataSource) -> Arc {
22 | match source {
23 | DataSource::Yahoo => self.yahoo.clone(),
24 | DataSource::Manual => panic!("Manual provider does not support market data operations"),
25 | }
26 | }
27 |
28 | pub fn get_profiler(&self, source: DataSource) -> Arc {
29 | match source {
30 | DataSource::Manual => self.manual.clone(),
31 | DataSource::Yahoo => self.yahoo.clone(),
32 | }
33 | }
34 |
35 | pub fn default_provider(&self) -> Arc {
36 | self.yahoo.clone()
37 | }
38 | }
--------------------------------------------------------------------------------
/src-core/src/portfolio/holdings/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod holdings_model;
2 | pub mod holdings_service;
3 | pub mod holdings_valuation_service;
4 |
5 | pub use holdings_model::*;
6 | pub use holdings_service::*;
7 | pub use holdings_valuation_service::*;
8 |
9 | #[cfg(test)]
10 | mod holdings_valuation_service_tests;
11 |
12 |
--------------------------------------------------------------------------------
/src-core/src/portfolio/income/income_model.rs:
--------------------------------------------------------------------------------
1 | use rust_decimal::Decimal;
2 | use serde::{Deserialize, Serialize};
3 | use std::collections::HashMap;
4 | use crate::activities::activities_model::IncomeData;
5 |
6 |
7 | #[derive(Debug, Serialize, Deserialize)]
8 | #[serde(rename_all = "camelCase")]
9 | pub struct IncomeSummary {
10 | pub period: String,
11 | pub by_month: HashMap,
12 | pub by_type: HashMap,
13 | pub by_symbol: HashMap,
14 | pub by_currency: HashMap,
15 | pub total_income: Decimal,
16 | pub currency: String,
17 | pub monthly_average: Decimal,
18 | pub yoy_growth: Option,
19 | }
20 |
21 | impl IncomeSummary {
22 | pub fn new(period: &str, currency: String) -> Self {
23 | IncomeSummary {
24 | period: period.to_string(),
25 | by_month: HashMap::new(),
26 | by_type: HashMap::new(),
27 | by_symbol: HashMap::new(),
28 | by_currency: HashMap::new(),
29 | total_income: Decimal::ZERO,
30 | currency,
31 | monthly_average: Decimal::ZERO,
32 | yoy_growth: None,
33 | }
34 | }
35 |
36 | pub fn add_income(&mut self, data: &IncomeData, converted_amount: Decimal) {
37 | *self.by_month.entry(data.date.to_string()).or_insert_with(|| Decimal::ZERO) += &converted_amount;
38 | *self.by_type.entry(data.income_type.clone()).or_insert_with(|| Decimal::ZERO) += &converted_amount;
39 | *self
40 | .by_symbol
41 | .entry(format!("[{}]-{}", data.symbol, data.symbol_name))
42 | .or_insert_with(|| Decimal::ZERO) += &converted_amount;
43 | *self.by_currency.entry(data.currency.clone()).or_insert_with(|| Decimal::ZERO) += &data.amount;
44 | self.total_income += &converted_amount;
45 | }
46 |
47 | pub fn calculate_monthly_average(&mut self, num_months: Option) {
48 | let months = num_months.unwrap_or_else(|| self.by_month.len() as u32);
49 | if months > 0 {
50 | self.monthly_average = &self.total_income / Decimal::new(months as i64, 0);
51 | }
52 | }
53 | }
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src-core/src/portfolio/income/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod income_service;
2 | pub mod income_model;
3 |
4 | pub use income_service::{IncomeServiceTrait, IncomeService};
5 | pub use income_model::*;
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src-core/src/portfolio/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod income;
2 | pub mod performance;
3 | pub mod snapshot;
4 | pub mod valuation;
5 | pub mod holdings;
6 |
--------------------------------------------------------------------------------
/src-core/src/portfolio/performance/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod performance_model;
2 | pub mod performance_service;
3 |
4 | pub use performance_model::*;
5 | pub use performance_service::*;
--------------------------------------------------------------------------------
/src-core/src/portfolio/performance/performance_model.rs:
--------------------------------------------------------------------------------
1 | use chrono::NaiveDate;
2 | use serde::{Deserialize, Serialize};
3 | use rust_decimal::Decimal;
4 |
5 | #[derive(Debug, Clone, Serialize, Deserialize)]
6 | pub struct CumulativeReturn {
7 | pub date: NaiveDate,
8 | pub value: Decimal,
9 | }
10 |
11 | #[derive(Debug, Serialize, Deserialize, PartialEq)]
12 | #[serde(rename_all = "camelCase")]
13 | pub struct TotalReturn {
14 | pub rate: Decimal,
15 | pub amount: Decimal,
16 | }
17 |
18 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
19 | #[serde(rename_all = "camelCase")]
20 | pub enum ReturnMethod {
21 | TimeWeighted,
22 | MoneyWeighted,
23 | SimpleReturn,
24 | SymbolPriceBased,
25 | NotApplicable,
26 | }
27 |
28 | impl Default for ReturnMethod {
29 | fn default() -> Self {
30 | ReturnMethod::TimeWeighted
31 | }
32 | }
33 |
34 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35 | pub struct ReturnData {
36 | pub date: NaiveDate,
37 | pub value: Decimal,
38 | }
39 |
40 | #[derive(Debug, Serialize, Deserialize)]
41 | #[serde(rename_all = "camelCase")]
42 | pub struct PerformanceMetrics {
43 | pub id: String,
44 | pub returns: Vec,
45 | pub period_start_date: Option,
46 | pub period_end_date: Option,
47 | pub currency: String,
48 | pub cumulative_twr: Decimal,
49 | pub gain_loss_amount: Option,
50 | pub annualized_twr: Decimal,
51 | pub simple_return: Decimal,
52 | pub annualized_simple_return: Decimal,
53 | pub cumulative_mwr: Decimal,
54 | pub annualized_mwr: Decimal,
55 | pub volatility: Decimal,
56 | pub max_drawdown: Decimal,
57 | }
58 |
59 |
60 | // This struct now only holds the calculated performance metrics.
61 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
62 | #[serde(rename_all = "camelCase")]
63 | pub struct SimplePerformanceMetrics {
64 | pub account_id: String,
65 | pub account_currency: Option,
66 | pub base_currency: Option,
67 | pub fx_rate_to_base: Option,
68 | pub total_value: Option,
69 | pub total_gain_loss_amount: Option,
70 | pub cumulative_return_percent: Option,
71 | pub day_gain_loss_amount: Option,
72 | pub day_return_percent_mod_dietz: Option,
73 | pub portfolio_weight: Option,
74 | }
75 |
--------------------------------------------------------------------------------
/src-core/src/portfolio/snapshot/mod.rs:
--------------------------------------------------------------------------------
1 | // src-core/src/portfolio/snapshot/mod.rs
2 |
3 | mod snapshot_repository;
4 | pub mod snapshot_service;
5 | pub mod holdings_calculator;
6 | mod positions_model;
7 | mod snapshot_model;
8 |
9 | pub use snapshot_repository::*;
10 | pub use snapshot_service::*;
11 | pub use holdings_calculator::*;
12 | pub use positions_model::*;
13 | pub use snapshot_model::*;
14 |
15 | #[cfg(test)]
16 | mod holdings_calculator_tests;
17 |
18 | #[cfg(test)]
19 | pub mod snapshot_service_tests;
--------------------------------------------------------------------------------
/src-core/src/portfolio/valuation/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod valuation_calculator;
2 | pub mod valuation_service;
3 | pub mod valuation_model;
4 | pub mod valuation_repository;
5 |
6 | pub use valuation_calculator::*;
7 | pub use valuation_service::ValuationService;
8 | pub use valuation_service::ValuationServiceTrait;
9 | pub use valuation_model::*;
10 | pub use valuation_repository::*;
11 |
12 |
--------------------------------------------------------------------------------
/src-core/src/settings/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod settings_repository;
2 | pub mod settings_service;
3 | pub mod settings_model;
4 | pub use settings_repository::SettingsRepositoryTrait;
5 | pub use settings_service::{SettingsService, SettingsServiceTrait};
6 | pub use settings_model::*;
--------------------------------------------------------------------------------
/src-core/src/settings/settings_model.rs:
--------------------------------------------------------------------------------
1 | use diesel::prelude::*;
2 | use diesel::Queryable;
3 | use serde::{Deserialize, Serialize};
4 |
5 | #[derive(Serialize, Deserialize, Debug, Clone)]
6 | #[serde(rename_all = "camelCase")]
7 | pub struct Settings {
8 | pub theme: String,
9 | pub font: String,
10 | pub base_currency: String,
11 | pub instance_id: String,
12 | pub onboarding_completed: bool,
13 | }
14 |
15 | impl Default for Settings {
16 | fn default() -> Self {
17 | Self {
18 | theme: "light".to_string(),
19 | font: "font-mono".to_string(),
20 | base_currency: "".to_string(),
21 | instance_id: "".to_string(),
22 | onboarding_completed: false,
23 | }
24 | }
25 | }
26 |
27 | #[derive(Serialize, Deserialize, Debug, Clone)]
28 | #[serde(rename_all = "camelCase")]
29 | pub struct SettingsUpdate {
30 | pub theme: Option,
31 | pub font: Option,
32 | pub base_currency: Option,
33 | pub onboarding_completed: Option,
34 | }
35 |
36 | #[derive(Serialize, Deserialize, Debug, Clone)]
37 | #[serde(rename_all = "camelCase")]
38 | pub struct Sort {
39 | pub id: String,
40 | pub desc: bool,
41 | }
42 |
43 | #[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
44 | #[diesel(table_name= crate::schema::app_settings)]
45 | #[serde(rename_all = "camelCase")]
46 | pub struct AppSetting {
47 | pub setting_key: String,
48 | pub setting_value: String,
49 | }
50 |
--------------------------------------------------------------------------------
/src-core/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | // This file declares utility modules
2 | pub mod time_utils;
--------------------------------------------------------------------------------
/src-core/src/utils/time_utils.rs:
--------------------------------------------------------------------------------
1 | use chrono::NaiveDate;
2 |
3 | pub fn get_days_between(start: NaiveDate, end: NaiveDate) -> Vec {
4 | if start > end {
5 | return Vec::new();
6 | }
7 | let mut days = Vec::new();
8 | let mut current = start;
9 | while current <= end {
10 | days.push(current);
11 | if let Some(next) = current.succ_opt() {
12 | current = next;
13 | } else {
14 | // Should not happen for typical date ranges
15 | break;
16 | }
17 | }
18 | days
19 | }
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # default DB path
6 | /app.db
7 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "wealthfolio-app"
3 | version = "1.1.3"
4 | description = "Portfolio tracker"
5 | authors = ["Aziz Fadil"]
6 | license = "AGPL-3.0"
7 | repository = ""
8 | edition = "2021"
9 |
10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
11 |
12 | [build-dependencies]
13 | tauri-build = { version = "2.1.1", features = [] }
14 |
15 | [dependencies]
16 | wealthfolio_core = { path = "../src-core" }
17 | tauri = { version = "2.4.1", features = [] }
18 | diesel = { version = "2.2", features = ["sqlite", "chrono", "r2d2", "numeric", "returning_clauses_for_sqlite_3_35"] }
19 | dotenvy = "0.15.7"
20 | serde = { version = "1.0", features = ["derive"] }
21 | serde_json = "1.0.128"
22 | chrono = { version = "0.4.38", features = ["serde"] }
23 | tauri-plugin-fs = "2.2.1"
24 | tauri-plugin-dialog = "2.2.1"
25 | tauri-plugin-shell = "2.2.1"
26 | tauri-plugin-log = "2.3.1"
27 | log = "0.4"
28 | futures = "0.3"
29 |
30 | [features]
31 | # this feature is used for production builds or when `devPath` points to the filesystem
32 | # DO NOT REMOVE!!
33 | custom-protocol = ["tauri/custom-protocol"]
34 |
35 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
36 | tauri-plugin-updater = "2.7.0"
37 | tauri-plugin-window-state = "2"
38 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/capabilities/desktop.json:
--------------------------------------------------------------------------------
1 | {
2 | "identifier": "desktop-capability",
3 | "description": "desktop-capability",
4 | "local": true,
5 | "windows": [
6 | "main"
7 | ],
8 | "platforms": [
9 | "macOS",
10 | "windows",
11 | "linux"
12 | ],
13 | "permissions": [
14 | "core:default",
15 | "fs:allow-read-file",
16 | "fs:allow-write-file",
17 | "fs:allow-read-dir",
18 | "fs:allow-copy-file",
19 | "fs:allow-mkdir",
20 | "fs:allow-remove",
21 | "fs:allow-remove",
22 | "fs:allow-rename",
23 | "fs:allow-exists",
24 | {
25 | "identifier": "fs:scope",
26 | "allow": [
27 | "$APPDATA/**"
28 | ]
29 | },
30 | "core:window:allow-start-dragging",
31 | "shell:allow-open",
32 | "dialog:allow-open",
33 | "dialog:allow-save",
34 | "fs:default",
35 | "dialog:default",
36 | "shell:default",
37 | "core:app:allow-set-app-theme",
38 | "core:window:allow-set-theme",
39 | "updater:default",
40 | "log:default",
41 | "window-state:default"
42 | ]
43 | }
--------------------------------------------------------------------------------
/src-tauri/gen/schemas/capabilities.json:
--------------------------------------------------------------------------------
1 | {"desktop-capability":{"identifier":"desktop-capability","description":"desktop-capability","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["$APPDATA/**"]},"core:window:allow-start-dragging","shell:allow-open","dialog:allow-open","dialog:allow-save","fs:default","dialog:default","shell:default","core:app:allow-set-app-theme","core:window:allow-set-theme","updater:default","log:default","window-state:default"],"platforms":["macOS","windows","linux"]}}
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-20x20@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-20x20@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-20x20@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-29x29@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-29x29@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-29x29@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-40x40@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-40x40@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-40x40@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-512@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-512@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-60x60@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-60x60@3x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-76x76@1x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-76x76@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/src-tauri/src/commands/asset.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use crate::{
4 | context::ServiceContext,
5 | events::{emit_portfolio_trigger_recalculate, PortfolioRequestPayload},
6 | };
7 | use tauri::{AppHandle, State};
8 | use wealthfolio_core::assets::{Asset, AssetData, UpdateAssetProfile};
9 |
10 | #[tauri::command]
11 | pub async fn get_asset_data(
12 | asset_id: String,
13 | state: State<'_, Arc>,
14 | ) -> Result {
15 | state
16 | .asset_service()
17 | .get_asset_data(&asset_id)
18 | .await
19 | .map_err(|e| e.to_string())
20 | }
21 |
22 | #[tauri::command]
23 | pub async fn update_asset_profile(
24 | id: String,
25 | payload: UpdateAssetProfile,
26 | state: State<'_, Arc>,
27 | ) -> Result {
28 | state
29 | .asset_service()
30 | .update_asset_profile(&id, payload)
31 | .await
32 | .map_err(|e| e.to_string())
33 | }
34 |
35 | #[tauri::command]
36 | pub async fn update_asset_data_source(
37 | id: String,
38 | data_source: String,
39 | state: State<'_, Arc>,
40 | handle: AppHandle,
41 | ) -> Result {
42 | let asset = state
43 | .asset_service()
44 | .update_asset_data_source(&id, data_source)
45 | .await
46 | .map_err(|e| e.to_string())?;
47 |
48 | let handle = handle.clone();
49 | tauri::async_runtime::spawn(async move {
50 | // Emit event to trigger market data sync using the builder
51 | let payload = PortfolioRequestPayload::builder()
52 | .account_ids(None)
53 | .refetch_all_market_data(true)
54 | .symbols(Some(vec![id]))
55 | .build();
56 | emit_portfolio_trigger_recalculate(&handle, payload);
57 | });
58 |
59 | Ok(asset)
60 | }
61 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/goal.rs:
--------------------------------------------------------------------------------
1 | use std::sync::Arc;
2 |
3 | use crate::context::ServiceContext;
4 | use log::debug;
5 | use tauri::State;
6 | use wealthfolio_core::goals::goals_model::{Goal, GoalsAllocation, NewGoal};
7 |
8 | #[tauri::command]
9 | pub async fn get_goals(state: State<'_, Arc>) -> Result, String> {
10 | debug!("Fetching active goals...");
11 | state.goal_service().get_goals().map_err(|e| e.to_string())
12 | }
13 |
14 | #[tauri::command]
15 | pub async fn create_goal(
16 | goal: NewGoal,
17 | state: State<'_, Arc>,
18 | ) -> Result {
19 | debug!("Adding new goal...");
20 | state
21 | .goal_service()
22 | .create_goal(goal)
23 | .await
24 | .map_err(|e| e.to_string())
25 | }
26 |
27 | #[tauri::command]
28 | pub async fn update_goal(
29 | goal: Goal,
30 | state: State<'_, Arc>,
31 | ) -> Result {
32 | debug!("Updating goal...");
33 | state
34 | .goal_service()
35 | .update_goal(goal)
36 | .await
37 | .map_err(|e| e.to_string())
38 | }
39 |
40 | #[tauri::command]
41 | pub async fn delete_goal(
42 | goal_id: String,
43 | state: State<'_, Arc>,
44 | ) -> Result {
45 | debug!("Deleting goal...");
46 | state
47 | .goal_service()
48 | .delete_goal(goal_id)
49 | .await
50 | .map_err(|e| e.to_string())
51 | }
52 |
53 | #[tauri::command]
54 | pub async fn update_goal_allocations(
55 | allocations: Vec,
56 | state: State<'_, Arc>,
57 | ) -> Result {
58 | debug!("Updating goal allocations...");
59 | state
60 | .goal_service()
61 | .upsert_goal_allocations(allocations)
62 | .await
63 | .map_err(|e| e.to_string())
64 | }
65 |
66 | #[tauri::command]
67 | pub async fn load_goals_allocations(
68 | state: State<'_, Arc>,
69 | ) -> Result, String> {
70 | debug!("Loading goal allocations...");
71 | state
72 | .goal_service()
73 | .load_goals_allocations()
74 | .map_err(|e| e.to_string())
75 | }
76 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod account;
2 | pub mod activity;
3 | pub mod asset;
4 | pub mod goal;
5 | pub mod limits;
6 | pub mod market_data;
7 | pub mod portfolio;
8 | pub mod settings;
9 | pub mod utilities;
10 |
--------------------------------------------------------------------------------
/src-tauri/src/commands/utilities.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::Read;
3 | use std::path::Path;
4 | use tauri::AppHandle;
5 | use tauri::Manager;
6 | use wealthfolio_core::db;
7 |
8 | #[tauri::command]
9 | pub async fn backup_database(app_handle: AppHandle) -> Result<(String, Vec), String> {
10 | let app_data_dir = app_handle
11 | .path()
12 | .app_data_dir()
13 | .expect("failed to get app data dir")
14 | .to_str()
15 | .expect("failed to convert path to string")
16 | .to_string();
17 |
18 | let backup_path = db::backup_database(&app_data_dir).map_err(|e| e.to_string())?;
19 |
20 | // Read the backup file
21 | let mut file =
22 | File::open(&backup_path).map_err(|e| format!("Failed to open backup file: {}", e))?;
23 | let mut buffer = Vec::new();
24 | file.read_to_end(&mut buffer)
25 | .map_err(|e| format!("Failed to read backup file: {}", e))?;
26 |
27 | // Get the filename
28 | let filename = Path::new(&backup_path)
29 | .file_name()
30 | .and_then(|name| name.to_str())
31 | .ok_or_else(|| "Failed to get backup filename".to_string())?
32 | .to_string();
33 |
34 | Ok((filename, buffer))
35 | }
36 |
--------------------------------------------------------------------------------
/src-tauri/src/context/mod.rs:
--------------------------------------------------------------------------------
1 | // context/mod.rs
2 | mod providers;
3 | mod registry;
4 |
5 | pub use providers::initialize_context;
6 | pub use registry::ServiceContext;
7 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "beforeDevCommand": "pnpm dev",
4 | "beforeBuildCommand": "pnpm build",
5 | "frontendDist": "../dist",
6 | "devUrl": "http://localhost:1420"
7 | },
8 | "bundle": {
9 | "active": true,
10 | "targets": "all",
11 | "icon": [
12 | "icons/32x32.png",
13 | "icons/128x128.png",
14 | "icons/128x128@2x.png",
15 | "icons/icon.icns",
16 | "icons/icon.ico"
17 | ],
18 | "copyright": "2025 Teymz Inc.",
19 | "category": "Finance",
20 | "createUpdaterArtifacts": "v1Compatible"
21 | },
22 | "productName": "Wealthfolio",
23 | "mainBinaryName": "Wealthfolio",
24 | "version": "1.1.3",
25 | "identifier": "com.teymz.wealthfolio",
26 | "plugins": {
27 | "updater": {
28 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDY0RjI4MkRFOUYwRDM3N0QKUldSOU53MmYzb0x5WlBySmErU21wcjVHZFNtMW9zdGcyZTBBRWpVcHRDMkoyKzF1bkQ1VmdKbkYK",
29 | "endpoints": [
30 | "https://wealthfolio.app/releases/{{target}}/{{arch}}/{{current_version}}"
31 | ],
32 | "windows": {
33 | "installMode": "passive"
34 | }
35 | }
36 | },
37 | "app": {
38 | "withGlobalTauri": true,
39 | "windows": [
40 | {
41 | "dragDropEnabled": false,
42 | "fullscreen": false,
43 | "resizable": true,
44 | "theme": "Light",
45 | "titleBarStyle": "Overlay",
46 | "hiddenTitle": true,
47 | "title": "Wealthfolio",
48 | "width": 1440,
49 | "height": 960,
50 | "center": true
51 | }
52 | ],
53 | "security": {
54 | "csp": null
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2 | import { SettingsProvider } from '@/lib/settings-provider';
3 | import { PrivacyProvider } from './context/privacy-context';
4 | import { AppRoutes } from './routes';
5 | import { useState } from 'react';
6 |
7 | function App() {
8 | const [queryClient] = useState(
9 | () =>
10 | new QueryClient({
11 | defaultOptions: {
12 | queries: {
13 | refetchOnWindowFocus: false,
14 | staleTime: 5 * 60 * 1000,
15 | retry: false,
16 | },
17 | },
18 | }),
19 | );
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | export default App;
32 |
--------------------------------------------------------------------------------
/src/adapters/index.ts:
--------------------------------------------------------------------------------
1 | export enum RUN_ENV {
2 | DESKTOP = 'desktop',
3 | MOBILE = 'mobile',
4 | BROWSER = 'browser',
5 | UNSUPPORTED = 'unsupported',
6 | }
7 |
8 | declare global {
9 | interface Window {
10 | __TAURI__?: any;
11 | }
12 | }
13 |
14 | export const getRunEnv = (): RUN_ENV => {
15 | if (typeof window !== 'undefined' && window.__TAURI__) {
16 | return RUN_ENV.DESKTOP;
17 | }
18 | if (typeof window !== 'undefined' && window.indexedDB) {
19 | return RUN_ENV.BROWSER;
20 | }
21 | return RUN_ENV.UNSUPPORTED;
22 | };
23 |
24 | export type { EventCallback, UnlistenFn } from './tauri';
25 |
26 |
27 | export {
28 | invokeTauri,
29 | openCsvFileDialogTauri,
30 | listenFileDropHoverTauri,
31 | listenFileDropTauri,
32 | listenFileDropCancelledTauri,
33 | listenPortfolioUpdateStartTauri,
34 | listenPortfolioUpdateCompleteTauri,
35 | listenPortfolioUpdateErrorTauri,
36 | openFileSaveDialogTauri,
37 | logger,
38 | } from './tauri';
39 |
40 | export * from './tauri';
41 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afadil/wealthfolio/be21a6b1fca377b19e057f6796d5d1abfc183580/src/assets/logo.png
--------------------------------------------------------------------------------
/src/commands/README.md:
--------------------------------------------------------------------------------
1 | # Notes
2 |
3 | - This `src/commands` directory contains all actions to communicate with Tauri via `@tauti-apps`
4 | library, e.g. invoke, listen, open
5 |
6 | - All other directories under `src` are native React code only
7 |
--------------------------------------------------------------------------------
/src/commands/account.ts:
--------------------------------------------------------------------------------
1 | import z from 'zod';
2 | import { Account } from '@/lib/types';
3 | import { newAccountSchema } from '@/lib/schemas';
4 | import { getRunEnv, RUN_ENV, invokeTauri } from '@/adapters';
5 | import { logger } from '@/adapters';
6 |
7 | type NewAccount = z.infer;
8 |
9 | export const getAccounts = async (): Promise => {
10 | try {
11 | switch (getRunEnv()) {
12 | case RUN_ENV.DESKTOP:
13 | return invokeTauri('get_accounts');
14 | default:
15 | throw new Error(`Unsupported`);
16 | }
17 | } catch (error) {
18 | logger.error('Error fetching accounts.');
19 | throw error;
20 | }
21 | };
22 |
23 | // createAccount
24 | export const createAccount = async (account: NewAccount): Promise => {
25 | try {
26 | switch (getRunEnv()) {
27 | case RUN_ENV.DESKTOP:
28 | return invokeTauri('create_account', { account: account });
29 | default:
30 | throw new Error(`Unsupported`);
31 | }
32 | } catch (error) {
33 | logger.error('Error creating account.');
34 | throw error;
35 | }
36 | };
37 |
38 | // updateAccount
39 | export const updateAccount = async (account: NewAccount): Promise => {
40 | try {
41 | switch (getRunEnv()) {
42 | case RUN_ENV.DESKTOP:
43 | const { currency, ...updatedAccountData } = account;
44 | return invokeTauri('update_account', { accountUpdate: updatedAccountData });
45 | default:
46 | throw new Error(`Unsupported`);
47 | }
48 | } catch (error) {
49 | logger.error('Error updating account.');
50 | throw error;
51 | }
52 | };
53 |
54 | // deleteAccount
55 | export const deleteAccount = async (accountId: string): Promise => {
56 | try {
57 | switch (getRunEnv()) {
58 | case RUN_ENV.DESKTOP:
59 | await invokeTauri('delete_account', { accountId });
60 | return;
61 | default:
62 | throw new Error(`Unsupported`);
63 | }
64 | } catch (error) {
65 | logger.error('Error deleting account.');
66 | throw error;
67 | }
68 | };
69 |
--------------------------------------------------------------------------------
/src/commands/activity-import.ts:
--------------------------------------------------------------------------------
1 | import { ActivityImport, ImportMappingData } from '@/lib/types';
2 | import { getRunEnv, RUN_ENV, invokeTauri } from '@/adapters';
3 | import { logger } from '@/adapters';
4 |
5 |
6 | export const importActivities = async ({
7 | activities,
8 | }: {
9 | activities: ActivityImport[];
10 | }): Promise => {
11 | try {
12 | switch (getRunEnv()) {
13 | case RUN_ENV.DESKTOP:
14 | return invokeTauri('import_activities', {
15 | accountId: activities[0].accountId,
16 | activities: activities,
17 | });
18 | default:
19 | throw new Error(`Unsupported`);
20 | }
21 | } catch (error) {
22 | logger.error('Error checking activities import.');
23 | throw error;
24 | }
25 | };
26 |
27 | export const checkActivitiesImport = async ({
28 | account_id,
29 | activities,
30 | }: {
31 | account_id: string;
32 | activities: ActivityImport[];
33 | }): Promise => {
34 | try {
35 | switch (getRunEnv()) {
36 | case RUN_ENV.DESKTOP:
37 | return invokeTauri('check_activities_import', {
38 | accountId: account_id,
39 | activities: activities,
40 | });
41 | default:
42 | throw new Error(`Unsupported`);
43 | }
44 | } catch (error) {
45 | logger.error('Error checking activities import.');
46 | throw error;
47 | }
48 | };
49 |
50 | export const getAccountImportMapping = async (accountId: string): Promise => {
51 | try {
52 | switch (getRunEnv()) {
53 | case RUN_ENV.DESKTOP:
54 | return invokeTauri('get_account_import_mapping', { accountId });
55 | default:
56 | throw new Error(`Unsupported`);
57 | }
58 | } catch (error) {
59 | logger.error('Error fetching mapping.');
60 | throw error;
61 | }
62 | };
63 |
64 | export const saveAccountImportMapping = async (
65 | mapping: ImportMappingData,
66 | ): Promise => {
67 | try {
68 | switch (getRunEnv()) {
69 | case RUN_ENV.DESKTOP:
70 | return invokeTauri('save_account_import_mapping', {
71 | mapping,
72 | });
73 | default:
74 | throw new Error(`Unsupported`);
75 | }
76 | } catch (error) {
77 | logger.error('Error saving mapping.');
78 | throw error;
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/src/commands/exchange-rates.ts:
--------------------------------------------------------------------------------
1 | import type { ExchangeRate } from '@/lib/types';
2 | import { getRunEnv, RUN_ENV, invokeTauri, logger } from '@/adapters';
3 |
4 | export const getExchangeRates = async (): Promise => {
5 | try {
6 | switch (getRunEnv()) {
7 | case RUN_ENV.DESKTOP:
8 | return invokeTauri('get_latest_exchange_rates');
9 | default:
10 | throw new Error('Unsupported environment');
11 | }
12 | } catch (error) {
13 | logger.error('Error fetching exchange rates.');
14 | return [];
15 | }
16 | };
17 |
18 | export const updateExchangeRate = async (updatedRate: ExchangeRate): Promise => {
19 | try {
20 | switch (getRunEnv()) {
21 | case RUN_ENV.DESKTOP:
22 | return invokeTauri('update_exchange_rate', { rate: updatedRate });
23 | default:
24 | throw new Error('Unsupported environment');
25 | }
26 | } catch (error) {
27 | logger.error('Error updating exchange rate.');
28 | throw error;
29 | }
30 | };
31 |
32 | export const addExchangeRate = async (newRate: Omit): Promise => {
33 | try {
34 | switch (getRunEnv()) {
35 | case RUN_ENV.DESKTOP:
36 | return invokeTauri('add_exchange_rate', { newRate });
37 | default:
38 | throw new Error('Unsupported environment');
39 | }
40 | } catch (error) {
41 | logger.error('Error adding exchange rate.');
42 | throw error;
43 | }
44 | };
45 |
46 | export const deleteExchangeRate = async (rateId: string): Promise => {
47 | try {
48 | switch (getRunEnv()) {
49 | case RUN_ENV.DESKTOP:
50 | return invokeTauri('delete_exchange_rate', { rateId });
51 | default:
52 | throw new Error('Unsupported environment');
53 | }
54 | } catch (error) {
55 | logger.error('Error deleting exchange rate.');
56 | throw error;
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/src/commands/file.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getRunEnv,
3 | openCsvFileDialogTauri,
4 | openFileSaveDialogTauri,
5 | RUN_ENV,
6 | logger,
7 | } from '@/adapters';
8 |
9 | // openCsvFileDialog
10 | export const openCsvFileDialog = async (): Promise => {
11 | try {
12 | switch (getRunEnv()) {
13 | case RUN_ENV.DESKTOP:
14 | return openCsvFileDialogTauri();
15 | default:
16 | throw new Error(`Unsupported`);
17 | }
18 | } catch (error) {
19 | logger.error('Error open csv file.');
20 | throw error;
21 | }
22 | };
23 |
24 | // Function for downloading file content
25 | export async function openFileSaveDialog(
26 | fileContent: Uint8Array | Blob | string,
27 | fileName: string,
28 | ) {
29 | switch (getRunEnv()) {
30 | case RUN_ENV.DESKTOP:
31 | return openFileSaveDialogTauri(fileContent, fileName);
32 | default:
33 | throw new Error(`Unsupported environment for file download`);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/commands/import-listener.ts:
--------------------------------------------------------------------------------
1 | import type { EventCallback, UnlistenFn } from '@/adapters';
2 | import {
3 | getRunEnv,
4 | RUN_ENV,
5 | listenFileDropCancelledTauri,
6 | listenFileDropHoverTauri,
7 | listenFileDropTauri,
8 | logger,
9 | } from '@/adapters';
10 |
11 | // listenImportFileDropHover
12 | export const listenImportFileDropHover = async (
13 | handler: EventCallback,
14 | ): Promise => {
15 | try {
16 | switch (getRunEnv()) {
17 | case RUN_ENV.DESKTOP:
18 | return listenFileDropHoverTauri(handler);
19 | default:
20 | throw new Error(`Unsupported`);
21 | }
22 | } catch (error) {
23 | logger.error('Error listen tauri://file-drop-hover.');
24 | throw error;
25 | }
26 | };
27 |
28 | // listenImportFileDrop
29 | export const listenImportFileDrop = async (handler: EventCallback): Promise => {
30 | try {
31 | switch (getRunEnv()) {
32 | case RUN_ENV.DESKTOP:
33 | return listenFileDropTauri(handler);
34 | default:
35 | throw new Error(`Unsupported`);
36 | }
37 | } catch (error) {
38 | logger.error('Error listen tauri://file-drop.');
39 | throw error;
40 | }
41 | };
42 |
43 | // listenImportFileDropCancelled
44 | export const listenImportFileDropCancelled = async (
45 | handler: EventCallback,
46 | ): Promise => {
47 | try {
48 | switch (getRunEnv()) {
49 | case RUN_ENV.DESKTOP:
50 | return listenFileDropCancelledTauri(handler);
51 | default:
52 | throw new Error(`Unsupported`);
53 | }
54 | } catch (error) {
55 | logger.error('Error listen tauri://file-drop-cancelled.');
56 | throw error;
57 | }
58 | };
59 |
60 | export type { UnlistenFn };
61 |
--------------------------------------------------------------------------------
/src/commands/settings.ts:
--------------------------------------------------------------------------------
1 | import { Settings } from '@/lib/types';
2 | import { getRunEnv, RUN_ENV, invokeTauri, logger } from '@/adapters';
3 |
4 | export const getSettings = async (): Promise => {
5 | try {
6 | switch (getRunEnv()) {
7 | case RUN_ENV.DESKTOP:
8 | return invokeTauri('get_settings');
9 | default:
10 | throw new Error(`Unsupported`);
11 | }
12 | } catch (error) {
13 | logger.error('Error fetching settings.');
14 | return {} as Settings;
15 | }
16 | };
17 |
18 | export const updateSettings = async (settingsUpdate: Settings): Promise => {
19 | try {
20 | switch (getRunEnv()) {
21 | case RUN_ENV.DESKTOP:
22 | return invokeTauri('update_settings', { settingsUpdate });
23 | default:
24 | throw new Error(`Unsupported`);
25 | }
26 | } catch (error) {
27 | logger.error('Error updating settings.');
28 | throw error;
29 | }
30 | };
31 |
32 | export const backupDatabase = async (): Promise<{ filename: string; data: Uint8Array }> => {
33 | try {
34 | switch (getRunEnv()) {
35 | case RUN_ENV.DESKTOP:
36 | const result = await invokeTauri<[string, number[]]>('backup_database');
37 | const [filename, data] = result;
38 | return { filename, data: new Uint8Array(data) };
39 | default:
40 | throw new Error(`Unsupported environment for database backup`);
41 | }
42 | } catch (error) {
43 | logger.error('Error backing up database.');
44 | throw error;
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/src/components/alert-feedback.tsx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
2 | import { Icons } from '@/components/icons';
3 |
4 | interface ApplicationShellProps extends React.HTMLAttributes {
5 | title?: string;
6 | variant?: 'success' | 'error' | 'warning';
7 | }
8 |
9 | export function AlertFeedback({
10 | title,
11 | children,
12 | variant,
13 | className,
14 | ...props
15 | }: ApplicationShellProps) {
16 | let alertIcon;
17 |
18 | switch (variant) {
19 | case 'success':
20 | alertIcon = ;
21 | break;
22 | case 'warning':
23 | alertIcon = ;
24 | break;
25 | case 'error':
26 | default:
27 | alertIcon = ;
28 | }
29 |
30 | return (
31 |
32 | {alertIcon}
33 | {title}
34 | {children}
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/amount-display.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { formatAmount } from '@/lib/utils';
3 |
4 | interface AmountDisplayProps {
5 | value: number;
6 | currency: string;
7 | isHidden?: boolean;
8 | colorFormat?: boolean;
9 | className?: string;
10 | }
11 |
12 | export function AmountDisplay({
13 | value,
14 | currency = 'USD',
15 | isHidden,
16 | colorFormat,
17 | className,
18 | }: AmountDisplayProps) {
19 | const formattedAmount = formatAmount(value, currency);
20 | const colorClass = colorFormat ? (value >= 0 ? 'text-success' : 'text-destructive') : '';
21 |
22 | return (
23 |
24 | {isHidden ? '••••' : formattedAmount}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/empty-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 | import { Icons } from '@/components/icons';
3 |
4 | interface EmptyPlaceholderProps extends React.HTMLAttributes {}
5 |
6 | export function EmptyPlaceholder({ className, children, ...props }: EmptyPlaceholderProps) {
7 | return (
8 |
15 |
16 | {children}
17 |
18 |
19 | );
20 | }
21 |
22 | interface EmptyPlaceholderIconProps extends Partial> {
23 | name: keyof typeof Icons;
24 | }
25 |
26 | EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
27 | name,
28 | className,
29 | ...props
30 | }: EmptyPlaceholderIconProps) {
31 | const Icon = Icons[name] as React.ElementType;
32 |
33 | if (!Icon) {
34 | return null;
35 | }
36 |
37 | return (
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | interface EmptyPlacholderTitleProps extends React.HTMLAttributes {}
45 |
46 | EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
47 | className,
48 | ...props
49 | }: EmptyPlacholderTitleProps) {
50 | return ;
51 | };
52 |
53 | interface EmptyPlacholderDescriptionProps extends React.HTMLAttributes {}
54 |
55 | EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
56 | className,
57 | ...props
58 | }: EmptyPlacholderDescriptionProps) {
59 | return (
60 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/components/error-boundary.tsx:
--------------------------------------------------------------------------------
1 | import { Component, ErrorInfo, ReactNode } from 'react';
2 | import { Button } from './ui/button';
3 | import { XCircle } from 'lucide-react';
4 | import { logger } from '@/adapters';
5 |
6 | interface Props {
7 | children: ReactNode;
8 | }
9 |
10 | interface State {
11 | hasError: boolean;
12 | error?: Error;
13 | }
14 |
15 | class ErrorBoundary extends Component {
16 | public state: State = {
17 | hasError: false,
18 | error: undefined,
19 | };
20 |
21 | public static getDerivedStateFromError(error: Error): State {
22 | return { hasError: true, error };
23 | }
24 |
25 | public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
26 | logger.error(`Error Boundary Caught Error:
27 | Message: ${error.message}
28 | Stack: ${error.stack}
29 | Component Stack: ${errorInfo.componentStack}
30 | `);
31 | }
32 |
33 | public render() {
34 | if (this.state.hasError) {
35 | return (
36 |
37 |
38 |
39 |
Something went wrong
40 |
41 | An unexpected error occurred. Please try refreshing the page.
42 |
43 |
46 |
47 |
48 | );
49 | }
50 |
51 | return this.props.children;
52 | }
53 | }
54 |
55 | export { ErrorBoundary };
56 |
--------------------------------------------------------------------------------
/src/components/gain-amount.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import NumberFlow from '@number-flow/react';
4 | import { useBalancePrivacy } from '@/context/privacy-context';
5 |
6 | interface GainAmountProps extends React.HTMLAttributes {
7 | value: number;
8 | displayCurrency?: boolean;
9 | currency: string;
10 | displayDecimal?: boolean;
11 | showSign?: boolean;
12 | }
13 |
14 | export function GainAmount({
15 | value,
16 | currency,
17 | displayCurrency = true,
18 | className,
19 | displayDecimal = true,
20 | showSign = true,
21 | ...props
22 | }: GainAmountProps) {
23 | const { isBalanceHidden } = useBalancePrivacy();
24 |
25 | return (
26 |
27 |
0 ? 'text-success' : value < 0 ? 'text-destructive' : 'text-foreground',
31 | )}
32 | >
33 | {isBalanceHidden ? (
34 | ••••
35 | ) : (
36 | <>
37 | {value > 0 ? '+' : value < 0 ? '-' : null}
38 |
50 | >
51 | )}
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/gain-percent.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cn } from '@/lib/utils';
3 | import NumberFlow from '@number-flow/react';
4 |
5 | type GainPercentVariant = 'text' | 'badge';
6 |
7 | interface GainPercentProps extends React.HTMLAttributes {
8 | value: number;
9 | animated?: boolean;
10 | variant?: GainPercentVariant;
11 | showSign?: boolean;
12 | }
13 |
14 | export function GainPercent({
15 | value,
16 | animated = false,
17 | variant = 'text',
18 | showSign = true,
19 | className,
20 | ...props
21 | }: GainPercentProps) {
22 | return (
23 | 0 ? 'text-success' : value < 0 ? 'text-destructive' : 'text-foreground',
27 | variant === 'badge' && [
28 | 'rounded-md py-[1px] pl-[9px] pr-[12px] font-light',
29 | value > 0 ? 'bg-success/10' : value < 0 ? 'bg-destructive/10' : 'bg-foreground/10',
30 | ],
31 | className,
32 | )}
33 | {...props}
34 | >
35 | {showSign && (value > 0 ? '+' : value < 0 ? '-' : null)}
36 |
44 | %
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from '@/components/icons';
2 | import { Button } from '@/components/ui/button';
3 | import { cn } from '@/lib/utils';
4 | import { Link, useNavigate } from 'react-router-dom';
5 |
6 | interface ApplicationHeaderProps {
7 | heading: string;
8 | headingPrefix?: string;
9 | text?: string;
10 | className?: string;
11 | children?: React.ReactNode;
12 | displayBack?: boolean;
13 | backUrl?: string;
14 | }
15 |
16 | export function ApplicationHeader({
17 | heading,
18 | headingPrefix,
19 | text,
20 | className,
21 | children,
22 | displayBack,
23 | backUrl,
24 | }: ApplicationHeaderProps) {
25 | const navigate = useNavigate();
26 | return (
27 |
28 |
29 | {displayBack ? (
30 | backUrl ? (
31 |
32 |
35 |
36 | ) : (
37 |
40 | )
41 | ) : null}
42 |
43 | {headingPrefix && (
44 | <>
45 |
46 | {headingPrefix}
47 |
48 |
49 | >
50 | )}
51 |
52 |
{heading}
53 | {text &&
{text}
}
54 |
55 |
56 | {children}
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/privacy-amount.tsx:
--------------------------------------------------------------------------------
1 | import { useBalancePrivacy } from '@/context/privacy-context';
2 | import { cn } from '@/lib/utils';
3 | import { formatAmount } from '@/lib/utils';
4 |
5 | interface PrivacyAmountProps extends React.HTMLAttributes {
6 | value: number;
7 | currency: string;
8 | }
9 |
10 | export function PrivacyAmount({ value, currency, className, ...props }: PrivacyAmountProps) {
11 | const { isBalanceHidden } = useBalancePrivacy();
12 |
13 | return (
14 |
15 | {isBalanceHidden ? '••••' : formatAmount(value, currency)}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/privacy-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { useBalancePrivacy } from '@/context/privacy-context';
2 | import { Button } from '@/components/ui/button';
3 | import { Icons } from '@/components/icons';
4 | import { cn } from '@/lib/utils';
5 |
6 | interface PrivacyToggleProps {
7 | className?: string;
8 | }
9 |
10 | export function PrivacyToggle({ className }: PrivacyToggleProps) {
11 | const { isBalanceHidden, toggleBalanceVisibility } = useBalancePrivacy();
12 |
13 | return (
14 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/quantity-display.tsx:
--------------------------------------------------------------------------------
1 | import { formatStockQuantity } from '@/lib/utils';
2 |
3 | interface QuantityDisplayProps {
4 | value: number;
5 | isHidden: boolean;
6 | }
7 |
8 | export function QuantityDisplay({ value, isHidden }: QuantityDisplayProps) {
9 | return {isHidden ? '••••' : formatStockQuantity(value)};
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | interface ApplicationShellProps extends React.HTMLAttributes {}
6 |
7 | export function ApplicationShell({ children, className, ...props }: ApplicationShellProps) {
8 | return (
9 |
10 | {children}
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === 'production') return null;
3 |
4 | return (
5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 |
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
57 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const alertVariants = cva(
7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'bg-background text-foreground',
12 | destructive:
13 | 'border-destructive/50 text-destructive bg-destructive/20 dark:border-destructive [&>svg]:text-destructive',
14 |
15 | error:
16 | 'border-destructive/50 text-destructive bg-destructive/20 dark:border-destructive [&>svg]:text-destructive',
17 |
18 | success:
19 | 'success group border-success bg-success text-success-foreground ',
20 |
21 | warning:
22 | 'border-orange-500 bg-orange-100 text-orange-800 dark:border-orange-500 dark:bg-orange-800/50 [&>svg]:text-orange-200 dark:text-orange-100',
23 | },
24 | },
25 | defaultVariants: {
26 | variant: 'default',
27 | },
28 | },
29 | );
30 |
31 | const Alert = React.forwardRef<
32 | HTMLDivElement,
33 | React.HTMLAttributes & VariantProps
34 | >(({ className, variant, ...props }, ref) => (
35 |
36 | ));
37 | Alert.displayName = 'Alert';
38 |
39 | const AlertTitle = React.forwardRef>(
40 | ({ className, ...props }, ref) => (
41 |
46 | ),
47 | );
48 | AlertTitle.displayName = 'AlertTitle';
49 |
50 | const AlertDescription = React.forwardRef<
51 | HTMLParagraphElement,
52 | React.HTMLAttributes
53 | >(({ className, ...props }, ref) => (
54 |
55 | ));
56 | AlertDescription.displayName = 'AlertDescription';
57 |
58 | export { Alert, AlertTitle, AlertDescription };
59 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
12 | secondary:
13 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
14 | destructive:
15 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
16 | outline: 'text-foreground',
17 | success:
18 | 'border-transparent bg-success text-success-foreground hover:bg-success/80 dark:bg-success',
19 | warning:
20 | 'border-transparent bg-warning text-warning-foreground hover:bg-warning/80 dark:bg-warning',
21 | info:
22 | 'border-transparent bg-blue-400 text-white hover:bg-blue-500 dark:bg-blue-600 dark:hover:bg-blue-700',
23 | },
24 | },
25 | defaultVariants: {
26 | variant: 'default',
27 | },
28 | },
29 | );
30 |
31 | export interface BadgeProps
32 | extends React.HTMLAttributes,
33 | VariantProps {}
34 |
35 | function Badge({ className, variant, ...props }: BadgeProps) {
36 | return ;
37 | }
38 |
39 | export { Badge, badgeVariants };
40 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16 | ghost: 'hover:bg-accent hover:text-accent-foreground',
17 | link: 'text-primary underline-offset-4 hover:underline',
18 | },
19 | size: {
20 | default: 'h-10 px-4 py-2',
21 | sm: 'h-9 rounded-md px-3',
22 | lg: 'h-11 rounded-md px-8',
23 | icon: 'h-10 w-10',
24 | },
25 | },
26 | defaultVariants: {
27 | variant: 'default',
28 | size: 'default',
29 | },
30 | },
31 | );
32 |
33 | export interface ButtonProps
34 | extends React.ButtonHTMLAttributes,
35 | VariantProps {
36 | asChild?: boolean;
37 | }
38 |
39 | const Button = React.forwardRef(
40 | ({ className, variant, size, asChild = false, ...props }, ref) => {
41 | const Comp = asChild ? Slot : 'button';
42 | return (
43 |
44 | );
45 | },
46 | );
47 | Button.displayName = 'Button';
48 |
49 | export { Button, buttonVariants };
50 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { Check } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | const Collapsible = CollapsiblePrimitive.Root
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
10 |
--------------------------------------------------------------------------------
/src/components/ui/date-range-picker.tsx:
--------------------------------------------------------------------------------
1 | import { format } from 'date-fns';
2 | import { Calendar as CalendarIcon } from 'lucide-react';
3 | import { DateRange } from 'react-day-picker';
4 | import { cn } from '@/lib/utils';
5 | import { Button } from '@/components/ui/button';
6 | import { Calendar } from '@/components/ui/calendar';
7 | import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
8 |
9 | interface DatePickerWithRangeProps {
10 | date: DateRange | undefined;
11 | onDateChange: (date: DateRange | undefined) => void;
12 | className?: string;
13 | }
14 |
15 | export function DatePickerWithRange({ date, onDateChange, className }: DatePickerWithRangeProps) {
16 | return (
17 |
18 |
19 |
20 |
41 |
42 |
43 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const HoverCard = HoverCardPrimitive.Root
7 |
8 | const HoverCardTrigger = HoverCardPrimitive.Trigger
9 |
10 | const HoverCardContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
24 | ))
25 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
26 |
27 | export { HoverCard, HoverCardTrigger, HoverCardContent }
28 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as PopoverPrimitive from '@radix-ui/react-popover';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const Popover = PopoverPrimitive.Root;
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger;
9 |
10 | const PopoverClose = PopoverPrimitive.Close;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
32 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ProgressPrimitive from '@radix-ui/react-progress';
3 |
4 | import { cn } from '@/lib/utils';
5 | const Progress = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef & {
8 | showPercentage?: boolean;
9 | indicatorClassName?: string;
10 | }
11 | >(({ className, value, showPercentage = false, indicatorClassName, ...props }, ref) => {
12 | const clampedValue = Math.min(value || 0, 100);
13 | return (
14 |
19 |
23 | {showPercentage && (
24 |
25 | 50 ? 'text-primary-foreground' : 'text-primary',
29 | )}
30 | >
31 | {Math.round(value || 0)}%
32 |
33 |
34 | )}
35 |
36 | );
37 | });
38 | Progress.displayName = ProgressPrimitive.Root.displayName;
39 |
40 | export { Progress };
41 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
3 | import { Circle } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | )
18 | })
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | )
39 | })
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
41 |
42 | export { RadioGroup, RadioGroupItem }
43 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ))
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = "vertical", ...props }, ref) => (
28 |
41 |
42 |
43 | ))
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45 |
46 | export { ScrollArea, ScrollBar }
47 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitives from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ))
25 | Switch.displayName = SwitchPrimitives.Root.displayName
26 |
27 | export { Switch }
28 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from "@/components/ui/toast"
9 | import { useToast } from "@/components/ui/use-toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ))
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext)
39 |
40 | return (
41 |
52 | {children}
53 |
54 | )
55 | })
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
58 |
59 | export { ToggleGroup, ToggleGroupItem }
60 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
15 | },
16 | size: {
17 | default: "h-10 px-3",
18 | sm: "h-9 px-2.5",
19 | lg: "h-11 px-5",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/src/context/privacy-context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from 'react';
2 |
3 | interface PrivacyContextType {
4 | isBalanceHidden: boolean;
5 | toggleBalanceVisibility: () => void;
6 | }
7 |
8 | const PrivacyContext = createContext(undefined);
9 |
10 | const STORAGE_KEY = 'privacy-settings';
11 |
12 | export function PrivacyProvider({ children }: { children: React.ReactNode }) {
13 | const [isBalanceHidden, setIsBalanceHidden] = useState(() => {
14 | const stored = localStorage.getItem(STORAGE_KEY);
15 | return stored ? JSON.parse(stored) : false;
16 | });
17 |
18 | useEffect(() => {
19 | localStorage.setItem(STORAGE_KEY, JSON.stringify(isBalanceHidden));
20 | }, [isBalanceHidden]);
21 |
22 | function toggleBalanceVisibility() {
23 | setIsBalanceHidden((prev: boolean) => !prev);
24 | }
25 |
26 | return (
27 |
28 | {children}
29 |
30 | );
31 | }
32 |
33 | export function useBalancePrivacy() {
34 | const context = useContext(PrivacyContext);
35 | if (context === undefined) {
36 | throw new Error('useBalancePrivacy must be used within a PrivacyProvider');
37 | }
38 | return context;
39 | }
40 |
--------------------------------------------------------------------------------
/src/hooks/use-accounts-simple-performance.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { useMemo } from 'react';
3 | import { calculateAccountsSimplePerformance } from '@/commands/portfolio';
4 | import { Account, SimplePerformanceMetrics } from '@/lib/types';
5 | import { QueryKeys } from '@/lib/query-keys';
6 |
7 | export const useAccountsSimplePerformance = (accounts: Account[] | undefined) => {
8 | const accountIds = useMemo(() => accounts?.map((acc) => acc.id) ?? [], [accounts]);
9 |
10 | const { data, isLoading, isFetching, isError, error } = useQuery<
11 | SimplePerformanceMetrics[],
12 | Error
13 | >(
14 | {
15 | queryKey: QueryKeys.accountsSimplePerformance(accountIds),
16 | queryFn: () => {
17 | return calculateAccountsSimplePerformance(accountIds);
18 | },
19 | }
20 | );
21 |
22 | return {
23 | data,
24 | isLoading,
25 | isFetching,
26 | isError,
27 | error,
28 | };
29 | };
--------------------------------------------------------------------------------
/src/hooks/use-accounts.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { Account } from '@/lib/types';
3 | import { getAccounts } from '@/commands/account';
4 | import { QueryKeys } from '@/lib/query-keys';
5 |
6 |
7 | export function useAccounts(filterActive: boolean = true) {
8 | const { data: fetchedAccounts = [], isLoading, isError, error } = useQuery({
9 | queryKey: [QueryKeys.ACCOUNTS, filterActive],
10 | queryFn: getAccounts,
11 | });
12 |
13 | // Apply active filter if requested
14 | const filteredAccounts = filterActive
15 | ? fetchedAccounts.filter((account) => account.isActive)
16 | : fetchedAccounts;
17 |
18 | return { accounts: filteredAccounts, isLoading, isError, error };
19 | }
--------------------------------------------------------------------------------
/src/hooks/use-calculate-portfolio.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query';
2 | import { toast } from '@/components/ui/use-toast';
3 | import { updatePortfolio, recalculatePortfolio } from '@/commands/portfolio';
4 | import { logger } from '@/adapters';
5 |
6 |
7 | export function useUpdatePortfolioMutation() {
8 | const queryClient = useQueryClient();
9 |
10 | return useMutation({
11 | mutationFn: updatePortfolio,
12 | onError: (error) => {
13 | queryClient.invalidateQueries();
14 | toast({
15 | title: 'Failed to update portfolio data.',
16 | description: 'Please try again or report an issue if the problem persists.',
17 | variant: 'destructive',
18 | });
19 | logger.error(`Error calculating historical data: ${error}`);
20 | },
21 | });
22 | }
23 |
24 | export function useRecalculatePortfolioMutation() {
25 | const queryClient = useQueryClient();
26 | return useMutation({
27 | mutationFn: recalculatePortfolio,
28 | onError: (error) => {
29 | queryClient.invalidateQueries();
30 | toast({
31 | title: 'Failed to recalculate portfolio.',
32 | description: 'Please try again or report an issue if the problem persists.',
33 | variant: 'destructive',
34 | });
35 | logger.error(`Error recalculating portfolio: ${error}`);
36 | },
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/hooks/use-market-data-providers.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { getMarketDataProviders } from '@/commands/market-data';
3 | import { QueryKeys } from '@/lib/query-keys';
4 | import { MarketDataProviderInfo } from '@/lib/types';
5 | import { logger } from '@/adapters';
6 |
7 | export function useMarketDataProviders() {
8 | return useQuery({
9 | queryKey: [QueryKeys.MARKET_DATA_PROVIDERS],
10 | queryFn: async () => {
11 | try {
12 | const providers = await getMarketDataProviders();
13 | return providers;
14 | } catch (error) {
15 | let errorMessage = "Unknown error";
16 | if (error instanceof Error) {
17 | errorMessage = error.message;
18 | }
19 | logger.error(`Error fetching market data providers in useMarketDataProviders: ${errorMessage}`);
20 | throw new Error(errorMessage);
21 | }
22 | },
23 | });
24 | }
--------------------------------------------------------------------------------
/src/hooks/use-settings-mutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query';
2 | import { toast } from '@/components/ui/use-toast';
3 | import { updateSettings } from '@/commands/settings';
4 | import { Settings } from '@/lib/types';
5 | import { logger } from '@/adapters';
6 | import { QueryKeys } from '@/lib/query-keys';
7 |
8 | export function useSettingsMutation(
9 | setSettings: React.Dispatch>,
10 | applySettingsToDocument: (newSettings: Settings) => void,
11 | ) {
12 | const queryClient = useQueryClient();
13 | return useMutation({
14 | mutationFn: updateSettings,
15 | onSuccess: (updatedSettings) => {
16 | queryClient.invalidateQueries({ queryKey: [QueryKeys.SETTINGS] });
17 | setSettings(updatedSettings);
18 | applySettingsToDocument(updatedSettings);
19 | toast({
20 | title: 'Settings updated successfully.',
21 | variant: 'success',
22 | });
23 | },
24 | onError: (error) => {
25 | logger.error(`Error updating settings: ${error}`);
26 | toast({
27 | title: 'Uh oh! Something went wrong.',
28 | description: 'There was a problem updating your settings.',
29 | variant: 'destructive',
30 | });
31 | },
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/src/hooks/use-settings.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from '@tanstack/react-query';
2 | import { Settings } from '@/lib/types';
3 | import { getSettings } from '@/commands/settings';
4 | import { QueryKeys } from '@/lib/query-keys';
5 |
6 | export function useSettings() {
7 | return useQuery