Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34223960f8 | |||
| 5f583de857 | |||
| bb0a0ef928 | |||
| 2d4c7002a7 | |||
| 939e46eb61 | |||
| 04f1d8cbcd | |||
| c6032c5b66 | |||
| 8908e2da02 | |||
| 892d5a30e0 | |||
| b896513be9 | |||
| 013ccba050 | |||
| a10f94a570 | |||
| 5b8b9ae39e | |||
| 3e84ed7572 | |||
| 39458ef44e | |||
| 0e47b7538e | |||
| 0aaa51e736 |
@@ -33,7 +33,24 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
*/
|
*/
|
||||||
public function collection()
|
public function collection()
|
||||||
{
|
{
|
||||||
return $this->empty ? collect() : DailyChange::myDailyChanges()->withDailyPerformance()->get();
|
if ($this->empty) {
|
||||||
|
return collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return DailyChange::myDailyChanges()
|
||||||
|
->withDailyPerformance()
|
||||||
|
->get()
|
||||||
|
->map(function ($daily_change) {
|
||||||
|
return [
|
||||||
|
'date' => date_format($daily_change->date, 'Y-m-d'),
|
||||||
|
'portfolio_id' => $daily_change->portfolio_id,
|
||||||
|
'total_market_value' => $daily_change->total_market_value,
|
||||||
|
'total_cost_basis' => $daily_change->total_cost_basis,
|
||||||
|
'realized_gains' => $daily_change->realized_gain_dollars,
|
||||||
|
'total_dividends_earned' => $daily_change->total_dividends_earned,
|
||||||
|
'annotation' => $daily_change->annotation,
|
||||||
|
];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function title(): string
|
public function title(): string
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
|||||||
'currency' => $transaction->market_data_currency,
|
'currency' => $transaction->market_data_currency,
|
||||||
'split' => $transaction->split,
|
'split' => $transaction->split,
|
||||||
'reinvested_dividend' => $transaction->reinvested_dividend,
|
'reinvested_dividend' => $transaction->reinvested_dividend,
|
||||||
'date' => $transaction->date,
|
'date' => date_format($transaction->date, 'Y-m-d'),
|
||||||
'created_at' => $transaction->created_at,
|
'created_at' => $transaction->created_at,
|
||||||
'updated_at' => $transaction->updated_at,
|
'updated_at' => $transaction->updated_at,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TransactionResource extends JsonResource
|
|||||||
'sale_price' => $this->sale_price,
|
'sale_price' => $this->sale_price,
|
||||||
'split' => $this->split,
|
'split' => $this->split,
|
||||||
'reinvested_dividend' => $this->reinvested_dividend,
|
'reinvested_dividend' => $this->reinvested_dividend,
|
||||||
'date' => $this->date,
|
'date' => date_format($this->date, 'Y-m-d'),
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'updated_at' => $this->updated_at,
|
'updated_at' => $this->updated_at,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
|||||||
return new Dividend([
|
return new Dividend([
|
||||||
'symbol' => $symbol,
|
'symbol' => $symbol,
|
||||||
'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')),
|
'date' => Carbon::parse(Arr::get($dividend, 'ex_dividend_date')),
|
||||||
'dividend_amount' => Arr::get($dividend, 'amount'),
|
'dividend_amount' => (float) Arr::get($dividend, 'amount'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
|||||||
return new Split([
|
return new Split([
|
||||||
'symbol' => $symbol,
|
'symbol' => $symbol,
|
||||||
'date' => Carbon::parse(Arr::get($split, 'effective_date')),
|
'date' => Carbon::parse(Arr::get($split, 'effective_date')),
|
||||||
'split_amount' => Arr::get($split, 'split_factor'),
|
'split_amount' => (float) Arr::get($split, 'split_factor'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Livewire\Datatables;
|
namespace App\Livewire\Datatables;
|
||||||
|
|
||||||
use App\Models\Holding;
|
use App\Models\Holding;
|
||||||
use Illuminate\Support\Number;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
use Illuminate\Support\Number;
|
||||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||||
|
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||||
|
|
||||||
class HoldingsTable extends DataTableComponent
|
class HoldingsTable extends DataTableComponent
|
||||||
{
|
{
|
||||||
public $portfolio;
|
public $portfolio;
|
||||||
|
|
||||||
public array $hiddenColumns = [];
|
public array $hiddenColumns = [];
|
||||||
|
|
||||||
public function mount ($portfolio): void
|
public function mount($portfolio): void
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public function builder(): Builder
|
public function builder(): Builder
|
||||||
{
|
{
|
||||||
return Holding::query()
|
return Holding::query()
|
||||||
->portfolio($this->portfolio->id)
|
->portfolio($this->portfolio->id)
|
||||||
->with(['market_data'])
|
->with(['market_data'])
|
||||||
@@ -34,65 +37,65 @@ class HoldingsTable extends DataTableComponent
|
|||||||
$this->hiddenColumns = ['name', 'average_cost_basis', 'market_value', 'fifty_two_week_low', 'fifty_two_week_high'];
|
$this->hiddenColumns = ['name', 'average_cost_basis', 'market_value', 'fifty_two_week_low', 'fifty_two_week_high'];
|
||||||
|
|
||||||
$this->setTableWrapperAttributes([
|
$this->setTableWrapperAttributes([
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'overflow-scroll'
|
'class' => 'overflow-scroll',
|
||||||
]);
|
]);
|
||||||
$this->setTableAttributes([
|
$this->setTableAttributes([
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'table',
|
'class' => 'table',
|
||||||
]);
|
]);
|
||||||
$this->setTheadAttributes([
|
$this->setTheadAttributes([
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => true,
|
'default-styling' => true,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
]);
|
]);
|
||||||
$this->setThAttributes(function(Column $column) {
|
$this->setThAttributes(function (Column $column) {
|
||||||
|
|
||||||
$attributes = [
|
$attributes = [
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap'
|
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $attributes;
|
return $attributes;
|
||||||
});
|
});
|
||||||
$this->setThSortButtonAttributes(fn() => [
|
$this->setThSortButtonAttributes(fn () => [
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => true,
|
'default-styling' => true,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'cursor-pointer'
|
'class' => 'cursor-pointer',
|
||||||
]);
|
]);
|
||||||
$this->setTbodyAttributes([
|
$this->setTbodyAttributes([
|
||||||
'default' => false,
|
|
||||||
'default-styling' => true,
|
|
||||||
'default-colors' => false,
|
|
||||||
]);
|
|
||||||
$this->setTrAttributes(fn() => [
|
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => true,
|
'default-styling' => true,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'cursor-pointer hover:bg-neutral/25'
|
|
||||||
]);
|
]);
|
||||||
$this->setTdAttributes(function(Column $column) {
|
$this->setTrAttributes(fn () => [
|
||||||
|
'default' => false,
|
||||||
|
'default-styling' => true,
|
||||||
|
'default-colors' => false,
|
||||||
|
'class' => 'cursor-pointer hover:bg-neutral/25',
|
||||||
|
]);
|
||||||
|
$this->setTdAttributes(function (Column $column) {
|
||||||
|
|
||||||
$attributes = [
|
$attributes = [
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'text-nowrap'
|
'class' => 'text-nowrap',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $attributes;
|
return $attributes;
|
||||||
@@ -107,11 +110,11 @@ class HoldingsTable extends DataTableComponent
|
|||||||
|
|
||||||
$this->setPrimaryKey('id');
|
$this->setPrimaryKey('id');
|
||||||
|
|
||||||
$this->setTableRowUrl(function($row) {
|
$this->setTableRowUrl(function ($row) {
|
||||||
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
||||||
|
|
||||||
})->setTableRowUrlTarget(function($row) {
|
})->setTableRowUrlTarget(function ($row) {
|
||||||
|
|
||||||
return 'navigate';
|
return 'navigate';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -127,43 +130,42 @@ class HoldingsTable extends DataTableComponent
|
|||||||
->sortable(),
|
->sortable(),
|
||||||
Column::make(__('Average Cost Basis'), 'average_cost_basis')
|
Column::make(__('Average Cost Basis'), 'average_cost_basis')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('Total Cost Basis'), 'total_cost_basis')
|
Column::make(__('Total Cost Basis'), 'total_cost_basis')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('Market Value'), 'market_data.market_value')
|
Column::make(__('Market Value'), 'market_data.market_value')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('Total Market Value'))
|
Column::make(__('Total Market Value'))
|
||||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('total_market_value', $direction))
|
->sortable(fn (Builder $query, string $direction) => $query->orderBy('total_market_value', $direction))
|
||||||
->label(fn ($row) => Number::currency($row->total_market_value ?? 0, $row->market_data->currency)),
|
->label(fn ($row) => Number::currency($row->total_market_value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('Market Gain/Loss'))
|
Column::make(__('Market Gain/Loss'))
|
||||||
->html()
|
->html()
|
||||||
->label(fn($row) => Number::currency($row->market_gain_dollars ?? 0, $row->market_data->currency) . view('components.ui.gain-loss-arrow-badge', [
|
->label(fn ($row) => Number::currency($row->market_gain_dollars ?? 0, $row->market_data?->currency).view('components.ui.gain-loss-arrow-badge', [
|
||||||
'costBasis' => $row->average_cost_basis,
|
'costBasis' => $row->average_cost_basis,
|
||||||
'marketValue' => $row->market_data->market_value,
|
'marketValue' => $row->market_data?->market_value,
|
||||||
'small' => true,
|
'small' => true,
|
||||||
]))
|
]))
|
||||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('market_gain_dollars', $direction)),
|
->sortable(fn (Builder $query, string $direction) => $query->orderBy('market_gain_dollars', $direction)),
|
||||||
Column::make(__('Realized Gain/Loss'), 'realized_gain_dollars')
|
Column::make(__('Realized Gain/Loss'), 'realized_gain_dollars')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) )
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
|
||||||
Column::make(__('Dividends Earned'), 'dividends_earned')
|
Column::make(__('Dividends Earned'), 'dividends_earned')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('52 week low'), 'market_data.fifty_two_week_low')
|
Column::make(__('52 week low'), 'market_data.fifty_two_week_low')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('52 week high'), 'market_data.fifty_two_week_high')
|
Column::make(__('52 week high'), 'market_data.fifty_two_week_high')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value, $row) => Number::currency($value ?? 0, $row->market_data->currency) ),
|
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||||
Column::make(__('Number of Transactions'))
|
Column::make(__('Number of Transactions'))
|
||||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('num_transactions', $direction))
|
->sortable(fn (Builder $query, string $direction) => $query->orderBy('num_transactions', $direction))
|
||||||
->label(fn ($row) => $row->num_transactions),
|
->label(fn ($row) => $row->num_transactions),
|
||||||
Column::make(__('Last Refreshed'), 'market_data.updated_at')
|
Column::make(__('Last Refreshed'), 'market_data.updated_at')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value) => \Carbon\Carbon::parse($value)->diffForHumans() )
|
->format(fn ($value) => \Carbon\Carbon::parse($value)->diffForHumans()),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Livewire\Datatables;
|
namespace App\Livewire\Datatables;
|
||||||
|
|
||||||
use App\Models\Transaction;
|
use App\Models\Transaction;
|
||||||
use Illuminate\Support\Number;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
use Illuminate\Support\Number;
|
||||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||||
|
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||||
|
|
||||||
class TransactionsTable extends DataTableComponent
|
class TransactionsTable extends DataTableComponent
|
||||||
{
|
{
|
||||||
public array $hiddenColumns = [];
|
public array $hiddenColumns = [];
|
||||||
|
|
||||||
public function mount (): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
public function builder(): Builder
|
public function builder(): Builder
|
||||||
{
|
{
|
||||||
return Transaction::query()
|
return Transaction::query()
|
||||||
->with(['portfolio', 'market_data'])
|
->with(['portfolio', 'market_data'])
|
||||||
->myTransactions()
|
->myTransactions()
|
||||||
->addSelect(['portfolio_id', 'transaction_type', 'split'])
|
->addSelect(['portfolio_id', 'transaction_type', 'split', 'cost_basis'])
|
||||||
->selectRaw('
|
->selectRaw('
|
||||||
CASE
|
(CASE
|
||||||
WHEN transaction_type = \'SELL\'
|
WHEN transaction_type = \'SELL\'
|
||||||
THEN COALESCE(transactions.sale_price - transactions.cost_basis, 0)
|
THEN COALESCE(transactions.sale_price, 0)
|
||||||
ELSE COALESCE(market_data.market_value - transactions.cost_basis, 0)
|
ELSE COALESCE(market_data.market_value, 0)
|
||||||
END AS gain_dollars');
|
END) - COALESCE(transactions.cost_basis, 0) AS gain_dollars');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure(): void
|
public function configure(): void
|
||||||
@@ -36,65 +38,65 @@ class TransactionsTable extends DataTableComponent
|
|||||||
$this->hiddenColumns = ['name', 'cost_basis', 'gain_dollars'];
|
$this->hiddenColumns = ['name', 'cost_basis', 'gain_dollars'];
|
||||||
|
|
||||||
$this->setTableWrapperAttributes([
|
$this->setTableWrapperAttributes([
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'overflow-scroll'
|
'class' => 'overflow-scroll',
|
||||||
]);
|
]);
|
||||||
$this->setTableAttributes([
|
$this->setTableAttributes([
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'table',
|
'class' => 'table',
|
||||||
]);
|
]);
|
||||||
$this->setTheadAttributes([
|
$this->setTheadAttributes([
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => true,
|
'default-styling' => true,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
]);
|
]);
|
||||||
$this->setThAttributes(function(Column $column) {
|
$this->setThAttributes(function (Column $column) {
|
||||||
|
|
||||||
$attributes = [
|
$attributes = [
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap'
|
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $attributes;
|
return $attributes;
|
||||||
});
|
});
|
||||||
$this->setThSortButtonAttributes(fn() => [
|
$this->setThSortButtonAttributes(fn () => [
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => true,
|
'default-styling' => true,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'cursor-pointer'
|
'class' => 'cursor-pointer',
|
||||||
]);
|
]);
|
||||||
$this->setTbodyAttributes([
|
$this->setTbodyAttributes([
|
||||||
'default' => false,
|
|
||||||
'default-styling' => true,
|
|
||||||
'default-colors' => false,
|
|
||||||
]);
|
|
||||||
$this->setTrAttributes(fn() => [
|
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => true,
|
'default-styling' => true,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'cursor-pointer hover:bg-neutral/25'
|
|
||||||
]);
|
]);
|
||||||
$this->setTdAttributes(function(Column $column) {
|
$this->setTrAttributes(fn () => [
|
||||||
|
'default' => false,
|
||||||
|
'default-styling' => true,
|
||||||
|
'default-colors' => false,
|
||||||
|
'class' => 'cursor-pointer hover:bg-neutral/25',
|
||||||
|
]);
|
||||||
|
$this->setTdAttributes(function (Column $column) {
|
||||||
|
|
||||||
$attributes = [
|
$attributes = [
|
||||||
'default' => false,
|
'default' => false,
|
||||||
'default-styling' => false,
|
'default-styling' => false,
|
||||||
'default-colors' => false,
|
'default-colors' => false,
|
||||||
'class' => 'text-nowrap'
|
'class' => 'text-nowrap',
|
||||||
];
|
];
|
||||||
|
|
||||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||||
$attributes['class'] = $attributes['class'] . ' hidden md:table-cell';
|
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $attributes;
|
return $attributes;
|
||||||
@@ -111,11 +113,11 @@ class TransactionsTable extends DataTableComponent
|
|||||||
|
|
||||||
$this->setPrimaryKey('id');
|
$this->setPrimaryKey('id');
|
||||||
|
|
||||||
$this->setTableRowUrl(function($row) {
|
$this->setTableRowUrl(function ($row) {
|
||||||
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
||||||
|
|
||||||
})->setTableRowUrlTarget(function($row) {
|
})->setTableRowUrlTarget(function ($row) {
|
||||||
|
|
||||||
return 'navigate';
|
return 'navigate';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -123,10 +125,10 @@ class TransactionsTable extends DataTableComponent
|
|||||||
public function columns(): array
|
public function columns(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
||||||
Column::make(__('Date'), 'date')
|
Column::make(__('Date'), 'date')
|
||||||
->sortable()
|
->sortable()
|
||||||
->format(fn($value) => \Carbon\Carbon::parse($value)->format('M d, Y') ),
|
->format(fn ($value) => \Carbon\Carbon::parse($value)->format('M d, Y')),
|
||||||
Column::make(__('Portfolio'), 'portfolio.title')
|
Column::make(__('Portfolio'), 'portfolio.title')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Column::make(__('Symbol'), 'symbol')
|
Column::make(__('Symbol'), 'symbol')
|
||||||
@@ -134,14 +136,14 @@ class TransactionsTable extends DataTableComponent
|
|||||||
Column::make(__('Name'), 'market_data.name')
|
Column::make(__('Name'), 'market_data.name')
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Column::make(__('Type'), 'transaction_type')
|
Column::make(__('Type'), 'transaction_type')
|
||||||
->label(fn($row) => view('components.ui.badge', [
|
->label(fn ($row) => view('components.ui.badge', [
|
||||||
'value' => $row->split ? 'SPLIT'
|
'value' => $row->split ? 'SPLIT'
|
||||||
: ($row->reinvested_dividend
|
: ($row->reinvested_dividend
|
||||||
? 'REINVEST'
|
? 'REINVEST'
|
||||||
: $row->transaction_type),
|
: $row->transaction_type),
|
||||||
'class' => ($row->transaction_type == 'BUY'
|
'class' => ($row->transaction_type == 'BUY'
|
||||||
? 'badge-success'
|
? 'badge-success'
|
||||||
: 'badge-error') . ' badge-sm mr-3',
|
: 'badge-error').' badge-sm mr-3',
|
||||||
]))
|
]))
|
||||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('transaction_type', $direction)),
|
->sortable(fn (Builder $query, string $direction) => $query->orderBy('transaction_type', $direction)),
|
||||||
Column::make(__('Quantity'), 'quantity')
|
Column::make(__('Quantity'), 'quantity')
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class DailyChange extends Model
|
|||||||
->groupBy('portfolio_id', 'date');
|
->groupBy('portfolio_id', 'date');
|
||||||
|
|
||||||
return $query
|
return $query
|
||||||
->select(['daily_change.portfolio_id', 'daily_change.date'])
|
->select(['daily_change.date', 'daily_change.portfolio_id'])
|
||||||
->selectRaw('daily_change.total_market_value * COALESCE(cr.rate, 1) AS total_market_value')
|
->selectRaw('daily_change.total_market_value * COALESCE(cr.rate, 1) AS total_market_value')
|
||||||
->selectRaw('SUM(COALESCE(ccb.cumulative_cost_basis, 0)) AS total_cost_basis')
|
->selectRaw('SUM(COALESCE(ccb.cumulative_cost_basis, 0)) AS total_cost_basis')
|
||||||
->selectRaw('daily_change.total_market_value * COALESCE(cr.rate, 1)
|
->selectRaw('daily_change.total_market_value * COALESCE(cr.rate, 1)
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ trait HasProfilePhoto
|
|||||||
tap($this->profile_photo_path, function ($previous) use ($photo, $storagePath) {
|
tap($this->profile_photo_path, function ($previous) use ($photo, $storagePath) {
|
||||||
$this->forceFill([
|
$this->forceFill([
|
||||||
'profile_photo_path' => $photo->storePublicly(
|
'profile_photo_path' => $photo->storePublicly(
|
||||||
$storagePath, ['disk' => 'public']
|
$storagePath, ['disk' => $this->profilePhotoDisk()]
|
||||||
),
|
),
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
if ($previous) {
|
if ($previous) {
|
||||||
Storage::disk('public')->delete($previous);
|
Storage::disk($this->profilePhotoDisk())->delete($previous);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ trait HasProfilePhoto
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage::disk('public')->delete($this->profile_photo_path);
|
Storage::disk($this->profilePhotoDisk())->delete($this->profile_photo_path);
|
||||||
|
|
||||||
$this->forceFill([
|
$this->forceFill([
|
||||||
'profile_photo_path' => null,
|
'profile_photo_path' => null,
|
||||||
@@ -56,7 +56,7 @@ trait HasProfilePhoto
|
|||||||
{
|
{
|
||||||
return Attribute::get(function (): string {
|
return Attribute::get(function (): string {
|
||||||
return $this->profile_photo_path
|
return $this->profile_photo_path
|
||||||
? Storage::disk('public')->url($this->profile_photo_path)
|
? Storage::disk($this->profilePhotoDisk())->url($this->profile_photo_path)
|
||||||
: $this->defaultProfilePhotoUrl();
|
: $this->defaultProfilePhotoUrl();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,4 +74,14 @@ trait HasProfilePhoto
|
|||||||
|
|
||||||
return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=7F9CF5&background=EBF4FF';
|
return 'https://ui-avatars.com/api/?name='.urlencode($name).'&color=7F9CF5&background=EBF4FF';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the disk that profile photos should be stored on.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function profilePhotoDisk()
|
||||||
|
{
|
||||||
|
return config('filesystems.default', 'local');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,6 @@ trait Toast
|
|||||||
|
|
||||||
$this->js('toast('.json_encode(['toast' => $toast]).')');
|
$this->js('toast('.json_encode(['toast' => $toast]).')');
|
||||||
|
|
||||||
// session()->flash('ib.toast.title', $title);
|
|
||||||
// session()->flash('ib.toast.description', $description);
|
|
||||||
|
|
||||||
if ($redirectTo) {
|
if ($redirectTo) {
|
||||||
return $this->redirect($redirectTo, navigate: true);
|
return $this->redirect($redirectTo, navigate: true);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@
|
|||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
"laravel/pint": "^1.13",
|
"laravel/pint": "^1.25",
|
||||||
"laravel/sail": "^1.26",
|
"laravel/sail": "^1.26",
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "^8.0",
|
"nunomaduro/collision": "^8.0",
|
||||||
|
|||||||
Generated
+6
-6
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "3c3015ad711087ed55daf3aebcf8b06f",
|
"content-hash": "ad05656be3a8913bba187945f9683b48",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aws/aws-crt-php",
|
"name": "aws/aws-crt-php",
|
||||||
@@ -9288,16 +9288,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/pint",
|
"name": "laravel/pint",
|
||||||
"version": "v1.25.0",
|
"version": "v1.25.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/pint.git",
|
"url": "https://github.com/laravel/pint.git",
|
||||||
"reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96"
|
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/pint/zipball/595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96",
|
"url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
||||||
"reference": "595de38458c6b0ab4cae4bcc769c2e5c5d5b8e96",
|
"reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -9350,7 +9350,7 @@
|
|||||||
"issues": "https://github.com/laravel/pint/issues",
|
"issues": "https://github.com/laravel/pint/issues",
|
||||||
"source": "https://github.com/laravel/pint"
|
"source": "https://github.com/laravel/pint"
|
||||||
},
|
},
|
||||||
"time": "2025-09-17T01:36:44+00:00"
|
"time": "2025-09-19T02:57:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/sail",
|
"name": "laravel/sail",
|
||||||
|
|||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Build stage
|
# Stage 1: Build stage
|
||||||
FROM php:8.3-fpm AS builder
|
FROM php:8.4-fpm AS builder
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV APP_NAME=Investbrain
|
ENV APP_NAME=Investbrain
|
||||||
@@ -39,7 +39,7 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
|
|||||||
&& rm -rf node_modules
|
&& rm -rf node_modules
|
||||||
|
|
||||||
# Stage 2: Production stage
|
# Stage 2: Production stage
|
||||||
FROM php:8.3-fpm-alpine
|
FROM php:8.4-fpm-alpine
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /var/app
|
WORKDIR /var/app
|
||||||
@@ -71,8 +71,8 @@ RUN apk add --no-cache \
|
|||||||
RUN rm -rf /var/www/html \
|
RUN rm -rf /var/www/html \
|
||||||
&& ln -s /var/app /var/www/app
|
&& ln -s /var/app /var/www/app
|
||||||
|
|
||||||
# Create required directories for supervisord
|
# Create required directories
|
||||||
RUN mkdir -p /var/log/supervisor /var/run/supervisor
|
RUN mkdir -p /var/log/supervisor /var/run/supervisor /var/run/nginx
|
||||||
|
|
||||||
# Copy over configs
|
# Copy over configs
|
||||||
COPY ./docker/nginx.conf /etc/nginx/http.d/default.conf
|
COPY ./docker/nginx.conf /etc/nginx/http.d/default.conf
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ mkdir -p storage/framework/cache \
|
|||||||
storage/app \
|
storage/app \
|
||||||
storage/logs
|
storage/logs
|
||||||
|
|
||||||
|
timestamp=$(date -u "+[%Y-%m-%d %H:%M:%S]")
|
||||||
|
echo "$timestamp Investbrain starting ($VERSION)..." >> storage/logs/laravel.log
|
||||||
|
|
||||||
echo -e "\n > Storage directory scaffolding is OK... "
|
echo -e "\n > Storage directory scaffolding is OK... "
|
||||||
|
|
||||||
# Ensure storage directory is permissioned for www-data
|
# Ensure storage directory is permissioned for www-data
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
link="{{ route('oauth.redirect', ['provider' => $provider]) }}"
|
link="{{ route('oauth.redirect', ['provider' => $provider]) }}"
|
||||||
class="btn-sm btn-block my-1 text-white"
|
class="btn-sm btn-block my-1 text-white"
|
||||||
style='background-color: {{ config("services.$provider.color") }}'
|
style='background-color: {{ config("services.$provider.color") }}'
|
||||||
no-wire-navigate
|
external
|
||||||
>
|
>
|
||||||
@include("components.social.$provider-icon")
|
@include("components.social.$provider-icon")
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
'link' => null,
|
'link' => null,
|
||||||
'route' => null,
|
'route' => null,
|
||||||
'external' => false,
|
'external' => false,
|
||||||
'noWireNavigate' => false,
|
|
||||||
'badge' => null,
|
'badge' => null,
|
||||||
'badgeClasses' => null,
|
'badgeClasses' => null,
|
||||||
'badge' => false,
|
'badge' => false,
|
||||||
@@ -40,7 +39,7 @@
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@if(!$external && !$noWireNavigate)
|
@if(!$external)
|
||||||
{{ $attributes->wire('navigate')->value() ? $attributes->wire('navigate') : 'wire:navigate' }}
|
{{ $attributes->wire('navigate')->value() ? $attributes->wire('navigate') : 'wire:navigate' }}
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
@persist('toast')
|
@persist('toast')
|
||||||
<div
|
<div
|
||||||
x-cloak
|
x-cloak
|
||||||
x-data="{ show: false, timer: '', toast: ''}"
|
x-data="{ show: false, timer: '', toast: '' }"
|
||||||
@toast.window="
|
@toast.window="
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
toast = $event.detail.toast
|
toast = $event.detail.toast;
|
||||||
setTimeout(() => show = true, 100);
|
setTimeout(() => show = true, 100);
|
||||||
timer = setTimeout(() => show = false, $event.detail.toast.timeout);
|
timer = setTimeout(() => show = false, $event.detail.toast.timeout);
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ new class extends Component
|
|||||||
<div>
|
<div>
|
||||||
@foreach ($holding->dividends->take(5) as $dividend)
|
@foreach ($holding->dividends->take(5) as $dividend)
|
||||||
|
|
||||||
<x-ui.list-item :item="$dividend" no-separator>
|
<x-ui.list-item :item="$dividend" no-separator no-hover>
|
||||||
<x-slot:value>
|
<x-slot:value>
|
||||||
|
|
||||||
@php
|
@php
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
@foreach ($holding->splits->take(5) as $split)
|
@foreach ($holding->splits->take(5) as $split)
|
||||||
|
|
||||||
<x-ui.list-item :item="$split" no-separator>
|
<x-ui.list-item :item="$split" no-separator no-hover>
|
||||||
<x-slot:value>
|
<x-slot:value>
|
||||||
|
|
||||||
1:{{ $split->split_amount }}
|
1:{{ $split->split_amount }}
|
||||||
@@ -202,7 +202,9 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
'system_prompt' => "
|
'system_prompt' => "
|
||||||
You are an investment portfolio assistant providing advice to an investor. Use the following information to provide relevant recommendations. Use the words 'likely' or 'may' instead of concrete statements (except for obvious statements of fact or common sense):
|
Most recent training data: " . now()->toDateString() . ".
|
||||||
|
|
||||||
|
You are an investment portfolio assistant providing advice to an investor. Use the following information to provide relevant recommendations. Use the words 'likely' or 'may' instead of concrete statements (except for obvious statements of fact or common sense). . Do not apologize. Be polite, but minimize gratuitous niceties. When referencing numbers, always round to the nearest 100th decimal place.
|
||||||
|
|
||||||
The investor owns ". ($holding->quantity > 0 ? 'a total of '.$holding->quantity : 'ZERO') ." shares of {$holding->market_data->name} (ticker: {$holding->symbol}) with an average cost basis of {$holding->average_cost_basis}. Here are the relevant transactions - sales and purchases of {$holding->symbol}:
|
The investor owns ". ($holding->quantity > 0 ? 'a total of '.$holding->quantity : 'ZERO') ." shares of {$holding->market_data->name} (ticker: {$holding->symbol}) with an average cost basis of {$holding->average_cost_basis}. Here are the relevant transactions - sales and purchases of {$holding->symbol}:
|
||||||
|
|
||||||
@@ -219,7 +221,7 @@
|
|||||||
* 52 week high: {$holding->market_data->fifty_two_week_high}
|
* 52 week high: {$holding->market_data->fifty_two_week_high}
|
||||||
* Dividend yield: {$holding->market_data->dividend_yield}
|
* Dividend yield: {$holding->market_data->dividend_yield}
|
||||||
|
|
||||||
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.
|
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:"
|
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:"
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
|
|
||||||
<div class="mt-6 grid md:grid-cols-7 gap-5">
|
<div class="mt-6 grid md:grid-cols-7 gap-5">
|
||||||
|
|
||||||
<x-ui.card title="{{ __('Holdings') }}" class="md:col-span-4">
|
<x-ui.card title="{{ __('Holdings') }}" class="overflow-hidden col-span-7 md:col-span-4">
|
||||||
|
|
||||||
@if($portfolio->holdings->isEmpty())
|
@if($portfolio->holdings->isEmpty())
|
||||||
<div class="flex justify-center items-center h-full pb-10 text-secondary">
|
<div class="flex justify-center items-center h-full pb-10 text-secondary">
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
@endif
|
@endif
|
||||||
</x-ui.card>
|
</x-ui.card>
|
||||||
|
|
||||||
<x-ui.card title="{{ __('Recent activity') }}" class="md:col-span-3">
|
<x-ui.card title="{{ __('Recent activity') }}" class="col-span-7 md:col-span-3">
|
||||||
|
|
||||||
@if($portfolio->transactions->isEmpty())
|
@if($portfolio->transactions->isEmpty())
|
||||||
<div class="flex justify-center items-center h-full pb-10 text-secondary">
|
<div class="flex justify-center items-center h-full pb-10 text-secondary">
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
</x-ui.card>
|
</x-ui.card>
|
||||||
|
|
||||||
<x-ui.card title="{{ __('Top performers') }}" class="md:col-span-3">
|
<x-ui.card title="{{ __('Top performers') }}" class="col-span-7 md:col-span-3">
|
||||||
|
|
||||||
@if($portfolio->holdings->isEmpty())
|
@if($portfolio->holdings->isEmpty())
|
||||||
<div class="flex justify-center items-center h-full pb-10 text-secondary">
|
<div class="flex justify-center items-center h-full pb-10 text-secondary">
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
</x-ui.card>
|
</x-ui.card>
|
||||||
|
|
||||||
{{-- <x-ui.card title="{{ __('Top headlines') }}" class="md:col-span-3">
|
{{-- <x-ui.card title="{{ __('Top headlines') }}" class="col-span-7 md:col-span-3">
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$users = App\Models\User::take(3)->get();
|
$users = App\Models\User::take(3)->get();
|
||||||
@@ -165,13 +165,15 @@
|
|||||||
]
|
]
|
||||||
],
|
],
|
||||||
'system_prompt' => "
|
'system_prompt' => "
|
||||||
You are an investment portfolio assistant providing advice to an investor. Use the following information to provide relevant recommendations. Use the words 'likely' or 'may' instead of concrete statements (except for obvious statements of fact or common sense):
|
Most recent training data: " . now()->toDateString() . ".
|
||||||
|
|
||||||
|
You are an investment portfolio assistant providing advice to an investor. Use the following information to provide relevant recommendations. Use the words 'likely' or 'may' in lieu of concrete statements (except for obvious statements of fact or common sense). Do not apologize. Be polite, but minimize gratuitous niceties. When referencing numbers, always round to the nearest 100th decimal place.
|
||||||
|
|
||||||
The investor has the following holdings in this portfolio:
|
The investor has the following holdings in this portfolio:
|
||||||
|
|
||||||
{$formattedHoldings}
|
{$formattedHoldings}
|
||||||
|
|
||||||
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.
|
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:"
|
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:"
|
||||||
])
|
])
|
||||||
|
|||||||
Reference in New Issue
Block a user