Feat: Adds multi currency support (#88)

This commit is contained in:
hackerESQ
2025-04-09 19:25:15 -05:00
committed by GitHub
parent 6d6f968f42
commit eae345f243
100 changed files with 17735 additions and 35761 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
<x-input id="password_confirmation" label="{{ __('Confirm Password') }}" class="block mt-1 w-full" type="password" name="password_confirmation" required autocomplete="new-password" />
</div>
@if (Laravel\Jetstream\Jetstream::hasTermsAndPrivacyPolicyFeature())
@if (! config('investbrain.self_hosted'))
<div class="mt-4">
<label>
<div class="flex items-center">
@@ -73,7 +73,7 @@ new class extends Component {
'model' => config('openai.model'),
'messages' => [
['role' => 'system', 'content' => "Today's date is "
.now()->format('Y-m-d')
.now()->toDateString()
.".\n\n".$this->system_prompt],
...array_slice($this->messages, -10)
],
@@ -1,18 +1,18 @@
<span
class=""
style="width:90em;overflow: hidden; white-space: nowrap;"
title="{{ Number::currency($low ?? 0) }} - {{ Number::currency($high ?? 0) }}"
title="{{ Number::currency($marketData->fifty_two_week_low ?? 0, $marketData->currency) }} - {{ Number::currency($marketData->fifty_two_week_high ?? 0, $marketData->currency) }}"
>
@php
// 52-week low must be a non-zero
if (empty($low)) {
$low = 1;
if (empty($marketData->fifty_two_week_low)) {
$marketData->fifty_two_week_low = 1;
}
@endphp
@for ($x = 0; $x < 10; $x++)
@if ((($current - $low) * 100) / ($high - $low) > ($x * 10))
@if ((($marketData->market_value - $marketData->fifty_two_week_low) * 100) / ($marketData->fifty_two_week_high - $marketData->fifty_two_week_low) > ($x * 10))
&#9679;
@@ -94,7 +94,7 @@
}
this.data.yaxis.labels.formatter = function (value) {
return `$${value}`
return `{{ Number::currencySymbol(auth()->user()->getCurrency()) }}${value}`
}
this.data.tooltip = {
@@ -103,7 +103,7 @@
formatter: (value, { series, seriesIndex, dataPointIndex, w }) => {
const firstDataPoint = this.data.series[seriesIndex].data[0][1]
const percentageChange = ((value - firstDataPoint) / firstDataPoint) * 100;
return `$${parseFloat(value.toFixed(2))} (${percentageChange.toFixed(2)}%)`;
return `${parseFloat(value.toFixed(2))} (${percentageChange.toFixed(2)}%)`;
}
},
}
@@ -0,0 +1,64 @@
@props([
'sidebar' => null,
'content' => null,
'footer' => null,
'fullWidth' => false,
'withNav' => false,
'collapseText' => 'Collapse',
'collapseIcon' => 'o-bars-3-bottom-right',
'collapsible' => false,
'url' => route('mary.toogle-sidebar', absolute: false),
])
<main class="{{ !$fullWidth ? 'max-w-screen-2xl' : '' }} w-full mx-auto">
<div class="drawer {{ $sidebar?->attributes['right'] ? 'drawer-end' : '' }} lg:drawer-open">
<input id="{{ $sidebar?->attributes['drawer'] }}" type="checkbox" class="drawer-toggle" />
<div {{ $content->attributes->class(["drawer-content w-full mx-auto p-5 lg:px-10 lg:py-5"]) }}>
{{-- MAIN CONTENT --}}
{{ $content }}
</div>
{{-- SIDEBAR --}}
@if($sidebar)
<div
x-data="{
collapsed: {{ session('mary-sidebar-collapsed', 'false') }},
collapseText: '{{ $collapseText }}',
toggle() {
this.collapsed = !this.collapsed;
fetch('{{ $url }}?collapsed=' + this.collapsed);
this.$dispatch('sidebar-toggled', this.collapsed);
}
}"
@menu-sub-clicked="if(collapsed) { toggle() }"
@class(["drawer-side z-20 lg:z-auto", "top-0 lg:top-[73px] lg:h-[calc(100vh-73px)]" => $withNav])
>
<label for="{{ $sidebar?->attributes['drawer'] }}" aria-label="close sidebar" class="drawer-overlay"></label>
{{-- SIDEBAR CONTENT --}}
<div>
{{ $sidebar }}
{{-- SIDEBAR COLLAPSE --}}
@if($sidebar->attributes['collapsible'])
<x-mary-menu class="hidden !bg-inherit lg:block">
<x-mary-menu-item
@click="toggle"
icon="{{ $sidebar->attributes['collapse-icon'] ?? $collapseIcon }}"
title="{{ $sidebar->attributes['collapse-text'] ?? $collapseText }}" />
</x-mary-menu>
@endif
</div>
</div>
@endif
{{-- END SIDEBAR--}}
</div>
</main>
{{-- FOOTER --}}
@if($footer)
<footer {{ $footer?->attributes->class(["mx-auto w-full", "max-w-screen-2xl" => !$fullWidth ]) }}>
{{ $footer }}
</footer>
@endif
@@ -1,4 +1,23 @@
<?php
use Livewire\Volt\Component;
new class extends Component
{
// props
/**
* The component's listeners.
*
* @var array
*/
protected $listeners = [
'refresh-navigation-menu' => '$refresh',
];
// methods
}; ?>
<div class="bg-base-100 border-base-300 border-b sticky top-0 z-10">
<div class="flex justify-between items-center px-7 py-3 gap-4 mx-auto">
<div class="flex flex-0 items-center">
@@ -1,54 +1,94 @@
<x-menu activate-by-route>
<?php
<x-menu-item title="{{ __('Dashboard') }}" icon="o-home" link="{{ route('dashboard') }}" />
<x-menu-sub title="{{ __('Portfolios') }}" icon="o-document-duplicate">
@foreach (auth()->user()->portfolios as $portfolio)
<x-menu-item icon="o-document" link="{{ route('portfolio.show', ['portfolio' => $portfolio->id ]) }}" >
<x-slot:title>
{{ $portfolio->title }}
@if($portfolio->wishlist)
<x-badge value="{{ __('Wishlist') }}" class="badge-secondary badge-sm ml-2" />
@endif
</x-slot:title>
</x-menu-item>
@endforeach
use Livewire\Volt\Component;
<x-menu-item title="{{ __('Create Portfolio') }}" icon="o-document-plus" link="{{ route('portfolio.create') }}" />
</x-menu-sub>
<x-menu-item title="{{ __('Transactions') }}" icon="o-banknotes" link="{{ route('transaction.index') }}" />
{{-- <x-menu-item title="{{ __('Reporting') }}" icon="o-chart-bar-square" link="####" /> --}}
new class extends Component
{
// props
</x-menu>
/**
* The component's listeners.
*
* @var array
*/
protected $listeners = [
'refresh-navigation-menu' => '$refresh',
];
</div>
<div class="px-3">
// methods
<x-section-border />
}; ?>
@php
$user = auth()->user();
@endphp
<div class="
flex
flex-col
!transition-all
!duration-100
ease-out
overflow-x-hidden
overflow-y-auto
h-screen
lg:h-[calc(100vh-73px)]
bg-base-100
lg:bg-inherit
{{ session('mary-sidebar-collapsed') == 'true' ? 'w-[70px] [&>*_summary::after]:hidden [&_.mary-hideable]:hidden [&_.display-when-collapsed]:block [&_.hidden-when-collapsed]:hidden' : null }}
{{ session('mary-sidebar-collapsed') != 'true' ? 'w-[270px] [&>*_summary::after]:block [&_.mary-hideable]:block [&_.hidden-when-collapsed]:block [&_.display-when-collapsed]:hidden' : null }}
">
<div class="flex-1">
<x-menu activate-by-route>
<x-list-item :item="$user" avatar="profile_photo_url" value="name" sub-value="email" no-separator no-hover class="mb-3 !-mt-3 rounded">
<x-slot:actions>
<x-dropdown>
<x-slot:trigger>
<x-button icon="o-cog-6-tooth" class="btn-circle btn-ghost btn-xs" />
</x-slot:trigger>
<x-menu-item title="{{ __('Manage Profile') }}" icon="o-user" link="{{ @route('profile.show') }}" />
<x-menu-item title="{{ __('API Tokens') }}" icon="o-command-line" link="{{ @route('api-tokens.index') }}" />
<x-menu-item title="{{ __('Import / Export Data') }}" icon="o-cloud-arrow-down" link="{{ @route('import-export') }}" />
<x-menu-item title="{{ __('Dashboard') }}" icon="o-home" link="{{ route('dashboard') }}" />
<x-menu-sub title="{{ __('Portfolios') }}" icon="o-document-duplicate">
@foreach (auth()->user()->portfolios as $portfolio)
<x-menu-item icon="o-document" link="{{ route('portfolio.show', ['portfolio' => $portfolio->id ]) }}" >
<x-slot:title>
{{ $portfolio->title }}
@if($portfolio->wishlist)
<x-badge value="{{ __('Wishlist') }}" class="badge-secondary badge-sm ml-2" />
@endif
</x-slot:title>
</x-menu-item>
@endforeach
<x-section-border class="py-1" />
<x-menu-item title="{{ __('Create Portfolio') }}" icon="o-document-plus" link="{{ route('portfolio.create') }}" />
</x-menu-sub>
<x-menu-item title="{{ __('Transactions') }}" icon="o-banknotes" link="{{ route('transaction.index') }}" />
{{-- <x-menu-item title="{{ __('Reporting') }}" icon="o-chart-bar-square" link="####" /> --}}
<x-menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" />
<form id="logout" action="{{ route('logout') }}" method="POST" style="display: none;">
{{ csrf_field() }}
</form>
</x-menu>
</x-dropdown>
</x-slot:actions>
</x-list-item>
</div>
<div class="px-3">
<x-section-border />
@php
$user = auth()->user();
@endphp
<x-list-item :item="$user" avatar="profile_photo_url" value="name" sub-value="email" no-separator no-hover class="mb-3 !-mt-3 rounded">
<x-slot:actions>
<x-dropdown>
<x-slot:trigger>
<x-button icon="o-cog-6-tooth" class="btn-circle btn-ghost btn-xs" />
</x-slot:trigger>
<x-menu-item title="{{ __('Manage Profile') }}" icon="o-user" link="{{ @route('profile.show') }}" />
<x-menu-item title="{{ __('API Tokens') }}" icon="o-command-line" link="{{ @route('api-tokens.index') }}" />
<x-menu-item title="{{ __('Import / Export Data') }}" icon="o-cloud-arrow-down" link="{{ @route('import-export') }}" />
<x-section-border class="py-1" />
<x-menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" />
<form id="logout" action="{{ route('logout') }}" method="POST" style="display: none;">
{{ csrf_field() }}
</form>
</x-dropdown>
</x-slot:actions>
</x-list-item>
</div>
</div>
+7 -5
View File
@@ -1,3 +1,5 @@
@use('App\Models\Currency')
<x-app-layout>
@livewire('portfolio-performance-chart', [
@@ -7,27 +9,27 @@
<div class="grid sm:grid-cols-5 gap-5">
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Market Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_gain_dollars) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_gain_dollars', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Cost Basis') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_cost_basis) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_cost_basis', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Market Value') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_market_value) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_value', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Realized Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->realized_gain_dollars) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('realized_gain_dollars', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Dividends Earned') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_dividends_earned) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_dividends_earned', 0)) }} </div>
</x-card>
</div>
@@ -27,9 +27,9 @@ new class extends Component
$owned = ($dividend->purchased - $dividend->sold);
@endphp
{{ Number::currency($dividend->dividend_amount) }}
{{ Number::currency($dividend->dividend_amount, $holding->market_data->currency) }}
x {{ $owned }}
= {{ Number::currency($owned * $dividend->dividend_amount) }}
= {{ Number::currency($owned * $dividend->dividend_amount, $holding->market_data->currency) }}
</x-slot:value>
<x-slot:sub-value>
@@ -3,27 +3,27 @@
use App\Models\Holding;
use Livewire\Volt\Component;
new class extends Component {
new class extends Component
{
// props
public Holding $holding;
protected $listeners = [
'transaction-updated' => '$refresh',
'transaction-saved' => '$refresh'
'transaction-saved' => '$refresh',
];
// methods
}; ?>
<div>
<div class="font-bold text-2xl py-1 flex items-center">
{{ Number::currency($holding->market_data->market_value ?? 0) }}
{{ Number::currency($holding->market_data->market_value ?? 0, $holding->market_data->currency) }}
<x-gain-loss-arrow-badge
:cost-basis="$holding->average_cost_basis"
:market-value="$holding->market_data->market_value"
:market-value="$holding->market_data->market_value_base"
/>
</div>
@@ -34,22 +34,22 @@ new class extends Component {
<p>
<span class="font-bold">{{ __('Average Cost Basis') }}: </span>
{{ Number::currency($holding->average_cost_basis ?? 0) }}
{{ Number::currency($holding->average_cost_basis ?? 0, $holding->market_data->currency) }}
</p>
<p>
<span class="font-bold">{{ __('Total Cost Basis') }}: </span>
{{ Number::currency($holding->total_cost_basis ?? 0) }}
{{ Number::currency($holding->total_cost_basis ?? 0, $holding->market_data->currency) }}
</p>
<p>
<span class="font-bold">{{ __('Realized Gain/Loss') }}: </span>
{{ Number::currency($holding->realized_gain_dollars ?? 0) }}
{{ Number::currency($holding->realized_gain_dollars ?? 0, $holding->market_data->currency) }}
</p>
<p>
<span class="font-bold">{{ __('Dividends Earned') }}: </span>
{{ Number::currency($holding->dividends_earned ?? 0) }}
{{ Number::currency($holding->dividends_earned ?? 0, $holding->market_data->currency) }}
</p>
<p class="pt-2 text-sm" title="{{ \Carbon\Carbon::parse($holding->market_data->updated_at)->toIso8601String() }}">
@@ -1,13 +1,12 @@
<?php
use App\Models\Holding;
use App\Models\Portfolio;
use App\Models\Transaction;
use Illuminate\Support\Collection;
use Livewire\Volt\Component;
use App\Models\Currency;
new class extends Component {
new class extends Component
{
// props
public Portfolio $portfolio;
@@ -40,13 +39,13 @@ new class extends Component {
{
$holdings = $this->portfolio
->holdings()
->withCount(['transactions as num_transactions' => function($query) {
return $query->whereRaw('transactions.symbol = holdings.symbol');
}])
->orderBy(...array_values($this->sortBy))
->holdings()
->withCount(['transactions as num_transactions' => function ($query) {
return $query->whereRaw('transactions.symbol = holdings.symbol');
}])
->orderBy(...array_values($this->sortBy))
// ->where('holdings.quantity', '>', 0)
->get();
->get();
return $holdings;
}
@@ -55,7 +54,6 @@ new class extends Component {
{
return $this->redirect(route('holding.show', ['portfolio' => $holding['portfolio_id'], 'symbol' => $holding['symbol']]));
}
}; ?>
@@ -66,16 +64,17 @@ new class extends Component {
@row-click="$wire.goToHolding($event.detail)"
>
@scope('cell_average_cost_basis', $row)
{{ Number::currency($row->average_cost_basis ?? 0) }}
{{ Number::currency($row->average_cost_basis ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_total_cost_basis', $row)
{{ Number::currency($row->total_cost_basis ?? 0) }}
{{ Number::currency($row->total_cost_basis ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_realized_gain_dollars', $row)
{{ Number::currency($row->realized_gain_dollars ?? 0) }}
{{ Number::currency($row->realized_gain_dollars ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_gain_dollars', $row)
{{ Number::currency($row->market_gain_dollars ?? 0) }}
{{ Number::currency($row->market_gain_dollars ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_gain_percent', $row)
<x-gain-loss-arrow-badge
@@ -84,19 +83,19 @@ new class extends Component {
/>
@endscope
@scope('cell_market_data_market_value', $row)
{{ Number::currency($row->market_data_market_value ?? 0) }}
{{ Number::currency($row->market_data_market_value ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_fifty_two_week_low', $row)
{{ Number::currency($row->market_data_fifty_two_week_low ?? 0) }}
{{ Number::currency($row->market_data_fifty_two_week_low ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_fifty_two_week_high', $row)
{{ Number::currency($row->market_data_fifty_two_week_high ?? 0) }}
{{ Number::currency($row->market_data_fifty_two_week_high ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_total_market_value', $row)
{{ Number::currency($row->total_market_value ?? 0) }}
{{ Number::currency($row->total_market_value ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_dividends_earned', $row)
{{ Number::currency($row->dividends_earned ?? 0) }}
{{ Number::currency($row->dividends_earned ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_updated_at', $row)
{{ \Carbon\Carbon::parse($row->market_data_updated_at)->diffForHumans() }}
+24 -14
View File
@@ -1,3 +1,5 @@
@use('App\Models\Currency')
<x-app-layout>
<div x-data>
@@ -67,48 +69,56 @@
<x-ib-card title="{{ __('Fundamentals') }}" class="md:col-span-4">
@if(!empty($holding->market_data->market_cap))
<p>
<span class="font-bold">{{ __('Market Cap') }}: </span>
${{ Number::forHumans($holding->market_data->market_cap ?? 0) }}
{{ Currency::forHumans($holding->market_data->market_cap, $holding->market_data->currency) }}
</p>
@endif
@if(!empty($holding->market_data->forward_pe))
<p>
<span class="font-bold">{{ __('Forward PE') }}: </span>
{{ $holding->market_data->forward_pe }}
</p>
@endif
@if(!empty($holding->market_data->trailing_pe))
<p>
<span class="font-bold">{{ __('Trailing PE') }}: </span>
{{ $holding->market_data->trailing_pe }}
</p>
@endif
<p>
<span class="font-bold">{{ __('Book Value') }}: </span>
{{ $holding->market_data->book_value }}
</p>
@if(!empty($holding->market_data->book_value))
<p>
<span class="font-bold">{{ __('Book Value') }}: </span>
{{ Number::currency($holding->market_data->book_value, $holding->market_data->currency) }}
</p>
@endif
<p>
<span class="font-bold">{{ __('52 week') }}: </span>
<x-fifty-two-week-range
:low="$holding->market_data->fifty_two_week_low"
:high="$holding->market_data->fifty_two_week_high"
:current="$holding->market_data->market_value"
/>
<x-fifty-two-week-range :market-data="$holding->market_data" />
</p>
@if(!empty($holding->market_data->dividend_yield))
<p>
<span class="font-bold">{{ __('Dividend Yield') }}: </span>
{{ Number::percentage(
$holding->market_data->dividend_yield ?? 0,
$holding->market_data->dividend_yield,
$holding->market_data->dividend_yield < 1 ? 2 : 0
) }}
</p>
@endif
@if(!empty($holding->market_data->last_dividend_date))
<p>
<span class="font-bold">{{ __('Last Dividend Paid') }}: </span>
{{ $holding->market_data?->last_dividend_date?->format('F d, Y') ?? '' }}
{{ $holding->market_data->last_dividend_date->format('F d, Y') }}
</p>
@endif
</x-ib-card>
@@ -164,7 +174,7 @@
</x-ib-card>
@if(config('services.ai_chat_enabled'))
{{-- // TODO: add to system prompt:
{{-- // todo: add to system prompt:
// Additionally, here is some recent news about {$this->holding->symbol}:
// And their latest SEC filings: --}}
@livewire('ai-chat-window', [
@@ -209,7 +219,7 @@
* 52 week high: {$holding->market_data->fifty_two_week_high}
* Dividend yield: {$holding->market_data->dividend_yield}
This data is current as of today's date: " . now()->format('Y-m-d') . ". Based on this current market data, quantity owned, and average cost basis, you should determine if the {$holding->symbol} holding is making or losing money.
This data is current as of today's date: " . now()->toDateString() . ". Based on this current market data, quantity owned, and average cost basis, you should determine if the {$holding->symbol} holding is making or losing money.
Below is the question from the investor. Considering these facts, provide a concise response to the following question (give a direct response). Limit your response to no more than 75 words and consider using a common decision framework. Use github style markdown for any formatting:"
])
View File
@@ -35,16 +35,7 @@ new class extends Component
{
$filterMethod = collect($this->scopeOptions)->where('id', $this->scope)->first();
$dailyChangeQuery = DailyChange::myDailyChanges()->selectRaw('
date,
SUM(total_market_value) as total_market_value,
SUM(total_cost_basis) as total_cost_basis,
SUM(total_gain) as total_gain
/* ,
SUM(realized_gains) as realized_gains,
SUM(total_dividends_earned) as total_dividends_earned
*/
');
$dailyChangeQuery = DailyChange::withDailyPerformance();
if (isset($this->portfolio)) {
@@ -54,18 +45,30 @@ new class extends Component
} else {
// dashboard
$dailyChangeQuery->withoutWishlists();
$dailyChangeQuery->myDailyChanges()->withoutWishlists();
}
if ($filterMethod['method']) {
$dailyChangeQuery->whereDate('date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args']));
$dailyChangeQuery->whereDate('daily_change.date', '>=', now()->{$filterMethod['method']}(...$filterMethod['args']));
}
$dailyChange = $dailyChangeQuery
->orderBy('date')
$dailyChange = $dailyChangeQuery->get();
$dailyChange = $dailyChange
->sortBy('date')
->groupBy('date')
->get();
->map(function ($group) {
return (object) [
'date' => $group->first()->date->toDateString(),
'total_market_value' => $group->sum('total_market_value'),
'total_cost_basis' => $group->sum('total_cost_basis'),
'total_gain' => $group->sum('total_gain'),
'realized_gain_dollars' => $group->sum('realized_gain_dollars'),
'total_dividends_earned' => $group->sum('total_dividends_earned'),
];
})
->values();
return [
'series' => [
+8 -7
View File
@@ -1,3 +1,5 @@
@use('App\Models\Currency')
<x-app-layout>
<div x-data>
@@ -63,30 +65,29 @@
<div class="grid sm:grid-cols-5 gap-5">
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Market Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_gain_dollars) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_gain_dollars', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Cost Basis') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_cost_basis) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_cost_basis', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Total Market Value') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_market_value) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_market_value', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Realized Gain/Loss') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->realized_gain_dollars) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('realized_gain_dollars', 0)) }} </div>
</x-card>
<x-card class="col-span-5 sm:col-span-1 bg-slate-100 dark:bg-base-200 rounded-lg">
<div class="text-sm text-gray-400 whitespace-nowrap truncate">{{ __('Dividends Earned') }}</div>
<div class="font-black text-xl"> {{ Number::currency($metrics->total_dividends_earned) }} </div>
<div class="font-black text-xl"> {{ Number::currency($metrics->get('total_dividends_earned', 0)) }} </div>
</x-card>
</div>
@@ -175,7 +176,7 @@
{$formattedHoldings}
This data is current as of today's date: " . now()->format('Y-m-d') . ". Based on the current market data, quantity owned, and average cost basis, you can determine the performance of any holding.
This data is current as of today's date: " . now()->toDateString() . ". Based on the current market data, quantity owned, and average cost basis, you can determine the performance of any holding.
Below is the question from the investor. Considering these facts, provide a concise response to the following question (give a direct response). Limit your response to no more than 75 words and consider using a common decision framework. Use github style markdown for any formatting:"
])
@@ -1,15 +1,15 @@
<?php
use Livewire\WithFileUploads;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
use App\Models\BackupImport as BackupImportModel;
use App\Imports\BackupImport;
use App\Exports\BackupExport;
use App\Models\BackupImport as BackupImportModel;
use Livewire\Attributes\Rule;
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use Maatwebsite\Excel\Facades\Excel;
use Mary\Traits\Toast;
new class extends Component {
new class extends Component
{
use Toast;
use WithFileUploads;
@@ -18,23 +18,26 @@ new class extends Component {
public $file;
public bool $importStatusDialog = false;
public ?BackupImportModel $backupImport = null;
public int $percent = 10;
// methods
public function import()
public function import()
{
$this->validate();
if (!RateLimiter::attempt('import:'.auth()->user()->id, $perMinute = 3, fn()=>null)) {
if (! RateLimiter::attempt('import:'.auth()->user()->id, $perMinute = 3, fn () => null)) {
$this->error(__('Hang on! You\'re doing that too much.'));
return;
}
$this->backupImport = BackupImportModel::create([
'user_id' => auth()->user()->id,
'path' => $this->file->getPathname()
'path' => $this->file->getPathname(),
]);
$this->importStatusDialog = true;
@@ -45,17 +48,17 @@ new class extends Component {
{
if (Str::contains($this->backupImport?->message, 'portfolios')) {
$this->percent = (1/2) * 100;
$this->percent = (1 / 2) * 100;
}
if (Str::contains($this->backupImport?->message, 'transactions')) {
$this->percent = (3/4) * 100;
$this->percent = (3 / 4) * 100;
}
if (Str::contains($this->backupImport?->message, 'daily changes')) {
$this->percent = (7/8) * 100;
$this->percent = (7 / 8) * 100;
}
if ($this->backupImport?->status == 'failed') {
@@ -75,9 +78,8 @@ new class extends Component {
public function downloadTemplate()
{
return Excel::download(new BackupExport(empty: true), now()->format('Y_m_d') . '_investbrain_template.xlsx');
return Excel::download(new BackupExport(empty: true), now()->format('Y_m_d').'_investbrain_template.xlsx');
}
}; ?>
<x-forms.form-section submit="import">
@@ -87,13 +89,13 @@ new class extends Component {
<x-slot name="description">
{{ __('Upload or recover your Investbrain portfolio and holdings.') }}
<strong><a href="#" title="{{ __('Click to download import template.') }}" @click="$wire.downloadTemplate()"> {{ __('Download import template.') }}</a></strong>
</x-slot>
<x-slot:form>
<div class="col-span-6 sm:col-span-4">
<x-file wire:model="file" label="{{ __('Select a file') }}" hint="" accept=".xlsx" required />
<p class="mt-4 text-xs text-secondary leading-tight"><a href="#" title="{{ __('Click to download import template.') }}" @click="$wire.downloadTemplate()"> {{ __('Download import template.') }}</a></p>
</div>
<x-dialog-modal wire:model.live="importStatusDialog" persistent>
@@ -0,0 +1,107 @@
<?php
use App\Models\Currency;
use App\Models\User;
use Illuminate\Support\Collection;
use Livewire\Volt\Component;
new class extends Component
{
// props
public Collection $currencies;
public string $display_currency;
public ?string $locale;
public ?User $user;
// methods
public function rules()
{
return [
'locale' => ['required', 'in:'.implode(',', Arr::pluck(config('app.available_locales'), 'locale'))],
'display_currency' => ['required', 'exists:currencies,currency'],
];
}
public function mount()
{
$this->currencies = Currency::get();
$this->display_currency = auth()->user()->getCurrency();
$this->locale = auth()->user()->getLocale();
$this->user = auth()->user();
}
public function updateProfileInformation()
{
$this->resetErrorBag();
$this->validate();
$this->user->options = array_merge($this->user->options ?? [], [
'locale' => $this->locale,
'display_currency' => $this->display_currency,
]);
$this->user->save();
cache()->tags(['metrics-'.$this->user->id])->flush();
$this->dispatch('saved');
//$this->js('window.location.reload();');
}
}; ?>
<x-forms.form-section submit="updateProfileInformation">
<x-slot name="title">
{{ __('Locale Options') }}
</x-slot>
<x-slot name="description">
{{ __('Adjust localization options for your preferred region.') }}
</x-slot>
<x-slot name="form">
<div class="col-span-6 sm:col-span-4">
<x-select
label="{{ __('Locale') }}"
class="select block mt-1 w-full"
:options="config('app.available_locales')"
option-value="locale"
option-label="label"
placeholder="Choose a locale"
wire:model="locale"
id="locale"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<x-select
label="{{ __('Display Currency') }}"
class="select block mt-1 w-full"
:options="$currencies"
option-value="currency"
option-label="label"
placeholder="Choose a display currency"
wire:model="display_currency"
id="display_currency"
/>
</div>
</x-slot>
<x-slot name="actions">
<x-forms.action-message class="me-3" on="saved">
{{ __('Saved.') }}
</x-forms.action-message>
<x-button type="submit">
{{ __('Save') }}
</x-button>
</x-slot>
</x-forms.form-section>
+6
View File
@@ -7,7 +7,13 @@
<x-section-border hide-on-mobile />
@endif
<div class="mt-10 sm:mt-0">
@livewire('localization-form')
</div>
<x-section-border hide-on-mobile />
@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))
<div class="mt-10 sm:mt-0">
@livewire('profile.update-password-form')
@@ -1,10 +1,13 @@
<?php
use App\Models\Currency;
use App\Models\MarketData;
use App\Models\Portfolio;
use App\Models\Transaction;
use App\Rules\QuantityValidationRule;
use App\Rules\SymbolValidationRule;
use App\Traits\WithTrimStrings;
use Illuminate\Support\Collection;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
@@ -34,6 +37,10 @@ new class extends Component
public bool $confirmingTransactionDeletion = false;
public Collection $currencies;
public string $currency;
// methods
public function rules()
{
@@ -41,13 +48,14 @@ new class extends Component
'symbol' => ['required', 'string', new SymbolValidationRule],
'transaction_type' => 'required|string|in:BUY,SELL',
'portfolio_id' => 'required|exists:portfolios,id',
'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:'.now()->format('Y-m-d')],
'date' => ['required', 'date_format:Y-m-d', 'before_or_equal:'.now()->toDateString()],
'quantity' => [
'required',
'numeric',
'min:0',
'gt:0',
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date),
],
'currency' => ['required', 'exists:currencies,currency'],
'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric',
'sale_price' => 'exclude_if:transaction_type,BUY|min:0|numeric',
];
@@ -55,20 +63,31 @@ new class extends Component
public function mount()
{
$this->currencies = Currency::list();
$this->currency = auth()->user()->getCurrency();
if (isset($this->transaction)) {
$this->currency = $this->transaction->market_data->currency;
$this->symbol = $this->transaction->symbol;
$this->transaction_type = $this->transaction->transaction_type;
$this->portfolio_id = $this->transaction->portfolio_id;
$this->date = $this->transaction->date->format('Y-m-d');
$this->date = $this->transaction->date->toDateString();
$this->quantity = $this->transaction->quantity;
$this->cost_basis = $this->transaction->cost_basis;
$this->sale_price = $this->transaction->sale_price;
} else {
if (isset($this->symbol)) {
$this->currency = MarketData::getMarketData($this->symbol)?->currency;
}
$this->transaction_type = 'BUY';
$this->portfolio_id = isset($this->portfolio) ? $this->portfolio->id : '';
$this->date = now()->format('Y-m-d');
$this->date = now()->toDateString();
}
}
@@ -100,7 +119,7 @@ new class extends Component
$this->dispatch('transaction-saved');
$this->success(__('Transaction created'), redirectTo: route('holding.show', ['portfolio' => $this->portfolio->id, 'symbol' => $this->symbol]));
$this->success(__('Transaction created'), redirectTo: route('holding.show', ['portfolio' => $this->portfolio->id, 'symbol' => $transaction->symbol]));
}
public function delete()
@@ -111,11 +130,6 @@ new class extends Component
$this->success(__('Transaction deleted'), redirectTo: route('holding.show', ['portfolio' => $this->portfolio->id, 'symbol' => $this->symbol]));
}
public function updatedSymbol($value)
{
$this->symbol = strtoupper($value);
}
}; ?>
<div class="" x-data="{ transaction_type: @entangle('transaction_type') }">
@@ -149,21 +163,44 @@ new class extends Component
label="{{ __('Sale Price') }}"
wire:model.number="sale_price"
required
prefix="USD"
type="number"
step="any"
/>
{{-- money --}}
>
<x-slot:prepend>
<x-select
class="rounded-e-none border-e-0 bg-base-200"
icon="o-banknotes"
:options="$currencies"
option-value="currency"
option-label="currency"
wire:model="currency"
id="currency"
/>
</x-slot:prepend>
</x-input>
@else
<x-input
label="{{ __('Cost Basis') }}"
wire:model.number="cost_basis"
required
prefix="USD"
type="number"
step="any"
/>
{{-- money --}}
>
<x-slot:prepend>
<x-select
class="rounded-e-none border-e-0 bg-base-200"
icon="o-banknotes"
:options="$currencies"
option-value="currency"
option-label="currency"
wire:model="currency"
id="currency"
/>
</x-slot:prepend>
</x-input>
@endif
<x-slot:actions>
@@ -6,30 +6,38 @@ use Illuminate\Support\Collection;
use Livewire\Volt\Component;
use Mary\Traits\Toast;
new class extends Component {
use Toast;
new class extends Component
{
use Toast;
// props
public Collection $transactions;
public ?Portfolio $portfolio;
public ?Transaction $editingTransaction;
public Bool $shouldGoToHolding = true;
public Bool $showPortfolio = false;
public Bool $paginate = true;
public Int $perPage = 5;
public Int $offset = 0;
public bool $shouldGoToHolding = true;
public bool $showPortfolio = false;
public bool $paginate = true;
public int $perPage = 5;
public int $offset = 0;
protected $listeners = [
'transaction-updated' => '$refresh',
'transaction-saved' => '$refresh'
'transaction-saved' => '$refresh',
];
// methods
public function showTransactionDialog($transactionId)
{
if (!auth()->user()->can('fullAccess', $this->portfolio)) {
if (! auth()->user()->can('fullAccess', $this->portfolio)) {
$this->error(__('You do not have permission to manage transactions for this portfolio'));
return;
}
@@ -46,7 +54,6 @@ new class extends Component {
{
$this->offset = $this->offset + $amount;
}
}; ?>
<div class="">
@@ -86,9 +93,12 @@ new class extends Component {
/>
{{ $transaction->symbol }}
({{ $transaction->quantity }}
@ {{ $transaction->transaction_type == 'BUY'
? Number::currency($transaction->cost_basis)
: Number::currency($transaction->sale_price) }})
@ {{ Number::currency(
$transaction->transaction_type == 'BUY'
? $transaction->cost_basis
: $transaction->sale_price,
$transaction->market_data->currency
) }})
<x-loading x-show="loading" x-cloak class="text-gray-400 ml-2" />
</x-slot:value>
@@ -1,22 +1,23 @@
<?php
use App\Models\User;
use App\Models\Transaction;
use Illuminate\Support\Collection;
use App\Models\User;
use Livewire\Volt\Component;
use Livewire\WithPagination;
use App\Models\Currency;
new class extends Component {
new class extends Component
{
use WithPagination;
// props
public User $user;
public ?Transaction $editingTransaction;
protected $listeners = [
'transaction-updated' => '$refresh',
'transaction-saved' => '$refresh'
'transaction-saved' => '$refresh',
];
public array $sortBy = ['column' => 'date', 'direction' => 'desc'];
@@ -47,12 +48,11 @@ new class extends Component {
public function transactions()
{
return auth()
->user()
->transactions()
->orderBy(...array_values($this->sortBy))
->paginate(10);
->user()
->transactions()
->orderBy(...array_values($this->sortBy))
->paginate(10);
}
}; ?>
<div class="">
@@ -96,19 +96,19 @@ new class extends Component {
/>
@endscope
@scope('cell_cost_basis', $row)
{{ Number::currency($row->cost_basis ?? 0) }}
{{ Number::currency($row->cost_basis ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_total_cost_basis', $row)
{{ Number::currency($row->total_cost_basis ?? 0) }}
{{ Number::currency($row->total_cost_basis ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_gain_dollars', $row)
{{ Number::currency($row->gain_dollars ?? 0) }}
{{ Number::currency($row->gain_dollars ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_market_data_market_value', $row)
{{ Number::currency($row->market_data_market_value ?? 0) }}
{{ Number::currency($row->market_data_market_value ?? 0, $row->market_data->currency) }}
@endscope
@scope('cell_total_market_value', $row)
{{ Number::currency($row->total_market_value ?? 0) }}
{{ Number::currency($row->total_market_value ?? 0, $row->market_data->currency) }}
@endscope
</x-table>