359 lines
14 KiB
PHP
359 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Admin\Manage;
|
|
|
|
use App\Models\Cutting;
|
|
use App\Models\CuttingMaterial;
|
|
use App\Models\CuttingMaterialCombination;
|
|
use App\Models\CuttingResult;
|
|
use App\Models\RawMaterialPrice;
|
|
use App\Services\Admin\Master\RawMaterial\RawMaterialService;
|
|
use App\Services\Concerns\RegistersMedia;
|
|
use App\Services\S3PresignedService;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class CuttingService
|
|
{
|
|
use RegistersMedia;
|
|
|
|
public function __construct(
|
|
private S3PresignedService $s3Service,
|
|
private RawMaterialService $rawMaterialService,
|
|
) {}
|
|
|
|
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
|
|
{
|
|
$paginator = Cutting::query()
|
|
->select(['id', 'created_by_id', 'status', 'description', 'total_material_cost', 'cost_per_unit', 'created_at'])
|
|
->with([
|
|
'createdBy:id',
|
|
'createdBy.userProfile:id,user_id,full_name',
|
|
'cuttingResults:id,cutting_id,product_name,cutting_result,sample,original_outside_sample',
|
|
'cuttingMaterials:id,cutting_id,raw_material_price_id,material_usage,material_result,combination_id',
|
|
'cuttingMaterials.rawMaterialPrice:id,raw_material_id,variant,price,stock',
|
|
'cuttingMaterials.rawMaterialPrice.rawMaterial:id,name,unit',
|
|
'cuttingMaterialCombinations:id,cutting_id,material_result',
|
|
])
|
|
->when($search, function ($q) use ($search) {
|
|
$q->whereHas('cuttingResults', fn ($rq) => $rq->where('product_name', 'like', "%{$search}%"))
|
|
->orWhere('description', 'like', "%{$search}%");
|
|
})
|
|
->orderBy($sort, $direction)
|
|
->paginate($perPage);
|
|
|
|
$paginator->getCollection()->each(function (Cutting $cutting) {
|
|
$cuttingMedia = $cutting->getFirstMedia('photos');
|
|
$cutting->photo_url = $cuttingMedia
|
|
? $this->s3Service->getTemporaryUrl($cuttingMedia->file_name)
|
|
: null;
|
|
|
|
$cutting->cuttingMaterials->each(function (CuttingMaterial $material) {
|
|
$media = $material->rawMaterialPrice?->getFirstMedia('photos');
|
|
if ($material->rawMaterialPrice) {
|
|
$material->rawMaterialPrice->photo_url = $media
|
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
: null;
|
|
}
|
|
});
|
|
});
|
|
|
|
return $paginator;
|
|
}
|
|
|
|
public function getForCreate(): array
|
|
{
|
|
return [
|
|
'rawMaterials' => $this->rawMaterialService->getVariantsForCutting(),
|
|
];
|
|
}
|
|
|
|
public function getForEdit(Cutting $cutting): array
|
|
{
|
|
$cutting->load([
|
|
'cuttingResults',
|
|
'cuttingMaterials.rawMaterialPrice.rawMaterial',
|
|
'cuttingMaterialCombinations',
|
|
]);
|
|
|
|
$result = $cutting->cuttingResults->first();
|
|
|
|
$materials = $cutting->cuttingMaterials->map(function (CuttingMaterial $material) {
|
|
$media = $material->rawMaterialPrice?->getFirstMedia('photos');
|
|
|
|
return [
|
|
'id' => $material->id,
|
|
'raw_material_price_id' => $material->raw_material_price_id,
|
|
'material_usage' => $material->material_usage,
|
|
'material_result' => $material->material_result,
|
|
'combination_id' => $material->combination_id,
|
|
'variant' => $material->rawMaterialPrice?->variant,
|
|
'photo_url' => $media
|
|
? $this->s3Service->getTemporaryUrl($media->file_name)
|
|
: null,
|
|
];
|
|
});
|
|
|
|
$combinations = $cutting->cuttingMaterialCombinations->map(function (CuttingMaterialCombination $combination) use ($materials) {
|
|
$materialIndices = $materials
|
|
->filter(fn ($m) => $m['combination_id'] === $combination->id)
|
|
->keys()
|
|
->values();
|
|
|
|
return [
|
|
'id' => $combination->id,
|
|
'material_result' => $combination->material_result,
|
|
'material_indices' => $materialIndices,
|
|
];
|
|
});
|
|
|
|
$cuttingMedia = $cutting->getFirstMedia('photos');
|
|
$photoKey = $cuttingMedia?->file_name;
|
|
$photoUrl = $cuttingMedia
|
|
? $this->s3Service->getTemporaryUrl($cuttingMedia->file_name)
|
|
: null;
|
|
|
|
return [
|
|
'id' => $cutting->id,
|
|
'status' => $cutting->status->value,
|
|
'description' => $cutting->description,
|
|
'product_name' => $result?->product_name ?? '',
|
|
'sample' => $result?->sample ?? 0,
|
|
'original_outside_sample' => $result?->original_outside_sample ?? 0,
|
|
'cutting_result' => $result?->cutting_result ?? 0,
|
|
'materials' => $materials,
|
|
'combinations' => $combinations,
|
|
'photo_key' => $photoKey,
|
|
'photo_url' => $photoUrl,
|
|
];
|
|
}
|
|
|
|
public function create(array $data): Cutting
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
foreach ($data['materials'] as $materialData) {
|
|
$usage = (int) ($materialData['material_usage'] ?? 0);
|
|
if ($usage <= 0) {
|
|
continue;
|
|
}
|
|
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
|
if (! $price) {
|
|
continue;
|
|
}
|
|
if ($usage > $price->stock) {
|
|
throw ValidationException::withMessages([
|
|
'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.",
|
|
]);
|
|
}
|
|
}
|
|
|
|
$cutting = Cutting::create([
|
|
'created_by_id' => auth()->id(),
|
|
'status' => 'in_progress',
|
|
'description' => $data['description'] ?? null,
|
|
]);
|
|
|
|
$totalMaterialCost = 0;
|
|
$now = now();
|
|
|
|
$combinations = $data['combinations'] ?? [];
|
|
$combinationMap = [];
|
|
|
|
foreach ($combinations as $index => $combo) {
|
|
$combination = CuttingMaterialCombination::create([
|
|
'cutting_id' => $cutting->id,
|
|
'user_id' => auth()->id(),
|
|
'material_result' => $combo['material_result'] ?? null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
$combinationMap[$index] = $combination->id;
|
|
}
|
|
|
|
foreach ($data['materials'] as $materialData) {
|
|
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
|
$materialCost = $price ? $price->price * ($materialData['material_usage'] ?? 0) : 0;
|
|
$totalMaterialCost += $materialCost;
|
|
|
|
$combinationId = null;
|
|
if (isset($materialData['combination_index']) && isset($combinationMap[$materialData['combination_index']])) {
|
|
$combinationId = $combinationMap[$materialData['combination_index']];
|
|
}
|
|
|
|
CuttingMaterial::create([
|
|
'cutting_id' => $cutting->id,
|
|
'user_id' => auth()->id(),
|
|
'raw_material_price_id' => $materialData['raw_material_price_id'],
|
|
'material_usage' => $materialData['material_usage'] ?? 0,
|
|
'material_result' => $materialData['material_result'] ?? null,
|
|
'combination_id' => $combinationId,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
if ($price && ($materialData['material_usage'] ?? 0) > 0) {
|
|
$price->decrement('stock', (int) $materialData['material_usage']);
|
|
}
|
|
}
|
|
|
|
$costPerUnit = 0;
|
|
if (($data['cutting_result'] ?? 0) > 0) {
|
|
$costPerUnit = (int) ($totalMaterialCost / $data['cutting_result']);
|
|
}
|
|
|
|
$cutting->update([
|
|
'total_material_cost' => $totalMaterialCost,
|
|
'cost_per_unit' => $costPerUnit,
|
|
]);
|
|
|
|
CuttingResult::create([
|
|
'cutting_id' => $cutting->id,
|
|
'user_id' => auth()->id(),
|
|
'product_name' => $data['product_name'] ?? null,
|
|
'sample' => $data['sample'] ?? null,
|
|
'original_outside_sample' => $data['original_outside_sample'] ?? null,
|
|
'cutting_result' => $data['cutting_result'] ?? null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
if (! empty($data['photo_key'])) {
|
|
$this->registerMedia(
|
|
model: $cutting,
|
|
s3Key: $data['photo_key'],
|
|
collectionName: 'photos',
|
|
orderColumn: 1,
|
|
);
|
|
}
|
|
|
|
return $cutting;
|
|
});
|
|
}
|
|
|
|
public function update(Cutting $cutting, array $data): Cutting
|
|
{
|
|
return DB::transaction(function () use ($cutting, $data) {
|
|
$cutting->load(['cuttingMaterials.rawMaterialPrice']);
|
|
|
|
foreach ($cutting->cuttingMaterials as $oldMaterial) {
|
|
if ($oldMaterial->material_usage > 0 && $oldMaterial->rawMaterialPrice) {
|
|
$oldMaterial->rawMaterialPrice->increment('stock', (int) $oldMaterial->material_usage);
|
|
}
|
|
}
|
|
|
|
foreach ($data['materials'] as $materialData) {
|
|
$usage = (int) ($materialData['material_usage'] ?? 0);
|
|
if ($usage <= 0) {
|
|
continue;
|
|
}
|
|
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
|
if (! $price) {
|
|
continue;
|
|
}
|
|
if ($usage > $price->stock) {
|
|
throw ValidationException::withMessages([
|
|
'materials' => "Stok {$price->variant} tidak mencukupi. Tersedia: {$price->stock}, dibutuhkan: {$usage}.",
|
|
]);
|
|
}
|
|
}
|
|
|
|
$cutting->load(['cuttingMaterials', 'cuttingMaterialCombinations', 'cuttingResults']);
|
|
|
|
$cutting->cuttingResults()->delete();
|
|
$cutting->cuttingMaterials()->delete();
|
|
$cutting->cuttingMaterialCombinations()->delete();
|
|
|
|
$totalMaterialCost = 0;
|
|
$now = now();
|
|
|
|
$combinations = $data['combinations'] ?? [];
|
|
$combinationMap = [];
|
|
|
|
foreach ($combinations as $index => $combo) {
|
|
$combination = CuttingMaterialCombination::create([
|
|
'cutting_id' => $cutting->id,
|
|
'user_id' => auth()->id(),
|
|
'material_result' => $combo['material_result'] ?? null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
$combinationMap[$index] = $combination->id;
|
|
}
|
|
|
|
foreach ($data['materials'] as $materialData) {
|
|
$price = RawMaterialPrice::find($materialData['raw_material_price_id']);
|
|
$materialCost = $price ? $price->price * ($materialData['material_usage'] ?? 0) : 0;
|
|
$totalMaterialCost += $materialCost;
|
|
|
|
$combinationId = null;
|
|
if (isset($materialData['combination_index']) && isset($combinationMap[$materialData['combination_index']])) {
|
|
$combinationId = $combinationMap[$materialData['combination_index']];
|
|
}
|
|
|
|
CuttingMaterial::create([
|
|
'cutting_id' => $cutting->id,
|
|
'user_id' => auth()->id(),
|
|
'raw_material_price_id' => $materialData['raw_material_price_id'],
|
|
'material_usage' => $materialData['material_usage'] ?? 0,
|
|
'material_result' => $materialData['material_result'] ?? null,
|
|
'combination_id' => $combinationId,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
if ($price && ($materialData['material_usage'] ?? 0) > 0) {
|
|
$price->decrement('stock', (int) $materialData['material_usage']);
|
|
}
|
|
}
|
|
|
|
$costPerUnit = 0;
|
|
if (($data['cutting_result'] ?? 0) > 0) {
|
|
$costPerUnit = (int) ($totalMaterialCost / $data['cutting_result']);
|
|
}
|
|
|
|
$cutting->update([
|
|
'description' => $data['description'] ?? null,
|
|
'total_material_cost' => $totalMaterialCost,
|
|
'cost_per_unit' => $costPerUnit,
|
|
]);
|
|
|
|
CuttingResult::create([
|
|
'cutting_id' => $cutting->id,
|
|
'user_id' => auth()->id(),
|
|
'product_name' => $data['product_name'] ?? null,
|
|
'sample' => $data['sample'] ?? null,
|
|
'original_outside_sample' => $data['original_outside_sample'] ?? null,
|
|
'cutting_result' => $data['cutting_result'] ?? null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
|
|
$this->syncPhoto($cutting, $data);
|
|
|
|
return $cutting;
|
|
});
|
|
}
|
|
|
|
public function delete(Cutting $cutting): bool
|
|
{
|
|
return DB::transaction(function () use ($cutting) {
|
|
$cutting->load('cuttingMaterials.rawMaterialPrice');
|
|
|
|
foreach ($cutting->cuttingMaterials as $material) {
|
|
if ($material->material_usage > 0 && $material->rawMaterialPrice) {
|
|
$material->rawMaterialPrice->increment('stock', (int) $material->material_usage);
|
|
}
|
|
}
|
|
|
|
$cutting->clearMediaCollection('photos');
|
|
$cutting->cuttingResults()->delete();
|
|
$cutting->cuttingMaterials()->delete();
|
|
$cutting->cuttingMaterialCombinations()->delete();
|
|
$cutting->delete();
|
|
|
|
return true;
|
|
});
|
|
}
|
|
}
|