feat: implement owner verification actions in various management tables and enhance request handling in the OwnerVerificationRowActions component
This commit is contained in:
parent
d5ca41a764
commit
5cffe43a07
@ -63,7 +63,9 @@ public function approveCutting(ApproveVerificationRequest $request, Cutting $cut
|
||||
|
||||
$this->flashSuccess('Verifikasi cutting berhasil disetujui. Stok produk telah ditambahkan ke toko.');
|
||||
|
||||
return redirect()->route('admin.manage.owner_verifications.index');
|
||||
$fallback = route('admin.manage.owner_verifications.index');
|
||||
|
||||
return back(302, [], $fallback);
|
||||
}
|
||||
|
||||
public function rejectCutting(RejectVerificationRequest $request, Cutting $cutting): RedirectResponse
|
||||
@ -76,7 +78,9 @@ public function rejectCutting(RejectVerificationRequest $request, Cutting $cutti
|
||||
|
||||
$this->flashSuccess('Verifikasi cutting berhasil ditolak.');
|
||||
|
||||
return redirect()->route('admin.manage.owner_verifications.index');
|
||||
$fallback = route('admin.manage.owner_verifications.index');
|
||||
|
||||
return back(302, [], $fallback);
|
||||
}
|
||||
|
||||
public function approveRequest(
|
||||
@ -91,7 +95,9 @@ public function approveRequest(
|
||||
|
||||
$this->flashSuccess('Verifikasi berhasil disetujui.');
|
||||
|
||||
return redirect()->route('admin.manage.owner_verifications.index');
|
||||
$fallback = route('admin.manage.owner_verifications.index');
|
||||
|
||||
return back(302, [], $fallback);
|
||||
}
|
||||
|
||||
public function rejectRequest(
|
||||
@ -106,6 +112,8 @@ public function rejectRequest(
|
||||
|
||||
$this->flashSuccess('Verifikasi berhasil ditolak.');
|
||||
|
||||
return redirect()->route('admin.manage.owner_verifications.index');
|
||||
$fallback = route('admin.manage.owner_verifications.index');
|
||||
|
||||
return back(302, [], $fallback);
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
||||
$pendingRequest = $purchase->pendingOwnerVerificationRequest;
|
||||
|
||||
$purchase->setAttribute('has_pending_request', $pendingRequest !== null);
|
||||
$purchase->setAttribute('pending_request_id', $pendingRequest?->id);
|
||||
$purchase->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||
$purchase->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||
|
||||
|
||||
@ -77,6 +77,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
||||
$pendingRequest = $product->pendingOwnerVerificationRequest;
|
||||
|
||||
$product->setAttribute('has_pending_request', $pendingRequest !== null);
|
||||
$product->setAttribute('pending_request_id', $pendingRequest?->id);
|
||||
$product->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||
$product->setAttribute(
|
||||
|
||||
@ -85,6 +85,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
|
||||
$pendingRequest = $rawMaterial->pendingOwnerVerificationRequest;
|
||||
|
||||
$rawMaterial->setAttribute('has_pending_request', $pendingRequest !== null);
|
||||
$rawMaterial->setAttribute('pending_request_id', $pendingRequest?->id);
|
||||
$rawMaterial->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||
$rawMaterial->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||
$rawMaterial->setAttribute(
|
||||
|
||||
@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { X } from '@lucide/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { RowApproveAction, RowRejectAction } from '@/components/button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
} from '@/components/ui/field';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { approve, reject, approve_request, reject_request } from '@/routes/admin/manage/owner_verifications';
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'request' | 'cutting';
|
||||
id: number;
|
||||
title?: string;
|
||||
subjectLabel?: string;
|
||||
actionLabel?: string;
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const rejectDialogOpen = ref(false);
|
||||
|
||||
const approveForm = useForm({});
|
||||
const rejectForm = useForm({
|
||||
reason: '',
|
||||
});
|
||||
|
||||
const approveUrl = computed(() => (
|
||||
props.type === 'cutting'
|
||||
? approve.url(props.id)
|
||||
: approve_request.url(props.id)
|
||||
));
|
||||
|
||||
const rejectUrl = computed(() => (
|
||||
props.type === 'cutting'
|
||||
? reject.url(props.id)
|
||||
: reject_request.url(props.id)
|
||||
));
|
||||
|
||||
const canApprove = computed(() => can('owner_verifications.verify'));
|
||||
const canReject = computed(() => can('owner_verifications.reject'));
|
||||
|
||||
function submitApprove() {
|
||||
approveForm
|
||||
.post(approveUrl.value, {
|
||||
preserveScroll: true,
|
||||
onError: (errors: Record<string, string>) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function submitReject() {
|
||||
rejectForm
|
||||
.post(rejectUrl.value, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
rejectDialogOpen.value = false;
|
||||
rejectForm.reset();
|
||||
},
|
||||
onError: (errors: Record<string, string>) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="canApprove">
|
||||
<RowApproveAction size="sm" tooltip="Setujui" :disabled="approveForm.processing" @click="submitApprove" />
|
||||
</template>
|
||||
|
||||
<template v-if="canReject">
|
||||
<RowRejectAction size="sm" tooltip="Tolak" :disabled="rejectForm.processing" @click="rejectDialogOpen = true" />
|
||||
</template>
|
||||
|
||||
<Dialog v-model:open="rejectDialogOpen">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tolak Verifikasi</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-if="subjectLabel || title || actionLabel" class="rounded-md border p-3 text-xs space-y-2">
|
||||
<div v-if="subjectLabel">
|
||||
<span class="text-muted-foreground">Modul:</span>
|
||||
<span class="ml-1 font-medium">{{ subjectLabel }}</span>
|
||||
</div>
|
||||
<div v-if="title">
|
||||
<span class="text-muted-foreground">Item:</span>
|
||||
<span class="ml-1 font-medium">{{ title }}</span>
|
||||
</div>
|
||||
<div v-if="actionLabel">
|
||||
<span class="text-muted-foreground">Aksi:</span>
|
||||
<span class="ml-1 font-medium">{{ actionLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="owner-verification-reject-reason">Alasan Penolakan</FieldLabel>
|
||||
<Textarea id="owner-verification-reject-reason" v-model="rejectForm.reason"
|
||||
placeholder="Jelaskan alasan penolakan..." rows="3" />
|
||||
<FieldError :errors="rejectForm.errors.reason ? [rejectForm.errors.reason] : []" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="rejectDialogOpen = false">
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="button" variant="destructive" :disabled="rejectForm.processing" @click="submitReject">
|
||||
<X class="size-4" />
|
||||
Tolak
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@ -1,6 +1,7 @@
|
||||
export const CuttingStatus = {
|
||||
IN_PROGRESS: 'in_progress',
|
||||
COMPLETED: 'completed',
|
||||
PENDING_VERIFICATION: 'pending_verification',
|
||||
VERIFIED: 'verified',
|
||||
REJECTED: 'rejected',
|
||||
} as const;
|
||||
|
||||
@ -6,6 +6,7 @@ import { toast } from 'vue-sonner';
|
||||
import { RowDeleteAction, RowEditAction, RowStatusAction } from '@/components/button';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -286,6 +287,10 @@ function actionIcon(status: string) {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center justify-end gap-1">
|
||||
<OwnerVerificationRowActions v-if="cutting.status === CuttingStatus.PENDING_VERIFICATION" type="cutting"
|
||||
:id="cutting.id" :title="cutting.description ?? `Cutting #${cutting.id}`" subject-label="Cutting"
|
||||
action-label="Verifikasi Stok" />
|
||||
|
||||
<template v-for="action in availableActions" :key="action.status">
|
||||
<RowStatusAction v-if="canPerformAction(action)" :size="'sm'" :icon="actionIcon(action.status)"
|
||||
:label="action.label" :destructive="action.destructive" @click="openStatusConfirm(action)" />
|
||||
|
||||
@ -1,25 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { X } from '@lucide/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { RowApproveAction, RowDetailAction, RowRejectAction } from '@/components/button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
FieldLabel,
|
||||
} from '@/components/ui/field';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import { approve, reject, approve_request, reject_request } from '@/routes/admin/manage/owner_verifications';
|
||||
import { RowDetailAction } from '@/components/button';
|
||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||
import type { OwnerVerificationItem } from '@/types/owner-verification';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -29,125 +10,13 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
view: [];
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const rejectDialogOpen = ref(false);
|
||||
|
||||
const approveForm = useForm({});
|
||||
const rejectForm = useForm({
|
||||
reason: '',
|
||||
});
|
||||
|
||||
const approveUrl = computed(() => (
|
||||
props.item.source === 'cutting'
|
||||
? approve.url(props.item.id)
|
||||
: approve_request.url(props.item.id)
|
||||
));
|
||||
|
||||
const rejectUrl = computed(() => (
|
||||
props.item.source === 'cutting'
|
||||
? reject.url(props.item.id)
|
||||
: reject_request.url(props.item.id)
|
||||
));
|
||||
|
||||
function submitApprove() {
|
||||
approveForm
|
||||
.post(approveUrl.value, {
|
||||
preserveScroll: true,
|
||||
onError: (errors: Record<string, string>) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function submitReject() {
|
||||
rejectForm
|
||||
.post(rejectUrl.value, {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
rejectDialogOpen.value = false;
|
||||
rejectForm.reset();
|
||||
},
|
||||
onError: (errors: Record<string, string>) => {
|
||||
if (errors.system) {
|
||||
toast.error(errors.system);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<RowDetailAction tooltip="Lihat" @click="emit('view')" />
|
||||
|
||||
<template v-if="item.is_pending && can('owner_verifications.verify')">
|
||||
<RowApproveAction
|
||||
size="sm"
|
||||
tooltip="Setujui"
|
||||
:disabled="approveForm.processing"
|
||||
@click="submitApprove"
|
||||
/>
|
||||
<RowRejectAction
|
||||
size="sm"
|
||||
tooltip="Tolak"
|
||||
:disabled="rejectForm.processing"
|
||||
@click="rejectDialogOpen = true"
|
||||
/>
|
||||
</template>
|
||||
<OwnerVerificationRowActions v-if="item.is_pending" :type="item.source === 'cutting' ? 'cutting' : 'request'"
|
||||
:id="item.id" :title="item.title" :subject-label="item.subject_label" :action-label="item.action_label" />
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="rejectDialogOpen">
|
||||
<DialogContent class="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tolak Verifikasi</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-md border p-3 text-xs space-y-2">
|
||||
<div>
|
||||
<span class="text-muted-foreground">Modul:</span>
|
||||
<span class="ml-1 font-medium">{{ item.subject_label }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">Item:</span>
|
||||
<span class="ml-1 font-medium">{{ item.title }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">Aksi:</span>
|
||||
<span class="ml-1 font-medium">{{ item.action_label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="reject-reason">Alasan Penolakan</FieldLabel>
|
||||
<Textarea
|
||||
id="reject-reason"
|
||||
v-model="rejectForm.reason"
|
||||
placeholder="Jelaskan alasan penolakan..."
|
||||
rows="3"
|
||||
/>
|
||||
<FieldError :errors="rejectForm.errors.reason ? [rejectForm.errors.reason] : []" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" @click="rejectDialogOpen = false">
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
:disabled="rejectForm.processing"
|
||||
@click="submitReject"
|
||||
>
|
||||
<X class="size-4" />
|
||||
Tolak
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import type { PurchaseListItem } from '@/types/purchase';
|
||||
import { edit, destroy } from '@/routes/admin/manage/purchases';
|
||||
import type { PurchaseListItem } from '@/types/purchase';
|
||||
|
||||
defineProps<{
|
||||
purchase: PurchaseListItem;
|
||||
@ -13,20 +14,18 @@ const { can } = useCan();
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<RowEditAction
|
||||
v-if="can('purchases.update')"
|
||||
:href="edit.url(purchase.id)"
|
||||
<OwnerVerificationRowActions v-if="purchase.has_pending_request && purchase.pending_request_id" type="request"
|
||||
:id="purchase.pending_request_id"
|
||||
:title="`${purchase.total_formatted} — ${purchase.supplier?.name ?? purchase.supplier_name ?? 'Supplier'}`"
|
||||
subject-label="Belanja" :action-label="purchase.pending_request_action_label" />
|
||||
|
||||
<RowEditAction v-if="can('purchases.update')" :href="edit.url(purchase.id)"
|
||||
:disabled="purchase.has_pending_request"
|
||||
:tooltip="purchase.has_pending_request ? 'Menunggu verifikasi owner' : 'Ubah'"
|
||||
/>
|
||||
<RowDeleteAction
|
||||
v-if="can('purchases.delete')"
|
||||
:action-url="destroy.url(purchase.id)"
|
||||
:tooltip="purchase.has_pending_request ? 'Menunggu verifikasi owner' : 'Ubah'" />
|
||||
<RowDeleteAction v-if="can('purchases.delete')" :action-url="destroy.url(purchase.id)"
|
||||
:disabled="purchase.has_pending_request"
|
||||
:tooltip="purchase.has_pending_request ? 'Menunggu verifikasi owner' : 'Hapus'"
|
||||
title="Hapus belanja?"
|
||||
:tooltip="purchase.has_pending_request ? 'Menunggu verifikasi owner' : 'Hapus'" title="Hapus belanja?"
|
||||
:description="`Pengajuan hapus belanja ${purchase.total_formatted} dari ${purchase.supplier?.name ?? purchase.supplier_name} akan dikirim ke owner untuk verifikasi.`"
|
||||
error-message="Gagal mengajukan penghapusan belanja."
|
||||
/>
|
||||
error-message="Gagal mengajukan penghapusan belanja." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import type { ProductListItem } from '@/types/product';
|
||||
import { edit, destroy } from '@/routes/admin/master/products';
|
||||
import type { ProductListItem } from '@/types/product';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
product: ProductListItem;
|
||||
}>();
|
||||
|
||||
@ -13,20 +14,17 @@ const { can } = useCan();
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<RowEditAction
|
||||
v-if="can('products.update')"
|
||||
:href="edit.url(product.id)"
|
||||
<OwnerVerificationRowActions v-if="product.has_pending_request && product.pending_request_id" type="request"
|
||||
:id="product.pending_request_id" :title="product.name" subject-label="Produk"
|
||||
:action-label="product.pending_request_action_label" />
|
||||
|
||||
<RowEditAction v-if="can('products.update')" :href="edit.url(product.id)"
|
||||
:disabled="product.has_pending_request"
|
||||
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Ubah'"
|
||||
/>
|
||||
<RowDeleteAction
|
||||
v-if="can('products.delete')"
|
||||
:action-url="destroy.url(product.id)"
|
||||
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Ubah'" />
|
||||
<RowDeleteAction v-if="can('products.delete')" :action-url="destroy.url(product.id)"
|
||||
:disabled="product.has_pending_request"
|
||||
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Hapus'"
|
||||
title="Hapus produk?"
|
||||
:tooltip="product.has_pending_request ? 'Menunggu verifikasi owner' : 'Hapus'" title="Hapus produk?"
|
||||
:description="`Pengajuan hapus produk ${product.name} akan dikirim ke owner untuk verifikasi.`"
|
||||
error-message="Gagal mengajukan penghapusan produk."
|
||||
/>
|
||||
error-message="Gagal mengajukan penghapusan produk." />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { RowDeleteAction, RowEditAction } from '@/components/button';
|
||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import type { RawMaterialListItem } from '@/types/raw-material';
|
||||
import { edit, destroy } from '@/routes/admin/master/raw_materials';
|
||||
@ -13,6 +14,15 @@ const { can } = useCan();
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<OwnerVerificationRowActions
|
||||
v-if="material.has_pending_request && material.pending_request_id"
|
||||
type="request"
|
||||
:id="material.pending_request_id"
|
||||
:title="material.name"
|
||||
subject-label="Bahan Baku"
|
||||
:action-label="material.pending_request_action_label"
|
||||
/>
|
||||
|
||||
<RowEditAction
|
||||
v-if="can('raw_materials.update')"
|
||||
:href="edit.url(material.id)"
|
||||
|
||||
@ -64,6 +64,7 @@ export interface Product {
|
||||
|
||||
export interface ProductListItem extends Product {
|
||||
has_pending_request?: boolean;
|
||||
pending_request_id?: number;
|
||||
pending_request_action?: string;
|
||||
pending_request_action_label?: string;
|
||||
display_is_active?: boolean;
|
||||
|
||||
@ -39,6 +39,7 @@ export type PurchaseListItem = {
|
||||
photos?: MediaItem | null;
|
||||
items: any[];
|
||||
has_pending_request?: boolean;
|
||||
pending_request_id?: number;
|
||||
pending_request_action?: string;
|
||||
pending_request_action_label?: string;
|
||||
};
|
||||
|
||||
@ -28,6 +28,7 @@ export type RawMaterialListItem = {
|
||||
is_active: boolean;
|
||||
prices: RawMaterialPrice[];
|
||||
has_pending_request?: boolean;
|
||||
pending_request_id?: number;
|
||||
pending_request_action?: string;
|
||||
pending_request_action_label?: string;
|
||||
display_is_active?: boolean;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user