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:
Yoga Pangestu 2026-06-21 10:29:45 +07:00
parent e522b5f922
commit 9c205cf3d0
13 changed files with 844 additions and 151 deletions

View File

@ -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);

View File

@ -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]);
}
}

View File

@ -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'],
];
}
}

View 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'],
];
}
}

View File

@ -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;
} }
/** /**

View File

@ -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);

View File

@ -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);

View File

@ -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'));

View File

@ -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();
});
}
};

View File

@ -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"

View File

@ -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; const nextQty = (Number(item.material_usage) || 0) - 1;
if (nextQty <= 0) { if (nextQty <= 0) {
removeMaterial(index); const index = materialCart.value.findIndex(
} else { (i) => i.raw_material_price_id === priceId,
item.material_usage = String(nextQty); );
}
} if (index !== -1) {
await removeMaterial(index);
} }
function addMaterial( 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);
}
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; const nextQty = (Number(item.cutting_result) || 0) - 1;
if (nextQty <= 0) { if (nextQty <= 0) {
removeResult(index); const index = resultCart.value.findIndex(
} else { (i) => i.product_variant_id === variantId,
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);
} if (index !== -1) {
} await removeResult(index);
} }
function addResult( 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);
}
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);
} }
function buildPayload() { async function syncMaterialField(index: number) {
return { if (!isCreateMode.value) {
description: form.description, return;
sewing_cost: Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0, }
other_cost: Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
materials: materialCart.value.map((item) => ({ 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, 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) => ({ });
} 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, 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,
})), }),
}; });
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Gagal memperbarui item.');
}
} }
function validateBeforeSave(): boolean { function buildPayload() {
const payload: Record<string, unknown> = {
description: form.description,
sewing_cost: Number.parseInt(parseRupiah(form.sewing_cost), 10) || 0,
other_cost: Number.parseInt(parseRupiah(form.other_cost), 10) || 0,
};
if (!isCreateMode.value) {
payload.materials = materialCart.value.map((item) => ({
raw_material_price_id: item.raw_material_price_id,
material_usage: item.material_usage,
remaining_material: item.remaining_material,
}));
payload.results = resultCart.value.map((item) => ({
product_variant_id: item.product_variant_id,
cutting_result: item.cutting_result,
warehouse_stock: item.warehouse_stock,
cutting_reject: item.cutting_reject,
}));
}
return payload;
}
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>

View File

@ -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">{{

View File

@ -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');