refactor: remove owner verification index functionality and related components, streamline approval and rejection processes
This commit is contained in:
parent
2b51f42a7a
commit
1f27acc63a
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Admin\Manage;
|
namespace App\Http\Controllers\Admin\Manage;
|
||||||
|
|
||||||
use App\Enums\OwnerVerificationAction;
|
|
||||||
use App\Enums\OwnerVerificationStatus;
|
|
||||||
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
use App\Http\Controllers\Concerns\FlashesEntityMessage;
|
||||||
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Manage\ApproveVerificationRequest;
|
use App\Http\Requests\Admin\Manage\ApproveVerificationRequest;
|
||||||
use App\Http\Requests\Admin\Manage\RejectVerificationRequest;
|
use App\Http\Requests\Admin\Manage\RejectVerificationRequest;
|
||||||
@ -14,45 +11,16 @@
|
|||||||
use App\Services\Manage\OwnerVerificationService;
|
use App\Services\Manage\OwnerVerificationService;
|
||||||
use App\Services\Manage\StockService;
|
use App\Services\Manage\StockService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
use Inertia\Response;
|
|
||||||
|
|
||||||
class OwnerVerificationController extends Controller
|
class OwnerVerificationController extends Controller
|
||||||
{
|
{
|
||||||
use FlashesEntityMessage, ParsesDataTableQuery;
|
use FlashesEntityMessage;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OwnerVerificationService $ownerVerificationService,
|
private readonly OwnerVerificationService $ownerVerificationService,
|
||||||
private readonly StockService $stockService,
|
private readonly StockService $stockService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
|
||||||
{
|
|
||||||
$tableQuery = $this->parseDataTableQuery($request);
|
|
||||||
$status = $request->string('status')->toString();
|
|
||||||
$subjectType = $request->string('subject_type')->toString();
|
|
||||||
$action = $request->string('action')->toString();
|
|
||||||
|
|
||||||
return Inertia::render('admin/manage/owner-verifications/Index', [
|
|
||||||
'verificationRequests' => $this->ownerVerificationService->paginateForIndex(
|
|
||||||
$request->user(),
|
|
||||||
$tableQuery,
|
|
||||||
$status,
|
|
||||||
$subjectType,
|
|
||||||
$action,
|
|
||||||
),
|
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
|
||||||
'status' => $status,
|
|
||||||
'subject_type' => $subjectType,
|
|
||||||
'action' => $action,
|
|
||||||
]),
|
|
||||||
'statusOptions' => OwnerVerificationStatus::selectOptions(),
|
|
||||||
'subjectOptions' => $this->ownerVerificationService->subjectTypeOptions($request->user()),
|
|
||||||
'actionOptions' => OwnerVerificationAction::selectOptions(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function approveCutting(ApproveVerificationRequest $request, Cutting $cutting): RedirectResponse
|
public function approveCutting(ApproveVerificationRequest $request, Cutting $cutting): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->stockService->approveVerification(
|
$this->stockService->approveVerification(
|
||||||
@ -63,9 +31,7 @@ public function approveCutting(ApproveVerificationRequest $request, Cutting $cut
|
|||||||
|
|
||||||
$this->flashSuccess('Verifikasi cutting berhasil disetujui. Stok produk telah ditambahkan ke toko.');
|
$this->flashSuccess('Verifikasi cutting berhasil disetujui. Stok produk telah ditambahkan ke toko.');
|
||||||
|
|
||||||
$fallback = route('admin.manage.owner_verifications.index');
|
return back(302);
|
||||||
|
|
||||||
return back(302, [], $fallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rejectCutting(RejectVerificationRequest $request, Cutting $cutting): RedirectResponse
|
public function rejectCutting(RejectVerificationRequest $request, Cutting $cutting): RedirectResponse
|
||||||
@ -78,9 +44,7 @@ public function rejectCutting(RejectVerificationRequest $request, Cutting $cutti
|
|||||||
|
|
||||||
$this->flashSuccess('Verifikasi cutting berhasil ditolak.');
|
$this->flashSuccess('Verifikasi cutting berhasil ditolak.');
|
||||||
|
|
||||||
$fallback = route('admin.manage.owner_verifications.index');
|
return back(302);
|
||||||
|
|
||||||
return back(302, [], $fallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function approveRequest(
|
public function approveRequest(
|
||||||
@ -95,9 +59,7 @@ public function approveRequest(
|
|||||||
|
|
||||||
$this->flashSuccess('Verifikasi berhasil disetujui.');
|
$this->flashSuccess('Verifikasi berhasil disetujui.');
|
||||||
|
|
||||||
$fallback = route('admin.manage.owner_verifications.index');
|
return back(302);
|
||||||
|
|
||||||
return back(302, [], $fallback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function rejectRequest(
|
public function rejectRequest(
|
||||||
@ -112,8 +74,6 @@ public function rejectRequest(
|
|||||||
|
|
||||||
$this->flashSuccess('Verifikasi berhasil ditolak.');
|
$this->flashSuccess('Verifikasi berhasil ditolak.');
|
||||||
|
|
||||||
$fallback = route('admin.manage.owner_verifications.index');
|
return back(302);
|
||||||
|
|
||||||
return back(302, [], $fallback);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
use App\Models\Cutting;
|
use App\Models\Cutting;
|
||||||
use App\Models\EmployeeAdvance;
|
use App\Models\EmployeeAdvance;
|
||||||
use App\Models\LeaveRequest;
|
use App\Models\LeaveRequest;
|
||||||
use App\Services\Manage\OwnerVerificationService;
|
|
||||||
use App\Services\System\Setting\SystemService;
|
use App\Services\System\Setting\SystemService;
|
||||||
use App\Settings\SystemSettings;
|
use App\Settings\SystemSettings;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -72,7 +71,6 @@ public function share(Request $request): array
|
|||||||
'pendingLeaveRequests' => fn () => $this->pendingLeaveRequests($request),
|
'pendingLeaveRequests' => fn () => $this->pendingLeaveRequests($request),
|
||||||
'pendingEmployeeAdvances' => fn () => $this->pendingEmployeeAdvances($request),
|
'pendingEmployeeAdvances' => fn () => $this->pendingEmployeeAdvances($request),
|
||||||
'pendingCuttings' => fn () => $this->pendingCuttings($request),
|
'pendingCuttings' => fn () => $this->pendingCuttings($request),
|
||||||
'pendingOwnerVerifications' => fn () => $this->pendingOwnerVerifications($request),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,15 +112,4 @@ private function pendingCuttings(Request $request): int
|
|||||||
->completed()
|
->completed()
|
||||||
->count();
|
->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function pendingOwnerVerifications(Request $request): int
|
|
||||||
{
|
|
||||||
$user = $request->user();
|
|
||||||
|
|
||||||
if ($user === null || ! $user->can('owner_verifications.view')) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return app(OwnerVerificationService::class)->pendingCountForUser($user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,19 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin\Manage;
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
use App\Enums\Permission;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class ApproveVerificationRequest extends FormRequest
|
class ApproveVerificationRequest extends FormRequest
|
||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return $this->user()?->can(Permission::OWNER_VERIFICATIONS_VERIFY->value) ?? false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -2,19 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin\Manage;
|
namespace App\Http\Requests\Admin\Manage;
|
||||||
|
|
||||||
use App\Enums\Permission;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class RejectVerificationRequest extends FormRequest
|
class RejectVerificationRequest extends FormRequest
|
||||||
{
|
{
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return $this->user()?->can(Permission::OWNER_VERIFICATIONS_REJECT->value) ?? false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -265,7 +265,7 @@ public function rejectRequest(
|
|||||||
'❌ Pengajuan Ditolak',
|
'❌ Pengajuan Ditolak',
|
||||||
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
|
"Pengajuan {$request->action->label()} {$subjectLabel} '{$title}' ditolak dengan alasan: '{$reason}'.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.dashboard'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->notifyRequestSubmitter(
|
$this->notifyRequestSubmitter(
|
||||||
@ -527,7 +527,7 @@ private function notifyRequestSubmitter(
|
|||||||
RawMaterial::class => route('admin.master.raw_materials.index'),
|
RawMaterial::class => route('admin.master.raw_materials.index'),
|
||||||
Purchase::class => route('admin.manage.purchases.index'),
|
Purchase::class => route('admin.manage.purchases.index'),
|
||||||
MarketplaceSettings::class => route('admin.system.settings.index'),
|
MarketplaceSettings::class => route('admin.system.settings.index'),
|
||||||
default => route('admin.manage.owner_verifications.index'),
|
default => route('admin.dashboard'),
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->pushNotificationService->sendToUser(
|
$this->pushNotificationService->sendToUser(
|
||||||
|
|||||||
@ -544,7 +544,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
|
|||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.manage.purchases.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToUser(
|
$this->pushNotificationService->sendToUser(
|
||||||
|
|||||||
@ -134,7 +134,7 @@ public function submitVerification(
|
|||||||
'📦 Verifikasi Stok Menunggu Persetujuan',
|
'📦 Verifikasi Stok Menunggu Persetujuan',
|
||||||
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
|
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ public function approveVerification(
|
|||||||
'📦 Stok Cutting Diverifikasi',
|
'📦 Stok Cutting Diverifikasi',
|
||||||
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
|
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ public function rejectVerification(
|
|||||||
'📦 Verifikasi Cutting Ditolak Owner',
|
'📦 Verifikasi Cutting Ditolak Owner',
|
||||||
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
|
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($cutting->submitted_by_id) {
|
if ($cutting->submitted_by_id) {
|
||||||
@ -240,7 +240,7 @@ public function rejectVerification(
|
|||||||
'📦 Verifikasi Cutting Ditolak Owner',
|
'📦 Verifikasi Cutting Ditolak Owner',
|
||||||
"Cutting dengan deskripsi '{$description}' yang Anda verifikasi ditolak oleh owner dengan alasan: '{$reason}'.",
|
"Cutting dengan deskripsi '{$description}' yang Anda verifikasi ditolak oleh owner dengan alasan: '{$reason}'.",
|
||||||
$cutting->submitted_by_id,
|
$cutting->submitted_by_id,
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.manage.stocks.index'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -432,7 +432,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
|
|||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.master.products.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToUser(
|
$this->pushNotificationService->sendToUser(
|
||||||
|
|||||||
@ -307,7 +307,7 @@ private function notifyForPendingRequest(User $user, string $typeLabel, string $
|
|||||||
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
||||||
$body,
|
$body,
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.master.raw_materials.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToUser(
|
$this->pushNotificationService->sendToUser(
|
||||||
|
|||||||
@ -125,7 +125,7 @@ public function updateMarketplace(array $validated, User $user): void
|
|||||||
'⚙️ Pengaturan Marketplace Menunggu Persetujuan Owner',
|
'⚙️ Pengaturan Marketplace Menunggu Persetujuan Owner',
|
||||||
"Pengajuan ubah pengaturan marketplace oleh '{$user->username}' menunggu verifikasi owner.",
|
"Pengajuan ubah pengaturan marketplace oleh '{$user->username}' menunggu verifikasi owner.",
|
||||||
['owner', 'developer'],
|
['owner', 'developer'],
|
||||||
route('admin.manage.owner_verifications.index'),
|
route('admin.system.settings.index'),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->pushNotificationService->sendToUser(
|
$this->pushNotificationService->sendToUser(
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link, usePage } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
import { Banknote, CalendarDays, CheckCircle, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, Shield, ShoppingBag, ShoppingCart, TrendingUp, User, UserCheck, Users, Wallet, WalletCards, Warehouse } from '@lucide/vue';
|
import { Banknote, CalendarDays, Clock, FolderTree, History, Layers, LayoutDashboard, Package, Receipt, Scissors, Settings2, Shield, ShoppingBag, ShoppingCart, TrendingUp, User, UserCheck, Users, Wallet, WalletCards, Warehouse } from '@lucide/vue';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@ -25,7 +25,7 @@ interface MenuItem {
|
|||||||
href: string;
|
href: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
permission?: string;
|
permission?: string;
|
||||||
badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings' | 'pendingOwnerVerifications';
|
badgeKey?: 'pendingLeaveRequests' | 'pendingEmployeeAdvances' | 'pendingCuttings';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuGroup {
|
interface MenuGroup {
|
||||||
@ -57,7 +57,6 @@ const menuGroups: MenuGroup[] = [
|
|||||||
{ title: 'Belanja', href: admin.manage.purchases.index.url(), icon: ShoppingBag, permission: 'purchases.view' },
|
{ title: 'Belanja', href: admin.manage.purchases.index.url(), icon: ShoppingBag, permission: 'purchases.view' },
|
||||||
{ title: 'Cutting', href: admin.manage.cuttings.index.url(), icon: Scissors, permission: 'cuttings.view' },
|
{ title: 'Cutting', href: admin.manage.cuttings.index.url(), icon: Scissors, permission: 'cuttings.view' },
|
||||||
{ title: 'Stok Gudang', href: admin.manage.stocks.index.url(), icon: Warehouse, permission: 'stocks.view', badgeKey: 'pendingCuttings' },
|
{ title: 'Stok Gudang', href: admin.manage.stocks.index.url(), icon: Warehouse, permission: 'stocks.view', badgeKey: 'pendingCuttings' },
|
||||||
{ title: 'Verifikasi Owner', href: admin.manage.owner_verifications.index.url(), icon: CheckCircle, permission: 'owner_verifications.view', badgeKey: 'pendingOwnerVerifications' },
|
|
||||||
{ title: 'Pesanan', href: admin.manage.orders.index.url(), icon: ShoppingCart, permission: 'orders.view' },
|
{ title: 'Pesanan', href: admin.manage.orders.index.url(), icon: ShoppingCart, permission: 'orders.view' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Link, usePage } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
import { CalendarDays, CheckCircle, Clock, ShoppingCart } from '@lucide/vue';
|
import { CalendarDays, Clock, ShoppingCart } from '@lucide/vue';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import admin from '@/routes/admin';
|
import admin from '@/routes/admin';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ interface NavItem {
|
|||||||
href: string;
|
href: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
permission?: string;
|
permission?: string;
|
||||||
badgeKey?: 'pendingLeaveRequests' | 'pendingOwnerVerifications' | 'pendingCuttings';
|
badgeKey?: 'pendingLeaveRequests' | 'pendingCuttings';
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
@ -22,13 +22,6 @@ const navItems: NavItem[] = [
|
|||||||
icon: ShoppingCart,
|
icon: ShoppingCart,
|
||||||
permission: 'orders.view',
|
permission: 'orders.view',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Verifikasi',
|
|
||||||
href: admin.manage.owner_verifications.index.url(),
|
|
||||||
icon: CheckCircle,
|
|
||||||
permission: 'owner_verifications.view',
|
|
||||||
badgeKey: 'pendingOwnerVerifications',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Presensi',
|
title: 'Presensi',
|
||||||
href: admin.hr.attendances.index.url(),
|
href: admin.hr.attendances.index.url(),
|
||||||
|
|||||||
@ -1,143 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Head } from '@inertiajs/vue3';
|
|
||||||
import { computed, ref, watch } from 'vue';
|
|
||||||
import { DataTable } from '@/components/data-table';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
|
||||||
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
|
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
||||||
import type {
|
|
||||||
OwnerVerificationItem,
|
|
||||||
PaginatedOwnerVerificationRequests,
|
|
||||||
SelectOption,
|
|
||||||
} from '@/types/owner-verification';
|
|
||||||
import type { DataTableSort } from '@/types/data-table';
|
|
||||||
import OwnerVerificationDetailModal from './table/OwnerVerificationDetailModal.vue';
|
|
||||||
import { createColumns } from './table/columns';
|
|
||||||
import { index } from '@/routes/admin/manage/owner_verifications';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
verificationRequests: PaginatedOwnerVerificationRequests;
|
|
||||||
filters: {
|
|
||||||
search: string;
|
|
||||||
sort?: string;
|
|
||||||
direction?: 'asc' | 'desc';
|
|
||||||
status?: string;
|
|
||||||
subject_type?: string;
|
|
||||||
action?: string;
|
|
||||||
};
|
|
||||||
statusOptions: SelectOption[];
|
|
||||||
subjectOptions: SelectOption[];
|
|
||||||
actionOptions: SelectOption[];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const search = ref(props.filters.search ?? '');
|
|
||||||
const detailModalOpen = ref(false);
|
|
||||||
const selectedItem = ref<OwnerVerificationItem | null>(null);
|
|
||||||
|
|
||||||
const { query, setSearch, setSort, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
|
||||||
url: index.url(),
|
|
||||||
initial: { ...props.filters },
|
|
||||||
filterKeys: ['status', 'subject_type', 'action'],
|
|
||||||
});
|
|
||||||
|
|
||||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
|
||||||
|
|
||||||
const columns = computed(() => createColumns(openDetailModal));
|
|
||||||
|
|
||||||
const filterDefs = computed(() => [
|
|
||||||
{
|
|
||||||
key: 'status',
|
|
||||||
label: 'Status',
|
|
||||||
type: 'select' as const,
|
|
||||||
options: props.statusOptions,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'subject_type',
|
|
||||||
label: 'Modul',
|
|
||||||
type: 'select' as const,
|
|
||||||
options: props.subjectOptions,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'action',
|
|
||||||
label: 'Aksi',
|
|
||||||
type: 'select' as const,
|
|
||||||
options: props.actionOptions,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const filterValues = computed(() => ({
|
|
||||||
status: query.value.status ?? '',
|
|
||||||
subject_type: query.value.subject_type ?? '',
|
|
||||||
action: query.value.action ?? '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const currentSort = computed<DataTableSort | null>(() => {
|
|
||||||
if (!query.value.sort || !query.value.direction) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
column: query.value.sort,
|
|
||||||
direction: query.value.direction,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const pagination = computed(() => ({
|
|
||||||
currentPage: props.verificationRequests.current_page,
|
|
||||||
perPage: props.verificationRequests.per_page,
|
|
||||||
lastPage: props.verificationRequests.last_page,
|
|
||||||
total: props.verificationRequests.total,
|
|
||||||
}));
|
|
||||||
|
|
||||||
function openDetailModal(item: OwnerVerificationItem) {
|
|
||||||
selectedItem.value = item;
|
|
||||||
detailModalOpen.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(search, (value) => {
|
|
||||||
setSearch(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.filters.search,
|
|
||||||
(value) => {
|
|
||||||
search.value = value ?? '';
|
|
||||||
},
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Head title="Verifikasi Owner" />
|
|
||||||
|
|
||||||
<AdminLayout>
|
|
||||||
<div class="space-y-1">
|
|
||||||
<h2 class="text-2xl font-bold tracking-tight">
|
|
||||||
Verifikasi Owner
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card class="min-w-0">
|
|
||||||
<CardContent class="min-w-0">
|
|
||||||
<DataTable
|
|
||||||
v-model:search="search"
|
|
||||||
:columns="columns"
|
|
||||||
:data="verificationRequests.data"
|
|
||||||
:pagination="pagination"
|
|
||||||
:pagination-links="verificationRequests.links"
|
|
||||||
:sort="currentSort"
|
|
||||||
:filter-defs="filterDefs"
|
|
||||||
:filter-values="filterValues"
|
|
||||||
search-placeholder="Cari verifikasi..."
|
|
||||||
@sort-change="setSort"
|
|
||||||
@filter-change="setFilter"
|
|
||||||
@filters-reset="resetFilters"
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<OwnerVerificationDetailModal
|
|
||||||
v-model:open="detailModalOpen"
|
|
||||||
:item="selectedItem"
|
|
||||||
/>
|
|
||||||
</AdminLayout>
|
|
||||||
</template>
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import type { OwnerVerificationItem } from '@/types/owner-verification';
|
|
||||||
|
|
||||||
const open = defineModel<boolean>('open', { default: false });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
item?: OwnerVerificationItem | null;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const hasChanges = computed(() => (props.item?.changes.length ?? 0) > 0);
|
|
||||||
|
|
||||||
function formatValue(value: unknown): string {
|
|
||||||
if (value === null || value === undefined || value === '') {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return value ? 'Aktif' : 'Nonaktif';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Dialog v-model:open="open">
|
|
||||||
<DialogContent class="max-h-[85vh] overflow-y-auto sm:max-w-lg">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Detail Verifikasi</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div v-if="item" class="space-y-4 text-sm">
|
|
||||||
<div class="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<p class="text-muted-foreground">Waktu</p>
|
|
||||||
<p class="font-medium">{{ item.created_at_formatted ?? '-' }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-muted-foreground">Diajukan Oleh</p>
|
|
||||||
<p class="font-medium">{{ item.submitted_by_name }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-muted-foreground">Aksi</p>
|
|
||||||
<p class="font-medium">{{ item.action_label }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-muted-foreground">Modul</p>
|
|
||||||
<p class="font-medium">{{ item.subject_label }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-muted-foreground">Item</p>
|
|
||||||
<p class="font-medium">{{ item.title }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-muted-foreground">Status</p>
|
|
||||||
<Badge
|
|
||||||
:variant="item.status === 'approved' ? 'default' : item.status === 'rejected' ? 'destructive' : 'outline'"
|
|
||||||
>
|
|
||||||
{{ item.status_label }}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.verified_by_name">
|
|
||||||
<p class="text-muted-foreground">Diverifikasi Oleh</p>
|
|
||||||
<p class="font-medium">{{ item.verified_by_name }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="item.verified_at_formatted">
|
|
||||||
<p class="text-muted-foreground">Waktu Verifikasi</p>
|
|
||||||
<p class="font-medium">{{ item.verified_at_formatted }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="item.summary" class="rounded-md border p-3">
|
|
||||||
<p class="text-muted-foreground">Ringkasan</p>
|
|
||||||
<p class="font-medium">{{ item.summary }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="item.rejection_reason" class="rounded-md border border-destructive/30 bg-destructive/5 p-3">
|
|
||||||
<p class="text-muted-foreground">Alasan Penolakan</p>
|
|
||||||
<p class="font-medium">{{ item.rejection_reason }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="hasChanges" class="space-y-2">
|
|
||||||
<p class="font-medium">Perubahan Data</p>
|
|
||||||
<div class="overflow-hidden rounded-md border">
|
|
||||||
<table class="w-full text-sm">
|
|
||||||
<thead class="bg-muted/50">
|
|
||||||
<tr>
|
|
||||||
<th class="px-3 py-2 text-left font-medium">Field</th>
|
|
||||||
<th class="px-3 py-2 text-left font-medium">Sebelum</th>
|
|
||||||
<th class="px-3 py-2 text-left font-medium">Sesudah</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="change in item.changes" :key="change.field" class="border-t">
|
|
||||||
<td class="px-3 py-2 align-top font-medium">{{ change.field }}</td>
|
|
||||||
<td class="px-3 py-2 align-top text-muted-foreground">
|
|
||||||
{{ formatValue(change.old) }}
|
|
||||||
</td>
|
|
||||||
<td class="px-3 py-2 align-top">{{ formatValue(change.new) }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import type { ColumnDef } from '@tanstack/vue-table';
|
|
||||||
import { h } from 'vue';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { DataTableColumnHeader } from '@/components/data-table';
|
|
||||||
import type { OwnerVerificationItem } from '@/types/owner-verification';
|
|
||||||
import DataTableActions from './data-table-actions.vue';
|
|
||||||
|
|
||||||
function statusBadgeVariant(status: string): 'default' | 'destructive' | 'outline' | 'secondary' {
|
|
||||||
if (status === 'approved') {
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'rejected') {
|
|
||||||
return 'destructive';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'outline';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createColumns(
|
|
||||||
onView: (item: OwnerVerificationItem) => void,
|
|
||||||
): ColumnDef<OwnerVerificationItem>[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
accessorKey: 'created_at_formatted',
|
|
||||||
enableSorting: true,
|
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Waktu', column: 'created_at' }),
|
|
||||||
cell: ({ row }) => row.original.created_at_formatted ?? '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'submitted_by_name',
|
|
||||||
enableSorting: false,
|
|
||||||
header: 'Diajukan Oleh',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'action_label',
|
|
||||||
enableSorting: true,
|
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Aksi', column: 'action' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'subject_label',
|
|
||||||
enableSorting: false,
|
|
||||||
header: 'Modul',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'title',
|
|
||||||
enableSorting: false,
|
|
||||||
header: 'Item',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: 'status_label',
|
|
||||||
enableSorting: true,
|
|
||||||
header: () => h(DataTableColumnHeader, { title: 'Status', column: 'status' }),
|
|
||||||
cell: ({ row }) => h(
|
|
||||||
Badge,
|
|
||||||
{ variant: statusBadgeVariant(row.original.status) },
|
|
||||||
() => row.original.status_label,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
cell: ({ row }) => h(DataTableActions, {
|
|
||||||
item: row.original,
|
|
||||||
onView: () => onView(row.original),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { RowDetailAction } from '@/components/button';
|
|
||||||
import OwnerVerificationRowActions from '@/components/owner-verification/OwnerVerificationRowActions.vue';
|
|
||||||
import type { OwnerVerificationItem } from '@/types/owner-verification';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
item: OwnerVerificationItem;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
view: [];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex items-center justify-end gap-1">
|
|
||||||
<RowDetailAction tooltip="Lihat" @click="emit('view')" />
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
export type OwnerVerificationSource = 'cutting' | 'request';
|
|
||||||
|
|
||||||
export interface VerificationChange {
|
|
||||||
field: string;
|
|
||||||
old: unknown;
|
|
||||||
new: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OwnerVerificationItem {
|
|
||||||
id: number;
|
|
||||||
source: OwnerVerificationSource;
|
|
||||||
subject_type: string | null;
|
|
||||||
subject_label: string;
|
|
||||||
subject_id: number | null;
|
|
||||||
action: string;
|
|
||||||
action_label: string;
|
|
||||||
status: string;
|
|
||||||
status_label: string;
|
|
||||||
title: string;
|
|
||||||
summary?: string;
|
|
||||||
submitted_by_name: string;
|
|
||||||
verified_by_name?: string | null;
|
|
||||||
verified_at_formatted?: string | null;
|
|
||||||
created_at?: string | null;
|
|
||||||
created_at_formatted: string | null;
|
|
||||||
changes: VerificationChange[];
|
|
||||||
rejection_reason?: string | null;
|
|
||||||
is_pending: boolean;
|
|
||||||
detail?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Use OwnerVerificationItem */
|
|
||||||
export type OwnerVerificationRequestItem = OwnerVerificationItem;
|
|
||||||
|
|
||||||
export interface SelectOption {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginatedOwnerVerificationRequests {
|
|
||||||
data: OwnerVerificationItem[];
|
|
||||||
current_page: number;
|
|
||||||
per_page: number;
|
|
||||||
last_page: number;
|
|
||||||
total: number;
|
|
||||||
links: Array<{
|
|
||||||
url: string | null;
|
|
||||||
label: string;
|
|
||||||
active: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
@ -356,10 +356,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
Route::prefix('owner-verifications')->name('owner_verifications.')
|
Route::prefix('owner-verifications')->name('owner_verifications.')
|
||||||
->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VIEW->value)
|
|
||||||
->group(function () {
|
->group(function () {
|
||||||
Route::get('/', [OwnerVerificationController::class, 'index'])->name('index');
|
|
||||||
|
|
||||||
Route::post('cuttings/{cutting}/approve', [OwnerVerificationController::class, 'approveCutting'])
|
Route::post('cuttings/{cutting}/approve', [OwnerVerificationController::class, 'approveCutting'])
|
||||||
->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VERIFY->value)
|
->middleware('permission:'.Permission::OWNER_VERIFICATIONS_VERIFY->value)
|
||||||
->name('approve');
|
->name('approve');
|
||||||
|
|||||||
@ -1,354 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Enums\CuttingStatus;
|
|
||||||
use App\Enums\OwnerVerificationAction;
|
|
||||||
use App\Enums\OwnerVerificationStatus;
|
|
||||||
use App\Enums\Permission as PermissionEnum;
|
|
||||||
use App\Enums\Role as RoleEnum;
|
|
||||||
use App\Models\Cutting;
|
|
||||||
use App\Models\OwnerVerificationRequest;
|
|
||||||
use App\Models\Product;
|
|
||||||
use App\Models\Purchase;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Support\ActivityLog\ModelLabel;
|
|
||||||
use Database\Seeders\RolePermissionSeeder;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
$this->seed(RolePermissionSeeder::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
function createOwnerVerificationRequestFor(User $submitter, ?Product $product = null): OwnerVerificationRequest
|
|
||||||
{
|
|
||||||
$product ??= Product::factory()->create(['is_active' => false]);
|
|
||||||
|
|
||||||
return OwnerVerificationRequest::query()->create([
|
|
||||||
'action' => OwnerVerificationAction::CREATE,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
|
||||||
'subject_type' => Product::class,
|
|
||||||
'subject_id' => $product->id,
|
|
||||||
'submitted_by_id' => $submitter->id,
|
|
||||||
'payload' => ['old' => null, 'new' => ['name' => $product->name]],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOwnerVerifierUser(): User
|
|
||||||
{
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$user->assignRole(RoleEnum::OWNER->value);
|
|
||||||
$user->forgetCachedPermissions();
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPendingCutting(?User $submitter = null): Cutting
|
|
||||||
{
|
|
||||||
$submitter ??= User::factory()->create();
|
|
||||||
|
|
||||||
return Cutting::factory()->create([
|
|
||||||
'status' => CuttingStatus::PENDING_VERIFICATION,
|
|
||||||
'submitted_by_id' => $submitter->id,
|
|
||||||
'created_by_id' => $submitter->id,
|
|
||||||
'description' => 'Cutting test pending',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Owner Verification Index', function () {
|
|
||||||
test('submitter with view permission only sees own verification requests', function () {
|
|
||||||
$submitter = User::factory()->create();
|
|
||||||
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
|
|
||||||
$submitter->forgetCachedPermissions();
|
|
||||||
|
|
||||||
$otherUser = User::factory()->create();
|
|
||||||
$otherUser->assignRole(RoleEnum::ADMIN_TOKO->value);
|
|
||||||
$otherUser->forgetCachedPermissions();
|
|
||||||
|
|
||||||
$ownRequest = createOwnerVerificationRequestFor($submitter);
|
|
||||||
createOwnerVerificationRequestFor($otherUser);
|
|
||||||
|
|
||||||
$response = $this->actingAs($submitter)
|
|
||||||
->get(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->component('admin/manage/owner-verifications/Index')
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.id', $ownRequest->id)
|
|
||||||
->where('verificationRequests.data.0.source', 'request')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('verifier sees all verification requests', function () {
|
|
||||||
$submitter = User::factory()->create();
|
|
||||||
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
|
|
||||||
$submitter->forgetCachedPermissions();
|
|
||||||
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
|
|
||||||
createOwnerVerificationRequestFor($submitter);
|
|
||||||
createOwnerVerificationRequestFor($submitter);
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->component('admin/manage/owner-verifications/Index')
|
|
||||||
->has('verificationRequests.data', 2)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('submitter cannot approve verification request', function () {
|
|
||||||
$submitter = User::factory()->create();
|
|
||||||
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
|
|
||||||
$submitter->forgetCachedPermissions();
|
|
||||||
|
|
||||||
$request = createOwnerVerificationRequestFor($submitter);
|
|
||||||
|
|
||||||
$this->actingAs($submitter)
|
|
||||||
->post(route('admin.manage.owner_verifications.approve_request', $request))
|
|
||||||
->assertForbidden();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('admin toko role has owner verification view permission', function () {
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$user->assignRole(RoleEnum::ADMIN_TOKO->value);
|
|
||||||
$user->forgetCachedPermissions();
|
|
||||||
|
|
||||||
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VIEW->value))->toBeTrue();
|
|
||||||
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value))->toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('admin bahan baku role has owner verification view permission', function () {
|
|
||||||
$user = User::factory()->create();
|
|
||||||
$user->assignRole(RoleEnum::ADMIN_BAHAN_BAKU->value);
|
|
||||||
$user->forgetCachedPermissions();
|
|
||||||
|
|
||||||
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VIEW->value))->toBeTrue();
|
|
||||||
expect($user->can(PermissionEnum::OWNER_VERIFICATIONS_VERIFY->value))->toBeFalse();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Owner Verification Unified Cutting List', function () {
|
|
||||||
test('verifier sees pending cuttings in the same verification table', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
$cutting = createPendingCutting();
|
|
||||||
$request = createOwnerVerificationRequestFor(User::factory()->create());
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->component('admin/manage/owner-verifications/Index')
|
|
||||||
->has('verificationRequests.data', 2)
|
|
||||||
->where('verificationRequests.data.0.source', 'cutting')
|
|
||||||
->where('verificationRequests.data.0.id', $cutting->id)
|
|
||||||
->where('verificationRequests.data.0.action', OwnerVerificationAction::STOCK_VERIFY->value)
|
|
||||||
->where('verificationRequests.data.0.subject_type', Cutting::class)
|
|
||||||
->where('verificationRequests.data.1.source', 'request')
|
|
||||||
->where('verificationRequests.data.1.id', $request->id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('submitter does not see pending cuttings in verification table', function () {
|
|
||||||
$submitter = User::factory()->create();
|
|
||||||
$submitter->assignRole(RoleEnum::ADMIN_TOKO->value);
|
|
||||||
$submitter->forgetCachedPermissions();
|
|
||||||
|
|
||||||
createPendingCutting();
|
|
||||||
createOwnerVerificationRequestFor($submitter);
|
|
||||||
|
|
||||||
$response = $this->actingAs($submitter)
|
|
||||||
->get(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->component('admin/manage/owner-verifications/Index')
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.source', 'request')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('filtering by cutting module shows only pending cuttings', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
$cutting = createPendingCutting();
|
|
||||||
createOwnerVerificationRequestFor(User::factory()->create());
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index', [
|
|
||||||
'subject_type' => Cutting::class,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.source', 'cutting')
|
|
||||||
->where('verificationRequests.data.0.id', $cutting->id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('filtering by stock verify action shows only pending cuttings', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
$cutting = createPendingCutting();
|
|
||||||
createOwnerVerificationRequestFor(User::factory()->create());
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index', [
|
|
||||||
'action' => OwnerVerificationAction::STOCK_VERIFY->value,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.source', 'cutting')
|
|
||||||
->where('verificationRequests.data.0.id', $cutting->id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('filtering by approved status hides pending cuttings', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
createPendingCutting();
|
|
||||||
|
|
||||||
OwnerVerificationRequest::query()->create([
|
|
||||||
'action' => OwnerVerificationAction::CREATE,
|
|
||||||
'status' => OwnerVerificationStatus::APPROVED,
|
|
||||||
'subject_type' => Product::class,
|
|
||||||
'subject_id' => Product::factory()->create()->id,
|
|
||||||
'submitted_by_id' => User::factory()->create()->id,
|
|
||||||
'payload' => ['old' => null, 'new' => ['name' => 'Approved Product']],
|
|
||||||
'verified_by_id' => $verifier->id,
|
|
||||||
'verified_at' => now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index', [
|
|
||||||
'status' => OwnerVerificationStatus::APPROVED->value,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.source', 'request')
|
|
||||||
->where('verificationRequests.data.0.status', OwnerVerificationStatus::APPROVED->value)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('subject options include cutting when pending cuttings exist', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
createPendingCutting();
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->where('subjectOptions', fn ($options) => collect($options)->contains(
|
|
||||||
fn (array $option) => $option['value'] === Cutting::class
|
|
||||||
&& $option['label'] === ModelLabel::for(Cutting::class),
|
|
||||||
))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('verifier can approve pending cutting from verification page', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
$cutting = createPendingCutting();
|
|
||||||
|
|
||||||
$this->actingAs($verifier)
|
|
||||||
->post(route('admin.manage.owner_verifications.approve', $cutting))
|
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
expect($cutting->fresh()->status)->toBe(CuttingStatus::VERIFIED);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('verifier can reject pending cutting from verification page', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
$cutting = createPendingCutting();
|
|
||||||
|
|
||||||
$this->actingAs($verifier)
|
|
||||||
->post(route('admin.manage.owner_verifications.reject', $cutting), [
|
|
||||||
'reason' => 'Data tidak sesuai',
|
|
||||||
])
|
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
|
||||||
|
|
||||||
$cutting->refresh();
|
|
||||||
|
|
||||||
expect($cutting->status)->toBe(CuttingStatus::COMPLETED);
|
|
||||||
expect($cutting->rejection?->reason)->toBe('Data tidak sesuai');
|
|
||||||
expect($cutting->rejection?->rejected_by_id)->toBe($verifier->id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Owner Verification Purchase Module', function () {
|
|
||||||
test('verifier sees purchase verification requests in index', function () {
|
|
||||||
$verifier = createOwnerVerifierUser();
|
|
||||||
$submitter = User::factory()->create();
|
|
||||||
$purchase = Purchase::factory()->create();
|
|
||||||
|
|
||||||
OwnerVerificationRequest::query()->create([
|
|
||||||
'action' => OwnerVerificationAction::CREATE,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
'subject_id' => $purchase->id,
|
|
||||||
'submitted_by_id' => $submitter->id,
|
|
||||||
'payload' => [
|
|
||||||
'old' => null,
|
|
||||||
'new' => ['supplier_name' => $purchase->supplier?->name ?? 'Supplier'],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->actingAs($verifier)
|
|
||||||
->get(route('admin.manage.owner_verifications.index', [
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.source', 'request')
|
|
||||||
->where('verificationRequests.data.0.subject_type', Purchase::class)
|
|
||||||
->where('verificationRequests.data.0.subject_id', $purchase->id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('purchase submitter sees only own purchase verification requests', function () {
|
|
||||||
$submitter = User::factory()->create();
|
|
||||||
$submitter->assignRole(RoleEnum::ADMIN_BAHAN_BAKU->value);
|
|
||||||
$submitter->forgetCachedPermissions();
|
|
||||||
|
|
||||||
$otherUser = User::factory()->create();
|
|
||||||
$ownPurchase = Purchase::factory()->create(['created_by_id' => $submitter->id]);
|
|
||||||
$otherPurchase = Purchase::factory()->create(['created_by_id' => $otherUser->id]);
|
|
||||||
|
|
||||||
OwnerVerificationRequest::query()->create([
|
|
||||||
'action' => OwnerVerificationAction::CREATE,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
'subject_id' => $ownPurchase->id,
|
|
||||||
'submitted_by_id' => $submitter->id,
|
|
||||||
'payload' => ['old' => null, 'new' => ['supplier_name' => 'Own']],
|
|
||||||
]);
|
|
||||||
|
|
||||||
OwnerVerificationRequest::query()->create([
|
|
||||||
'action' => OwnerVerificationAction::CREATE,
|
|
||||||
'status' => OwnerVerificationStatus::PENDING,
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
'subject_id' => $otherPurchase->id,
|
|
||||||
'submitted_by_id' => $otherUser->id,
|
|
||||||
'payload' => ['old' => null, 'new' => ['supplier_name' => 'Other']],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->actingAs($submitter)
|
|
||||||
->get(route('admin.manage.owner_verifications.index', [
|
|
||||||
'subject_type' => Purchase::class,
|
|
||||||
]));
|
|
||||||
|
|
||||||
$response->assertOk();
|
|
||||||
$response->assertInertia(fn ($page) => $page
|
|
||||||
->has('verificationRequests.data', 1)
|
|
||||||
->where('verificationRequests.data.0.subject_id', $ownPurchase->id)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -56,7 +56,7 @@ function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerifica
|
|||||||
|
|
||||||
test()->actingAs($verifier)
|
test()->actingAs($verifier)
|
||||||
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
->assertRedirect();
|
||||||
|
|
||||||
return $verificationRequest->fresh();
|
return $verificationRequest->fresh();
|
||||||
}
|
}
|
||||||
@ -263,7 +263,7 @@ function approveLatestPurchaseVerificationRequest(User $verifier): OwnerVerifica
|
|||||||
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
||||||
'reason' => 'Tidak sesuai',
|
'reason' => 'Tidak sesuai',
|
||||||
])
|
])
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
->assertRedirect();
|
||||||
|
|
||||||
expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse();
|
expect(Purchase::query()->whereKey($purchase->id)->exists())->toBeFalse();
|
||||||
expect(PurchaseItem::query()->where('purchase_id', $purchase->id)->exists())->toBeFalse();
|
expect(PurchaseItem::query()->where('purchase_id', $purchase->id)->exists())->toBeFalse();
|
||||||
|
|||||||
@ -76,7 +76,7 @@ function approveLatestOwnerVerificationRequest(User $verifier): OwnerVerificatio
|
|||||||
|
|
||||||
test()->actingAs($verifier)
|
test()->actingAs($verifier)
|
||||||
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
->assertRedirect();
|
||||||
|
|
||||||
return $verificationRequest->fresh();
|
return $verificationRequest->fresh();
|
||||||
}
|
}
|
||||||
@ -431,7 +431,7 @@ function variantWithImage(string $name = 'All Size', int $stock = 10): array
|
|||||||
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
->post(route('admin.manage.owner_verifications.reject_request', $verificationRequest), [
|
||||||
'reason' => 'Tidak sesuai standar',
|
'reason' => 'Tidak sesuai standar',
|
||||||
])
|
])
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
->assertRedirect();
|
||||||
|
|
||||||
$this->assertSoftDeleted('products', ['id' => $product->id]);
|
$this->assertSoftDeleted('products', ['id' => $product->id]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -84,7 +84,7 @@ function approveLatestOwnerVerificationRequest(User $verifier): OwnerVerificatio
|
|||||||
|
|
||||||
test()->actingAs($verifier)
|
test()->actingAs($verifier)
|
||||||
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
->post(route('admin.manage.owner_verifications.approve_request', $verificationRequest))
|
||||||
->assertRedirect(route('admin.manage.owner_verifications.index'));
|
->assertRedirect();
|
||||||
|
|
||||||
return $verificationRequest->fresh();
|
return $verificationRequest->fresh();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user