279 lines
13 KiB
Vue
279 lines
13 KiB
Vue
<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>
|