>, * }>, * }> */ public function outOfStockGroups(): array { return $this->inventoryAlertGroups( fn (RawMaterialPrice $price): bool => (float) $price->stock <= 0, ); } /** * @return list>, * }>, * }> */ public function stockWarningGroups(): array { return $this->inventoryAlertGroups(function (RawMaterialPrice $price): bool { $stock = (float) $price->stock; if ($stock <= 0) { return false; } $minStock = $price->rawMaterial->unit->minStock(); return $stock < $minStock; }); } /** * @return list>, * }>, * }> */ private function inventoryAlertGroups(callable $filter): array { return RawMaterialPrice::query() ->with(['rawMaterial', 'media']) ->get() ->filter($filter) ->sortBy([ fn (RawMaterialPrice $price) => $price->rawMaterial->name, fn (RawMaterialPrice $price) => $price->variant, ]) ->groupBy('raw_material_id') ->map(function ($prices, $rawMaterialId) { $material = $prices->first()->rawMaterial; return [ 'id' => (int) $rawMaterialId, 'name' => $material->name, 'unit_label' => $material->unit->label(), 'edit_url' => route('admin.master.raw-materials.edit', $rawMaterialId), 'variants' => $prices->map(function (RawMaterialPrice $price) use ($material): array { return [ 'id' => $price->id, 'name' => $price->variant, 'stock_formatted' => $this->formatStockAmount((float) $price->stock, $material->unit->abbreviation()), 'images' => MediaPresenter::collection($price, 'images'), ]; })->values()->all(), ]; }) ->values() ->all(); } private function formatStockAmount(float $amount, ?string $unit = null): string { $formatted = rtrim(rtrim(number_format($amount, 4, ',', '.'), '0'), ','); if ($unit === null) { return $formatted; } return "{$formatted} {$unit}"; } /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ public function paginateForIndex(array $tableQuery, string $isActive): LengthAwarePaginator { $query = RawMaterial::query() ->with([ 'prices' => fn ($query) => $query ->orderBy('created_at') ->with(['rawMaterial:id,unit', 'media']), ]) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { $query->where('name', 'like', "%{$search}%") ->orWhereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%")); }); }) ->when( $isActive !== '', fn (Builder $query) => $query->where('is_active', $isActive === '1') ); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(10) ->withQueryString() ->through(function (RawMaterial $rawMaterial) { $rawMaterial->prices->each(function (RawMaterialPrice $price): void { $price->setAttribute( 'images', MediaPresenter::collection($price, 'images'), ); }); return $rawMaterial; }); } /** * @param array $validated */ public function create(array $validated): void { DB::transaction(function () use ($validated): void { $rawMaterial = RawMaterial::create([ 'name' => $validated['name'], 'unit' => $validated['unit'], ]); foreach ($validated['prices'] as $index => $priceData) { $this->createPrice($rawMaterial, $priceData, $index); } }); } /** * @param array $validated */ public function update(RawMaterial $rawMaterial, array $validated): void { DB::transaction(function () use ($validated, $rawMaterial): void { $rawMaterial->name = $validated['name']; $rawMaterial->unit = $validated['unit']; $rawMaterial->save(); $submittedPriceIds = collect($validated['prices']) ->pluck('id') ->filter() ->map(fn ($id) => (int) $id) ->all(); $rawMaterial->prices() ->whereNotIn('id', $submittedPriceIds) ->get() ->each(function (RawMaterialPrice $price): void { $price->clearMediaCollection('images'); $price->delete(); }); foreach ($validated['prices'] as $index => $priceData) { if (! empty($priceData['id'])) { $price = $rawMaterial->prices()->findOrFail($priceData['id']); $price->variant = $priceData['variant']; $price->price = $priceData['price']; $price->stock = $priceData['stock']; $price->save(); $this->syncPriceImages($price, $priceData, $index); continue; } $this->createPrice($rawMaterial, $priceData, $index); } }); } public function toggleStatus(RawMaterial $rawMaterial, bool $isActive): void { $rawMaterial->is_active = $isActive; $rawMaterial->save(); } public function delete(RawMaterial $rawMaterial): void { DB::transaction(function () use ($rawMaterial): void { $rawMaterial->prices()->each(function (RawMaterialPrice $price): void { $price->clearMediaCollection('images'); }); $rawMaterial->prices()->delete(); $rawMaterial->delete(); }); } private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['name', 'unit', 'is_active'], true)) { $query->orderBy($sort, $direction); return; } $query->latest(); } /** * @param array $priceData */ private function createPrice(RawMaterial $rawMaterial, array $priceData, int $index): RawMaterialPrice { $price = $rawMaterial->prices()->create([ 'variant' => $priceData['variant'], 'price' => $priceData['price'], 'stock' => $priceData['stock'], ]); $this->syncPriceImages($price, $priceData, $index); return $price; } /** * @param array $priceData */ private function syncPriceImages(RawMaterialPrice $price, array $priceData, int $index): void { $this->mediaService->syncCollection( $price, 'images', $priceData['images'] ?? null, $priceData['remove_media_ids'] ?? null, self::MAX_VARIANT_IMAGES, required: true, errorKey: "prices.{$index}.images", ); } }