feat: enhance cutting management by adding draft materials and results functionality, improving user experience with new API endpoints and UI components for better material handling
This commit is contained in:
parent
e522b5f922
commit
9c205cf3d0
@ -36,9 +36,13 @@ public function index(Request $request): Response
|
|||||||
|
|
||||||
public function create(): Response
|
public function create(): Response
|
||||||
{
|
{
|
||||||
|
$user = request()->user();
|
||||||
|
|
||||||
return Inertia::render('admin/manage/cuttings/Create', [
|
return Inertia::render('admin/manage/cuttings/Create', [
|
||||||
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(),
|
'rawMaterialCatalog' => $this->cuttingService->rawMaterialCatalog(user: $user),
|
||||||
'productCatalog' => $this->cuttingService->productCatalog(),
|
'productCatalog' => $this->cuttingService->productCatalog(user: $user),
|
||||||
|
'draftMaterials' => $this->cuttingService->draftMaterialsForUser($user),
|
||||||
|
'draftResults' => $this->cuttingService->draftResultsForUser($user),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +50,7 @@ public function store(CuttingRequest $request): RedirectResponse
|
|||||||
{
|
{
|
||||||
$this->cuttingService->create($request->validated(), $request->user());
|
$this->cuttingService->create($request->validated(), $request->user());
|
||||||
|
|
||||||
$this->flashCreated('Proses potong');
|
$this->flashCreated('Proses cutting');
|
||||||
|
|
||||||
return redirect()->route('admin.manage.cuttings.index');
|
return redirect()->route('admin.manage.cuttings.index');
|
||||||
}
|
}
|
||||||
@ -54,7 +58,7 @@ public function store(CuttingRequest $request): RedirectResponse
|
|||||||
public function edit(Cutting $cutting): Response|RedirectResponse
|
public function edit(Cutting $cutting): Response|RedirectResponse
|
||||||
{
|
{
|
||||||
if (! $cutting->status->isEditable()) {
|
if (! $cutting->status->isEditable()) {
|
||||||
$this->flashError('Proses potong tidak dapat diubah.');
|
$this->flashError('Proses cutting tidak dapat diubah.');
|
||||||
|
|
||||||
return redirect()->route('admin.manage.cuttings.index');
|
return redirect()->route('admin.manage.cuttings.index');
|
||||||
}
|
}
|
||||||
@ -70,7 +74,7 @@ public function update(CuttingRequest $request, Cutting $cutting): RedirectRespo
|
|||||||
{
|
{
|
||||||
$this->cuttingService->update($cutting, $request->validated());
|
$this->cuttingService->update($cutting, $request->validated());
|
||||||
|
|
||||||
$this->flashUpdated('Proses potong');
|
$this->flashUpdated('Proses cutting');
|
||||||
|
|
||||||
return redirect()->route('admin.manage.cuttings.index');
|
return redirect()->route('admin.manage.cuttings.index');
|
||||||
}
|
}
|
||||||
@ -79,7 +83,7 @@ public function destroy(Cutting $cutting): RedirectResponse
|
|||||||
{
|
{
|
||||||
$this->cuttingService->delete($cutting);
|
$this->cuttingService->delete($cutting);
|
||||||
|
|
||||||
$this->flashDeleted('Proses potong');
|
$this->flashDeleted('Proses cutting');
|
||||||
|
|
||||||
return redirect()->route('admin.manage.cuttings.index');
|
return redirect()->route('admin.manage.cuttings.index');
|
||||||
}
|
}
|
||||||
@ -99,11 +103,11 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
|
|||||||
);
|
);
|
||||||
|
|
||||||
$message = match ($status) {
|
$message = match ($status) {
|
||||||
CuttingStatus::COMPLETED => 'Proses potong berhasil diselesaikan. Menunggu verifikasi admin toko.',
|
CuttingStatus::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
|
||||||
CuttingStatus::VERIFIED => 'Proses potong berhasil diverifikasi. Stok produk telah diperbarui.',
|
CuttingStatus::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
|
||||||
CuttingStatus::REJECTED => 'Proses potong berhasil ditolak.',
|
CuttingStatus::REJECTED => 'Proses cutting berhasil ditolak.',
|
||||||
CuttingStatus::IN_PROGRESS => 'Proses potong dikembalikan ke proses.',
|
CuttingStatus::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
|
||||||
default => 'Status proses potong berhasil diperbarui.',
|
default => 'Status proses cutting berhasil diperbarui.',
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->flashSuccess($message);
|
$this->flashSuccess($message);
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\Manage\CuttingDraftMaterialRequest;
|
||||||
|
use App\Http\Requests\Admin\Manage\CuttingDraftResultRequest;
|
||||||
|
use App\Models\ProductVariant;
|
||||||
|
use App\Models\RawMaterialPrice;
|
||||||
|
use App\Services\Manage\CuttingService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CuttingDraftItemController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CuttingService $cuttingService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function storeMaterial(CuttingDraftMaterialRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$item = $this->cuttingService->syncDraftMaterial($request->validated(), $request->user());
|
||||||
|
|
||||||
|
return response()->json(['item' => $item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeResult(CuttingDraftResultRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$item = $this->cuttingService->syncDraftResult($request->validated(), $request->user());
|
||||||
|
|
||||||
|
return response()->json(['item' => $item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyMaterial(Request $request, RawMaterialPrice $rawMaterialPrice): JsonResponse
|
||||||
|
{
|
||||||
|
$this->cuttingService->removeDraftMaterial($request->user(), $rawMaterialPrice);
|
||||||
|
|
||||||
|
return response()->json(['ok' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyResult(Request $request, ProductVariant $productVariant): JsonResponse
|
||||||
|
{
|
||||||
|
$this->cuttingService->removeDraftResult($request->user(), $productVariant);
|
||||||
|
|
||||||
|
return response()->json(['ok' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\Permission;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CuttingDraftMaterialRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()?->can(Permission::CUTTINGS_CREATE->value) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'raw_material_price_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||||
|
],
|
||||||
|
'material_usage' => ['required', 'numeric', 'decimal:0,2', 'gt:0'],
|
||||||
|
'remaining_material' => ['required', 'numeric', 'decimal:0,2', 'gte:0'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Http/Requests/Admin/Manage/CuttingDraftResultRequest.php
Normal file
32
app/Http/Requests/Admin/Manage/CuttingDraftResultRequest.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
|
use App\Enums\Permission;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CuttingDraftResultRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()?->can(Permission::CUTTINGS_CREATE->value) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'product_variant_id' => [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
|
||||||
|
],
|
||||||
|
'cutting_result' => ['required', 'integer', 'min:1'],
|
||||||
|
'warehouse_stock' => ['required', 'integer', 'min:0'],
|
||||||
|
'cutting_reject' => ['required', 'integer', 'min:0'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,33 +22,39 @@ public function authorize(): bool
|
|||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
$isCreate = $this->isMethod('POST');
|
||||||
|
|
||||||
|
$rules = [
|
||||||
'description' => ['nullable', 'string', 'max:100'],
|
'description' => ['nullable', 'string', 'max:100'],
|
||||||
|
|
||||||
'materials' => ['required', 'array', 'min:1'],
|
|
||||||
'materials.*.raw_material_price_id' => [
|
|
||||||
'required',
|
|
||||||
'integer',
|
|
||||||
'distinct',
|
|
||||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
|
||||||
],
|
|
||||||
'materials.*.material_usage' => ['required', 'numeric', 'decimal:0,2', 'gt:0'],
|
|
||||||
'materials.*.remaining_material' => ['required', 'numeric', 'decimal:0,2', 'gte:0'],
|
|
||||||
|
|
||||||
'results' => ['required', 'array', 'min:1'],
|
|
||||||
'results.*.product_variant_id' => [
|
|
||||||
'required',
|
|
||||||
'integer',
|
|
||||||
'distinct',
|
|
||||||
Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
|
|
||||||
],
|
|
||||||
'results.*.cutting_result' => ['required', 'integer', 'min:1'],
|
|
||||||
'results.*.warehouse_stock' => ['required', 'integer', 'min:0'],
|
|
||||||
'results.*.cutting_reject' => ['required', 'integer', 'min:0'],
|
|
||||||
|
|
||||||
'sewing_cost' => ['nullable', 'integer', 'min:0'],
|
'sewing_cost' => ['nullable', 'integer', 'min:0'],
|
||||||
'other_cost' => ['nullable', 'integer', 'min:0'],
|
'other_cost' => ['nullable', 'integer', 'min:0'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (! $isCreate) {
|
||||||
|
$rules['materials'] = ['required', 'array', 'min:1'];
|
||||||
|
$rules['materials.*.raw_material_price_id'] = [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
'distinct',
|
||||||
|
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||||
|
];
|
||||||
|
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,2', 'gt:0'];
|
||||||
|
$rules['materials.*.remaining_material'] = ['required', 'numeric', 'decimal:0,2', 'gte:0'];
|
||||||
|
|
||||||
|
$rules['results'] = ['required', 'array', 'min:1'];
|
||||||
|
$rules['results.*.product_variant_id'] = [
|
||||||
|
'required',
|
||||||
|
'integer',
|
||||||
|
'distinct',
|
||||||
|
Rule::exists('product_variants', 'id')->whereNull('deleted_at'),
|
||||||
|
];
|
||||||
|
$rules['results.*.cutting_result'] = ['required', 'integer', 'min:1'];
|
||||||
|
$rules['results.*.warehouse_stock'] = ['required', 'integer', 'min:0'];
|
||||||
|
$rules['results.*.cutting_reject'] = ['required', 'integer', 'min:0'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -36,6 +36,11 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function cutting(): BelongsTo
|
public function cutting(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Cutting::class);
|
return $this->belongsTo(Cutting::class);
|
||||||
|
|||||||
@ -23,6 +23,11 @@ protected function casts(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function cutting(): BelongsTo
|
public function cutting(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Cutting::class);
|
return $this->belongsTo(Cutting::class);
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
use App\Support\Media\MediaPresenter;
|
use App\Support\Media\MediaPresenter;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
@ -133,11 +134,11 @@ public function getCompletedCuttings(User $user): Collection
|
|||||||
/**
|
/**
|
||||||
* @return Collection<int, RawMaterial>
|
* @return Collection<int, RawMaterial>
|
||||||
*/
|
*/
|
||||||
public function rawMaterialCatalog(?Cutting $cutting = null): Collection
|
public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null): Collection
|
||||||
{
|
{
|
||||||
$selectedPriceIds = $cutting
|
$selectedPriceIds = $cutting
|
||||||
? $cutting->materials()->pluck('raw_material_price_id')->all()
|
? $cutting->materials()->pluck('raw_material_price_id')->all()
|
||||||
: [];
|
: ($user ? $this->draftMaterialsQuery($user)->pluck('raw_material_price_id')->all() : []);
|
||||||
|
|
||||||
return RawMaterial::query()
|
return RawMaterial::query()
|
||||||
->with([
|
->with([
|
||||||
@ -170,11 +171,11 @@ public function rawMaterialCatalog(?Cutting $cutting = null): Collection
|
|||||||
/**
|
/**
|
||||||
* @return Collection<int, Product>
|
* @return Collection<int, Product>
|
||||||
*/
|
*/
|
||||||
public function productCatalog(?Cutting $cutting = null): Collection
|
public function productCatalog(?Cutting $cutting = null, ?User $user = null): Collection
|
||||||
{
|
{
|
||||||
$selectedVariantIds = $cutting
|
$selectedVariantIds = $cutting
|
||||||
? $cutting->results()->pluck('product_variant_id')->all()
|
? $cutting->results()->pluck('product_variant_id')->all()
|
||||||
: [];
|
: ($user ? $this->draftResultsQuery($user)->pluck('product_variant_id')->all() : []);
|
||||||
|
|
||||||
return Product::query()
|
return Product::query()
|
||||||
->with([
|
->with([
|
||||||
@ -240,14 +241,197 @@ public function findForEdit(Cutting $cutting): Cutting
|
|||||||
return $cutting;
|
return $cutting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function draftMaterialsForUser(User $user): array
|
||||||
|
{
|
||||||
|
return $this->draftMaterialsQuery($user)
|
||||||
|
->with([
|
||||||
|
'rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
|
'rawMaterialPrice.media',
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->map(fn (CuttingMaterial $item) => $this->presentDraftMaterial($item))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function draftResultsForUser(User $user): array
|
||||||
|
{
|
||||||
|
return $this->draftResultsQuery($user)
|
||||||
|
->with([
|
||||||
|
'productVariant.product:id,name',
|
||||||
|
'productVariant.media',
|
||||||
|
])
|
||||||
|
->get()
|
||||||
|
->map(fn (CuttingResult $item) => $this->presentDraftResult($item))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function syncDraftMaterial(array $validated, User $user): array
|
||||||
|
{
|
||||||
|
$price = RawMaterialPrice::query()
|
||||||
|
->with('rawMaterial')
|
||||||
|
->findOrFail($validated['raw_material_price_id']);
|
||||||
|
|
||||||
|
$unit = $price->rawMaterial?->unit;
|
||||||
|
$usageInput = round((float) $validated['material_usage'], 2);
|
||||||
|
$remainingInput = round((float) $validated['remaining_material'], 2);
|
||||||
|
|
||||||
|
if ($unit !== null && $unit->usesLengthUnit()) {
|
||||||
|
$materialUsage = round($unit->fromCm($usageInput), 2);
|
||||||
|
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
|
||||||
|
} else {
|
||||||
|
$materialUsage = $usageInput;
|
||||||
|
$remainingMaterial = $remainingInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($materialUsage <= 0) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'material_usage' => 'Pemakaian bahan harus lebih dari 0.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remainingMaterial < 0) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'remaining_material' => 'Sisa bahan tidak boleh negatif.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = CuttingMaterial::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'raw_material_price_id' => $price->id,
|
||||||
|
'cutting_id' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'material_usage' => $materialUsage,
|
||||||
|
'remaining_material' => $remainingMaterial,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$item->load([
|
||||||
|
'rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
|
'rawMaterialPrice.media',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->presentDraftMaterial($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function syncDraftResult(array $validated, User $user): array
|
||||||
|
{
|
||||||
|
$variant = ProductVariant::query()
|
||||||
|
->with('product:id,name')
|
||||||
|
->findOrFail($validated['product_variant_id']);
|
||||||
|
|
||||||
|
$cuttingResult = (int) $validated['cutting_result'];
|
||||||
|
$warehouseStock = (int) $validated['warehouse_stock'];
|
||||||
|
$cuttingReject = (int) ($validated['cutting_reject'] ?? 0);
|
||||||
|
|
||||||
|
if ($cuttingResult < 1) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'cutting_result' => 'Hasil cutting minimal 1 pcs.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($warehouseStock < 0) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'warehouse_stock' => 'Stok gudang tidak boleh negatif.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($cuttingReject < 0) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'cutting_reject' => 'Reject tidak boleh negatif.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($warehouseStock + $cuttingReject) !== $cuttingResult) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'cutting_result' => 'Hasil cutting harus sama dengan stok gudang ditambah reject.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$item = CuttingResult::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'product_variant_id' => $variant->id,
|
||||||
|
'cutting_id' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'cutting_result' => $cuttingResult,
|
||||||
|
'warehouse_stock' => $warehouseStock,
|
||||||
|
'cutting_reject' => $cuttingReject,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$item->load([
|
||||||
|
'productVariant.product:id,name',
|
||||||
|
'productVariant.media',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->presentDraftResult($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeDraftMaterial(User $user, RawMaterialPrice $rawMaterialPrice): void
|
||||||
|
{
|
||||||
|
CuttingMaterial::query()
|
||||||
|
->whereNull('cutting_id')
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->where('raw_material_price_id', $rawMaterialPrice->id)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeDraftResult(User $user, ProductVariant $productVariant): void
|
||||||
|
{
|
||||||
|
CuttingResult::query()
|
||||||
|
->whereNull('cutting_id')
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->where('product_variant_id', $productVariant->id)
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $validated
|
* @param array<string, mixed> $validated
|
||||||
*/
|
*/
|
||||||
public function create(array $validated, User $user): Cutting
|
public function create(array $validated, User $user): Cutting
|
||||||
{
|
{
|
||||||
$cutting = DB::transaction(function () use ($validated, $user): Cutting {
|
$cutting = DB::transaction(function () use ($validated, $user): Cutting {
|
||||||
$materials = $this->buildMaterials($validated['materials']);
|
/** @var EloquentCollection<int, CuttingMaterial> $draftMaterials */
|
||||||
$results = $this->buildResults($validated['results']);
|
$draftMaterials = $this->draftMaterialsQuery($user)
|
||||||
|
->with('rawMaterialPrice.rawMaterial')
|
||||||
|
->lockForUpdate()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
/** @var EloquentCollection<int, CuttingResult> $draftResults */
|
||||||
|
$draftResults = $this->draftResultsQuery($user)
|
||||||
|
->lockForUpdate()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($draftMaterials->isEmpty()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'materials' => 'Tambahkan minimal satu bahan baku.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($draftResults->isEmpty()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'results' => 'Tambahkan minimal satu hasil produk.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$cutting = Cutting::create([
|
$cutting = Cutting::create([
|
||||||
'status' => CuttingStatus::IN_PROGRESS,
|
'status' => CuttingStatus::IN_PROGRESS,
|
||||||
@ -257,12 +441,16 @@ public function create(array $validated, User $user): Cutting
|
|||||||
'created_by_id' => $user->id,
|
'created_by_id' => $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach ($materials as $materialData) {
|
foreach ($draftMaterials as $material) {
|
||||||
$cutting->materials()->create($materialData);
|
$material->cutting_id = $cutting->id;
|
||||||
|
$material->user_id = null;
|
||||||
|
$material->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($results as $resultData) {
|
foreach ($draftResults as $result) {
|
||||||
$cutting->results()->create($resultData);
|
$result->cutting_id = $cutting->id;
|
||||||
|
$result->user_id = null;
|
||||||
|
$result->save();
|
||||||
}
|
}
|
||||||
|
|
||||||
$cutting->load('materials.rawMaterialPrice.rawMaterial');
|
$cutting->load('materials.rawMaterialPrice.rawMaterial');
|
||||||
@ -272,8 +460,8 @@ public function create(array $validated, User $user): Cutting
|
|||||||
});
|
});
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'✂️ Proses Potong Baru',
|
'✂️ Proses Cutting Baru',
|
||||||
"Proses potong dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.",
|
"Proses cutting dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
'/admin/manage/cuttings',
|
'/admin/manage/cuttings',
|
||||||
);
|
);
|
||||||
@ -288,7 +476,7 @@ public function update(Cutting $cutting, array $validated): void
|
|||||||
{
|
{
|
||||||
if (! $cutting->status->isEditable()) {
|
if (! $cutting->status->isEditable()) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Proses potong tidak dapat diubah.',
|
'status' => 'Proses cutting tidak dapat diubah.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +523,7 @@ public function delete(Cutting $cutting): void
|
|||||||
{
|
{
|
||||||
if (! in_array($cutting->status, [CuttingStatus::IN_PROGRESS, CuttingStatus::REJECTED], true)) {
|
if (! in_array($cutting->status, [CuttingStatus::IN_PROGRESS, CuttingStatus::REJECTED], true)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Proses potong hanya dapat dihapus saat masih proses atau ditolak.',
|
'status' => 'Proses cutting hanya dapat dihapus saat masih proses atau ditolak.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,8 +544,8 @@ public function delete(Cutting $cutting): void
|
|||||||
});
|
});
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
'🗑️ Proses Potong Dihapus',
|
'🗑️ Proses Cutting Dihapus',
|
||||||
"Proses potong dengan deskripsi {$description} telah dihapus.",
|
"Proses cutting dengan deskripsi {$description} telah dihapus.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
'/admin/manage/cuttings',
|
'/admin/manage/cuttings',
|
||||||
);
|
);
|
||||||
@ -374,7 +562,7 @@ public function transitionStatus(
|
|||||||
): void {
|
): void {
|
||||||
if (! $cutting->status->canTransitionTo($status)) {
|
if (! $cutting->status->canTransitionTo($status)) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'status' => 'Status proses potong tidak dapat diubah.',
|
'status' => 'Status proses cutting tidak dapat diubah.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,19 +613,19 @@ public function transitionStatus(
|
|||||||
|
|
||||||
$description = $cutting->description ?? '-';
|
$description = $cutting->description ?? '-';
|
||||||
$message = match ($status) {
|
$message = match ($status) {
|
||||||
CuttingStatus::COMPLETED => "Proses potong dengan deskripsi '{$description}' telah selesai dan menunggu verifikasi.",
|
CuttingStatus::COMPLETED => "Proses cutting dengan deskripsi '{$description}' telah selesai dan menunggu verifikasi.",
|
||||||
CuttingStatus::VERIFIED => "Proses potong dengan deskripsi '{$description}' telah diverifikasi.",
|
CuttingStatus::VERIFIED => "Proses cutting dengan deskripsi '{$description}' telah diverifikasi.",
|
||||||
CuttingStatus::REJECTED => "Proses potong dengan deskripsi '{$description}' ditolak".($reason ? " dengan alasan: '{$reason}'" : '').'.',
|
CuttingStatus::REJECTED => "Proses cutting dengan deskripsi '{$description}' ditolak".($reason ? " dengan alasan: '{$reason}'" : '').'.',
|
||||||
CuttingStatus::IN_PROGRESS => "Proses potong dengan deskripsi '{$description}' dikembalikan ke proses.",
|
CuttingStatus::IN_PROGRESS => "Proses cutting dengan deskripsi '{$description}' dikembalikan ke proses.",
|
||||||
default => "Status proses potong dengan deskripsi '{$description}' telah diperbarui ke: {$status->label()}.",
|
default => "Status proses cutting dengan deskripsi '{$description}' telah diperbarui ke: {$status->label()}.",
|
||||||
};
|
};
|
||||||
|
|
||||||
$title = match ($status) {
|
$title = match ($status) {
|
||||||
CuttingStatus::COMPLETED => '✂️ Proses Potong Selesai',
|
CuttingStatus::COMPLETED => '✂️ Proses Cutting Selesai',
|
||||||
CuttingStatus::VERIFIED => '✂️ Proses Potong Terverifikasi',
|
CuttingStatus::VERIFIED => '✂️ Proses Cutting Terverifikasi',
|
||||||
CuttingStatus::REJECTED => '✂️ Proses Potong Ditolak',
|
CuttingStatus::REJECTED => '✂️ Proses Cutting Ditolak',
|
||||||
CuttingStatus::IN_PROGRESS => '✂️ Proses Potong Dikembalikan',
|
CuttingStatus::IN_PROGRESS => '✂️ Proses Cutting Dikembalikan',
|
||||||
default => '✂️ Proses Potong Diperbarui',
|
default => '✂️ Proses Cutting Diperbarui',
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->pushNotificationService->sendToRoles(
|
$this->pushNotificationService->sendToRoles(
|
||||||
@ -521,7 +709,7 @@ private function buildResults(array $results): array
|
|||||||
|
|
||||||
if ($cuttingResult < 1) {
|
if ($cuttingResult < 1) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
"results.{$index}.cutting_result" => 'Hasil potong minimal 1 pcs.',
|
"results.{$index}.cutting_result" => 'Hasil cutting minimal 1 pcs.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -539,7 +727,7 @@ private function buildResults(array $results): array
|
|||||||
|
|
||||||
if (($warehouseStock + $cuttingReject) !== $cuttingResult) {
|
if (($warehouseStock + $cuttingReject) !== $cuttingResult) {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
"results.{$index}.cutting_result" => 'Hasil potong harus sama dengan stok gudang ditambah reject.',
|
"results.{$index}.cutting_result" => 'Hasil cutting harus sama dengan stok gudang ditambah reject.',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -677,6 +865,86 @@ private function applySorting(Builder $query, string $sort, string $direction):
|
|||||||
$query->latest();
|
$query->latest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Builder<CuttingMaterial>
|
||||||
|
*/
|
||||||
|
private function draftMaterialsQuery(User $user): Builder
|
||||||
|
{
|
||||||
|
return CuttingMaterial::query()
|
||||||
|
->whereNull('cutting_id')
|
||||||
|
->where('user_id', $user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Builder<CuttingResult>
|
||||||
|
*/
|
||||||
|
private function draftResultsQuery(User $user): Builder
|
||||||
|
{
|
||||||
|
return CuttingResult::query()
|
||||||
|
->whereNull('cutting_id')
|
||||||
|
->where('user_id', $user->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function presentDraftMaterial(CuttingMaterial $item): array
|
||||||
|
{
|
||||||
|
$price = $item->rawMaterialPrice;
|
||||||
|
$rawMaterial = $price?->rawMaterial;
|
||||||
|
$unit = $rawMaterial?->unit;
|
||||||
|
|
||||||
|
$usesLengthUnit = $unit?->usesLengthUnit() ?? false;
|
||||||
|
|
||||||
|
// Convert stored native unit back to cm for display
|
||||||
|
if ($usesLengthUnit && $unit !== null) {
|
||||||
|
$materialUsageCm = round($unit->toCm((float) $item->material_usage), 2);
|
||||||
|
$remainingMaterialCm = round($unit->toCm((float) $item->remaining_material), 2);
|
||||||
|
$materialUsageDisplay = (string) $materialUsageCm;
|
||||||
|
$remainingMaterialDisplay = (string) $remainingMaterialCm;
|
||||||
|
} else {
|
||||||
|
$materialUsageDisplay = $this->formatQuantityInput((float) $item->material_usage);
|
||||||
|
$remainingMaterialDisplay = $this->formatQuantityInput((float) $item->remaining_material);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'raw_material_price_id' => $item->raw_material_price_id,
|
||||||
|
'raw_material_name' => $rawMaterial?->name ?? '',
|
||||||
|
'variant' => $price?->variant ?? '',
|
||||||
|
'unit' => $rawMaterial?->unit?->value ?? '',
|
||||||
|
'uses_length_unit' => $usesLengthUnit,
|
||||||
|
'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
|
||||||
|
'stock_input' => $price?->stock_input ?? '',
|
||||||
|
'material_usage' => $materialUsageDisplay,
|
||||||
|
'remaining_material' => $remainingMaterialDisplay,
|
||||||
|
'images' => $price ? MediaPresenter::collection($price, 'images') : [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function presentDraftResult(CuttingResult $item): array
|
||||||
|
{
|
||||||
|
$variant = $item->productVariant;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'product_variant_id' => $item->product_variant_id,
|
||||||
|
'product_name' => $variant?->product?->name ?? '',
|
||||||
|
'variant_name' => $variant?->name ?? '',
|
||||||
|
'stock' => $variant?->stock ?? 0,
|
||||||
|
'cutting_result' => (string) $item->cutting_result,
|
||||||
|
'warehouse_stock' => (string) $item->warehouse_stock,
|
||||||
|
'cutting_reject' => (string) $item->cutting_reject,
|
||||||
|
'images' => $variant ? MediaPresenter::collection($variant, 'images') : [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatQuantityInput(float $value): string
|
||||||
|
{
|
||||||
|
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||||
|
}
|
||||||
|
|
||||||
private function appendCostPreview(Cutting $cutting): void
|
private function appendCostPreview(Cutting $cutting): void
|
||||||
{
|
{
|
||||||
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result'));
|
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result'));
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_materials', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('cutting_id')->nullable()->change();
|
||||||
|
$table->foreignId('user_id')->nullable()->after('id')->constrained()->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('cutting_results', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('cutting_id')->nullable()->change();
|
||||||
|
$table->foreignId('user_id')->nullable()->after('id')->constrained()->cascadeOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('cutting_materials', function (Blueprint $table): void {
|
||||||
|
$table->dropForeign(['user_id']);
|
||||||
|
$table->dropColumn('user_id');
|
||||||
|
$table->foreignId('cutting_id')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('cutting_results', function (Blueprint $table): void {
|
||||||
|
$table->dropForeign(['user_id']);
|
||||||
|
$table->dropColumn('user_id');
|
||||||
|
$table->foreignId('cutting_id')->nullable(false)->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -4,11 +4,13 @@ import { ArrowLeft } from '@lucide/vue';
|
|||||||
import CuttingPosForm from './form/CuttingPosForm.vue';
|
import CuttingPosForm from './form/CuttingPosForm.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import type { CuttingProductCatalogItem, CuttingRawMaterialCatalogItem } from '@/types/cutting';
|
import type { CuttingMaterialCartItem, CuttingProductCatalogItem, CuttingRawMaterialCatalogItem, CuttingResultCartItem } from '@/types/cutting';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
rawMaterialCatalog: CuttingRawMaterialCatalogItem[];
|
||||||
productCatalog: CuttingProductCatalogItem[];
|
productCatalog: CuttingProductCatalogItem[];
|
||||||
|
draftMaterials: CuttingMaterialCartItem[];
|
||||||
|
draftResults: CuttingResultCartItem[];
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -34,6 +36,8 @@ defineProps<{
|
|||||||
<CuttingPosForm
|
<CuttingPosForm
|
||||||
:raw-material-catalog="rawMaterialCatalog"
|
:raw-material-catalog="rawMaterialCatalog"
|
||||||
:product-catalog="productCatalog"
|
:product-catalog="productCatalog"
|
||||||
|
:draft-materials="draftMaterials"
|
||||||
|
:draft-results="draftResults"
|
||||||
submit-url="/admin/manage/cuttings"
|
submit-url="/admin/manage/cuttings"
|
||||||
method="post"
|
method="post"
|
||||||
submit-label="Simpan"
|
submit-label="Simpan"
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { apiFetch } from '@/lib/api';
|
||||||
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
import { getFirstCoverImage } from '@/lib/catalog-cover';
|
||||||
import { FIELD_LIMITS } from '@/lib/field-limits';
|
import { FIELD_LIMITS } from '@/lib/field-limits';
|
||||||
import { formErrors } from '@/lib/form';
|
import { formErrors } from '@/lib/form';
|
||||||
@ -59,11 +60,15 @@ const props = defineProps<{
|
|||||||
materials: CuttingMaterialCartItem[];
|
materials: CuttingMaterialCartItem[];
|
||||||
results: CuttingResultCartItem[];
|
results: CuttingResultCartItem[];
|
||||||
};
|
};
|
||||||
|
draftMaterials?: CuttingMaterialCartItem[];
|
||||||
|
draftResults?: CuttingResultCartItem[];
|
||||||
submitUrl: string;
|
submitUrl: string;
|
||||||
method: 'post' | 'put';
|
method: 'post' | 'put';
|
||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const isCreateMode = computed(() => props.method === 'post');
|
||||||
|
|
||||||
const materialSearch = ref('');
|
const materialSearch = ref('');
|
||||||
const productSearch = ref('');
|
const productSearch = ref('');
|
||||||
const materialCart = ref<CuttingMaterialCartItem[]>([]);
|
const materialCart = ref<CuttingMaterialCartItem[]>([]);
|
||||||
@ -179,6 +184,20 @@ function populateForm() {
|
|||||||
resultCart.value = props.initialData.results.map((item) => ({ ...item }));
|
resultCart.value = props.initialData.results.map((item) => ({ ...item }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function populateDraftItems() {
|
||||||
|
if (!isCreateMode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.draftMaterials?.length) {
|
||||||
|
materialCart.value = props.draftMaterials.map((item) => ({ ...item }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.draftResults?.length) {
|
||||||
|
resultCart.value = props.draftResults.map((item) => ({ ...item }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.initialData,
|
() => props.initialData,
|
||||||
() => {
|
() => {
|
||||||
@ -187,6 +206,8 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
populateDraftItems();
|
||||||
|
|
||||||
type CatalogPrice = CuttingRawMaterialCatalogItem['prices'][number];
|
type CatalogPrice = CuttingRawMaterialCatalogItem['prices'][number];
|
||||||
|
|
||||||
const filteredRawMaterials = computed(() => {
|
const filteredRawMaterials = computed(() => {
|
||||||
@ -221,6 +242,61 @@ const filteredProducts = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function upsertMaterialCartItem(item: CuttingMaterialCartItem) {
|
||||||
|
const index = materialCart.value.findIndex(
|
||||||
|
(cartItem) => cartItem.raw_material_price_id === item.raw_material_price_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
materialCart.value.push({ ...item });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
materialCart.value[index] = { ...item };
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertResultCartItem(item: CuttingResultCartItem) {
|
||||||
|
const index = resultCart.value.findIndex(
|
||||||
|
(cartItem) => cartItem.product_variant_id === item.product_variant_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
resultCart.value.push({ ...item });
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCart.value[index] = { ...item };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncDraftMaterial(rawMaterial: CuttingRawMaterialCatalogItem, price: CatalogPrice, materialUsage: string, remainingMaterial: string) {
|
||||||
|
const { item } = await apiFetch<{ item: CuttingMaterialCartItem }>('/admin/manage/cuttings/draft-materials', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
raw_material_price_id: price.id,
|
||||||
|
material_usage: materialUsage,
|
||||||
|
remaining_material: remainingMaterial,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
upsertMaterialCartItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncDraftResult(product: CuttingProductCatalogItem, variant: CuttingProductCatalogItem['variants'][number], cuttingResult: string, warehouseStock: string, cuttingReject: string) {
|
||||||
|
const { item } = await apiFetch<{ item: CuttingResultCartItem }>('/admin/manage/cuttings/draft-results', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
product_variant_id: variant.id,
|
||||||
|
cutting_result: cuttingResult,
|
||||||
|
warehouse_stock: warehouseStock,
|
||||||
|
cutting_reject: cuttingReject,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
upsertResultCartItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
function getMaterialCartItem(
|
function getMaterialCartItem(
|
||||||
priceId: number,
|
priceId: number,
|
||||||
): CuttingMaterialCartItem | undefined {
|
): CuttingMaterialCartItem | undefined {
|
||||||
@ -229,24 +305,48 @@ function getMaterialCartItem(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseMaterialQty(priceId: number) {
|
async function decreaseMaterialQty(priceId: number) {
|
||||||
const index = materialCart.value.findIndex(
|
const item = materialCart.value.find(
|
||||||
(item) => item.raw_material_price_id === priceId,
|
(i) => i.raw_material_price_id === priceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (!item) {
|
||||||
const item = materialCart.value[index];
|
return;
|
||||||
const nextQty = (Number(item.material_usage) || 0) - 1;
|
|
||||||
|
|
||||||
if (nextQty <= 0) {
|
|
||||||
removeMaterial(index);
|
|
||||||
} else {
|
|
||||||
item.material_usage = String(nextQty);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextQty = (Number(item.material_usage) || 0) - 1;
|
||||||
|
|
||||||
|
if (nextQty <= 0) {
|
||||||
|
const index = materialCart.value.findIndex(
|
||||||
|
(i) => i.raw_material_price_id === priceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
await removeMaterial(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
try {
|
||||||
|
await syncDraftMaterial(
|
||||||
|
{ id: 0, name: '', unit: '', unit_label: '', unit_abbreviation: '', prices: [] } as CuttingRawMaterialCatalogItem,
|
||||||
|
{ id: priceId, variant: '', price: 0, price_formatted: '', stock_input: '', stock_formatted: '', images: [] } as CatalogPrice,
|
||||||
|
String(nextQty),
|
||||||
|
item.remaining_material,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.material_usage = String(nextQty);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMaterial(
|
async function addMaterial(
|
||||||
rawMaterial: CuttingRawMaterialCatalogItem,
|
rawMaterial: CuttingRawMaterialCatalogItem,
|
||||||
price: CatalogPrice,
|
price: CatalogPrice,
|
||||||
) {
|
) {
|
||||||
@ -254,10 +354,24 @@ function addMaterial(
|
|||||||
(item) => item.raw_material_price_id === price.id,
|
(item) => item.raw_material_price_id === price.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const defaultUsage = usesLengthUnit(rawMaterial.unit) ? '100' : '1';
|
||||||
|
const nextUsage = existing
|
||||||
|
? String((Number(existing.material_usage) || 0) + 1)
|
||||||
|
: defaultUsage;
|
||||||
|
const remaining = existing?.remaining_material ?? '0';
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
try {
|
||||||
|
await syncDraftMaterial(rawMaterial, price, nextUsage, remaining);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.material_usage = String(
|
existing.material_usage = nextUsage;
|
||||||
(Number(existing.material_usage) || 0) + 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -270,13 +384,28 @@ function addMaterial(
|
|||||||
uses_length_unit: usesLengthUnit(rawMaterial.unit),
|
uses_length_unit: usesLengthUnit(rawMaterial.unit),
|
||||||
unit_abbreviation: rawMaterial.unit_abbreviation,
|
unit_abbreviation: rawMaterial.unit_abbreviation,
|
||||||
stock_input: price.stock_input,
|
stock_input: price.stock_input,
|
||||||
material_usage: usesLengthUnit(rawMaterial.unit) ? '100' : '1',
|
material_usage: defaultUsage,
|
||||||
remaining_material: '0',
|
remaining_material: '0',
|
||||||
images: price.images ?? [],
|
images: price.images ?? [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMaterial(index: number) {
|
async function removeMaterial(index: number) {
|
||||||
|
const item = materialCart.value[index];
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
try {
|
||||||
|
await apiFetch(
|
||||||
|
`/admin/manage/cuttings/draft-materials/${item.raw_material_price_id}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal menghapus item dari keranjang.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
materialCart.value.splice(index, 1);
|
materialCart.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,29 +417,54 @@ function getResultCartItem(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseResultQty(variantId: number) {
|
async function decreaseResultQty(variantId: number) {
|
||||||
const index = resultCart.value.findIndex(
|
const item = resultCart.value.find(
|
||||||
(item) => item.product_variant_id === variantId,
|
(i) => i.product_variant_id === variantId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (!item) {
|
||||||
const item = resultCart.value[index];
|
return;
|
||||||
const nextQty = (Number(item.cutting_result) || 0) - 1;
|
|
||||||
|
|
||||||
if (nextQty <= 0) {
|
|
||||||
removeResult(index);
|
|
||||||
} else {
|
|
||||||
item.cutting_result = String(nextQty);
|
|
||||||
// also adjust warehouse stock or run sync totals
|
|
||||||
item.warehouse_stock = String(
|
|
||||||
Math.max(0, (Number(item.warehouse_stock) || 0) - 1),
|
|
||||||
);
|
|
||||||
syncResultTotals(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextQty = (Number(item.cutting_result) || 0) - 1;
|
||||||
|
|
||||||
|
if (nextQty <= 0) {
|
||||||
|
const index = resultCart.value.findIndex(
|
||||||
|
(i) => i.product_variant_id === variantId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
await removeResult(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextWarehouse = Math.max(0, (Number(item.warehouse_stock) || 0) - 1);
|
||||||
|
const nextReject = Math.max(nextQty - nextWarehouse, 0);
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
try {
|
||||||
|
await syncDraftResult(
|
||||||
|
{ id: 0, name: '', variants: [] } as CuttingProductCatalogItem,
|
||||||
|
{ id: variantId, name: '', stock: 0, images: [] } as CuttingProductCatalogItem['variants'][number],
|
||||||
|
String(nextQty),
|
||||||
|
String(nextWarehouse),
|
||||||
|
String(nextReject),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui jumlah item.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.cutting_result = String(nextQty);
|
||||||
|
item.warehouse_stock = String(nextWarehouse);
|
||||||
|
syncResultTotals(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addResult(
|
async function addResult(
|
||||||
product: CuttingProductCatalogItem,
|
product: CuttingProductCatalogItem,
|
||||||
variant: CuttingProductCatalogItem['variants'][number],
|
variant: CuttingProductCatalogItem['variants'][number],
|
||||||
) {
|
) {
|
||||||
@ -318,13 +472,23 @@ function addResult(
|
|||||||
(item) => item.product_variant_id === variant.id,
|
(item) => item.product_variant_id === variant.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nextQty = existing ? (Number(existing.cutting_result) || 0) + 1 : 1;
|
||||||
|
const nextWarehouse = existing ? (Number(existing.warehouse_stock) || 0) + 1 : 1;
|
||||||
|
const nextReject = Math.max(nextQty - nextWarehouse, 0);
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
try {
|
||||||
|
await syncDraftResult(product, variant, String(nextQty), String(nextWarehouse), String(nextReject));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal menyimpan item ke keranjang.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.cutting_result = String(
|
existing.cutting_result = String(nextQty);
|
||||||
(Number(existing.cutting_result) || 0) + 1,
|
existing.warehouse_stock = String(nextWarehouse);
|
||||||
);
|
|
||||||
existing.warehouse_stock = String(
|
|
||||||
(Number(existing.warehouse_stock) || 0) + 1,
|
|
||||||
);
|
|
||||||
syncResultTotals(existing);
|
syncResultTotals(existing);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -342,7 +506,22 @@ function addResult(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeResult(index: number) {
|
async function removeResult(index: number) {
|
||||||
|
const item = resultCart.value[index];
|
||||||
|
|
||||||
|
if (isCreateMode.value) {
|
||||||
|
try {
|
||||||
|
await apiFetch(
|
||||||
|
`/admin/manage/cuttings/draft-results/${item.product_variant_id}`,
|
||||||
|
{ method: 'DELETE' },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal menghapus item dari keranjang.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resultCart.value.splice(index, 1);
|
resultCart.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,71 +533,90 @@ function syncResultTotals(item: CuttingResultCartItem) {
|
|||||||
item.cutting_reject = String(reject);
|
item.cutting_reject = String(reject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncMaterialField(index: number) {
|
||||||
|
if (!isCreateMode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = materialCart.value[index];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiFetch<{ item: CuttingMaterialCartItem }>('/admin/manage/cuttings/draft-materials', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
raw_material_price_id: item.raw_material_price_id,
|
||||||
|
material_usage: item.material_usage,
|
||||||
|
remaining_material: item.remaining_material,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui item.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncResultField(index: number) {
|
||||||
|
if (!isCreateMode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = resultCart.value[index];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiFetch<{ item: CuttingResultCartItem }>('/admin/manage/cuttings/draft-results', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
product_variant_id: item.product_variant_id,
|
||||||
|
cutting_result: item.cutting_result,
|
||||||
|
warehouse_stock: item.warehouse_stock,
|
||||||
|
cutting_reject: item.cutting_reject,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui item.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
return {
|
const payload: Record<string, unknown> = {
|
||||||
description: form.description,
|
description: form.description,
|
||||||
sewing_cost: Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
|
sewing_cost: Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
|
||||||
other_cost: Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
|
other_cost: Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
|
||||||
materials: materialCart.value.map((item) => ({
|
};
|
||||||
|
|
||||||
|
if (!isCreateMode.value) {
|
||||||
|
payload.materials = materialCart.value.map((item) => ({
|
||||||
raw_material_price_id: item.raw_material_price_id,
|
raw_material_price_id: item.raw_material_price_id,
|
||||||
material_usage: item.material_usage,
|
material_usage: item.material_usage,
|
||||||
remaining_material: item.remaining_material,
|
remaining_material: item.remaining_material,
|
||||||
})),
|
}));
|
||||||
results: resultCart.value.map((item) => ({
|
payload.results = resultCart.value.map((item) => ({
|
||||||
product_variant_id: item.product_variant_id,
|
product_variant_id: item.product_variant_id,
|
||||||
cutting_result: item.cutting_result,
|
cutting_result: item.cutting_result,
|
||||||
warehouse_stock: item.warehouse_stock,
|
warehouse_stock: item.warehouse_stock,
|
||||||
cutting_reject: item.cutting_reject,
|
cutting_reject: item.cutting_reject,
|
||||||
})),
|
}));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateBeforeSave(): boolean {
|
function submit() {
|
||||||
if (materialCart.value.length === 0) {
|
if (materialCart.value.length === 0) {
|
||||||
toast.error('Tambahkan minimal satu bahan baku.');
|
toast.error('Tambahkan minimal satu bahan baku.');
|
||||||
|
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultCart.value.length === 0) {
|
if (resultCart.value.length === 0) {
|
||||||
toast.error('Tambahkan minimal satu hasil produk.');
|
toast.error('Tambahkan minimal satu hasil produk.');
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const item of resultCart.value) {
|
|
||||||
const total = Number(item.cutting_result) || 0;
|
|
||||||
const warehouse = Number(item.warehouse_stock) || 0;
|
|
||||||
const reject = Number(item.cutting_reject) || 0;
|
|
||||||
|
|
||||||
if (warehouse + reject !== total) {
|
|
||||||
toast.error(
|
|
||||||
`Hasil cutting ${item.product_name} - ${item.variant_name} harus sama dengan stok bagus + reject.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit() {
|
|
||||||
if (!validateBeforeSave()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
costModalOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmSubmit() {
|
|
||||||
const payload = buildPayload();
|
const payload = buildPayload();
|
||||||
|
|
||||||
if (props.method === 'put') {
|
if (props.method === 'put') {
|
||||||
form.transform(() => payload).put(props.submitUrl, {
|
form.transform(() => payload).put(props.submitUrl, {
|
||||||
onSuccess: () => {
|
|
||||||
costModalOpen.value = false;
|
|
||||||
},
|
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
const message = Object.values(errors)[0];
|
const message = Object.values(errors)[0];
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -433,9 +631,6 @@ function confirmSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.transform(() => payload).post(props.submitUrl, {
|
form.transform(() => payload).post(props.submitUrl, {
|
||||||
onSuccess: () => {
|
|
||||||
costModalOpen.value = false;
|
|
||||||
},
|
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
const message = Object.values(errors)[0];
|
const message = Object.values(errors)[0];
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -817,6 +1012,11 @@ function confirmSubmit() {
|
|||||||
item.material_usage
|
item.material_usage
|
||||||
"
|
"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
|
@change="
|
||||||
|
syncMaterialField(
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
@ -828,6 +1028,11 @@ function confirmSubmit() {
|
|||||||
item.remaining_material
|
item.remaining_material
|
||||||
"
|
"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
|
@change="
|
||||||
|
syncMaterialField(
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
@ -910,7 +1115,8 @@ function confirmSubmit() {
|
|||||||
"
|
"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
@change="
|
@change="
|
||||||
syncResultTotals(item)
|
syncResultTotals(item);
|
||||||
|
syncResultField(index)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
@ -924,7 +1130,8 @@ function confirmSubmit() {
|
|||||||
"
|
"
|
||||||
class="h-8"
|
class="h-8"
|
||||||
@change="
|
@change="
|
||||||
syncResultTotals(item)
|
syncResultTotals(item);
|
||||||
|
syncResultField(index)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@ -137,6 +137,37 @@ function getMaterialUnit(materials: any[]): string | undefined {
|
|||||||
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation;
|
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CM_PER_YARD = 91.44;
|
||||||
|
const CM_PER_METER = 100;
|
||||||
|
|
||||||
|
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
|
||||||
|
if (!totalUsage || !materials.length) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
|
||||||
|
|
||||||
|
if (unit === 'yard') {
|
||||||
|
return (totalUsage * CM_PER_YARD).toFixed(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === 'meter') {
|
||||||
|
return (totalUsage * CM_PER_METER).toFixed(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalUsage.toFixed(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTotalMaterialUsageUnit(materials: any[]): string {
|
||||||
|
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
|
||||||
|
|
||||||
|
if (unit === 'yard' || unit === 'meter') {
|
||||||
|
return 'cm';
|
||||||
|
}
|
||||||
|
|
||||||
|
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -173,9 +204,9 @@ function getMaterialUnit(materials: any[]): string | undefined {
|
|||||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||||
<span>Total Hasil Cutting <strong class="text-primary">{{ cutting.total_result_pieces ??
|
<span>Total Hasil Cutting <strong class="text-primary">{{ cutting.total_result_pieces ??
|
||||||
0 }} pcs</strong></span>
|
0 }} pcs</strong></span>
|
||||||
<span>Total Pemakaian Bahan <strong class="text-primary">{{ cutting.total_material_usage
|
<span>Total Pemakaian Bahan <strong class="text-primary">{{
|
||||||
??
|
formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }}
|
||||||
0 }} {{ getMaterialUnit(cutting.materials) }}</strong></span>
|
{{ getTotalMaterialUsageUnit(cutting.materials) }}</strong></span>
|
||||||
<span>Total Biaya Produksi <strong class="text-primary">{{
|
<span>Total Biaya Produksi <strong class="text-primary">{{
|
||||||
cutting.total_production_cost_formatted ?? 0 }}</strong></span>
|
cutting.total_production_cost_formatted ?? 0 }}</strong></span>
|
||||||
<span>Harga Modal <strong class="text-primary">{{
|
<span>Harga Modal <strong class="text-primary">{{
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
use App\Http\Controllers\Admin\Hr\EmployeeController;
|
||||||
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
use App\Http\Controllers\Admin\Hr\LeaveRequestController;
|
||||||
use App\Http\Controllers\Admin\Manage\CuttingController;
|
use App\Http\Controllers\Admin\Manage\CuttingController;
|
||||||
|
use App\Http\Controllers\Admin\Manage\CuttingDraftItemController;
|
||||||
use App\Http\Controllers\Admin\Manage\OrderController;
|
use App\Http\Controllers\Admin\Manage\OrderController;
|
||||||
use App\Http\Controllers\Admin\Manage\OrderDraftItemController;
|
use App\Http\Controllers\Admin\Manage\OrderDraftItemController;
|
||||||
use App\Http\Controllers\Admin\Manage\PurchaseController;
|
use App\Http\Controllers\Admin\Manage\PurchaseController;
|
||||||
@ -258,6 +259,22 @@
|
|||||||
->middleware('permission:'.Permission::CUTTINGS_VIEW->value.'|'.Permission::CUTTINGS_CREATE->value)
|
->middleware('permission:'.Permission::CUTTINGS_VIEW->value.'|'.Permission::CUTTINGS_CREATE->value)
|
||||||
->name('create');
|
->name('create');
|
||||||
|
|
||||||
|
Route::post('draft-materials', [CuttingDraftItemController::class, 'storeMaterial'])
|
||||||
|
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
|
||||||
|
->name('draft-materials.store');
|
||||||
|
|
||||||
|
Route::delete('draft-materials/{rawMaterialPrice}', [CuttingDraftItemController::class, 'destroyMaterial'])
|
||||||
|
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
|
||||||
|
->name('draft-materials.destroy');
|
||||||
|
|
||||||
|
Route::post('draft-results', [CuttingDraftItemController::class, 'storeResult'])
|
||||||
|
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
|
||||||
|
->name('draft-results.store');
|
||||||
|
|
||||||
|
Route::delete('draft-results/{productVariant}', [CuttingDraftItemController::class, 'destroyResult'])
|
||||||
|
->middleware('permission:'.Permission::CUTTINGS_CREATE->value)
|
||||||
|
->name('draft-results.destroy');
|
||||||
|
|
||||||
Route::post('/', [CuttingController::class, 'store'])
|
Route::post('/', [CuttingController::class, 'store'])
|
||||||
->middleware('permission:'.Permission::CUTTINGS_VIEW->value.'|'.Permission::CUTTINGS_CREATE->value)
|
->middleware('permission:'.Permission::CUTTINGS_VIEW->value.'|'.Permission::CUTTINGS_CREATE->value)
|
||||||
->name('store');
|
->name('store');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user