314 lines
12 KiB
PHP
314 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\Master\RawMaterial;
|
|
|
|
use App\Enums\Role;
|
|
use App\Models\RawMaterial;
|
|
use App\Models\RawMaterialPrice;
|
|
use App\Services\Concerns\RegistersMedia;
|
|
use App\Services\NotificationService;
|
|
use App\Services\S3PresignedService;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class RawMaterialService
|
|
{
|
|
use RegistersMedia;
|
|
|
|
public function __construct(
|
|
private S3PresignedService $s3Service,
|
|
) {}
|
|
|
|
public function getNames(): Collection
|
|
{
|
|
return RawMaterial::select('name')
|
|
->distinct()
|
|
->pluck('name');
|
|
}
|
|
|
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = [], ?int $highlight = null): LengthAwarePaginator
|
|
{
|
|
$stockSumQuery = '(SELECT IFNULL(SUM(stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)';
|
|
$valueSumQuery = '(SELECT IFNULL(SUM(price * stock), 0) FROM raw_material_prices WHERE raw_material_prices.raw_material_id = raw_materials.id AND raw_material_prices.deleted_at IS NULL)';
|
|
|
|
return RawMaterial::query()
|
|
->select(['id', 'name', 'unit', 'is_active'])
|
|
->withCount('rawMaterialPrices as variants_count')
|
|
->selectRaw("{$stockSumQuery} as total_stock")
|
|
->selectRaw("{$valueSumQuery} as total_value")
|
|
->when($highlight, fn ($q) => $q->where('id', $highlight))
|
|
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")
|
|
->orWhereHas('rawMaterialPrices', fn ($vq) => $vq->where('variant', 'like', "%{$search}%"))
|
|
)
|
|
->when($filters['name'] ?? null, fn ($q, $name) => $q->where('name', 'like', "%{$name}%"))
|
|
->when(($filters['is_active'] ?? null) !== null && ($filters['is_active'] ?? null) !== '', function ($q) use ($filters) {
|
|
$q->where('is_active', $filters['is_active'] === 'true');
|
|
})
|
|
->when(($filters['stock'] ?? null) === 'empty', function ($q) use ($stockSumQuery) {
|
|
$q->whereRaw("{$stockSumQuery} = 0");
|
|
})
|
|
->when(($filters['stock'] ?? null) === 'low', function ($q) use ($stockSumQuery) {
|
|
$q->whereRaw("{$stockSumQuery} BETWEEN 0.0001 AND 9.9999");
|
|
})
|
|
->orderBy($sort, $direction)
|
|
->paginate($perPage);
|
|
}
|
|
|
|
public function getVariants(RawMaterial $rawMaterial): Collection
|
|
{
|
|
return $rawMaterial->rawMaterialPrices()
|
|
->select(['id', 'raw_material_id', 'variant', 'price', 'stock'])
|
|
->get()
|
|
->each(function (RawMaterialPrice $price) {
|
|
$media = $price->getFirstMedia('images');
|
|
if ($media) {
|
|
$price->photo_url = $this->s3Service->getTemporaryUrl($media->getPath());
|
|
$price->photo_conversion_url = $this->s3Service->getTemporaryUrl($media->getPath('thumb'));
|
|
} else {
|
|
$price->photo_url = null;
|
|
$price->photo_conversion_url = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
public function store(array $data): RawMaterial
|
|
{
|
|
$rawMaterial = DB::transaction(function () use ($data) {
|
|
$rawMaterial = RawMaterial::create([
|
|
'name' => $data['name'],
|
|
'unit' => $data['unit'],
|
|
'is_active' => $data['is_active'] ?? true,
|
|
]);
|
|
|
|
$now = now();
|
|
$priceRows = collect($data['variants'])->map(fn ($v) => [
|
|
'raw_material_id' => $rawMaterial->id,
|
|
'variant' => $v['variant'],
|
|
'price' => $v['price'],
|
|
'stock' => $v['stock'],
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
])->toArray();
|
|
|
|
DB::table('raw_material_prices')->insert($priceRows);
|
|
|
|
$insertedPrices = RawMaterialPrice::where('raw_material_id', $rawMaterial->id)->get();
|
|
$variantMap = $insertedPrices->mapWithKeys(fn ($p) => [$p->variant => $p->id]);
|
|
|
|
foreach ($data['variants'] as $variantData) {
|
|
if (! empty($variantData['photo_key'])) {
|
|
$priceId = $variantMap[$variantData['variant']];
|
|
$priceModel = RawMaterialPrice::find($priceId);
|
|
$this->registerPhoto($priceModel, $variantData['photo_key']);
|
|
}
|
|
}
|
|
|
|
return $rawMaterial;
|
|
});
|
|
|
|
NotificationService::notify(
|
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
|
title: 'Bahan Baku Baru',
|
|
body: "Bahan baku \"{$rawMaterial->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.',
|
|
url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]),
|
|
);
|
|
|
|
return $rawMaterial;
|
|
}
|
|
|
|
public function getForEdit(RawMaterial $rawMaterial): array
|
|
{
|
|
$rawMaterial->load([
|
|
'rawMaterialPrices.media',
|
|
]);
|
|
|
|
$variants = $rawMaterial->rawMaterialPrices->map(function (RawMaterialPrice $price) {
|
|
$media = $price->getMedia('images');
|
|
$photoKeys = $media->map(fn ($m) => $m->getCustomProperty('s3_key') ?? $m->file_name)->toArray();
|
|
$photoUrls = $media->map(fn ($m) => $this->s3Service->getTemporaryUrl($m->getPath()))->toArray();
|
|
|
|
return [
|
|
'id' => $price->id,
|
|
'variant' => $price->variant,
|
|
'price' => $price->price,
|
|
'stock' => $price->stock,
|
|
'photo_key' => $photoKeys[0] ?? null,
|
|
'photo_url' => $photoUrls[0] ?? null,
|
|
];
|
|
});
|
|
|
|
return [
|
|
'id' => $rawMaterial->id,
|
|
'name' => $rawMaterial->name,
|
|
'unit' => $rawMaterial->unit->value,
|
|
'is_active' => $rawMaterial->is_active,
|
|
'raw_material_prices' => $variants,
|
|
];
|
|
}
|
|
|
|
public function update(RawMaterial $rawMaterial, array $data): RawMaterial
|
|
{
|
|
$rawMaterial = DB::transaction(function () use ($rawMaterial, $data) {
|
|
$rawMaterial->update([
|
|
'name' => $data['name'],
|
|
'unit' => $data['unit'],
|
|
'is_active' => $data['is_active'] ?? $rawMaterial->is_active,
|
|
]);
|
|
|
|
$existingVariantIds = collect($data['variants'])
|
|
->pluck('id')
|
|
->filter()
|
|
->toArray();
|
|
|
|
$variantsToDelete = $rawMaterial->rawMaterialPrices()
|
|
->whereNotIn('id', $existingVariantIds)
|
|
->get();
|
|
|
|
foreach ($variantsToDelete as $price) {
|
|
if ($price->stock > 0) {
|
|
throw ValidationException::withMessages([
|
|
'variants' => "Varian \"{$price->variant}\" tidak dapat dihapus karena masih memiliki stok.",
|
|
]);
|
|
}
|
|
|
|
$activeCuttingUsage = $price->cuttingMaterials()
|
|
->whereHas('cutting', fn ($q) => $q->inProgress())
|
|
->exists();
|
|
|
|
if ($activeCuttingUsage) {
|
|
throw ValidationException::withMessages([
|
|
'variants' => "Varian \"{$price->variant}\" tidak dapat dihapus karena masih digunakan di cutting yang sedang diproses.",
|
|
]);
|
|
}
|
|
|
|
$price->clearMediaCollection('images');
|
|
$price->delete();
|
|
}
|
|
|
|
$existingPricesMap = RawMaterialPrice::whereIn('id', $existingVariantIds)
|
|
->with('media')
|
|
->get()
|
|
->mapWithKeys(fn ($p) => [$p->id => $p]);
|
|
|
|
$now = now();
|
|
$newVariantsData = collect($data['variants'])->filter(fn ($v) => ! isset($v['id']));
|
|
$newVariantIdMap = [];
|
|
|
|
if ($newVariantsData->isNotEmpty()) {
|
|
$newRows = $newVariantsData->map(fn ($v) => [
|
|
'raw_material_id' => $rawMaterial->id,
|
|
'variant' => $v['variant'],
|
|
'price' => $v['price'],
|
|
'stock' => $v['stock'],
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
])->toArray();
|
|
|
|
DB::table('raw_material_prices')->insert($newRows);
|
|
|
|
$newlyCreated = RawMaterialPrice::where('raw_material_id', $rawMaterial->id)
|
|
->whereIn('variant', $newVariantsData->pluck('variant')->toArray())
|
|
->get();
|
|
|
|
$newVariantIdMap = $newlyCreated->mapWithKeys(fn ($p) => [$p->variant => $p->id])->toArray();
|
|
}
|
|
|
|
foreach ($data['variants'] as $variantData) {
|
|
$priceId = $variantData['id'] ?? $newVariantIdMap[$variantData['variant']] ?? null;
|
|
if (! $priceId) {
|
|
continue;
|
|
}
|
|
|
|
$priceModel = $existingPricesMap[$priceId] ?? RawMaterialPrice::find($priceId);
|
|
if (! $priceModel) {
|
|
continue;
|
|
}
|
|
|
|
$priceModel->update([
|
|
'variant' => $variantData['variant'],
|
|
'price' => $variantData['price'],
|
|
'stock' => $variantData['stock'],
|
|
]);
|
|
|
|
if (isset($variantData['photo_key'])) {
|
|
$existingKey = $priceModel->getMedia('images')->first()?->getCustomProperty('s3_key');
|
|
if ($existingKey !== $variantData['photo_key']) {
|
|
$priceModel->clearMediaCollection('images');
|
|
if ($variantData['photo_key']) {
|
|
$this->registerPhoto($priceModel, $variantData['photo_key']);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $rawMaterial;
|
|
});
|
|
|
|
NotificationService::notify(
|
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
|
title: 'Bahan Baku Diperbarui',
|
|
body: "Bahan baku \"{$rawMaterial->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.',
|
|
url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]),
|
|
);
|
|
|
|
return $rawMaterial;
|
|
}
|
|
|
|
public function destroy(RawMaterial $rawMaterial): bool
|
|
{
|
|
foreach ($rawMaterial->rawMaterialPrices as $price) {
|
|
if ($price->stock > 0) {
|
|
throw ValidationException::withMessages([
|
|
'raw_material' => "Bahan baku \"{$rawMaterial->name}\" tidak dapat dihapus karena varian \"{$price->variant}\" masih memiliki stok.",
|
|
]);
|
|
}
|
|
|
|
$activeCuttingUsage = $price->cuttingMaterials()
|
|
->whereHas('cutting', fn ($q) => $q->inProgress())
|
|
->exists();
|
|
|
|
if ($activeCuttingUsage) {
|
|
throw ValidationException::withMessages([
|
|
'raw_material' => "Bahan baku \"{$rawMaterial->name}\" tidak dapat dihapus karena varian \"{$price->variant}\" masih digunakan di cutting yang sedang diproses.",
|
|
]);
|
|
}
|
|
}
|
|
|
|
$result = DB::transaction(function () use ($rawMaterial) {
|
|
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
|
|
$price->clearMediaCollection('images');
|
|
$price->delete();
|
|
});
|
|
|
|
return $rawMaterial->delete();
|
|
});
|
|
|
|
NotificationService::notify(
|
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
|
title: 'Bahan Baku Dihapus',
|
|
body: "Bahan baku \"{$rawMaterial->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.',
|
|
url: route('admin.master.raw-materials.index'),
|
|
);
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function toggleStatus(RawMaterial $rawMaterial): void
|
|
{
|
|
$rawMaterial->update([
|
|
'is_active' => ! $rawMaterial->is_active,
|
|
]);
|
|
|
|
$status = $rawMaterial->is_active ? 'diaktifkan' : 'dinonaktifkan';
|
|
|
|
NotificationService::notify(
|
|
roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
|
|
title: 'Status Bahan Baku Diubah',
|
|
body: "Bahan baku \"{$rawMaterial->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.',
|
|
url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]),
|
|
);
|
|
}
|
|
}
|