From 317b7fd21cf7e61ba117dcb94d2379a4a2bad199 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 7 Mar 2026 00:42:28 +0700 Subject: [PATCH] feat: Introduce warehouse stock management with dedicated tables and observers, and update egg collection totals to use decimal values. --- .../EggCollections/EggCollectionResource.php | 16 +++++- .../Master/Warehouses/WarehouseResource.php | 13 +++++ app/Models/EggCollection.php | 13 ++++- app/Models/Warehouse.php | 5 ++ app/Models/WarehouseStock.php | 36 ++++++++++++ app/Observers/EggCollectionItemObserver.php | 42 ++++++++++++-- app/Observers/EggCollectionObserver.php | 56 +++++++++++++++++++ ...7_003159_create_warehouse_stocks_table.php | 27 +++++++++ ...g_collections_totals_to_decimal_final.php} | 12 ++-- 9 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 app/Models/WarehouseStock.php create mode 100644 app/Observers/EggCollectionObserver.php create mode 100644 database/migrations/2026_03_07_003159_create_warehouse_stocks_table.php rename database/migrations/{2026_03_07_003126_add_warehouse_id_to_egg_collections_table.php => 2026_03_07_003345_modify_egg_collections_totals_to_decimal_final.php} (58%) diff --git a/app/Filament/Resources/Manage/EggCollections/EggCollectionResource.php b/app/Filament/Resources/Manage/EggCollections/EggCollectionResource.php index 1e227ac..11638db 100644 --- a/app/Filament/Resources/Manage/EggCollections/EggCollectionResource.php +++ b/app/Filament/Resources/Manage/EggCollections/EggCollectionResource.php @@ -11,6 +11,7 @@ use App\Filament\Columns\TimestampColumns; use App\Filament\Resources\Manage\EggCollections\Pages\ManageEggCollections; use App\Models\EggCollection; +use App\Models\Warehouse; use BackedEnum; use Filament\Actions\BulkActionGroup; use Filament\Forms\Components\Hidden; @@ -51,6 +52,14 @@ public static function form(Schema $schema): Schema Hidden::make('production_date') ->default(now()), + Select::make('warehouse_id') + ->label('Gudang') + ->relationship('warehouse', 'name') + ->required() + ->searchable() + ->preload() + ->default(fn () => Warehouse::first()?->id), + Repeater::make('items') ->label('Rincian Produksi') ->relationship('items') @@ -67,7 +76,7 @@ public static function form(Schema $schema): Schema ->placeholder('0') ->autocomplete(false) ->required() - ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 0) + ->currencyMask(thousandSeparator: '.', decimalSeparator: ',', precision: 1) ->dehydrateStateUsing(fn ($state) => (float) str()->replace(',', '.', str()->replace('.', '', (string) ($state ?? 0)))), Toggle::make('is_broken') @@ -91,6 +100,11 @@ public static function table(Table $table): Table { return $table ->columns([ + TextColumn::make('warehouse.name') + ->label('Gudang') + ->searchable() + ->sortable(), + TextColumn::make('production_date') ->label('Tanggal Produksi') ->dateTime('l, d F Y H:i') diff --git a/app/Filament/Resources/Master/Warehouses/WarehouseResource.php b/app/Filament/Resources/Master/Warehouses/WarehouseResource.php index 017ca12..d61c8fa 100644 --- a/app/Filament/Resources/Master/Warehouses/WarehouseResource.php +++ b/app/Filament/Resources/Master/Warehouses/WarehouseResource.php @@ -64,6 +64,19 @@ public static function table(Table $table): Table ->searchable() ->sortable(), + TextColumn::make('stocks') + ->label('Stok Telur') + ->getStateUsing(function (Warehouse $record) { + return $record->stocks() + ->where('quantity', '>', 0) + ->with('unit') + ->get() + ->map(fn ($stock) => (float) $stock->quantity.' '.$stock->unit?->alias.($stock->is_broken ? ' (Pecah)' : '')) + ->join(', ') ?: 'Kosong'; + }) + ->badge() + ->color('info'), + ...TimestampColumns::make(), ]) ->filters([ diff --git a/app/Models/EggCollection.php b/app/Models/EggCollection.php index 467cf9f..55b0a54 100644 --- a/app/Models/EggCollection.php +++ b/app/Models/EggCollection.php @@ -2,11 +2,15 @@ namespace App\Models; +use App\Observers\EggCollectionObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +#[ObservedBy(EggCollectionObserver::class)] class EggCollection extends Model { use HasFactory, SoftDeletes; @@ -18,6 +22,11 @@ public function items(): HasMany return $this->hasMany(EggCollectionItem::class); } + public function warehouse(): BelongsTo + { + return $this->belongsTo(Warehouse::class); + } + public function syncTotals() { $this->total_eggs = $this->items()->where('is_broken', false)->sum('quantity'); @@ -29,8 +38,8 @@ protected function casts(): array { return [ 'production_date' => 'datetime', - 'total_eggs' => 'integer', - 'total_broken_eggs' => 'integer', + 'total_eggs' => 'decimal:2', + 'total_broken_eggs' => 'decimal:2', ]; } } diff --git a/app/Models/Warehouse.php b/app/Models/Warehouse.php index 2269ef1..96b5179 100644 --- a/app/Models/Warehouse.php +++ b/app/Models/Warehouse.php @@ -11,4 +11,9 @@ class Warehouse extends Model use HasFactory, SoftDeletes; protected $guarded = ['id']; + + public function stocks(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(WarehouseStock::class); + } } diff --git a/app/Models/WarehouseStock.php b/app/Models/WarehouseStock.php new file mode 100644 index 0000000..5945ee2 --- /dev/null +++ b/app/Models/WarehouseStock.php @@ -0,0 +1,36 @@ +belongsTo(Warehouse::class); + } + + public function unit() + { + return $this->belongsTo(Unit::class); + } + + public static function adjustStock(int $warehouseId, int $unitId, bool $isBroken, float $quantity): void + { + $stock = self::firstOrCreate([ + 'warehouse_id' => $warehouseId, + 'unit_id' => $unitId, + 'is_broken' => $isBroken, + ], [ + 'quantity' => 0, + ]); + + $stock->increment('quantity', $quantity); + } +} diff --git a/app/Observers/EggCollectionItemObserver.php b/app/Observers/EggCollectionItemObserver.php index a1b7adb..8a81ebe 100644 --- a/app/Observers/EggCollectionItemObserver.php +++ b/app/Observers/EggCollectionItemObserver.php @@ -3,22 +3,54 @@ namespace App\Observers; use App\Models\EggCollectionItem; +use App\Models\WarehouseStock; class EggCollectionItemObserver { /** - * Handle the EggCollectionItem "saved" event. + * Handle the EggCollectionItem "created" event. */ - public function saved(EggCollectionItem $eggCollectionItem): void + public function created(EggCollectionItem $item): void { - $eggCollectionItem->eggCollection?->syncTotals(); + $item->eggCollection?->syncTotals(); + + if ($warehouseId = $item->eggCollection?->warehouse_id) { + WarehouseStock::adjustStock($warehouseId, $item->unit_id, $item->is_broken, $item->quantity); + } + } + + /** + * Handle the EggCollectionItem "updated" event. + */ + public function updated(EggCollectionItem $item): void + { + $item->eggCollection?->syncTotals(); + + $warehouseId = $item->eggCollection?->warehouse_id; + if (! $warehouseId) { + return; + } + + $oldUnitId = $item->getOriginal('unit_id'); + $oldIsBroken = $item->getOriginal('is_broken'); + $oldQuantity = $item->getOriginal('quantity'); + + // Reverse old stock + WarehouseStock::adjustStock($warehouseId, $oldUnitId, $oldIsBroken, -$oldQuantity); + + // Add new stock + WarehouseStock::adjustStock($warehouseId, $item->unit_id, $item->is_broken, $item->quantity); } /** * Handle the EggCollectionItem "deleted" event. */ - public function deleted(EggCollectionItem $eggCollectionItem): void + public function deleted(EggCollectionItem $item): void { - $eggCollectionItem->eggCollection?->syncTotals(); + $item->eggCollection?->syncTotals(); + + if ($warehouseId = $item->eggCollection?->warehouse_id) { + WarehouseStock::adjustStock($warehouseId, $item->unit_id, $item->is_broken, -$item->quantity); + } } } diff --git a/app/Observers/EggCollectionObserver.php b/app/Observers/EggCollectionObserver.php new file mode 100644 index 0000000..2d5d779 --- /dev/null +++ b/app/Observers/EggCollectionObserver.php @@ -0,0 +1,56 @@ +isDirty('warehouse_id')) { + $oldWarehouseId = $eggCollection->getOriginal('warehouse_id'); + $newWarehouseId = $eggCollection->warehouse_id; + + foreach ($eggCollection->items as $item) { + if ($oldWarehouseId) { + WarehouseStock::adjustStock($oldWarehouseId, $item->unit_id, $item->is_broken, -$item->quantity); + } + if ($newWarehouseId) { + WarehouseStock::adjustStock($newWarehouseId, $item->unit_id, $item->is_broken, $item->quantity); + } + } + } + } + + /** + * Handle the EggCollection "deleted" event. + */ + public function deleted(EggCollection $eggCollection): void + { + // Items are usually deleted via cascade or manual deletion which triggers ItemObserver. + // But if it's a soft delete, we should decide if we want to remove stock. + // Assuming we want to remove stock when production record is deleted. + if (! $eggCollection->isForceDeleting() && $warehouseId = $eggCollection->warehouse_id) { + foreach ($eggCollection->items as $item) { + WarehouseStock::adjustStock($warehouseId, $item->unit_id, $item->is_broken, -$item->quantity); + } + } + } + + /** + * Handle the EggCollection "restored" event. + */ + public function restored(EggCollection $eggCollection): void + { + if ($warehouseId = $eggCollection->warehouse_id) { + foreach ($eggCollection->items as $item) { + WarehouseStock::adjustStock($warehouseId, $item->unit_id, $item->is_broken, $item->quantity); + } + } + } +} diff --git a/database/migrations/2026_03_07_003159_create_warehouse_stocks_table.php b/database/migrations/2026_03_07_003159_create_warehouse_stocks_table.php new file mode 100644 index 0000000..ab7762d --- /dev/null +++ b/database/migrations/2026_03_07_003159_create_warehouse_stocks_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('warehouse_id')->constrained()->cascadeOnDelete(); + $table->foreignId('unit_id')->constrained()->cascadeOnDelete(); + $table->boolean('is_broken')->default(false); + $table->decimal('quantity', 12, 2)->default(0); + $table->timestamps(); + + $table->unique(['warehouse_id', 'unit_id', 'is_broken'], 'warehouse_unit_condition_unique'); + }); + } + + public function down(): void + { + Schema::dropIfExists('warehouse_stocks'); + } +}; diff --git a/database/migrations/2026_03_07_003126_add_warehouse_id_to_egg_collections_table.php b/database/migrations/2026_03_07_003345_modify_egg_collections_totals_to_decimal_final.php similarity index 58% rename from database/migrations/2026_03_07_003126_add_warehouse_id_to_egg_collections_table.php rename to database/migrations/2026_03_07_003345_modify_egg_collections_totals_to_decimal_final.php index 83c2064..15bea57 100644 --- a/database/migrations/2026_03_07_003126_add_warehouse_id_to_egg_collections_table.php +++ b/database/migrations/2026_03_07_003345_modify_egg_collections_totals_to_decimal_final.php @@ -6,23 +6,19 @@ return new class extends Migration { - /** - * Run the migrations. - */ public function up(): void { Schema::table('egg_collections', function (Blueprint $table) { - // + $table->decimal('total_eggs', 12, 2)->default(0)->change(); + $table->decimal('total_broken_eggs', 12, 2)->default(0)->change(); }); } - /** - * Reverse the migrations. - */ public function down(): void { Schema::table('egg_collections', function (Blueprint $table) { - // + $table->unsignedInteger('total_eggs')->default(0)->change(); + $table->unsignedInteger('total_broken_eggs')->default(0)->change(); }); } };