dstpabuaran.com/app/Services/Admin/Master/ProductService.php
Yoga Pangestu 95be00c9d1 feat: add push notification functionality and service worker
- Implemented push notifications with a service worker (sw.ts) to handle caching and notifications.
- Added API routes for push subscription and notification management in routes/api.php.
- Created NotificationTest to validate notification service functionality and ensure correct notifications are sent based on user roles and actions.
- Updated Permissions component to manage notification permissions and subscriptions.
- Enhanced CategoryIndex component to highlight categories based on notifications.
- Excluded service worker from TypeScript compilation in tsconfig.json.
- Updated Vite configuration to include service worker in the build process.
2026-08-01 11:36:20 +07:00

282 lines
10 KiB
PHP

<?php
namespace App\Services\Admin\Master;
use App\Models\Product;
use App\Models\ProductPrice;
use App\Models\ProductVariant;
use App\Services\NotificationService;
use App\Services\S3PresignedService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class ProductService
{
public function __construct(
private S3PresignedService $s3Service = new S3PresignedService,
) {}
public function getAll(array $filters = []): Collection
{
$products = Product::select('id', 'name', 'slug', 'description', 'status')
->with([
'categories:id,name',
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
'productVariants.productPrices:id,variant_id,type,price',
'productVariants.media',
])
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%"))
->latest()
->get();
$products->each(function ($product) {
$product->productVariants->each(function ($variant) {
$media = $variant->getMedia('photos')->first();
$variant->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->file_name)
: null;
});
});
return $products;
}
public function paginated(int $perPage = 15, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
{
$paginator = Product::query()
->select('id', 'name', 'slug', 'description', 'status')
->with([
'categories:id,name',
'productVariants:id,product_id,name,stock,reject_stock,retail_stock',
'productVariants.productPrices:id,variant_id,type,price',
])
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status))
->orderBy($sort, $direction)
->paginate($perPage);
$paginator->getCollection()->each(function ($product) {
$product->productVariants->each(function ($variant) {
$media = $variant->getMedia('photos')->first();
$variant->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->file_name)
: null;
});
});
return $paginator;
}
public function create(array $data): Product
{
$product = DB::transaction(function () use ($data) {
$product = Product::create([
'name' => $data['name'],
'description' => $data['description'] ?? null,
'status' => $data['status'] ?? 'active',
]);
$product->categories()->sync($data['category_ids']);
$useSamePrice = $data['use_same_price'] ?? false;
foreach ($data['variants'] as $index => $variantData) {
$variant = $product->productVariants()->create([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
'reject_stock' => $variantData['reject_stock'],
'retail_stock' => $variantData['retail_stock'],
]);
$prices = $useSamePrice
? $data['shared_prices']
: $variantData['prices'];
foreach ($prices as $priceData) {
ProductPrice::create([
'variant_id' => $variant->id,
'type' => $priceData['type'],
'price' => $priceData['price'],
]);
}
if (! empty($variantData['photo_key'])) {
$this->registerPhotos($variant, [$variantData['photo_key']]);
}
}
return $product;
});
NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
title: 'Produk Baru',
body: "Produk \"{$product->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'),
);
return $product;
}
public function getForEdit(Product $product): array
{
$product->load([
'categories:id,name',
'productVariants.productPrices',
'productVariants.media',
]);
$variants = $product->productVariants->map(function (ProductVariant $variant) {
$media = $variant->getMedia('photos')->first();
return [
'id' => $variant->id,
'name' => $variant->name,
'stock' => $variant->stock,
'reject_stock' => $variant->reject_stock,
'retail_stock' => $variant->retail_stock,
'photo_key' => $media?->file_name,
'photo_url' => $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null,
'prices' => $variant->productPrices->map(fn ($p) => [
'type' => $p->type->value,
'price' => $p->price,
]),
];
});
return [
'id' => $product->id,
'name' => $product->name,
'description' => $product->description,
'status' => $product->status->value,
'category_ids' => $product->categories->pluck('id'),
'product_variants' => $variants,
];
}
public function update(Product $product, array $data): Product
{
$product = DB::transaction(function () use ($product, $data) {
$product->update([
'name' => $data['name'],
'description' => $data['description'] ?? null,
'status' => $data['status'] ?? $product->status,
]);
$product->categories()->sync($data['category_ids']);
$useSamePrice = $data['use_same_price'] ?? false;
$existingVariantIds = collect($data['variants'])
->pluck('id')
->filter()
->toArray();
$product->productVariants()
->whereNotIn('id', $existingVariantIds)
->each(function (ProductVariant $variant) {
$variant->productPrices()->delete();
$variant->clearMediaCollection('photos');
$variant->delete();
});
foreach ($data['variants'] as $variantData) {
$variantId = $variantData['id'] ?? null;
$variant = $variantId
? $product->productVariants()->findOrFail($variantId)
: $product->productVariants()->create([]);
$variant->update([
'name' => $variantData['name'],
'stock' => $variantData['stock'],
'reject_stock' => $variantData['reject_stock'],
'retail_stock' => $variantData['retail_stock'],
]);
$variant->productPrices()->delete();
$prices = $useSamePrice
? $data['shared_prices']
: $variantData['prices'];
foreach ($prices as $priceData) {
ProductPrice::create([
'variant_id' => $variant->id,
'type' => $priceData['type'],
'price' => $priceData['price'],
]);
}
if (! empty($variantData['photo_key'])) {
$variant->clearMediaCollection('photos');
$this->registerPhotos($variant, [$variantData['photo_key']]);
}
}
return $product;
});
NotificationService::notify(
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
title: 'Produk Diperbarui',
body: "Produk \"{$product->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
url: route('admin.master.products.index'),
);
return $product;
}
public function delete(Product $product): bool
{
return DB::transaction(function () use ($product) {
$product->productVariants->each(function (ProductVariant $variant) {
$variant->productPrices()->delete();
$variant->clearMediaCollection('photos');
$variant->delete();
});
$product->categories()->detach();
return $product->delete();
});
}
public function toggleStatus(Product $product): void
{
$product->update([
'status' => $product->status->value === 'active' ? 'inactive' : 'active',
]);
}
private function registerPhotos(ProductVariant $variant, array $photoKeys): void
{
foreach ($photoKeys as $order => $s3Key) {
$fileName = pathinfo($s3Key, PATHINFO_BASENAME);
$name = pathinfo($s3Key, PATHINFO_FILENAME);
Media::create([
'model_type' => ProductVariant::class,
'model_id' => $variant->id,
'uuid' => Str::uuid(),
'collection_name' => 'photos',
'name' => $name,
'file_name' => $s3Key,
'mime_type' => 'image/jpeg',
'disk' => 's3',
'conversions_disk' => 's3',
'size' => 0,
'manipulations' => [],
'custom_properties' => [],
'generated_conversions' => [],
'responsive_images' => [],
'order_column' => $order + 1,
]);
}
}
}