Migrate to laravel ai sdk (#181)
Also * upgrade to livewire 4 * replace rappsoft tables with filament
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use App\Models\Holding;
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class ChatWithHoldingAgent implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
use RemembersConversations;
|
||||
|
||||
public function __construct(public readonly Holding $holding) {}
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
$holding = $this->holding;
|
||||
$quantity = $holding->quantity > 0
|
||||
? 'a total of '.$holding->quantity
|
||||
: 'ZERO';
|
||||
|
||||
return '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. If something is unclear, ask for clarification. When referencing numbers with precision, always round to the nearest 100th decimal place. If no precision, display numbers in integers.
|
||||
|
||||
The investor owns '.$quantity.' 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.':
|
||||
|
||||
'.$holding->getFormattedTransactions().'
|
||||
|
||||
This investor has earned $ '.$holding->dividends_earned.' in dividends so far and earned '.$holding->realized_gains_dollars.' in realized gains (sales) from '.$holding->symbol.' in this portfolio.
|
||||
|
||||
The current market price for '.$holding->symbol.' is '.$holding->market_data->market_value.'. Additionally, here\'s other critical fundamentals for '.$holding->market_data->name.' that might help:
|
||||
* Market cap: '.$holding->market_data->market_cap.'
|
||||
* Forward PE: '.$holding->market_data->forward_pe.'
|
||||
* Trailing PE: '.$holding->market_data->trailing_pe.'
|
||||
* Book value: '.$holding->market_data->book_value.'
|
||||
* 52 week low: '.$holding->market_data->fifty_two_week_low.'
|
||||
* 52 week high: '.$holding->market_data->fifty_two_week_high.'
|
||||
* Dividend yield: '.$holding->market_data->dividend_yield.'
|
||||
|
||||
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:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class ChatWithPortfolioAgent implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
use RemembersConversations;
|
||||
|
||||
public function __construct(public readonly Portfolio $portfolio) {}
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return '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 with precision, always round to the nearest 100th decimal place. If no precision, display numbers in integers.
|
||||
|
||||
The investor has the following holdings in this portfolio:
|
||||
|
||||
'.$this->portfolio->getFormattedHoldings().'
|
||||
|
||||
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:';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\HasStructuredOutput;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Messages;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class ChatWithSuggestedPromptsAgent implements Agent, HasStructuredOutput, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function __construct(
|
||||
public array $messages
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return 'Your role is a savvy helper that assists curious investors in asking thoughtful questions to investment advisors.
|
||||
|
||||
You should recommend between 1 and 5 (no more than 5) questions. You should ensure the questions you recommend are based on the provided context. Be sure to keep the questions short!
|
||||
|
||||
The questions you recommend might be based on natural follow up from the given context, requests to further refine a previous response, clarify undefined terms, common decision frameworks, possible risks or benefits, or commonly understood investing concepts that may require additional explanation.
|
||||
|
||||
Generate between 1 and 5 (no more than 5) follow up questions a curious investor might ask their advisor based on the provided conversation.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the messages available to the agent.
|
||||
*
|
||||
* @return Messages[]
|
||||
*/
|
||||
public function messages(): iterable
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the agent's structured output schema definition.
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'suggested_prompts' => $schema->array()->items(
|
||||
$schema->object([
|
||||
'text' => $schema->string()
|
||||
->description('Short description of suggested prompt (no more than 5 words)')
|
||||
->required(),
|
||||
'value' => $schema->string()
|
||||
->description('The detailed version of the prompt (think good prompt engineering!)')
|
||||
->required(),
|
||||
])->withoutAdditionalProperties()
|
||||
)->required(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Datatables;
|
||||
|
||||
use App\Models\Holding;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Number;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
|
||||
class HoldingsTable extends DataTableComponent
|
||||
{
|
||||
public $portfolio;
|
||||
|
||||
public array $hiddenColumns = [];
|
||||
|
||||
public function mount($portfolio): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Holding::query()
|
||||
->portfolio($this->portfolio->id)
|
||||
->with(['market_data'])
|
||||
->withCount(['transactions as num_transactions' => function ($query) {
|
||||
return $query->whereRaw('transactions.symbol = holdings.symbol');
|
||||
}])
|
||||
->withPerformance();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->hiddenColumns = ['name', 'average_cost_basis', 'market_value', 'fifty_two_week_low', 'fifty_two_week_high'];
|
||||
|
||||
$this->setTableWrapperAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'overflow-scroll',
|
||||
]);
|
||||
$this->setTableAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'table',
|
||||
]);
|
||||
$this->setTheadAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setThAttributes(function (Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap',
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
$this->setThSortButtonAttributes(fn () => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer',
|
||||
]);
|
||||
$this->setTbodyAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setTrAttributes(fn () => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer hover:bg-neutral/25',
|
||||
]);
|
||||
$this->setTdAttributes(function (Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-nowrap',
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
|
||||
$this->setDefaultSort('symbol', 'asc');
|
||||
|
||||
$this->setToolsDisabled();
|
||||
$this->setFooterDisabled();
|
||||
$this->setPaginationDisabled();
|
||||
$this->setDisplayPaginationDetailsDisabled();
|
||||
|
||||
$this->setPrimaryKey('id');
|
||||
|
||||
$this->setTableRowUrl(function ($row) {
|
||||
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
||||
|
||||
})->setTableRowUrlTarget(function ($row) {
|
||||
|
||||
return 'navigate';
|
||||
});
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make(__('Symbol'), 'symbol')
|
||||
->sortable(),
|
||||
Column::make(__('Name'), 'market_data.name')
|
||||
->sortable(),
|
||||
Column::make(__('Quantity'), 'quantity')
|
||||
->sortable(),
|
||||
Column::make(__('Average Cost Basis'), 'average_cost_basis')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('Total Cost Basis'), 'total_cost_basis')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('Market Value'), 'market_data.market_value')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('Total Market Value'))
|
||||
->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)),
|
||||
Column::make(__('Market Gain/Loss'))
|
||||
->html()
|
||||
->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,
|
||||
'marketValue' => $row->market_data?->market_value,
|
||||
'small' => true,
|
||||
]))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('market_gain_dollars', $direction)),
|
||||
Column::make(__('Realized Gain/Loss'), 'realized_gain_dollars')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('Dividends Earned'), 'dividends_earned')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('52 week low'), 'market_data.fifty_two_week_low')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('52 week high'), 'market_data.fifty_two_week_high')
|
||||
->sortable()
|
||||
->format(fn ($value, $row) => Number::currency($value ?? 0, $row->market_data?->currency)),
|
||||
Column::make(__('Number of Transactions'))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('num_transactions', $direction))
|
||||
->label(fn ($row) => $row->num_transactions),
|
||||
Column::make(__('Last Refreshed'), 'market_data.updated_at')
|
||||
->sortable()
|
||||
->format(fn ($value) => \Carbon\Carbon::parse($value)->diffForHumans()),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Datatables;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Number;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
|
||||
class TransactionsTable extends DataTableComponent
|
||||
{
|
||||
public array $hiddenColumns = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Transaction::query()
|
||||
->with(['portfolio', 'market_data'])
|
||||
->myTransactions()
|
||||
->addSelect(['portfolio_id', 'transaction_type', 'split', 'cost_basis'])
|
||||
->selectRaw('
|
||||
(CASE
|
||||
WHEN transaction_type = \'SELL\'
|
||||
THEN COALESCE(transactions.sale_price, 0)
|
||||
ELSE COALESCE(market_data.market_value, 0)
|
||||
END) - COALESCE(transactions.cost_basis, 0) AS gain_dollars');
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->hiddenColumns = ['name', 'cost_basis', 'gain_dollars'];
|
||||
|
||||
$this->setTableWrapperAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'overflow-scroll',
|
||||
]);
|
||||
$this->setTableAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'table',
|
||||
]);
|
||||
$this->setTheadAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setThAttributes(function (Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-xs font-medium whitespace-nowrap uppercase tracking-wider text-nowrap',
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
$this->setThSortButtonAttributes(fn () => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer',
|
||||
]);
|
||||
$this->setTbodyAttributes([
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
]);
|
||||
$this->setTrAttributes(fn () => [
|
||||
'default' => false,
|
||||
'default-styling' => true,
|
||||
'default-colors' => false,
|
||||
'class' => 'cursor-pointer hover:bg-neutral/25',
|
||||
]);
|
||||
$this->setTdAttributes(function (Column $column) {
|
||||
|
||||
$attributes = [
|
||||
'default' => false,
|
||||
'default-styling' => false,
|
||||
'default-colors' => false,
|
||||
'class' => 'text-nowrap',
|
||||
];
|
||||
|
||||
if (in_array($column->getField(), $this->hiddenColumns)) {
|
||||
$attributes['class'] = $attributes['class'].' hidden md:table-cell';
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
});
|
||||
|
||||
$this->setDefaultSort('date', 'desc');
|
||||
|
||||
$this->setPerPageAccepted([10, 15, 20]);
|
||||
$this->setPerPage(15);
|
||||
$this->setSearchDisabled();
|
||||
$this->setColumnSelectDisabled();
|
||||
$this->setPerPageVisibilityDisabled();
|
||||
$this->setFooterDisabled();
|
||||
|
||||
$this->setPrimaryKey('id');
|
||||
|
||||
$this->setTableRowUrl(function ($row) {
|
||||
return route('holding.show', ['portfolio' => $row->portfolio_id, 'symbol' => $row->symbol]);
|
||||
|
||||
})->setTableRowUrlTarget(function ($row) {
|
||||
|
||||
return 'navigate';
|
||||
});
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Column::make(__('Date'), 'date')
|
||||
->sortable()
|
||||
->format(fn ($value) => \Carbon\Carbon::parse($value)->format('M d, Y')),
|
||||
Column::make(__('Portfolio'), 'portfolio.title')
|
||||
->sortable(),
|
||||
Column::make(__('Symbol'), 'symbol')
|
||||
->sortable(),
|
||||
Column::make(__('Name'), 'market_data.name')
|
||||
->sortable(),
|
||||
Column::make(__('Type'), 'transaction_type')
|
||||
->label(fn ($row) => view('components.ui.badge', [
|
||||
'value' => $row->split ? 'SPLIT'
|
||||
: ($row->reinvested_dividend
|
||||
? 'REINVEST'
|
||||
: $row->transaction_type),
|
||||
'class' => ($row->transaction_type == 'BUY'
|
||||
? 'badge-success'
|
||||
: 'badge-error').' badge-sm mr-3',
|
||||
]))
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('transaction_type', $direction)),
|
||||
Column::make(__('Quantity'), 'quantity')
|
||||
->sortable(),
|
||||
Column::make(__('Cost Basis'), 'cost_basis')
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('cost_basis', $direction))
|
||||
->label(fn ($row) => Number::currency($row->cost_basis ?? 0, $row->market_data->currency)),
|
||||
Column::make(__('Gain/Loss'), 'gain_dollars')
|
||||
->sortable(fn (Builder $query, string $direction) => $query->orderBy('gain_dollars', $direction))
|
||||
->label(fn ($row) => Number::currency($row->gain_dollars ?? 0, $row->market_data->currency)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Tables;
|
||||
|
||||
use App\Models\Holding;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Schemas\Concerns\InteractsWithSchemas;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
use Filament\Support\Contracts\TranslatableContentDriver;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Number;
|
||||
use Livewire\Component;
|
||||
|
||||
class HoldingsTable extends Component implements HasActions, HasSchemas, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithSchemas;
|
||||
use InteractsWithTable;
|
||||
|
||||
public $portfolio;
|
||||
|
||||
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
|
||||
return $table
|
||||
->query(
|
||||
Holding::query()
|
||||
->portfolio($this->portfolio->id)
|
||||
->withMarketData()
|
||||
->withCount(['transactions as num_transactions' => function ($query) {
|
||||
return $query->whereRaw('transactions.symbol = holdings.symbol');
|
||||
}])
|
||||
->withPerformance()
|
||||
)
|
||||
->defaultSort('symbol', 'asc')
|
||||
->paginated(false)
|
||||
->recordUrl(fn ($record) => route('holding.show', ['portfolio' => $record->portfolio_id, 'symbol' => $record->symbol]))
|
||||
->columns([
|
||||
TextColumn::make('symbol')
|
||||
->label(__('Symbol'))
|
||||
->sortable(),
|
||||
TextColumn::make('market_data.name')
|
||||
->label(__('Name'))
|
||||
->sortable(),
|
||||
TextColumn::make('quantity')
|
||||
->label(__('Quantity'))
|
||||
->sortable(),
|
||||
TextColumn::make('average_cost_basis')
|
||||
->label(__('Average Cost Basis'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('total_cost_basis')
|
||||
->label(__('Total Cost Basis'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('market_data.market_value')
|
||||
->label(__('Market Value'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('total_market_value')
|
||||
->label(__('Total Market Value'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('market_gain_dollars')
|
||||
->label(__('Market Gain/Loss'))
|
||||
->sortable()
|
||||
->html()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency).view('components.ui.gain-loss-arrow-badge', [
|
||||
'costBasis' => $record->average_cost_basis,
|
||||
'marketValue' => $record->market_data?->market_value,
|
||||
'small' => true,
|
||||
])->render()),
|
||||
TextColumn::make('realized_gain_dollars')
|
||||
->label(__('Realized Gain/Loss'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('dividends_earned')
|
||||
->label(__('Dividends Earned'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('market_data.fifty_two_week_low')
|
||||
->label(__('52 week low'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('market_data.fifty_two_week_high')
|
||||
->label(__('52 week high'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data?->currency)),
|
||||
TextColumn::make('num_transactions')
|
||||
->label(__('Number of Transactions'))
|
||||
->sortable(),
|
||||
TextColumn::make('market_data.updated_at')
|
||||
->label(__('Last Refreshed'))
|
||||
->sortable()
|
||||
->since(),
|
||||
])
|
||||
->stackedOnMobile();
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div>
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Tables;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Actions\Concerns\InteractsWithActions;
|
||||
use Filament\Actions\Contracts\HasActions;
|
||||
use Filament\Schemas\Concerns\InteractsWithSchemas;
|
||||
use Filament\Schemas\Contracts\HasSchemas;
|
||||
use Filament\Support\Contracts\TranslatableContentDriver;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Number;
|
||||
use Livewire\Component;
|
||||
|
||||
class TransactionsTable extends Component implements HasActions, HasSchemas, HasTable
|
||||
{
|
||||
use InteractsWithActions;
|
||||
use InteractsWithSchemas;
|
||||
use InteractsWithTable;
|
||||
|
||||
public function makeFilamentTranslatableContentDriver(): ?TranslatableContentDriver
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->filters([
|
||||
SelectFilter::make('transaction_type')
|
||||
->options([
|
||||
'BUY' => 'BUY',
|
||||
'SELL' => 'SELL',
|
||||
]),
|
||||
SelectFilter::make('portfolio')
|
||||
->relationship('portfolio', 'title'),
|
||||
])
|
||||
->deferFilters(false)
|
||||
->query(
|
||||
Transaction::query()
|
||||
->with(['portfolio', 'market_data'])
|
||||
->myTransactions()
|
||||
->addSelect(['transactions.*'])
|
||||
->selectRaw('
|
||||
(CASE
|
||||
WHEN transaction_type = \'SELL\'
|
||||
THEN COALESCE(transactions.sale_price, 0)
|
||||
ELSE COALESCE((SELECT market_value FROM market_data WHERE market_data.symbol = transactions.symbol LIMIT 1), 0)
|
||||
END) - COALESCE(transactions.cost_basis, 0) AS gain_dollars')
|
||||
)
|
||||
->defaultSort('date', 'desc')
|
||||
->extremePaginationLinks()
|
||||
->paginated([10])
|
||||
->defaultPaginationPageOption(10)
|
||||
->recordUrl(fn ($record) => route('holding.show', ['portfolio' => $record->portfolio_id, 'symbol' => $record->symbol]))
|
||||
->columns([
|
||||
TextColumn::make('date')
|
||||
->label(__('Date'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state) => Carbon::parse($state)->format('M d, Y')),
|
||||
TextColumn::make('portfolio.title')
|
||||
->label(__('Portfolio'))
|
||||
->sortable(),
|
||||
TextColumn::make('symbol')
|
||||
->label(__('Symbol'))
|
||||
->sortable(),
|
||||
TextColumn::make('market_data.name')
|
||||
->label(__('Name'))
|
||||
->sortable(),
|
||||
TextColumn::make('transaction_type')
|
||||
->label(__('Type'))
|
||||
->sortable()
|
||||
->html()
|
||||
->formatStateUsing(fn ($state, $record) => view('components.ui.badge', [
|
||||
'value' => $record->split ? 'SPLIT' : ($record->reinvested_dividend ? 'REINVEST' : $record->transaction_type),
|
||||
'class' => ($record->transaction_type == 'BUY' ? 'badge-success' : 'badge-error').' badge-sm mr-3',
|
||||
])->render()),
|
||||
TextColumn::make('quantity')
|
||||
->label(__('Quantity'))
|
||||
->sortable(),
|
||||
TextColumn::make('cost_basis')
|
||||
->label(__('Cost Basis'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data->currency)),
|
||||
TextColumn::make('gain_dollars')
|
||||
->label(__('Gain/Loss'))
|
||||
->sortable()
|
||||
->formatStateUsing(fn ($state, $record) => Number::currency($state ?? 0, $record->market_data->currency)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
return <<<'HTML'
|
||||
<div>
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AgentConversationMessage extends Model
|
||||
{
|
||||
protected $table = 'agent_conversation_messages';
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'conversation_id',
|
||||
'user_id',
|
||||
'agent',
|
||||
'role',
|
||||
'content',
|
||||
];
|
||||
|
||||
public function conversation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ChatWithConversation::class, 'conversation_id');
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AiChat extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
protected $fillable = [
|
||||
'role',
|
||||
'content',
|
||||
];
|
||||
|
||||
protected $hidden = [];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($chat) {
|
||||
|
||||
$chat->user_id = auth()->user()->id;
|
||||
});
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function chatable()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ChatWithConversation extends Model
|
||||
{
|
||||
protected $table = 'agent_conversations';
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'user_id',
|
||||
'title',
|
||||
'chatable_type',
|
||||
'chatable_id',
|
||||
];
|
||||
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function (ChatWithConversation $model) {
|
||||
if (empty($model->id)) {
|
||||
$model->id = (string) Str::uuid7();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function chatable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function messages(): HasMany
|
||||
{
|
||||
return $this->hasMany(AgentConversationMessage::class, 'conversation_id');
|
||||
}
|
||||
}
|
||||
+8
-10
@@ -8,7 +8,9 @@ use App\Traits\HasMarketData;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -161,14 +163,10 @@ class Holding extends Model
|
||||
->orderBy('date', 'DESC');
|
||||
}
|
||||
|
||||
/**
|
||||
* Related chats for holding
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function chats()
|
||||
public function chatWithConversation(): MorphOne
|
||||
{
|
||||
return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id);
|
||||
return $this->morphOne(ChatWithConversation::class, 'chatable')
|
||||
->where('user_id', auth()->id());
|
||||
}
|
||||
|
||||
public function scopeWithMarketData($query)
|
||||
@@ -449,7 +447,7 @@ class Holding extends Model
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function qtyOwned(?\Illuminate\Support\Carbon $date = null)
|
||||
public function qtyOwned(?Carbon $date = null)
|
||||
{
|
||||
if ($date == null) {
|
||||
$date = now();
|
||||
@@ -470,8 +468,8 @@ class Holding extends Model
|
||||
* @return void
|
||||
*/
|
||||
public function dailyPerformance(
|
||||
?\Illuminate\Support\Carbon $start_date = null,
|
||||
?\Illuminate\Support\Carbon $end_date = null,
|
||||
?Carbon $start_date = null,
|
||||
?Carbon $end_date = null,
|
||||
) {
|
||||
if ($start_date == null) {
|
||||
$start_date = now();
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use App\Models\ChatWithConversation;
|
||||
use App\Notifications\InvitedOnboardingNotification;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
@@ -68,14 +69,10 @@ class Portfolio extends Model
|
||||
return $this->hasMany(DailyChange::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Related chats for portfolio
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function chats()
|
||||
public function chatWithConversation(): \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
{
|
||||
return $this->morphMany(AiChat::class, 'chatable')->where('user_id', auth()->user()->id);
|
||||
return $this->morphOne(ChatWithConversation::class, 'chatable')
|
||||
->where('user_id', auth()->id());
|
||||
}
|
||||
|
||||
public function scopeMyPortfolios()
|
||||
|
||||
@@ -4,6 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Interfaces\MarketData\FallbackInterface;
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use Filament\Support\Colors\Color;
|
||||
use Filament\Support\Facades\FilamentColor;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Number;
|
||||
@@ -18,8 +22,8 @@ class AppServiceProvider extends ServiceProvider
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(
|
||||
\App\Interfaces\MarketData\MarketDataInterface::class,
|
||||
\App\Interfaces\MarketData\FallbackInterface::class
|
||||
MarketDataInterface::class,
|
||||
FallbackInterface::class
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +32,15 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
FilamentColor::register([
|
||||
'primary' => Color::Stone,
|
||||
'gray' => Color::Zinc,
|
||||
'info' => Color::Blue,
|
||||
'success' => Color::Emerald,
|
||||
'warning' => Color::Amber,
|
||||
'danger' => Color::Red,
|
||||
]);
|
||||
|
||||
JsonResource::withoutWrapping();
|
||||
|
||||
Arr::macro('skipEmptyValues', function (array $array) {
|
||||
|
||||
Reference in New Issue
Block a user