dstpabuaran.com/app/Services/Admin/Manage/RestockService.php
Yoga Pangestu 22c9a4cf2a feat: add notification highlight feature to various services and frontend components
- Updated paginated methods in multiple services to accept a highlight parameter for filtering results.
- Modified notification URLs to include the highlight parameter for specific entity IDs.
- Enhanced frontend components to display a message when filtered by notification, with an option to show all entries.
- Implemented mark as read functionality in the notification bell component upon clicking a notification.
- Updated multiple index pages to handle the highlight prop and display relevant messages.
2026-08-15 00:11:40 +07:00

215 lines
8.3 KiB
PHP

<?php
namespace App\Services\Admin\Manage;
use App\Enums\PriceType;
use App\Enums\ProductStockQuality;
use App\Enums\Role;
use App\Models\ProductVariant;
use App\Models\Restock;
use App\Models\RestockItem;
use App\Services\Concerns\HasStockAdjustment;
use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService;
use App\Services\S3PresignedService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class RestockService
{
use HasStockAdjustment, RegistersMedia;
public function __construct(
private S3PresignedService $s3Service,
) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', ?int $highlight = null): LengthAwarePaginator
{
$itemsCountQuery = '(SELECT COUNT(*) FROM restock_items WHERE restock_items.restock_id = restocks.id AND restock_items.deleted_at IS NULL)';
$totalQtyQuery = '(SELECT IFNULL(SUM(quantity), 0) FROM restock_items WHERE restock_items.restock_id = restocks.id AND restock_items.deleted_at IS NULL)';
$productNamesQuery = '(SELECT GROUP_CONCAT(DISTINCT p.name ORDER BY p.name SEPARATOR \', \') FROM restock_items ri JOIN product_variants pv ON pv.id = ri.product_variant_id JOIN products p ON p.id = pv.product_id WHERE ri.restock_id = restocks.id AND ri.deleted_at IS NULL)';
$paginator = Restock::query()
->select(['id', 'created_by_id', 'total', 'notes', 'stock_type', 'created_at'])
->with([
'createdBy:id',
'createdBy.userProfile:id,user_id,full_name',
])
->selectRaw("{$itemsCountQuery} as items_count")
->selectRaw("{$totalQtyQuery} as total_qty")
->selectRaw("{$productNamesQuery} as product_names")
->when($highlight, fn ($q) => $q->where('id', $highlight))
->when($search, function ($q) use ($search) {
$q->whereHas('restockItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
->orWhere('notes', 'like', "%{$search}%");
})
->orderBy($sort, $direction)
->paginate($perPage);
return $paginator;
}
public function getItems(Restock $restock): \Illuminate\Support\Collection
{
return $restock->restockItems()
->select(['id', 'restock_id', 'product_variant_id', 'quantity', 'unit_price', 'subtotal'])
->with(['productVariant:id,product_id,name', 'productVariant.product:id,name'])
->orderByRaw('(SELECT name FROM product_variants WHERE product_variants.id = restock_items.product_variant_id)')
->get()
->each(function (RestockItem $item) {
if (! $item->productVariant) {
return;
}
$media = $item->productVariant->getFirstMedia('images');
$item->productVariant->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath())
: null;
$item->productVariant->photo_conversion_url = $media
? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
: null;
});
}
public function store(array $data): Restock
{
return DB::transaction(function () use ($data) {
$now = now();
$total = 0;
$stockType = $data['stock_type'] ?? ProductStockQuality::GOOD->value;
$itemRows = $this->buildItemRows($data['items'], $stockType, $now, $total);
$restock = Restock::create([
'created_by_id' => auth()->id(),
'total' => $total,
'notes' => $data['notes'] ?? null,
'stock_type' => $stockType,
]);
foreach ($itemRows as &$row) {
$row['restock_id'] = $restock->id;
}
DB::table('restock_items')->insert($itemRows);
$this->applyStock($data['items'], $stockType, 1);
$this->syncPhoto($restock, $data);
NotificationService::notify(
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Restock Baru',
body: 'Restock '.($stockType === ProductStockQuality::GOOD->value ? 'produk' : 'reject').' sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.restocks.index', ['highlight' => $restock->id]),
);
return $restock;
});
}
public function update(Restock $restock, array $data): Restock
{
$restock = DB::transaction(function () use ($restock, $data) {
$restock->load('restockItems');
$restock->restockItems->each(function (RestockItem $item) use ($restock) {
$this->adjustVariantStock($item->product_variant_id, $item->quantity, -1, $restock->stock_type->value);
});
$restock->restockItems()->delete();
$now = now();
$total = 0;
$stockType = $data['stock_type'] ?? $restock->stock_type->value;
$itemRows = $this->buildItemRows($data['items'], $stockType, $now, $total);
foreach ($itemRows as &$row) {
$row['restock_id'] = $restock->id;
}
DB::table('restock_items')->insert($itemRows);
$restock->update([
'total' => $total,
'notes' => $data['notes'] ?? null,
'stock_type' => $stockType,
]);
$this->applyStock($data['items'], $stockType, 1);
$this->syncPhoto($restock, $data);
return $restock;
});
NotificationService::notify(
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Restock Diperbarui',
body: 'Restock berhasil diperbarui oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.restocks.index', ['highlight' => $restock->id]),
);
return $restock;
}
public function destroy(Restock $restock): bool
{
$result = DB::transaction(function () use ($restock) {
$restock->load('restockItems');
$restock->restockItems->each(function (RestockItem $item) use ($restock) {
$this->adjustVariantStock($item->product_variant_id, $item->quantity, -1, $restock->stock_type->value);
});
$restock->restockItems()->delete();
$restock->delete();
return true;
});
NotificationService::notify(
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO],
title: 'Restock Dihapus',
body: 'Restock berhasil dihapus oleh '.auth()->user()->full_name.'.',
url: route('admin.manage.restocks.index'),
);
return $result;
}
private function buildItemRows(array $items, string $stockType, $now, int &$total): array
{
$priceType = $stockType === ProductStockQuality::REJECT->value
? PriceType::REJECT
: PriceType::CAPITAL;
$variantIds = collect($items)->pluck('product_variant_id')->unique()->all();
$prices = ProductVariant::query()
->whereKey($variantIds)
->with('productPrices:id,variant_id,type,price')
->get()
->mapWithKeys(function (ProductVariant $variant) use ($priceType) {
$price = $variant->productPrices
->first(fn ($p) => $p->type === $priceType);
return [$variant->id => $price?->price ?? 0];
});
return collect($items)->map(function ($item) use ($now, $prices, &$total) {
$quantity = (int) $item['quantity'];
$unitPrice = (int) ($prices[$item['product_variant_id']] ?? 0);
$itemSubtotal = $unitPrice * $quantity;
$total += $itemSubtotal;
return [
'restock_id' => null,
'user_id' => auth()->id(),
'product_variant_id' => $item['product_variant_id'],
'quantity' => $quantity,
'unit_price' => $unitPrice,
'subtotal' => $itemSubtotal,
'created_at' => $now,
'updated_at' => $now,
];
})->toArray();
}
}