feat: implement egg collection items management with relationships, totals synchronization, and UI enhancements

This commit is contained in:
Yoga Pangestu 2026-03-07 00:29:11 +07:00
parent b1353fbce9
commit 8ddbe5ed29
7 changed files with 152 additions and 30 deletions

View File

@ -14,8 +14,11 @@
use BackedEnum; use BackedEnum;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Support\Enums\Width; use Filament\Support\Enums\Width;
@ -48,21 +51,33 @@ public static function form(Schema $schema): Schema
Hidden::make('production_date') Hidden::make('production_date')
->default(now()), ->default(now()),
TextInput::make('total_eggs') Repeater::make('items')
->label('Jumlah Telur') ->label('Rincian Produksi')
->placeholder('0') ->relationship('items')
->autocomplete(false) ->schema([
->required() Select::make('unit_id')
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) ->label('Satuan')
->dehydrateStateUsing(fn ($state) => (int) str()->replace(',', '', (string) ($state ?? 0))), ->relationship('unit', 'name')
->required()
->searchable()
->preload(),
TextInput::make('total_broken_eggs') TextInput::make('quantity')
->label('Jumlah Telur Pecah') ->label('Jumlah')
->placeholder('0') ->placeholder('0')
->autocomplete(false) ->autocomplete(false)
->required() ->required()
->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0)
->dehydrateStateUsing(fn ($state) => (int) str()->replace(',', '', (string) ($state ?? 0))), ->dehydrateStateUsing(fn ($state) => (float) str()->replace(',', '.', str()->replace('.', '', (string) ($state ?? 0)))),
Toggle::make('is_broken')
->label('Pecah?')
->default(false),
])
->columns(3)
->defaultItems(1)
->addActionLabel('Tambah Satuan')
->columnSpanFull(),
Textarea::make('notes') Textarea::make('notes')
->label('Catatan') ->label('Catatan')
@ -82,23 +97,36 @@ public static function table(Table $table): Table
->searchable() ->searchable()
->sortable(), ->sortable(),
TextColumn::make('total_eggs') TextColumn::make('production_summary')
->label('Total Telur') ->label('Hasil Produksi')
->numeric() ->getStateUsing(function (EggCollection $record) {
->searchable() return $record->items()
->sortable(), ->where('is_broken', false)
->with('unit')
->get()
->map(fn ($item) => (float) $item->quantity." {$item->unit?->alias}")
->join(', ') ?: '-';
})
->badge()
->color('success'),
TextColumn::make('total_broken_eggs') TextColumn::make('broken_summary')
->label('Total Telur Pecah') ->label('Telur Pecah')
->numeric() ->getStateUsing(function (EggCollection $record) {
->searchable() return $record->items()
->sortable(), ->where('is_broken', true)
->with('unit')
->get()
->map(fn ($item) => (float) $item->quantity." {$item->unit?->alias}")
->join(', ') ?: '-';
})
->badge()
->color('danger'),
TextColumn::make('notes') TextColumn::make('notes')
->label('Catatan') ->label('Catatan')
->searchable() ->searchable()
->wrap() ->wrap(),
->toggleable(),
...TimestampColumns::make(), ...TimestampColumns::make(),
]) ])
@ -110,7 +138,7 @@ public static function table(Table $table): Table
->recordActions([ ->recordActions([
EditAction::make() EditAction::make()
->label('Ubah') ->label('Ubah')
->modalWidth(Width::Large) ->modalWidth(Width::FourExtraLarge)
->modalHeading(fn (EggCollection $record): string => 'Ubah '.$record->production_date->translatedFormat('l, d F Y H:i')), ->modalHeading(fn (EggCollection $record): string => 'Ubah '.$record->production_date->translatedFormat('l, d F Y H:i')),
DeleteAction::make(), DeleteAction::make(),

View File

@ -25,7 +25,7 @@ protected function getHeaderActions(): array
$action->makeModalSubmitAction('createAnother', arguments: ['another' => true]) $action->makeModalSubmitAction('createAnother', arguments: ['another' => true])
->label('Simpan dan Tambah Lagi'), ->label('Simpan dan Tambah Lagi'),
]) ])
->modalWidth(Width::Large), ->modalWidth(Width::FourExtraLarge),
]; ];
} }
} }

View File

@ -21,8 +21,8 @@ class StatsOverview extends BaseWidget
protected function getStats(): array protected function getStats(): array
{ {
return [ return [
Stat::make('Produksi Telur', number_format(EggCollection::sum('total_eggs'), 0, ',', '.').' Butir') Stat::make('Produksi Telur', number_format(EggCollection::sum('total_eggs'), 0, ',', '.'))
->description('Total telur yang terkumpul.') ->description('Total akumulasi produksi telur (berbagai satuan).')
->descriptionIcon('heroicon-m-circle-stack') ->descriptionIcon('heroicon-m-circle-stack')
->color('warning'), ->color('warning'),

View File

@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
class EggCollection extends Model class EggCollection extends Model
@ -12,6 +13,18 @@ class EggCollection extends Model
protected $guarded = ['id']; protected $guarded = ['id'];
public function items(): HasMany
{
return $this->hasMany(EggCollectionItem::class);
}
public function syncTotals()
{
$this->total_eggs = $this->items()->where('is_broken', false)->sum('quantity');
$this->total_broken_eggs = $this->items()->where('is_broken', true)->sum('quantity');
$this->saveQuietly();
}
protected function casts(): array protected function casts(): array
{ {
return [ return [

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Observers\EggCollectionItemObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[ObservedBy(EggCollectionItemObserver::class)]
class EggCollectionItem extends Model
{
use HasFactory;
protected $guarded = ['id'];
protected $casts = [
'is_broken' => 'boolean',
'quantity' => 'decimal:2',
];
public function eggCollection(): BelongsTo
{
return $this->belongsTo(EggCollection::class);
}
public function unit(): BelongsTo
{
return $this->belongsTo(Unit::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Observers;
use App\Models\EggCollectionItem;
class EggCollectionItemObserver
{
/**
* Handle the EggCollectionItem "saved" event.
*/
public function saved(EggCollectionItem $eggCollectionItem): void
{
$eggCollectionItem->eggCollection?->syncTotals();
}
/**
* Handle the EggCollectionItem "deleted" event.
*/
public function deleted(EggCollectionItem $eggCollectionItem): void
{
$eggCollectionItem->eggCollection?->syncTotals();
}
}

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('egg_collection_items', function (Blueprint $table) {
$table->id();
$table->foreignId('egg_collection_id')->constrained()->cascadeOnDelete();
$table->foreignId('unit_id')->constrained()->cascadeOnDelete();
$table->decimal('quantity', 12, 2);
$table->boolean('is_broken')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('egg_collection_items');
}
};