Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
31970e2
feat: add CurrencyColumn with numeric formatting and dollar prefix
ManukMinasyan Mar 19, 2026
05e0328
Fix styling
ManukMinasyan Mar 19, 2026
366e35e
feat: add CurrencyEntry with numeric formatting and dollar prefix
ManukMinasyan Mar 19, 2026
69389bc
Fix styling
ManukMinasyan Mar 19, 2026
aa7d789
fix: remove hardcoded constraints from CurrencyComponent, respect dec…
ManukMinasyan Mar 19, 2026
ff14d13
feat: wire CurrencyColumn/CurrencyEntry and add export transformer
ManukMinasyan Mar 19, 2026
32e2535
fix: restore nullsafe operator on validation_rules access
ManukMinasyan Mar 19, 2026
ad3a6bb
feat: add type-specific settings for currency fields (Attio-style)
ManukMinasyan Mar 19, 2026
f6b4fe8
fix: use afterStateHydrated instead of default for currency settings …
ManukMinasyan Mar 20, 2026
12da118
fix: preserve existing column formatters in table visibility wrapper
ManukMinasyan Mar 20, 2026
3021646
fix: resolve rector violations in currency-related files
ManukMinasyan Mar 20, 2026
dd1f06f
fix: review fixes for currency field settings and data consistency
ManukMinasyan Mar 25, 2026
3d9531e
fix: resolve rector violation in decimal places options
ManukMinasyan Mar 25, 2026
023390d
Fix styling
ManukMinasyan Mar 25, 2026
85879bd
feat: replace hardcoded currency list with ICU-powered CurrencyProvider
ManukMinasyan Mar 25, 2026
e784603
fix: address review feedback for currency field improvements
ManukMinasyan Apr 10, 2026
7bdef70
fix: add declare(strict_types=1) to satisfy rector
ManukMinasyan Apr 10, 2026
083cdb8
refactor: apply laravel best practices to currency field code
ManukMinasyan Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions config/custom-fields.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@
| Configure database table names and migration paths.
|
*/
/*
|--------------------------------------------------------------------------
| Currency Configuration
|--------------------------------------------------------------------------
|
| Default currency settings for currency field types.
|
*/
'currency' => [
'default_code' => env('CUSTOM_FIELDS_DEFAULT_CURRENCY', 'USD'),

// Override the currency list (null = auto-detect from PHP intl/ICU).
// Format: ['USD' => 'US Dollar', 'EUR' => 'Euro', ...]
'currencies' => null,
],

