- 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.
282 lines
10 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|
|
}
|