feat: add order detail view and service method for enhanced order management and user experience
This commit is contained in:
parent
44312e76c9
commit
ee8949947f
@ -36,6 +36,13 @@ public function index(Request $request): Response
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Order $order): Response
|
||||
{
|
||||
return Inertia::render('admin/manage/orders/Show', [
|
||||
'order' => $this->orderService->findForShow($order),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
@ -197,6 +197,40 @@ public function findForEdit(Order $order): Order
|
||||
return $order;
|
||||
}
|
||||
|
||||
public function findForShow(Order $order): Order
|
||||
{
|
||||
$order->load([
|
||||
'customer:id,name,phone_number,address',
|
||||
'createdBy.profile',
|
||||
'marketing.profile',
|
||||
'items.productVariant.product:id,name',
|
||||
'items.productVariant.media',
|
||||
'cashTransaction:id,amount,description,created_at',
|
||||
]);
|
||||
|
||||
$order->items->each(function (OrderItem $item): void {
|
||||
$variant = $item->productVariant;
|
||||
|
||||
if ($variant) {
|
||||
$item->setAttribute('product_name', $variant->product?->name);
|
||||
$item->setAttribute('variant_name', $variant->name);
|
||||
$variant->setAttribute(
|
||||
'images',
|
||||
MediaPresenter::collection($variant, 'images'),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$availableActions = collect($order->status->availableActions())
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$order->setAttribute('available_actions', $availableActions);
|
||||
$order->setAttribute('is_editable', $order->status->isEditable());
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Link, router } from '@inertiajs/vue3';
|
||||
import { Check, Pencil, Send, Trash2, X } from '@lucide/vue';
|
||||
import { Check, Eye, Pencil, Send, Trash2, X } from '@lucide/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import OrderPrintButton from '@/components/admin/manage/orders/OrderPrintButton.vue';
|
||||
@ -137,6 +137,18 @@ function actionIcon(status: string) {
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" as-child>
|
||||
<Link :href="`/admin/manage/orders/${order.id}`">
|
||||
<Eye class="size-4" />
|
||||
<span class="sr-only">Detail</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Detail</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip v-if="canEdit">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="size-8" as-child>
|
||||
|
||||
579
resources/js/pages/admin/manage/orders/Show.vue
Normal file
579
resources/js/pages/admin/manage/orders/Show.vue
Normal file
@ -0,0 +1,579 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
CreditCard,
|
||||
MapPin,
|
||||
Package,
|
||||
Pencil,
|
||||
Phone,
|
||||
Receipt,
|
||||
Send,
|
||||
Trash2,
|
||||
Truck,
|
||||
User,
|
||||
X,
|
||||
} from '@lucide/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import OrderPrintButton from '@/components/admin/manage/orders/OrderPrintButton.vue';
|
||||
import ConfirmDialog from '@/components/ConfirmDialog.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import type { OrderDetail, OrderStatusAction } from '@/types/order';
|
||||
|
||||
const props = defineProps<{
|
||||
order: OrderDetail;
|
||||
}>();
|
||||
|
||||
const { can } = useCan();
|
||||
|
||||
const deleteConfirmOpen = ref(false);
|
||||
const deleteProcessing = ref(false);
|
||||
const statusConfirmOpen = ref(false);
|
||||
const statusProcessing = ref(false);
|
||||
const pendingAction = ref<OrderStatusAction | null>(null);
|
||||
|
||||
const canEdit = computed(() => props.order.is_editable && can('orders.update'));
|
||||
const canDelete = computed(() => can('orders.delete'));
|
||||
|
||||
const creatorName = computed(() => {
|
||||
return props.order.created_by?.profile?.full_name ?? props.order.created_by?.username ?? '-';
|
||||
});
|
||||
|
||||
const marketingName = computed(() => {
|
||||
return props.order.marketing?.profile?.full_name ?? props.order.marketing?.username ?? '-';
|
||||
});
|
||||
|
||||
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
||||
if (status === 'completed') return 'default';
|
||||
if (status === 'cancelled') return 'destructive';
|
||||
if (status === 'processing') return 'secondary';
|
||||
return 'outline';
|
||||
}
|
||||
|
||||
function actionIcon(status: string) {
|
||||
if (status === 'processing') return Send;
|
||||
if (status === 'completed') return Check;
|
||||
return X;
|
||||
}
|
||||
|
||||
function canPerformAction(action: OrderStatusAction): boolean {
|
||||
return can(action.permission);
|
||||
}
|
||||
|
||||
function statusConfirmDescription(action: OrderStatusAction): string {
|
||||
if (action.status === 'processing') {
|
||||
return `Pesanan ${props.order.order_number} akan dikirim dan diproses.`;
|
||||
}
|
||||
if (action.status === 'completed') {
|
||||
return `Pesanan ${props.order.order_number} akan ditandai selesai.`;
|
||||
}
|
||||
return `Pesanan ${props.order.order_number} akan dibatalkan. Stok produk akan dikembalikan.`;
|
||||
}
|
||||
|
||||
function openStatusConfirm(action: OrderStatusAction) {
|
||||
pendingAction.value = action;
|
||||
statusConfirmOpen.value = true;
|
||||
}
|
||||
|
||||
function transitionStatus() {
|
||||
if (!pendingAction.value) return;
|
||||
|
||||
statusProcessing.value = true;
|
||||
|
||||
router.post(`/admin/manage/orders/${props.order.id}/status`, {
|
||||
status: pendingAction.value.status,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
statusConfirmOpen.value = false;
|
||||
pendingAction.value = null;
|
||||
},
|
||||
onError: (errors) => {
|
||||
const message = Object.values(errors)[0];
|
||||
toast.error(typeof message === 'string' ? message : 'Gagal memperbarui status pesanan.');
|
||||
},
|
||||
onFinish: () => {
|
||||
statusProcessing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function destroyOrder() {
|
||||
deleteProcessing.value = true;
|
||||
|
||||
router.delete(`/admin/manage/orders/${props.order.id}`, {
|
||||
onSuccess: () => {
|
||||
deleteConfirmOpen.value = false;
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Gagal menghapus pesanan.');
|
||||
},
|
||||
onFinish: () => {
|
||||
deleteProcessing.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const items = computed(() => props.order.items ?? []);
|
||||
|
||||
const summaryRows = computed(() => {
|
||||
const rows = [
|
||||
{ label: 'Subtotal', value: props.order.subtotal_formatted },
|
||||
];
|
||||
|
||||
if (props.order.discount > 0) {
|
||||
rows.push({ label: 'Diskon', value: `-${props.order.discount_formatted}` });
|
||||
}
|
||||
|
||||
if (props.order.shipping_cost > 0) {
|
||||
rows.push({ label: 'Ongkos Kirim', value: props.order.shipping_cost_formatted });
|
||||
}
|
||||
|
||||
return rows;
|
||||
});
|
||||
|
||||
const snapshot = computed(() => props.order.marketplace_settings_snapshot);
|
||||
|
||||
const TIKTOK_FEE_LABELS: Record<string, string> = {
|
||||
platform_commission: 'Komisi Platform',
|
||||
logistics_service_fee: 'Biaya Layanan Logistik',
|
||||
dynamic_commission: 'Komisi Dinamis',
|
||||
order_processing_fee: 'Biaya Pemrosesan Pesanan',
|
||||
affiliate: 'Komisi Afiliasi',
|
||||
pre_order_service_fee: 'Biaya Layanan Pre-Order',
|
||||
};
|
||||
|
||||
const SHOPEE_FEE_LABELS: Record<string, string> = {
|
||||
admin_fee: 'Biaya Admin',
|
||||
program_fee: 'Biaya Program',
|
||||
shipping_savings: 'Pengiriman Hemat',
|
||||
premium: 'Premium',
|
||||
service_fee: 'Biaya Layanan',
|
||||
order_processing_fee: 'Biaya Pemrosesan Pesanan',
|
||||
ams_commission_fee: 'Komisi AMS',
|
||||
pre_order: 'Pre-Order',
|
||||
live_extra: 'Live Extra',
|
||||
};
|
||||
|
||||
function feeLabel(key: string): string {
|
||||
if (snapshot.value?.platform === 'tiktok') {
|
||||
return TIKTOK_FEE_LABELS[key] ?? key;
|
||||
}
|
||||
|
||||
return SHOPEE_FEE_LABELS[key] ?? key;
|
||||
}
|
||||
|
||||
function formatRp(amount: number): string {
|
||||
return `Rp ${new Intl.NumberFormat('id-ID').format(amount)}`;
|
||||
}
|
||||
|
||||
function formatFeeRule(fee: { scope: string; value_type: string; value: number }): string {
|
||||
const scope = fee.scope === 'product' ? '/produk' : '/transaksi';
|
||||
|
||||
if (fee.value_type === 'percent') {
|
||||
return `${fee.value}%${scope}`;
|
||||
}
|
||||
|
||||
return `${formatRp(fee.value)}${scope}`;
|
||||
}
|
||||
|
||||
const feeEntries = computed(() => {
|
||||
if (!snapshot.value) return [];
|
||||
|
||||
return Object.entries(snapshot.value.fees)
|
||||
.filter(([, rule]) => rule.value > 0)
|
||||
.map(([key, rule]) => ({
|
||||
key,
|
||||
label: feeLabel(key),
|
||||
rule: formatFeeRule(rule),
|
||||
amount: snapshot.value!.results[key]?.amount ?? 0,
|
||||
balanceAfter: snapshot.value!.results[key]?.balance_after ?? 0,
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="`Detail Pesanan ${order.order_number}`" />
|
||||
|
||||
<AdminLayout>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-2xl font-bold tracking-tight">
|
||||
Detail Pesanan
|
||||
</h2>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{{ order.order_number }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center">
|
||||
<OrderPrintButton :order="order as any" />
|
||||
|
||||
<template v-for="action in order.available_actions" :key="action.status">
|
||||
<Button
|
||||
v-if="canPerformAction(action)"
|
||||
size="sm"
|
||||
:variant="action.destructive ? 'outline' : 'default'"
|
||||
:class="action.destructive ? 'text-destructive hover:text-destructive' : ''"
|
||||
@click="openStatusConfirm(action)"
|
||||
>
|
||||
<component :is="actionIcon(action.status)" class="size-3.5" />
|
||||
{{ action.label }}
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<Button v-if="canEdit" variant="outline" size="sm" as-child>
|
||||
<Link :href="`/admin/manage/orders/${order.id}/edit`">
|
||||
<Pencil class="size-3.5" />
|
||||
Ubah
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
v-if="canDelete"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="text-destructive hover:text-destructive"
|
||||
@click="deleteConfirmOpen = true"
|
||||
>
|
||||
<Trash2 class="size-3.5" />
|
||||
Hapus
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" as-child>
|
||||
<Link href="/admin/manage/orders">
|
||||
<ArrowLeft class="size-3.5" />
|
||||
Kembali
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<!-- Main content -->
|
||||
<div class="space-y-6 lg:col-span-2">
|
||||
<!-- Order Info -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Package class="size-5" />
|
||||
Informasi Pesanan
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Nomor Pesanan</p>
|
||||
<p class="font-medium">{{ order.order_number }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Status</p>
|
||||
<Badge :variant="statusVariant(order.status)" class="mt-1">
|
||||
{{ order.status_label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Channel</p>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-1.5">
|
||||
<Badge variant="secondary">{{ order.channel_label }}</Badge>
|
||||
<Badge v-if="order.tiktok_order_id" variant="outline">
|
||||
TikTok: {{ order.tiktok_order_id }}
|
||||
</Badge>
|
||||
<Badge v-if="order.shopee_order_id" variant="outline">
|
||||
Shopee: {{ order.shopee_order_id }}
|
||||
</Badge>
|
||||
<Badge v-if="order.is_affiliate" variant="outline">
|
||||
Afiliasi
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Tanggal Pesanan</p>
|
||||
<p class="font-medium">{{ order.created_at_formatted }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Tipe Harga</p>
|
||||
<p class="font-medium">{{ order.price_type_label }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Tipe Pembayaran</p>
|
||||
<p class="font-medium">{{ order.payment_type_label }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Dibuat Oleh</p>
|
||||
<p class="font-medium">{{ creatorName }}</p>
|
||||
</div>
|
||||
<div v-if="order.marketing">
|
||||
<p class="text-muted-foreground text-sm">Marketing</p>
|
||||
<p class="font-medium">{{ marketingName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="order.notes" class="mt-4 rounded-md border border-dashed p-3">
|
||||
<p class="text-muted-foreground mb-1 text-sm">Catatan</p>
|
||||
<p class="text-sm">{{ order.notes }}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Order Items -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Package class="size-5" />
|
||||
Produk Pesanan
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead class="w-12">#</TableHead>
|
||||
<TableHead>Produk</TableHead>
|
||||
<TableHead>Varian</TableHead>
|
||||
<TableHead>Kualitas</TableHead>
|
||||
<TableHead class="text-right">Jumlah</TableHead>
|
||||
<TableHead class="text-right">Harga Satuan</TableHead>
|
||||
<TableHead class="text-right">Subtotal</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-if="!items.length">
|
||||
<TableCell colspan="7" class="text-muted-foreground text-center">
|
||||
Tidak ada item
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-for="(item, index) in items" :key="item.id">
|
||||
<TableCell class="text-muted-foreground tabular-nums">
|
||||
{{ index + 1 }}
|
||||
</TableCell>
|
||||
<TableCell class="font-medium">
|
||||
{{ item.product_name || item.product_variant?.product?.name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{{ item.variant_name || item.product_variant?.name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" class="text-xs">
|
||||
{{ item.stock_quality === 'reject' ? 'Reject' : 'Bagus' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ item.quantity_formatted }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ item.unit_price_formatted }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums font-medium">
|
||||
{{ item.subtotal_formatted }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Marketplace Fee Breakdown -->
|
||||
<Card v-if="snapshot">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Receipt class="size-5" />
|
||||
Rincian Biaya {{ snapshot.platform === 'tiktok' ? 'TikTok Shop' : 'Shopee' }}
|
||||
<Badge v-if="snapshot.is_affiliate" variant="outline" class="ml-1">Afiliasi</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Jenis Biaya</TableHead>
|
||||
<TableHead>Aturan</TableHead>
|
||||
<TableHead class="text-right">Potongan</TableHead>
|
||||
<TableHead class="text-right">Sisa Saldo</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="entry in feeEntries" :key="entry.key">
|
||||
<TableCell class="font-medium">
|
||||
{{ entry.label }}
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground text-sm">
|
||||
{{ entry.rule }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums text-destructive">
|
||||
-{{ formatRp(entry.amount) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right tabular-nums">
|
||||
{{ formatRp(entry.balanceAfter) }}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Separator class="my-4" />
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Dasar Perhitungan</span>
|
||||
<span class="tabular-nums">{{ formatRp(snapshot.base_amount) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Total Potongan</span>
|
||||
<span class="tabular-nums text-destructive">-{{ formatRp(snapshot.total_fee_amount) }}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold">Pendapatan Bersih</span>
|
||||
<span class="text-lg font-bold tabular-nums text-green-600">
|
||||
{{ formatRp(snapshot.net_amount) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Customer Info -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<User class="size-5" />
|
||||
Pelanggan
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="order.customer" class="space-y-3">
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Nama</p>
|
||||
<p class="font-medium">{{ order.customer.name }}</p>
|
||||
</div>
|
||||
<div v-if="order.customer.phone_number" class="flex items-start gap-2">
|
||||
<Phone class="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Telepon</p>
|
||||
<p class="font-medium">{{ order.customer.phone_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="order.customer.address" class="flex items-start gap-2">
|
||||
<MapPin class="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
||||
<div>
|
||||
<p class="text-muted-foreground text-sm">Alamat</p>
|
||||
<p class="font-medium whitespace-pre-line">{{ order.customer.address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-muted-foreground text-sm">
|
||||
Tidak ada pelanggan terkait
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Financial Summary -->
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<CreditCard class="size-5" />
|
||||
Ringkasan Pembayaran
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="row in summaryRows"
|
||||
:key="row.label"
|
||||
class="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span class="text-muted-foreground">{{ row.label }}</span>
|
||||
<span class="tabular-nums">{{ row.value }}</span>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-lg font-semibold">Total</span>
|
||||
<span class="text-lg font-bold tabular-nums">
|
||||
{{ order.total_amount_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="order.cash_transaction" class="mt-4 rounded-md bg-muted/50 p-3">
|
||||
<p class="text-muted-foreground mb-1 text-xs">Transaksi Kas</p>
|
||||
<p class="text-sm font-medium">{{ order.cash_transaction.description }}</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
{{ order.cash_transaction.created_at }}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Shipping Info -->
|
||||
<Card v-if="order.shipping_cost > 0">
|
||||
<CardHeader>
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Truck class="size-5" />
|
||||
Pengiriman
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Ongkos Kirim</span>
|
||||
<span class="font-medium tabular-nums">
|
||||
{{ order.shipping_cost_formatted }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model:open="statusConfirmOpen"
|
||||
:title="pendingAction ? `${pendingAction.label} pesanan?` : 'Ubah status pesanan?'"
|
||||
:description="pendingAction ? statusConfirmDescription(pendingAction) : ''"
|
||||
:confirm-label="pendingAction?.label ?? 'Konfirmasi'"
|
||||
cancel-label="Batal"
|
||||
:destructive="pendingAction?.destructive ?? false"
|
||||
:loading="statusProcessing"
|
||||
@confirm="transitionStatus"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
v-if="canDelete"
|
||||
v-model:open="deleteConfirmOpen"
|
||||
title="Hapus pesanan?"
|
||||
:description="`Pesanan ${order.order_number} akan dihapus.${order.is_editable ? ' Stok produk akan dikembalikan.' : ''}`"
|
||||
confirm-label="Hapus"
|
||||
cancel-label="Batal"
|
||||
destructive
|
||||
:loading="deleteProcessing"
|
||||
@confirm="destroyOrder"
|
||||
/>
|
||||
</template>
|
||||
@ -114,6 +114,98 @@ export type OrderEditItem = {
|
||||
}>;
|
||||
};
|
||||
|
||||
export type OrderDetail = {
|
||||
id: number;
|
||||
order_number: string;
|
||||
channel: string;
|
||||
channel_label: string;
|
||||
price_type: string;
|
||||
price_type_label: string;
|
||||
payment_type: string;
|
||||
payment_type_label: string;
|
||||
is_affiliate: boolean;
|
||||
status: string;
|
||||
status_label: string;
|
||||
is_editable: boolean;
|
||||
available_actions: OrderStatusAction[];
|
||||
tiktok_order_id: string | null;
|
||||
shopee_order_id: string | null;
|
||||
subtotal: number;
|
||||
subtotal_formatted: string;
|
||||
discount: number;
|
||||
discount_formatted: string;
|
||||
shipping_cost: number;
|
||||
shipping_cost_formatted: string;
|
||||
total_amount: number;
|
||||
total_amount_formatted: string;
|
||||
notes: string | null;
|
||||
created_at_formatted: string;
|
||||
customer?: {
|
||||
id: number;
|
||||
name: string;
|
||||
phone_number: string | null;
|
||||
address: string | null;
|
||||
} | null;
|
||||
created_by?: {
|
||||
id: number;
|
||||
username: string;
|
||||
profile?: {
|
||||
full_name: string;
|
||||
} | null;
|
||||
};
|
||||
marketing?: {
|
||||
id: number;
|
||||
username: string;
|
||||
profile?: {
|
||||
full_name: string;
|
||||
} | null;
|
||||
} | null;
|
||||
items: Array<{
|
||||
id: number;
|
||||
product_name: string;
|
||||
variant_name: string;
|
||||
stock_quality: string;
|
||||
stock_quality_label?: string;
|
||||
quantity: number;
|
||||
quantity_formatted: string;
|
||||
unit_price: number;
|
||||
unit_price_formatted: string;
|
||||
subtotal: number;
|
||||
subtotal_formatted: string;
|
||||
product_variant?: {
|
||||
id: number;
|
||||
name: string;
|
||||
images?: MediaItem[];
|
||||
product?: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
cash_transaction?: {
|
||||
id: number;
|
||||
amount: number;
|
||||
description: string;
|
||||
created_at: string;
|
||||
} | null;
|
||||
marketplace_settings_snapshot?: {
|
||||
platform: string;
|
||||
is_affiliate: boolean;
|
||||
base_amount: number;
|
||||
total_fee_amount: number;
|
||||
net_amount: number;
|
||||
fees: Record<string, {
|
||||
scope: string;
|
||||
value_type: string;
|
||||
value: number;
|
||||
}>;
|
||||
results: Record<string, {
|
||||
amount: number;
|
||||
balance_after: number;
|
||||
}>;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type PaginatedOrders = {
|
||||
data: OrderListItem[];
|
||||
current_page: number;
|
||||
|
||||
@ -239,6 +239,8 @@
|
||||
->middleware('permission:'.Permission::ORDERS_CREATE->value)
|
||||
->name('store');
|
||||
|
||||
Route::get('{order}', [OrderController::class, 'show'])->name('show');
|
||||
|
||||
Route::get('{order}/edit', [OrderController::class, 'edit'])
|
||||
->middleware('permission:'.Permission::ORDERS_UPDATE->value)
|
||||
->name('edit');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user