├── .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 |