From cc3cd03e412a203fe938eea9f46a426b2c73f001 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 15 Aug 2026 22:40:36 +0700 Subject: [PATCH] feat: enhance raw material management by adding validation for updates and deletions, and improve error handling in the UI --- .../RawMaterial/RawMaterialController.php | 17 +++--- .../Master/RawMaterial/RawMaterialService.php | 56 ++++++++++++++++--- .../RawMaterial/RawMaterialVariantService.php | 21 ++++++- .../pages/admin/master/raw-material/edit.tsx | 26 +++++++-- .../pages/admin/master/raw-material/index.tsx | 2 + 5 files changed, 100 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/Admin/Master/RawMaterial/RawMaterialController.php b/app/Http/Controllers/Admin/Master/RawMaterial/RawMaterialController.php index 07cb5d8..086cf59 100644 --- a/app/Http/Controllers/Admin/Master/RawMaterial/RawMaterialController.php +++ b/app/Http/Controllers/Admin/Master/RawMaterial/RawMaterialController.php @@ -9,6 +9,7 @@ use App\Services\Admin\Master\RawMaterial\RawMaterialService; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; +use Illuminate\Validation\ValidationException; use Inertia\Inertia; use Inertia\Response; @@ -55,13 +56,15 @@ public function edit(RawMaterial $rawMaterial): Response public function update(RawMaterialRequest $request, RawMaterial $rawMaterial): RedirectResponse { - return $this->handleAction( - fn () => $this->service->update($rawMaterial, $request->validated()), - 'Bahan baku berhasil diperbarui.', - 'admin.master.raw-materials.index', - 'admin.master.raw-materials.edit', - ['rawMaterial' => $rawMaterial] - ); + try { + $this->service->update($rawMaterial, $request->validated()); + } catch (ValidationException $e) { + return back()->withErrors($e->errors()); + } + + Inertia::flash('toast', ['type' => 'success', 'message' => 'Bahan baku berhasil diperbarui.']); + + return to_route('admin.master.raw-materials.index'); } public function destroy(RawMaterial $rawMaterial): RedirectResponse diff --git a/app/Services/Admin/Master/RawMaterial/RawMaterialService.php b/app/Services/Admin/Master/RawMaterial/RawMaterialService.php index 703afc8..a436b63 100644 --- a/app/Services/Admin/Master/RawMaterial/RawMaterialService.php +++ b/app/Services/Admin/Master/RawMaterial/RawMaterialService.php @@ -11,6 +11,7 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; class RawMaterialService { @@ -108,7 +109,7 @@ public function store(array $data): RawMaterial }); NotificationService::notify( - roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], + roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Bahan Baku Baru', body: "Bahan baku \"{$rawMaterial->name}\" berhasil ditambahkan".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]), @@ -161,12 +162,30 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial ->filter() ->toArray(); - $rawMaterial->rawMaterialPrices() + $variantsToDelete = $rawMaterial->rawMaterialPrices() ->whereNotIn('id', $existingVariantIds) - ->each(function (RawMaterialPrice $price) { - $price->clearMediaCollection('images'); - $price->delete(); - }); + ->get(); + + foreach ($variantsToDelete as $price) { + if ($price->stock > 0) { + throw ValidationException::withMessages([ + 'variants' => "Varian \"{$price->variant}\" tidak dapat dihapus karena masih memiliki stok.", + ]); + } + + $activeCuttingUsage = $price->cuttingMaterials() + ->whereHas('cutting', fn ($q) => $q->inProgress()) + ->exists(); + + if ($activeCuttingUsage) { + throw ValidationException::withMessages([ + 'variants' => "Varian \"{$price->variant}\" tidak dapat dihapus karena masih digunakan di cutting yang sedang diproses.", + ]); + } + + $price->clearMediaCollection('images'); + $price->delete(); + } $existingPricesMap = RawMaterialPrice::whereIn('id', $existingVariantIds) ->with('media') @@ -228,7 +247,7 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial }); NotificationService::notify( - roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], + roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Bahan Baku Diperbarui', body: "Bahan baku \"{$rawMaterial->name}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]), @@ -239,8 +258,27 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial public function destroy(RawMaterial $rawMaterial): bool { + foreach ($rawMaterial->rawMaterialPrices as $price) { + if ($price->stock > 0) { + throw ValidationException::withMessages([ + 'raw_material' => "Bahan baku \"{$rawMaterial->name}\" tidak dapat dihapus karena varian \"{$price->variant}\" masih memiliki stok.", + ]); + } + + $activeCuttingUsage = $price->cuttingMaterials() + ->whereHas('cutting', fn ($q) => $q->inProgress()) + ->exists(); + + if ($activeCuttingUsage) { + throw ValidationException::withMessages([ + 'raw_material' => "Bahan baku \"{$rawMaterial->name}\" tidak dapat dihapus karena varian \"{$price->variant}\" masih digunakan di cutting yang sedang diproses.", + ]); + } + } + $result = DB::transaction(function () use ($rawMaterial) { $rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) { + $price->clearMediaCollection('images'); $price->delete(); }); @@ -248,7 +286,7 @@ public function destroy(RawMaterial $rawMaterial): bool }); NotificationService::notify( - roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], + roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Bahan Baku Dihapus', body: "Bahan baku \"{$rawMaterial->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.raw-materials.index'), @@ -266,7 +304,7 @@ public function toggleStatus(RawMaterial $rawMaterial): void $status = $rawMaterial->is_active ? 'diaktifkan' : 'dinonaktifkan'; NotificationService::notify( - roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], + roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Status Bahan Baku Diubah', body: "Bahan baku \"{$rawMaterial->name}\" berhasil {$status}".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.raw-materials.index', ['highlight' => $rawMaterial->id]), diff --git a/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php b/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php index a653857..015aa1a 100644 --- a/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php +++ b/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php @@ -9,6 +9,7 @@ use App\Services\NotificationService; use App\Services\S3PresignedService; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; class RawMaterialVariantService { @@ -85,7 +86,7 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice }); NotificationService::notify( - roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], + roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Varian Diperbarui', body: "Varian \"{$variant->variant}\" berhasil diperbarui".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.raw-materials.index', ['highlight' => $variant->raw_material_id]), @@ -96,12 +97,28 @@ public function update(RawMaterialPrice $variant, array $data): RawMaterialPrice public function destroy(RawMaterial $rawMaterial, RawMaterialPrice $variant): bool { + if ($variant->stock > 0) { + throw ValidationException::withMessages([ + 'variant' => 'Varian tidak dapat dihapus karena masih memiliki stok.', + ]); + } + + $activeCuttingUsage = $variant->cuttingMaterials() + ->whereHas('cutting', fn ($q) => $q->inProgress()) + ->exists(); + + if ($activeCuttingUsage) { + throw ValidationException::withMessages([ + 'variant' => 'Varian tidak dapat dihapus karena masih digunakan di cutting yang sedang diproses.', + ]); + } + $result = DB::transaction(function () use ($variant) { return $variant->delete(); }); NotificationService::notify( - roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], + roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU], title: 'Varian Dihapus', body: "Varian \"{$variant->variant}\" dari bahan baku \"{$rawMaterial->name}\" berhasil dihapus".' oleh '.auth()->user()->full_name.'.', url: route('admin.master.raw-materials.index'), diff --git a/resources/js/pages/admin/master/raw-material/edit.tsx b/resources/js/pages/admin/master/raw-material/edit.tsx index 1caca19..334692c 100644 --- a/resources/js/pages/admin/master/raw-material/edit.tsx +++ b/resources/js/pages/admin/master/raw-material/edit.tsx @@ -7,7 +7,7 @@ import { Plus, Trash2, } from 'lucide-react'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUpload } from '@/components/inputs'; @@ -134,6 +134,23 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { const [deleteVariantIndex, setDeleteVariantIndex] = useState( null, ); + const [removedVariants, setRemovedVariants] = useState([]); + const removedVariantsRef = useRef(removedVariants); + removedVariantsRef.current = removedVariants; + + const { errors: pageErrors } = usePage<{ errors?: Record }>().props; + + useEffect(() => { + if (pageErrors && Object.keys(pageErrors).length > 0) { + const removed = removedVariantsRef.current; + if (removed.length > 0) { + setVariants((prev) => [...prev, ...removed]); + setRemovedVariants([]); + } + const firstError = Object.values(pageErrors).flat().find(Boolean); + toast.error(firstError ?? 'Ada data yang belum sesuai, silakan periksa kembali input Anda.'); + } + }, [pageErrors]); const confirmRemoveVariant = useCallback((index: number) => { setDeleteVariantIndex(index); @@ -222,9 +239,6 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { ...data, ...getPayload(), })} - onError={() => { - toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); - }} > {({ errors, processing }) => ( <> @@ -555,6 +569,10 @@ export default function RawMaterialEdit({ rawMaterial }: Props) { confirmLabel="Hapus" onConfirm={() => { if (deleteVariantIndex !== null) { + const removed = variants[deleteVariantIndex]; + if (removed.id !== null) { + setRemovedVariants((prev) => [...prev, removed]); + } removeVariant(deleteVariantIndex); } diff --git a/resources/js/pages/admin/master/raw-material/index.tsx b/resources/js/pages/admin/master/raw-material/index.tsx index c04a54c..580f07f 100644 --- a/resources/js/pages/admin/master/raw-material/index.tsx +++ b/resources/js/pages/admin/master/raw-material/index.tsx @@ -141,6 +141,7 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte router.delete(destroy.url(deleting.id), { onSuccess: () => setDeleting(null), + onError: () => setDeleting(null), }); } @@ -156,6 +157,7 @@ export default function RawMaterialIndex({ rawMaterials, rawMaterialNames, filte }), { onSuccess: () => setDeletingVariant(null), + onError: () => setDeletingVariant(null), }, ); }