'database' => [
'migrations_path' => database_path('custom-fields'),
'table_names' => [
Expand Down
2 changes: 2 additions & 0 deletions src/Data/CustomFieldOptionSettingsData.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Data;

use Spatie\LaravelData\Attributes\MapName;
Expand Down
31 changes: 31 additions & 0 deletions src/Data/Settings/CurrencyFieldSettingsData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Data\Settings;

use Relaticle\CustomFields\Support\CurrencyProvider;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;

#[MapName(SnakeCaseMapper::class)]
class CurrencyFieldSettingsData extends Data
{
public function __construct(
public string $currencyCode = 'USD',
public string $displayType = 'symbol',
public int $decimalPlaces = 2,
) {}

public static function fromAdditional(array $additional): self
{
$code = $additional['currency_code'] ?? 'USD';

return new self(
currencyCode: $code,
displayType: $additional['display_type'] ?? 'symbol',
decimalPlaces: (int) ($additional['decimal_places'] ?? CurrencyProvider::getDecimalDigits($code)),
);
}
}
2 changes: 2 additions & 0 deletions src/Exceptions/CustomFieldAlreadyExistsException.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Exceptions;

use Exception;
Expand Down
2 changes: 2 additions & 0 deletions src/Exceptions/FieldTypeNotOptionableException.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Exceptions;

use Exception;
Expand Down
2 changes: 2 additions & 0 deletions src/Exceptions/MissingRecordTitleAttributeException.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Exceptions;

use Exception;
Expand Down
2 changes: 2 additions & 0 deletions src/Facades/CustomFields.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Facades;

use Illuminate\Support\Facades\Facade;
Expand Down
116 changes: 104 additions & 12 deletions src/FieldTypeSystem/Definitions/CurrencyFieldType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@

namespace Relaticle\CustomFields\FieldTypeSystem\Definitions;

use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Fieldset;
use Filament\Schemas\Components\Utilities\Get;
use NumberFormatter;
use Relaticle\CustomFields\Data\Settings\CurrencyFieldSettingsData;
use Relaticle\CustomFields\FieldTypeSystem\BaseFieldType;
use Relaticle\CustomFields\FieldTypeSystem\FieldSchema;
use Relaticle\CustomFields\Filament\Integration\Components\Forms\CurrencyComponent;
use Relaticle\CustomFields\Filament\Integration\Components\Infolists\TextEntry;
use Relaticle\CustomFields\Filament\Integration\Components\Tables\Columns\TextColumn;
use Relaticle\CustomFields\Validation\Capabilities\DecimalPlacesCapability;
use Relaticle\CustomFields\Filament\Integration\Components\Infolists\CurrencyEntry;
use Relaticle\CustomFields\Filament\Integration\Components\Tables\Columns\CurrencyColumn;
use Relaticle\CustomFields\Support\CurrencyProvider;
use Relaticle\CustomFields\Validation\Capabilities\MaxValueCapability;
use Relaticle\CustomFields\Validation\Capabilities\MinValueCapability;

/**
* ABOUTME: Field type definition for Currency fields
* ABOUTME: Provides Currency functionality with appropriate validation rules
*/
class CurrencyFieldType extends BaseFieldType
{
public function configure(): FieldSchema
Expand All @@ -26,26 +28,116 @@ public function configure(): FieldSchema
->label('Currency')
->icon('mdi-currency-usd')
->formComponent(CurrencyComponent::class)
->tableColumn(TextColumn::class)
->infolistEntry(TextEntry::class)
->tableColumn(CurrencyColumn::class)
->infolistEntry(CurrencyEntry::class)
->priority(25)
->withValidationCapabilities(
MinValueCapability::class,
MaxValueCapability::class,
DecimalPlacesCapability::class,
)
->withSettings(
CurrencyFieldSettingsData::class,
fn (): array => $this->settingsSchema(),
)
->importExample('99.99')
->importTransformer(function (mixed $state): ?float {
if (blank($state)) {
return null;
}

// Remove currency symbols and formatting chars
if (is_string($state)) {
$state = preg_replace('/[^0-9.-]/', '', $state);
}

return round(floatval($state), 2);
return (float) $state;
})
->exportTransformer(function (mixed $value): ?string {
if ($value === null) {
return null;
}

return rtrim(rtrim(number_format((float) $value, 10, '.', ''), '0'), '.');
});
Comment thread
ManukMinasyan marked this conversation as resolved.
}

/**
* @return array<int, Component>
*/
private function settingsSchema(): array
{
$defaultCode = config('custom-fields.currency.default_code', 'USD');

return [
Fieldset::make('Currency Settings')
->columnSpanFull()
->columns(2)
->schema([
Select::make('settings.additional.currency_code')
->label('Currency')
->searchable()
->options(fn (): array => CurrencyProvider::getOptions())
->afterStateHydrated(function (Select $component, mixed $state) use ($defaultCode): void {
if (blank($state)) {
$component->state($defaultCode);
}
})
->required()
->live(),

Select::make('settings.additional.display_type')
->label('Display')
->options([
'symbol' => 'Symbol ($1,200.50)',
'code' => 'Code (USD 1,200.50)',
])
->afterStateHydrated(function (Select $component, mixed $state): void {
if (blank($state)) {
$component->state('symbol');
}
})
->required(),

Select::make('settings.additional.decimal_places')
->label('Decimal Places')
->helperText('Auto-detected from currency. Override only if needed.')
->options(function (Get $get): array {
$code = $get('settings.additional.currency_code') ?? 'USD';

$options = [];

foreach ([0, 2, 3, 4] as $digits) {
$sample = number_format(1200.50, $digits);

if (class_exists(NumberFormatter::class)) {
$formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $digits);

$formatted = $formatter->formatCurrency(1200.50, $code);

if ($formatted !== false) {
$sample = $formatted;
}
}

$label = $digits === 0 ? 'No decimals' : $digits.' decimals';
$options[(string) $digits] = sprintf('%s (%s)', $label, $sample);
}

return $options;
})
->afterStateHydrated(function (Select $component, mixed $state, Get $get): void {
if ($state !== null) {
$component->state((string) $state);

return;
}

$code = $get('settings.additional.currency_code') ?? 'USD';
$component->state((string) CurrencyProvider::getDecimalDigits($code));
})
->required(),

]),
];
}
}
16 changes: 13 additions & 3 deletions src/Filament/Integration/Builders/TableBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Relaticle\CustomFields\Filament\Integration\Builders;

