diff --git a/app/Enums/CuttingStatus.php b/app/Enums/CuttingStatus.php index 23a85bd..770e32f 100644 --- a/app/Enums/CuttingStatus.php +++ b/app/Enums/CuttingStatus.php @@ -33,7 +33,8 @@ public function canTransitionTo(self $status): bool { return match ($this) { self::IN_PROGRESS => $status === self::COMPLETED, - self::COMPLETED => $status === self::VERIFIED, + self::COMPLETED => in_array($status, [self::VERIFIED, self::REJECTED, self::IN_PROGRESS], true), + self::REJECTED => $status === self::IN_PROGRESS, default => false, }; } @@ -72,6 +73,29 @@ public function availableActions(): array 'permission' => Permission::CUTTINGS_VERIFY->value, 'icon_only' => false, ], + [ + 'status' => self::REJECTED->value, + 'label' => 'Tolak', + 'destructive' => true, + 'permission' => Permission::CUTTINGS_REJECT->value, + 'icon_only' => false, + ], + [ + 'status' => self::IN_PROGRESS->value, + 'label' => 'Kembalikan ke Proses', + 'destructive' => true, + 'permission' => Permission::CUTTINGS_REJECT->value, + 'icon_only' => false, + ], + ], + self::REJECTED => [ + [ + 'status' => self::IN_PROGRESS->value, + 'label' => 'Kembalikan ke Proses', + 'destructive' => false, + 'permission' => Permission::CUTTINGS_UPDATE->value, + 'icon_only' => false, + ], ], default => [], }; diff --git a/app/Http/Controllers/Admin/Manage/CuttingController.php b/app/Http/Controllers/Admin/Manage/CuttingController.php index fe745be..3114381 100644 --- a/app/Http/Controllers/Admin/Manage/CuttingController.php +++ b/app/Http/Controllers/Admin/Manage/CuttingController.php @@ -31,6 +31,7 @@ public function index(Request $request): Response return Inertia::render('admin/manage/cuttings/Index', [ 'cuttings' => $this->cuttingService->paginateForIndex($tableQuery, $request->user()), 'inProgressCuttings' => $this->cuttingService->getInProgressCuttings($request->user()), + 'completedCuttings' => $this->cuttingService->getCompletedCuttings($request->user()), 'filters' => $this->dataTableFilters($tableQuery), ]); } diff --git a/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php b/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php index 60eb066..5173137 100644 --- a/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php +++ b/app/Http/Requests/Admin/Manage/CuttingStatusTransitionRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Admin\Manage; use App\Enums\CuttingStatus; +use App\Enums\Permission; use App\Models\Cutting; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -18,6 +19,12 @@ public function authorize(): bool return false; } + if ($status === CuttingStatus::IN_PROGRESS) { + return $this->user()?->can(Permission::CUTTINGS_REJECT->value) + || $this->user()?->can(Permission::CUTTINGS_UPDATE->value) + || false; + } + return $this->user()?->can($status->transitionPermission()->value) ?? false; } diff --git a/app/Services/Manage/CuttingService.php b/app/Services/Manage/CuttingService.php index 0f8d524..eddb800 100644 --- a/app/Services/Manage/CuttingService.php +++ b/app/Services/Manage/CuttingService.php @@ -98,6 +98,33 @@ public function getInProgressCuttings(User $user): Collection }); } + /** + * @return Collection + */ + public function getCompletedCuttings(User $user): Collection + { + return Cutting::query() + ->with([ + 'createdBy.profile', + 'rejection.rejectedBy.profile', + 'materials.rawMaterialPrice.rawMaterial:id,name,unit', + 'results.productVariant.product:id,name', + 'results.productVariant:id,product_id,name', + ]) + ->where('status', CuttingStatus::COMPLETED) + ->latest() + ->get() + ->each(function (Cutting $cutting) use ($user): void { + $actions = collect($cutting->status->availableActions()) + ->filter(fn (array $action) => $user->can($action['permission'])) + ->values() + ->all(); + + $cutting->setAttribute('available_actions', $actions); + $cutting->setAttribute('is_editable', $cutting->status->isEditable()); + }); + } + /** * @return Collection */ @@ -262,6 +289,8 @@ public function update(Cutting $cutting, array $validated): void if ($cutting->status === CuttingStatus::IN_PROGRESS) { $this->reverseTotalMaterialStock($cutting); + } elseif ($cutting->status === CuttingStatus::REJECTED) { + $this->reverseMaterialStock($cutting); } $cutting->materials()->delete(); @@ -271,6 +300,10 @@ public function update(Cutting $cutting, array $validated): void $results = $this->buildResults($validated['results']); $cutting->description = $validated['description'] ?? null; + if ($cutting->status === CuttingStatus::REJECTED) { + $cutting->status = CuttingStatus::IN_PROGRESS; + $cutting->rejection()?->delete(); + } $cutting->save(); foreach ($materials as $materialData) { @@ -285,18 +318,14 @@ public function update(Cutting $cutting, array $validated): void $cutting->load('materials.rawMaterialPrice.rawMaterial'); $this->deductMaterialStock($cutting); } - - if ($cutting->status === CuttingStatus::REJECTED) { - $cutting->rejection()?->delete(); - } }); } public function delete(Cutting $cutting): void { - if ($cutting->status !== CuttingStatus::IN_PROGRESS) { + if (! in_array($cutting->status, [CuttingStatus::IN_PROGRESS, CuttingStatus::REJECTED], true)) { throw ValidationException::withMessages([ - 'status' => 'Proses potong hanya dapat dihapus saat masih proses.', + 'status' => 'Proses potong hanya dapat dihapus saat masih proses atau ditolak.', ]); } @@ -304,7 +333,12 @@ public function delete(Cutting $cutting): void DB::transaction(function () use ($cutting): void { $cutting->load(['materials.rawMaterialPrice.rawMaterial']); - $this->reverseTotalMaterialStock($cutting); + + if ($cutting->status === CuttingStatus::IN_PROGRESS) { + $this->reverseTotalMaterialStock($cutting); + } elseif ($cutting->status === CuttingStatus::REJECTED) { + $this->reverseMaterialStock($cutting); + } $cutting->materials()->delete(); $cutting->results()->delete(); @@ -333,13 +367,22 @@ public function transitionStatus( ]); } - DB::transaction(function () use ($cutting, $status, $verificationNote, $results, $user): void { + DB::transaction(function () use ($cutting, $status, $verificationNote, $results, $user, $reason): void { $cutting->load(['materials', 'results']); if ($status === CuttingStatus::COMPLETED) { $this->applyRemainingMaterialStock($cutting); } + if ($status === CuttingStatus::IN_PROGRESS) { + $this->deductRemainingMaterialStock($cutting); + $cutting->rejection()?->delete(); + } + + if ($status === CuttingStatus::REJECTED) { + $this->storeRejection($cutting, $reason, $user); + } + if ($status === CuttingStatus::VERIFIED) { if ($results !== null) { foreach ($results as $item) { @@ -528,6 +571,17 @@ private function applyRemainingMaterialStock(Cutting $cutting): void } } + private function deductRemainingMaterialStock(Cutting $cutting): void + { + foreach ($cutting->materials as $material) { + if ((float) $material->remaining_material > 0) { + RawMaterialPrice::query() + ->whereKey($material->raw_material_price_id) + ->decrement('stock', $material->remaining_material); + } + } + } + private function reverseMaterialStock(Cutting $cutting): void { foreach ($cutting->materials as $material) { diff --git a/resources/js/components/admin/manage/cuttings/CuttingCompletedSection.vue b/resources/js/components/admin/manage/cuttings/CuttingCompletedSection.vue new file mode 100644 index 0000000..a22f5fc --- /dev/null +++ b/resources/js/components/admin/manage/cuttings/CuttingCompletedSection.vue @@ -0,0 +1,118 @@ + + + diff --git a/resources/js/components/admin/manage/cuttings/data-table-actions.vue b/resources/js/components/admin/manage/cuttings/data-table-actions.vue index ce6b4db..9877f96 100644 --- a/resources/js/components/admin/manage/cuttings/data-table-actions.vue +++ b/resources/js/components/admin/manage/cuttings/data-table-actions.vue @@ -82,7 +82,7 @@ const canEdit = computed( () => props.cutting.is_editable && can('cuttings.update'), ); const canDelete = computed( - () => can('cuttings.delete') && props.cutting.status === 'in_progress', + () => can('cuttings.delete') && ['in_progress', 'rejected'].includes(props.cutting.status), ); function canPerformAction(action: CuttingStatusAction): boolean { diff --git a/resources/js/pages/admin/manage/cuttings/Index.vue b/resources/js/pages/admin/manage/cuttings/Index.vue index 53c97bf..9edba52 100644 --- a/resources/js/pages/admin/manage/cuttings/Index.vue +++ b/resources/js/pages/admin/manage/cuttings/Index.vue @@ -4,6 +4,7 @@ import { Plus } from '@lucide/vue'; import { computed, ref, watch } from 'vue'; import CuttingGroupedTable from '@/components/admin/manage/cuttings/CuttingGroupedTable.vue'; import CuttingInProgressSection from '@/components/admin/manage/cuttings/CuttingInProgressSection.vue'; +import CuttingCompletedSection from '@/components/admin/manage/cuttings/CuttingCompletedSection.vue'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { useCan } from '@/composables/useCan'; @@ -14,6 +15,7 @@ import type { PaginatedCuttings, CuttingListItem } from '@/types/cutting'; const props = defineProps<{ cuttings: PaginatedCuttings; inProgressCuttings: CuttingListItem[]; + completedCuttings: CuttingListItem[]; filters: { search: string; sort?: string; @@ -21,7 +23,7 @@ const props = defineProps<{ }; }>(); -const { can } = useCan(); +const { can, hasAnyRole } = useCan(); const search = ref(props.filters.search ?? ''); const { setSearch, resetFilters, syncFromServer } = useDataTableQuery({ @@ -75,7 +77,15 @@ watch( - + + +