feat: enhance raw material management by adding validation for updates and deletions, and improve error handling in the UI

This commit is contained in:
Yoga Pangestu 2026-08-15 22:40:36 +07:00
parent 6fa9224983
commit cc3cd03e41
5 changed files with 100 additions and 22 deletions

View File

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

View File

@ -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]),

View File

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

View File

@ -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<number | null>(
null,
);
const [removedVariants, setRemovedVariants] = useState<VariantState[]>([]);
const removedVariantsRef = useRef(removedVariants);
removedVariantsRef.current = removedVariants;
const { errors: pageErrors } = usePage<{ errors?: Record<string, string[]> }>().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);
}

View File

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