feat: implement owner verification detail modal with data fetching and display functionality
This commit is contained in:
parent
67ae00384f
commit
86ee45c1fb
@ -10,6 +10,9 @@
|
|||||||
use App\Models\OwnerVerificationRequest;
|
use App\Models\OwnerVerificationRequest;
|
||||||
use App\Services\Manage\OwnerVerificationService;
|
use App\Services\Manage\OwnerVerificationService;
|
||||||
use App\Services\Manage\StockService;
|
use App\Services\Manage\StockService;
|
||||||
|
use App\Support\ActivityLog\ModelLabel;
|
||||||
|
use App\Support\OwnerVerification\VerificationChangeFormatter;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
class OwnerVerificationController extends Controller
|
class OwnerVerificationController extends Controller
|
||||||
@ -21,6 +24,22 @@ public function __construct(
|
|||||||
private readonly StockService $stockService,
|
private readonly StockService $stockService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function show(OwnerVerificationRequest $ownerVerificationRequest): JsonResponse
|
||||||
|
{
|
||||||
|
$payload = is_array($ownerVerificationRequest->payload) ? $ownerVerificationRequest->payload : [];
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $ownerVerificationRequest->id,
|
||||||
|
'action_label' => $ownerVerificationRequest->action->label(),
|
||||||
|
'subject_label' => ModelLabel::for($ownerVerificationRequest->subject_type),
|
||||||
|
'submitted_by_name' => $ownerVerificationRequest->submittedBy?->profile?->full_name
|
||||||
|
?? $ownerVerificationRequest->submittedBy?->username
|
||||||
|
?? '-',
|
||||||
|
'created_at_formatted' => $ownerVerificationRequest->created_at?->translatedFormat('l, d F Y H:i'),
|
||||||
|
'changes' => VerificationChangeFormatter::format($payload),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function approveCutting(ApproveVerificationRequest $request, Cutting $cutting): RedirectResponse
|
public function approveCutting(ApproveVerificationRequest $request, Cutting $cutting): RedirectResponse
|
||||||
{
|
{
|
||||||
$this->stockService->approveVerification(
|
$this->stockService->approveVerification(
|
||||||
|
|||||||
@ -37,7 +37,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
|||||||
->with([
|
->with([
|
||||||
'supplier:id,name',
|
'supplier:id,name',
|
||||||
'createdBy.profile',
|
'createdBy.profile',
|
||||||
'pendingOwnerVerificationRequest',
|
'pendingOwnerVerificationRequest.submittedBy.profile',
|
||||||
'items.rawMaterialPrice.rawMaterial:id,name,unit',
|
'items.rawMaterialPrice.rawMaterial:id,name,unit',
|
||||||
'media',
|
'media',
|
||||||
])
|
])
|
||||||
@ -70,6 +70,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
|
|||||||
$purchase->setAttribute('pending_request_id', $pendingRequest?->id);
|
$purchase->setAttribute('pending_request_id', $pendingRequest?->id);
|
||||||
$purchase->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
$purchase->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||||
$purchase->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
$purchase->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||||
|
$purchase->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
|
||||||
|
|
||||||
$purchase->items->each(fn (PurchaseItem $item) => $this->breakItemCircularReference($item));
|
$purchase->items->each(fn (PurchaseItem $item) => $this->breakItemCircularReference($item));
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
|||||||
$query = Product::query()
|
$query = Product::query()
|
||||||
->with([
|
->with([
|
||||||
'categories',
|
'categories',
|
||||||
'pendingOwnerVerificationRequest',
|
'pendingOwnerVerificationRequest.submittedBy.profile',
|
||||||
'variants' => fn ($query) => $query
|
'variants' => fn ($query) => $query
|
||||||
->with(['media', 'prices' => fn ($query) => $query->orderBy('type')])
|
->with(['media', 'prices' => fn ($query) => $query->orderBy('type')])
|
||||||
->orderBy('created_at'),
|
->orderBy('created_at'),
|
||||||
@ -89,6 +89,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
|
|||||||
$product->setAttribute('pending_request_id', $pendingRequest?->id);
|
$product->setAttribute('pending_request_id', $pendingRequest?->id);
|
||||||
$product->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
$product->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||||
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||||
|
$product->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
|
||||||
$product->setAttribute(
|
$product->setAttribute(
|
||||||
'display_is_active',
|
'display_is_active',
|
||||||
$pendingRequest?->pendingToggleIsActive() ?? $product->is_active,
|
$pendingRequest?->pendingToggleIsActive() ?? $product->is_active,
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
|
|||||||
{
|
{
|
||||||
$query = RawMaterial::query()
|
$query = RawMaterial::query()
|
||||||
->with([
|
->with([
|
||||||
'pendingOwnerVerificationRequest',
|
'pendingOwnerVerificationRequest.submittedBy.profile',
|
||||||
'prices' => fn ($query) => $query
|
'prices' => fn ($query) => $query
|
||||||
->orderBy('created_at')
|
->orderBy('created_at')
|
||||||
->with('media'),
|
->with('media'),
|
||||||
@ -85,6 +85,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
|
|||||||
$rawMaterial->setAttribute('pending_request_id', $pendingRequest?->id);
|
$rawMaterial->setAttribute('pending_request_id', $pendingRequest?->id);
|
||||||
$rawMaterial->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
$rawMaterial->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
||||||
$rawMaterial->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
$rawMaterial->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
||||||
|
$rawMaterial->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
|
||||||
$rawMaterial->setAttribute(
|
$rawMaterial->setAttribute(
|
||||||
'display_is_active',
|
'display_is_active',
|
||||||
$pendingRequest?->pendingToggleIsActive() ?? $rawMaterial->is_active,
|
$pendingRequest?->pendingToggleIsActive() ?? $rawMaterial->is_active,
|
||||||
|
|||||||
@ -144,38 +144,35 @@ private static function presentValue(string $field, mixed $value): mixed
|
|||||||
|
|
||||||
if ($field === 'variants' && is_array($value)) {
|
if ($field === 'variants' && is_array($value)) {
|
||||||
return collect($value)
|
return collect($value)
|
||||||
->map(function (array $variant): string {
|
->map(fn (array $variant) => [
|
||||||
$name = $variant['name'] ?? '-';
|
'name' => $variant['name'] ?? '-',
|
||||||
$stock = $variant['stock'] ?? 0;
|
'stock' => (int) ($variant['stock'] ?? 0),
|
||||||
|
])
|
||||||
return "{$name} (stok: {$stock})";
|
->values()
|
||||||
})
|
->all();
|
||||||
->implode(', ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field === 'prices' && is_array($value)) {
|
if ($field === 'prices' && is_array($value)) {
|
||||||
return collect($value)
|
return collect($value)
|
||||||
->map(function (array $price): string {
|
->map(fn (array $price) => [
|
||||||
$variant = $price['variant'] ?? '-';
|
'variant' => $price['variant'] ?? '-',
|
||||||
$stock = $price['stock'] ?? 0;
|
'stock' => (int) ($price['stock'] ?? 0),
|
||||||
$priceValue = $price['price'] ?? 0;
|
'price' => (int) ($price['price'] ?? 0),
|
||||||
|
])
|
||||||
return "{$variant} (stok: {$stock}, harga: {$priceValue})";
|
->values()
|
||||||
})
|
->all();
|
||||||
->implode(', ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field === 'items' && is_array($value)) {
|
if ($field === 'items' && is_array($value)) {
|
||||||
return collect($value)
|
return collect($value)
|
||||||
->map(function (array $item): string {
|
->map(fn (array $item) => [
|
||||||
$name = $item['raw_material_name'] ?? '-';
|
'raw_material_name' => $item['raw_material_name'] ?? '-',
|
||||||
$variant = $item['variant'] ?? '-';
|
'variant' => $item['variant'] ?? '-',
|
||||||
$quantity = $item['quantity'] ?? 0;
|
'quantity' => (int) ($item['quantity'] ?? 0),
|
||||||
$subtotal = $item['subtotal'] ?? 0;
|
'subtotal' => (int) ($item['subtotal'] ?? 0),
|
||||||
|
])
|
||||||
return "{$name} / {$variant} (jumlah: {$quantity}, subtotal: {$subtotal})";
|
->values()
|
||||||
})
|
->all();
|
||||||
->implode('; ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($field === 'has_photos') {
|
if ($field === 'has_photos') {
|
||||||
|
|||||||
@ -0,0 +1,278 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { show } from '@/routes/admin/manage/owner_verifications';
|
||||||
|
|
||||||
|
interface VariantItem {
|
||||||
|
name: string;
|
||||||
|
stock: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PriceItem {
|
||||||
|
variant: string;
|
||||||
|
stock: number;
|
||||||
|
price: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PurchaseItem {
|
||||||
|
raw_material_name: string;
|
||||||
|
variant: string;
|
||||||
|
quantity: number;
|
||||||
|
subtotal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VerificationChange {
|
||||||
|
field: string;
|
||||||
|
old: unknown;
|
||||||
|
new: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VerificationDetail {
|
||||||
|
id: number;
|
||||||
|
action_label: string;
|
||||||
|
subject_label: string;
|
||||||
|
submitted_by_name: string;
|
||||||
|
created_at_formatted: string;
|
||||||
|
changes: VerificationChange[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = defineModel<boolean>('open', { default: false });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
requestId?: number | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const detail = ref<VerificationDetail | null>(null);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
watch(() => open.value, async (isOpen) => {
|
||||||
|
if (isOpen && props.requestId) {
|
||||||
|
await fetchDetail();
|
||||||
|
} else {
|
||||||
|
detail.value = null;
|
||||||
|
error.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchDetail() {
|
||||||
|
if (!props.requestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(show.url(props.requestId));
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Gagal memuat detail verifikasi');
|
||||||
|
}
|
||||||
|
|
||||||
|
detail.value = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Terjadi kesalahan';
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChanges = computed(() => (detail.value?.changes.length ?? 0) > 0);
|
||||||
|
|
||||||
|
function isVariantArray(value: unknown): value is VariantItem[] {
|
||||||
|
return Array.isArray(value) && value.length > 0 && 'name' in value[0] && 'stock' in value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPriceArray(value: unknown): value is PriceItem[] {
|
||||||
|
return Array.isArray(value) && value.length > 0 && 'variant' in value[0] && 'price' in value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPurchaseItemArray(value: unknown): value is PurchaseItem[] {
|
||||||
|
return Array.isArray(value) && value.length > 0 && 'raw_material_name' in value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSimpleValue(value: unknown): string {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? 'Ya' : 'Tidak';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNumber(value: number): string {
|
||||||
|
return new Intl.NumberFormat('id-ID').format(value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog v-model:open="open">
|
||||||
|
<DialogContent class="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Detail Pengajuan Verifikasi</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<div class="text-muted-foreground text-sm">Memuat detail...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="py-4 text-center">
|
||||||
|
<p class="text-sm text-destructive">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="detail" 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">{{ detail.created_at_formatted ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted-foreground">Diajukan Oleh</p>
|
||||||
|
<p class="font-medium">{{ detail.submitted_by_name }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted-foreground">Aksi</p>
|
||||||
|
<p class="font-medium">{{ detail.action_label }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-muted-foreground">Modul</p>
|
||||||
|
<p class="font-medium">{{ detail.subject_label }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasChanges" class="space-y-4">
|
||||||
|
<p class="font-medium">Perubahan Data</p>
|
||||||
|
|
||||||
|
<template v-for="change in detail.changes" :key="change.field">
|
||||||
|
<!-- Variants Table -->
|
||||||
|
<div v-if="change.field === 'Varian' && (isVariantArray(change.old) || isVariantArray(change.new))" class="space-y-2">
|
||||||
|
<p class="text-sm font-medium text-muted-foreground">{{ change.field }}</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">Nama Varian</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">Stok Sebelum</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">Stok Sesudah</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, idx) in (change.new as VariantItem[] || change.old as VariantItem[])" :key="idx" class="border-t">
|
||||||
|
<td class="px-3 py-2 font-medium">{{ item.name }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums text-muted-foreground">
|
||||||
|
{{ isVariantArray(change.old) && change.old[idx] ? formatNumber(change.old[idx].stock) : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">
|
||||||
|
{{ isVariantArray(change.new) && change.new[idx] ? formatNumber(change.new[idx].stock) : '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prices Table -->
|
||||||
|
<div v-else-if="change.field === 'Varian Harga' && (isPriceArray(change.old) || isPriceArray(change.new))" class="space-y-2">
|
||||||
|
<p class="text-sm font-medium text-muted-foreground">{{ change.field }}</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">Varian</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">Stok</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">Harga</th>
|
||||||
|
<th class="px-3 py-2 text-center font-medium">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-if="isPriceArray(change.new)">
|
||||||
|
<tr v-for="(item, idx) in change.new" :key="idx" class="border-t">
|
||||||
|
<td class="px-3 py-2 font-medium">{{ item.variant }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">{{ formatNumber(item.stock) }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">Rp {{ formatNumber(item.price) }}</td>
|
||||||
|
<td class="px-3 py-2 text-center">
|
||||||
|
<span v-if="!isPriceArray(change.old) || !change.old.find(o => o.variant === item.variant)" class="text-green-600">Baru</span>
|
||||||
|
<span v-else class="text-muted-foreground">Ada</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isPriceArray(change.old)">
|
||||||
|
<tr v-for="(item, idx) in change.old" :key="idx" class="border-t">
|
||||||
|
<td class="px-3 py-2 font-medium">{{ item.variant }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">{{ formatNumber(item.stock) }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">Rp {{ formatNumber(item.price) }}</td>
|
||||||
|
<td class="px-3 py-2 text-center text-destructive">Dihapus</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Purchase Items Table -->
|
||||||
|
<div v-else-if="change.field === 'Item Belanja' && (isPurchaseItemArray(change.old) || isPurchaseItemArray(change.new))" class="space-y-2">
|
||||||
|
<p class="text-sm font-medium text-muted-foreground">{{ change.field }}</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">Bahan Baku</th>
|
||||||
|
<th class="px-3 py-2 text-left font-medium">Varian</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">Jumlah</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">Subtotal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, idx) in (change.new as PurchaseItem[] || change.old as PurchaseItem[])" :key="idx" class="border-t">
|
||||||
|
<td class="px-3 py-2 font-medium">{{ item.raw_material_name }}</td>
|
||||||
|
<td class="px-3 py-2">{{ item.variant }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">{{ formatNumber(item.quantity) }}</td>
|
||||||
|
<td class="px-3 py-2 text-right tabular-nums">Rp {{ formatNumber(item.subtotal) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Simple Field -->
|
||||||
|
<div v-else 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 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">{{ formatSimpleValue(change.old) }}</td>
|
||||||
|
<td class="px-3 py-2 align-top">{{ formatSimpleValue(change.new) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="py-2 text-center text-muted-foreground">
|
||||||
|
Tidak ada perubahan data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
|
import VerificationDetailModal from '@/components/owner-verification/VerificationDetailModal.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
@ -26,6 +26,7 @@ import type {
|
|||||||
DataTablePaginationLink,
|
DataTablePaginationLink,
|
||||||
} from '@/types/data-table';
|
} from '@/types/data-table';
|
||||||
import type { PurchaseItemDetail, PurchaseListItem } from '@/types/purchase';
|
import type { PurchaseItemDetail, PurchaseListItem } from '@/types/purchase';
|
||||||
|
import DataTableActions from './data-table-actions.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
purchases: PurchaseListItem[];
|
purchases: PurchaseListItem[];
|
||||||
@ -76,6 +77,16 @@ function getGroupedItems(items: PurchaseItemDetail[]): GroupedPurchaseItems[] {
|
|||||||
|
|
||||||
return Object.values(groups);
|
return Object.values(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verificationModalOpen = ref(false);
|
||||||
|
const selectedRequestId = ref<number | null>(null);
|
||||||
|
|
||||||
|
function openVerificationDetail(requestId: number | undefined) {
|
||||||
|
if (requestId) {
|
||||||
|
selectedRequestId.value = requestId;
|
||||||
|
verificationModalOpen.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -95,7 +106,8 @@ function getGroupedItems(items: PurchaseItemDetail[]): GroupedPurchaseItems[] {
|
|||||||
{{ purchase.supplier?.name }}
|
{{ purchase.supplier?.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<p v-if="purchase.has_pending_request" class="text-sm text-amber-600">
|
<p v-if="purchase.has_pending_request" class="text-sm text-amber-600">
|
||||||
Sedang menunggu verifikasi owner
|
{{ purchase.pending_request_submitted_by_name }} mengajukan {{ purchase.pending_request_action_label?.toLowerCase() }} belanja ini —
|
||||||
|
<button type="button" class="underline-offset-2 hover:underline" @click="openVerificationDetail(purchase.pending_request_id)">lihat</button>
|
||||||
</p>
|
</p>
|
||||||
<div class="text-muted-foreground space-y-1 text-sm">
|
<div class="text-muted-foreground space-y-1 text-sm">
|
||||||
<p>{{ purchase.created_at_formatted }}</p>
|
<p>{{ purchase.created_at_formatted }}</p>
|
||||||
@ -104,9 +116,9 @@ function getGroupedItems(items: PurchaseItemDetail[]): GroupedPurchaseItems[] {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||||
<span>Subtotal <strong class="text-primary">{{ purchase.subtotal_formatted
|
<span>Subtotal <strong class="text-primary">{{ purchase.subtotal_formatted
|
||||||
}}</strong></span>
|
}}</strong></span>
|
||||||
<span>Diskon <strong class="text-primary">{{ purchase.discount_formatted
|
<span>Diskon <strong class="text-primary">{{ purchase.discount_formatted
|
||||||
}}</strong></span>
|
}}</strong></span>
|
||||||
<span v-if="purchase.shipping_cost > 0">Ongkir <strong class="text-primary">{{
|
<span v-if="purchase.shipping_cost > 0">Ongkir <strong class="text-primary">{{
|
||||||
purchase.shipping_cost_formatted }}</strong></span>
|
purchase.shipping_cost_formatted }}</strong></span>
|
||||||
<span>Total <strong class="text-primary">{{ purchase.total_formatted }}</strong></span>
|
<span>Total <strong class="text-primary">{{ purchase.total_formatted }}</strong></span>
|
||||||
@ -178,10 +190,8 @@ function getGroupedItems(items: PurchaseItemDetail[]): GroupedPurchaseItems[] {
|
|||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupedTableFooter
|
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||||
:summary="paginationSummary"
|
|
||||||
:pagination="pagination"
|
<VerificationDetailModal v-model:open="verificationModalOpen" :request-id="selectedRequestId" />
|
||||||
:pagination-links="paginationLinks"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { DataTableEmpty } from '@/components/data-table';
|
import { DataTableEmpty } from '@/components/data-table';
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
|
import VerificationDetailModal from '@/components/owner-verification/VerificationDetailModal.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -25,7 +26,7 @@ import {
|
|||||||
PRICE_TYPES,
|
PRICE_TYPES,
|
||||||
PRICE_TYPE_LABELS,
|
PRICE_TYPE_LABELS,
|
||||||
} from '@/types/product';
|
} from '@/types/product';
|
||||||
import type { ProductListItem, Variant } from '@/types/product';
|
import type { ProductListItem } from '@/types/product';
|
||||||
import DataTableActions from './data-table-actions.vue';
|
import DataTableActions from './data-table-actions.vue';
|
||||||
import ProductStatusToggle from './product-status-toggle.vue';
|
import ProductStatusToggle from './product-status-toggle.vue';
|
||||||
|
|
||||||
@ -53,6 +54,16 @@ const paginationSummary = usePaginationSummary(() => props.pagination, showingCo
|
|||||||
function rowNumber(index: number): number {
|
function rowNumber(index: number): number {
|
||||||
return groupedTableRowNumber(props.firstItem, index);
|
return groupedTableRowNumber(props.firstItem, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verificationModalOpen = ref(false);
|
||||||
|
const selectedRequestId = ref<number | null>(null);
|
||||||
|
|
||||||
|
function openVerificationDetail(requestId: number | undefined) {
|
||||||
|
if (requestId) {
|
||||||
|
selectedRequestId.value = requestId;
|
||||||
|
verificationModalOpen.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -75,7 +86,8 @@ function rowNumber(index: number): number {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="product.has_pending_request" class="text-sm text-amber-600">
|
<p v-if="product.has_pending_request" class="text-sm text-amber-600">
|
||||||
Sedang menunggu verifikasi owner
|
{{ product.pending_request_submitted_by_name }} mengajukan {{ product.pending_request_action_label?.toLowerCase() }} produk ini —
|
||||||
|
<button type="button" class="underline-offset-2 hover:underline" @click="openVerificationDetail(product.pending_request_id)">lihat</button>
|
||||||
</p>
|
</p>
|
||||||
<div v-if="product.categories.length" class="flex flex-wrap gap-1">
|
<div v-if="product.categories.length" class="flex flex-wrap gap-1">
|
||||||
<Badge v-for="category in product.categories" :key="category.id" variant="outline">
|
<Badge v-for="category in product.categories" :key="category.id" variant="outline">
|
||||||
@ -167,10 +179,8 @@ function rowNumber(index: number): number {
|
|||||||
|
|
||||||
<DataTableEmpty v-else />
|
<DataTableEmpty v-else />
|
||||||
|
|
||||||
<GroupedTableFooter
|
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||||
:summary="paginationSummary"
|
|
||||||
:pagination="pagination"
|
<VerificationDetailModal v-model:open="verificationModalOpen" :request-id="selectedRequestId" />
|
||||||
:pagination-links="paginationLinks"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { DataTableEmpty } from '@/components/data-table';
|
import { DataTableEmpty } from '@/components/data-table';
|
||||||
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
import DataTableToolbar from '@/components/data-table/DataTableToolbar.vue';
|
||||||
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
import GroupedTableFooter from '@/components/data-table/GroupedTableFooter.vue';
|
||||||
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
import MediaThumbnailCell from '@/components/media/MediaThumbnailCell.vue';
|
||||||
|
import VerificationDetailModal from '@/components/owner-verification/VerificationDetailModal.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@ -46,6 +47,16 @@ const paginationSummary = usePaginationSummary(() => props.pagination, showingCo
|
|||||||
function rowNumber(index: number): number {
|
function rowNumber(index: number): number {
|
||||||
return groupedTableRowNumber(props.firstItem, index);
|
return groupedTableRowNumber(props.firstItem, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const verificationModalOpen = ref(false);
|
||||||
|
const selectedRequestId = ref<number | null>(null);
|
||||||
|
|
||||||
|
function openVerificationDetail(requestId: number | undefined) {
|
||||||
|
if (requestId) {
|
||||||
|
selectedRequestId.value = requestId;
|
||||||
|
verificationModalOpen.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -71,7 +82,8 @@ function rowNumber(index: number): number {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="material.has_pending_request" class="text-sm text-amber-600">
|
<p v-if="material.has_pending_request" class="text-sm text-amber-600">
|
||||||
Sedang menunggu verifikasi owner
|
{{ material.pending_request_submitted_by_name }} mengajukan {{ material.pending_request_action_label?.toLowerCase() }} bahan baku ini —
|
||||||
|
<button type="button" class="underline-offset-2 hover:underline" @click="openVerificationDetail(material.pending_request_id)">lihat</button>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm">
|
||||||
<span>
|
<span>
|
||||||
@ -130,10 +142,8 @@ function rowNumber(index: number): number {
|
|||||||
|
|
||||||
<DataTableEmpty v-else />
|
<DataTableEmpty v-else />
|
||||||
|
|
||||||
<GroupedTableFooter
|
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||||
:summary="paginationSummary"
|
|
||||||
:pagination="pagination"
|
<VerificationDetailModal v-model:open="verificationModalOpen" :request-id="selectedRequestId" />
|
||||||
:pagination-links="paginationLinks"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export interface ProductListItem extends Product {
|
|||||||
pending_request_id?: number;
|
pending_request_id?: number;
|
||||||
pending_request_action?: string;
|
pending_request_action?: string;
|
||||||
pending_request_action_label?: string;
|
pending_request_action_label?: string;
|
||||||
|
pending_request_submitted_by_name?: string;
|
||||||
display_is_active?: boolean;
|
display_is_active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export type PurchaseListItem = {
|
|||||||
pending_request_id?: number;
|
pending_request_id?: number;
|
||||||
pending_request_action?: string;
|
pending_request_action?: string;
|
||||||
pending_request_action_label?: string;
|
pending_request_action_label?: string;
|
||||||
|
pending_request_submitted_by_name?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseCartItem = {
|
export type PurchaseCartItem = {
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export type RawMaterialListItem = {
|
|||||||
pending_request_id?: number;
|
pending_request_id?: number;
|
||||||
pending_request_action?: string;
|
pending_request_action?: string;
|
||||||
pending_request_action_label?: string;
|
pending_request_action_label?: string;
|
||||||
|
pending_request_submitted_by_name?: string;
|
||||||
display_is_active?: boolean;
|
display_is_active?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -413,6 +413,9 @@
|
|||||||
|
|
||||||
Route::prefix('owner-verifications')->name('owner_verifications.')
|
Route::prefix('owner-verifications')->name('owner_verifications.')
|
||||||
->group(function () {
|
->group(function () {
|
||||||
|
Route::get('requests/{ownerVerificationRequest}', [OwnerVerificationController::class, 'show'])
|
||||||
|
->name('show');
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user