use Closure;
use Illuminate\Support\Collection;
use Relaticle\CustomFields\Enums\CustomFieldsFeature;
use Relaticle\CustomFields\FeatureSystem\FeatureManager;
Expand Down Expand Up @@ -38,10 +39,19 @@ public function columns(): Collection
return $column;
}

// Wrap the existing state with visibility check
$column->formatStateUsing(function (mixed $state, mixed $record) use ($field, $backendVisibilityService, $allFields): mixed {
$existingFormatter = (fn (): ?Closure => $this->formatStateUsing)->call($column); // @phpstan-ignore property.notFound

$column->formatStateUsing(function (mixed $state, mixed $record) use ($field, $backendVisibilityService, $allFields, $existingFormatter, $column): mixed {
if (! $backendVisibilityService->isFieldVisible($record, $field, $allFields)) {
return null; // Return null or empty value when field should be hidden
return null;
}

if ($existingFormatter) {
return $column->evaluate($existingFormatter, [
'state' => $state,
Comment thread
ManukMinasyan marked this conversation as resolved.
'record' => $record,
'column' => $column,
]);
}

return $state;
Expand Down
56 changes: 49 additions & 7 deletions src/Filament/Integration/Components/Forms/CurrencyComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,64 @@

use Filament\Forms\Components\TextInput;
use Illuminate\Support\Str;
use NumberFormatter;
use Relaticle\CustomFields\Filament\Integration\Base\AbstractFormComponent;
use Relaticle\CustomFields\Models\CustomField;

final readonly class CurrencyComponent extends AbstractFormComponent
{
public function create(CustomField $customField): TextInput
{
$decimalPlaces = $customField->getDecimalPlaces();
$currencyCode = $customField->getCurrencyCode();
$prefix = $this->getCurrencySymbol($currencyCode, $customField->getCurrencyDisplayType());

return TextInput::make($customField->getFieldName())
->prefix('$')
->prefix($prefix)
->numeric()
->inputMode('decimal')
->step(0.01)
->minValue(0)
->default(0)
->rules(['numeric', 'min:0'])
->formatStateUsing(fn (mixed $state): string => number_format((float) $state, 2))
->dehydrateStateUsing(fn (mixed $state): float => Str::of($state)->replace(['$', ','], '')->toFloat());
->step($decimalPlaces > 0 ? 1 / (10 ** $decimalPlaces) : 1)
Comment thread
ManukMinasyan marked this conversation as resolved.
->formatStateUsing(function (mixed $state) use ($decimalPlaces): ?string {
if ($state === null || $state === '') {
return null;
}

return number_format((float) $state, $decimalPlaces);
})
->dehydrateStateUsing(function (mixed $state): ?float {
if ($state === null || $state === '') {
return null;
}

return Str::of($state)->replace(',', '')->toFloat();
});
}

private function getCurrencySymbol(string $currencyCode, string $displayType): string
{
Comment thread
ManukMinasyan marked this conversation as resolved.
if ($displayType === 'code') {
return $currencyCode;
}

if (! class_exists(NumberFormatter::class)) {
return $currencyCode;
}

$formatter = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);

$formatted = $formatter->formatCurrency(0, $currencyCode);

if ($formatted === false) {
return $currencyCode;
}

$symbol = str_replace(['0', ' ', "\xC2\xA0"], '', $formatted);

if ($symbol === '') {
return $currencyCode;
}

return $symbol;
}
}
26 changes: 26 additions & 0 deletions src/Filament/Integration/Components/Infolists/CurrencyEntry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Relaticle\CustomFields\Filament\Integration\Components\Infolists;

use Filament\Infolists\Components\TextEntry as BaseTextEntry;
use Relaticle\CustomFields\Filament\Integration\Base\AbstractInfolistEntry;
use Relaticle\CustomFields\Filament\Integration\Concerns\Shared\ConfiguresCurrencyFormatting;
use Relaticle\CustomFields\Models\CustomField;

final class CurrencyEntry extends AbstractInfolistEntry
{
use ConfiguresCurrencyFormatting;

public function make(CustomField $customField): BaseTextEntry
{
$entry = BaseTextEntry::make($customField->getFieldName())
->label($customField->name)
->state(fn (mixed $record) => $record->getCustomFieldValue($customField));

$this->applyCurrencyFormatting($entry, $customField);

return $entry;
}
}
Loading