Compare commits

...

17 Commits

Author SHA1 Message Date
hackerESQ 34223960f8 chore: Bump PHP version to 8.4
see #150
2025-11-06 21:10:53 -06:00
hackerESQ 5f583de857 fix: export daily change dates transposition
reference #148
2025-11-04 19:59:33 -06:00
hackerESQ bb0a0ef928 fix: date for transactions in api requests
references #148
2025-11-04 19:44:14 -06:00
Fexiven 2d4c7002a7 fix: Create nginx directory (#143)
fixes:

nginx: [emerg] open() "/run/nginx/nginx.pid" failed (2: No such file or directory)
2025-10-30 16:43:26 -05:00
hackerESQ 939e46eb61 fix: dividends should be cast to float 2025-10-09 19:02:45 -05:00
hackerESQ 04f1d8cbcd fix: transactions table 2025-10-06 20:01:29 -05:00
hackerESQ c6032c5b66 cleanup 2025-09-28 21:13:52 -05:00
hackerESQ 8908e2da02 Fix: mobile responsive with table 2025-09-28 19:54:49 -05:00
hackerESQ 892d5a30e0 fix: default filesystem name 2025-09-27 21:52:57 -05:00
hackerESQ b896513be9 fix: disable wire navigate for social login 2025-09-26 20:36:29 -05:00
hackerESQ 013ccba050 update system prompt 2025-09-26 20:11:51 -05:00
hackerESQ a10f94a570 revert 2025-09-26 20:03:39 -05:00
hackerESQ 5b8b9ae39e wip 2025-09-26 18:32:00 -05:00
hackerESQ 3e84ed7572 string currency 2025-09-26 18:20:00 -05:00
hackerESQ 39458ef44e optional currency 2025-09-26 18:16:17 -05:00
hackerESQ 0e47b7538e Merge branch 'main' of https://github.com/investbrainapp/investbrain 2025-09-26 17:54:46 -05:00
hackerESQ 0aaa51e736 Fix: touch log file during start up
fixes laravel.log permissions #137
2025-09-26 17:54:44 -05:00
19 changed files with 154 additions and 120 deletions
+18 -1
View File
@@ -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
+1 -1
View File
@@ -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,
]; ];
+1 -1
View File
@@ -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'),
]); ]);
}); });
} }
+43 -41
View File
@@ -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()),
]; ];
} }
} }
+42 -40
View File
@@ -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')
+1 -1
View File
@@ -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)
+14 -4
View File
@@ -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');
}
} }
-3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
+5 -3
View File
@@ -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:"
]) ])
+8 -6
View File
@@ -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:"
]) ])