Skip to content

Instantly share code, notes, and snippets.

@binaryfire
Forked from ManukMinasyan/OpportunityBoard.php
Created August 24, 2025 14:33
Show Gist options
  • Save binaryfire/8a1bb35648c9d509c8f649c4a4e64d05 to your computer and use it in GitHub Desktop.
Save binaryfire/8a1bb35648c9d509c8f649c4a4e64d05 to your computer and use it in GitHub Desktop.
Flowforge - Custom Fields Integration Example
<?php
declare(strict_types=1);
namespace App\Filament\Clusters\Management\Pages;
use App\Models\Management\Opportunity;
use BackedEnum;
use Exception;
use Filament\Actions\CreateAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
use Filament\Support\Enums\Width;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Relaticle\CustomFields\Facades\CustomFields;
use Relaticle\CustomFields\Models\CustomField;
use Relaticle\Flowforge\Board;
use Relaticle\Flowforge\BoardPage;
use Relaticle\Flowforge\Column;
use UnitEnum;
final class OpportunityBoard extends BoardPage
{
protected static ?string $navigationLabel = 'Opportunity Board';
protected static ?string $title = 'Opportunities';
protected static string | null | UnitEnum $navigationGroup = 'Flowforge';
protected static string | null | BackedEnum $navigationIcon = 'heroicon-o-briefcase';
public function board(Board $board): Board
{
return $board
->query($this->getOpportunityQuery())
->columnIdentifier('cfv.string_value')
->positionIdentifier('position')
->recordTitleAttribute('title')
->columns($this->getDynamicColumns())
->columnActions([
CreateAction::make()
->label('Add Opportunity')
->iconButton()
->icon('heroicon-o-plus')
->model(Opportunity::class)
->modalWidth(Width::Large)
->schema([
TextInput::make('title')
->required()
->columnSpanFull(),
Select::make('user_id')
->label('Assign to')
->relationship('user', 'name')
->searchable()
->preload()
->required()
->placeholder('Unassigned')
->columnSpanFull(),
CustomFields::form()->forModel(Opportunity::class)->except(['status'])->build(),
])
->mutateDataUsing(function (array $data, array $arguments): array {
if (isset($arguments['column'])) {
$data['position'] = $this->getBoardPositionInColumn($arguments['column']);
}
return $data;
})
->after(function (Opportunity $record, array $arguments): void {
if (isset($arguments['column'])) {
$this->updateCustomFieldStatus($record, $arguments['column']);
}
}),
])
->cardActions([
EditAction::make()
->label('Edit')
->icon('heroicon-o-pencil')
->modalWidth(Width::Large)
->schema([
TextInput::make('title')
->required()
->columnSpanFull(),
Select::make('user_id')
->label('Assign to')
->relationship('user', 'name')
->searchable()
->preload()
->required()
->placeholder('Unassigned')
->columnSpanFull(),
CustomFields::form()->forModel(Opportunity::class)->except(['status'])->build(),
]),
])
->cardSchema(function (Schema $schema): Schema {
return $schema
->components([
TextEntry::make('user.name')
->hiddenLabel()
->badge()
->color('gray')
->icon('heroicon-o-user')
->placeholder('Unassigned'),
CustomFields::infolist()->forSchema($schema)->except(['status'])->build(),
]);
});
}
public function moveCard(
string $cardId,
string $targetColumnId,
?string $afterCardId = null,
?string $beforeCardId = null
): void {
$opportunity = Opportunity::query()->findOrFail($cardId);
$newPosition = $this->calculatePositionBetweenCards($afterCardId, $beforeCardId, $targetColumnId);
// Use transaction for atomicity
DB::transaction(function () use ($opportunity, $targetColumnId, $newPosition) {
$this->updateCustomFieldStatus($opportunity, $targetColumnId);
$opportunity->update(['position' => $newPosition]);
});
$this->dispatch('kanban-card-moved', [
'cardId' => $cardId,
'columnId' => $targetColumnId,
'position' => $newPosition,
]);
}
private function getOpportunityQuery(): Builder
{
return Opportunity::query()
->with('user', 'customFieldValues.customField')
->join('custom_field_values as cfv', 'management_opportunities.id', '=', 'cfv.entity_id')
->join('custom_fields as cf', 'cfv.custom_field_id', '=', 'cf.id')
->where('cfv.entity_type', Opportunity::class)
->where('cf.code', 'status')
->select('management_opportunities.*', 'cfv.string_value as board_status');
}
private function getStatusCustomField(): ?CustomField
{
return CustomField::where('code', 'status')
->where('entity_type', Opportunity::class)
->first();
}
private function updateCustomFieldStatus(Opportunity $opportunity, string $status): void
{
$statusField = $this->getStatusCustomField();
if (! $statusField) {
return;
}
DB::table('custom_field_values')
->updateOrInsert([
'entity_type' => Opportunity::class,
'entity_id' => $opportunity->id,
'custom_field_id' => $statusField->id,
], [
'string_value' => $status,
]);
}
/**
* @return array<Column>
*
* @throws Exception
*/
protected function getDynamicColumns(): array
{
$customField = $this->getStatusCustomField();
if (! $customField) {
return [];
}
$options = $customField->options()->orderBy('sort_order')->get();
$columns = [];
foreach ($options as $option) {
$columns[] = Column::make($option->name)
->label($option->name);
}
return $columns;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment