feat: add order detail page and link to order management table

This commit is contained in:
Yoga Pangestu 2026-05-02 00:13:19 +07:00
parent 43d399f07a
commit 85e04dbf6a
5 changed files with 273 additions and 1 deletions

View File

@ -8,6 +8,7 @@
use App\Enums\PriceType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Manage\Order\OrderRequest;
use App\Models\GeneralSetting;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
@ -115,6 +116,16 @@ public function edit(Order $order): Response
]);
}
public function show(Order $order): Response
{
$order->load(['items.product', 'user']);
return Inertia::render('admin/manage/order/show', [
'order' => $order,
'setting' => GeneralSetting::first(),
]);
}
public function update(OrderRequest $request, Order $order): RedirectResponse
{
$validated = $request->validated();

View File

@ -12,7 +12,7 @@ import type { Order } from '@/types/order';
import { ORDER_CHANNEL, ORDER_STATUS, PAYMENT_METHOD } from '@/types/order';
import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Printer, Trash2 } from 'lucide-react';
import { FileText, Pencil, Printer, Trash2 } from 'lucide-react';
interface ColumnProps {
onDelete: (order: Order) => void;
@ -182,6 +182,22 @@ export const getColumns = ({
return (
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Link href={orderRoutes.show(order.id).url}>
<Button
variant="ghost"
size="icon"
className="text-emerald-600 hover:bg-emerald-50 hover:text-emerald-700 dark:hover:bg-emerald-950/20"
>
<FileText className="size-4" />
</Button>
</Link>
</TooltipTrigger>
<TooltipContent>
<p>Invoice</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button

View File

@ -0,0 +1,194 @@
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 { Head, Link } from '@inertiajs/react';
import { ArrowLeft, Mail } from 'lucide-react';
import type { Order } from '@/types/order';
import { ORDER_STATUS, PAYMENT_METHOD, ORDER_CHANNEL } from '@/types/order';
import * as orderRoutes from '@/routes/order';
import type { GeneralSetting } from '@/types/general-setting';
interface ShowProps {
order: Order;
setting: GeneralSetting;
}
export default function OrderShow({ order, setting }: ShowProps) {
return (
<div className="flex flex-col gap-6 p-6 print:p-8">
<Head title={`Invoice #${order.order_number}`} />
{/* Header / Actions */}
<div className="relative z-10 flex items-center justify-between print:hidden">
<div className="flex items-center gap-4">
<Link
href={orderRoutes.index().url}
className="flex h-10 w-10 items-center justify-center rounded-full border border-input bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<ArrowLeft className="size-4" />
</Link>
<div>
<h1 className="text-2xl font-bold tracking-tight text-foreground">
Detail Pesanan
</h1>
<p className="text-sm text-muted-foreground">
Invoice #{order.order_number}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Actions removed as requested */}
</div>
</div>
{/* Invoice Card */}
<Card className="relative mx-auto w-full max-w-4xl overflow-hidden border-none bg-card shadow-xl print:shadow-none print:border print:max-w-none">
<div className="pointer-events-none absolute inset-0 bg-grid-slate-100 [mask-image:linear-gradient(0deg,#fff,rgba(255,255,255,0.6))] dark:bg-grid-slate-700/25 dark:[mask-image:linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.5))] print:hidden" />
<CardHeader className="relative space-y-6 pb-8 pt-10">
<div className="flex flex-col justify-between gap-6 md:flex-row md:items-start">
<div className="space-y-4">
<div className="flex items-center gap-3">
{setting?.logo_light_url ? (
<img src={setting.logo_light_url} alt="Logo" className="h-10 w-auto" />
) : (
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-2xl font-bold text-primary-foreground">
{setting?.name?.charAt(0) || 'V'}
</div>
)}
<div>
<h2 className="text-2xl font-bold text-foreground">{setting?.name || 'VN Grup'}</h2>
<p className="text-sm text-muted-foreground italic">Your Style, Our Passion</p>
</div>
</div>
<div className="max-w-xs space-y-1 text-sm text-muted-foreground">
<p>{setting?.address || 'Alamat Toko Belum Diatur'}</p>
<p>{setting?.phone || '-'}</p>
</div>
</div>
<div className="space-y-4 text-left md:text-right">
<div className="space-y-1">
<h3 className="text-3xl font-black uppercase tracking-tighter text-primary">INVOICE</h3>
<p className="text-lg font-mono font-medium">#{order.order_number}</p>
</div>
<div className="space-y-1 text-sm text-muted-foreground">
<p>Tanggal Pesanan: <span className="font-medium text-foreground">{order.created_at_formatted}</span></p>
<p>Channel: <span className="font-medium text-foreground">{ORDER_CHANNEL[order.order_channel] || order.order_channel}</span></p>
<div className="flex items-center gap-2 md:justify-end">
<span>Status:</span>
<Badge variant={ORDER_STATUS[order.order_status]?.color ?? 'outline'}>
{ORDER_STATUS[order.order_status]?.label ?? order.order_status}
</Badge>
</div>
</div>
</div>
</div>
<Separator className="bg-primary/10" />
<div className="grid gap-8 md:grid-cols-2">
<div className="space-y-3">
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Tagihan Untuk:</h4>
<div className="space-y-1">
<p className="text-lg font-bold text-foreground">{order.customer_name}</p>
<p className="text-sm text-muted-foreground">Pelanggan Setia</p>
</div>
</div>
<div className="space-y-3 text-left md:text-right">
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Pembayaran:</h4>
<div className="space-y-1">
<p className="text-lg font-bold text-foreground">{PAYMENT_METHOD[order.payment_method] || order.payment_method}</p>
</div>
</div>
</div>
</CardHeader>
<CardContent className="relative space-y-8 pb-12">
<div className="rounded-xl border bg-card/50 overflow-hidden shadow-sm">
<Table>
<TableHeader className="bg-muted/50">
<TableRow>
<TableHead className="w-[50px] text-center">#</TableHead>
<TableHead>Produk</TableHead>
<TableHead className="text-right">Harga</TableHead>
<TableHead className="text-center">Jumlah</TableHead>
<TableHead className="text-right">Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{order.items?.map((item, index) => (
<TableRow key={item.id} className="hover:bg-muted/30">
<TableCell className="text-center text-muted-foreground">{index + 1}</TableCell>
<TableCell>
<div className="font-medium">{item.product?.name || 'Produk Tidak Diketahui'}</div>
<div className="text-xs text-muted-foreground uppercase">{item.price_type}</div>
</TableCell>
<TableCell className="text-right">{item.price_formatted}</TableCell>
<TableCell className="text-center">{item.qty}</TableCell>
<TableCell className="text-right font-medium">{item.total_formatted}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex flex-col justify-between gap-8 md:flex-row">
<div className="flex-1 space-y-4">
<div className="rounded-xl bg-muted/30 p-4 space-y-2 border border-dashed">
<h4 className="text-xs font-bold uppercase tracking-wider text-muted-foreground">Catatan:</h4>
<p className="text-xs text-muted-foreground leading-relaxed">
Terima kasih telah berbelanja di {setting?.name || 'VN Grup'}.
Barang yang sudah dibeli tidak dapat ditukar atau dikembalikan kecuali ada perjanjian sebelumnya.
Simpan invoice ini sebagai bukti pembelian yang sah.
</p>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Mail className="size-3" />
<span>Dicatat oleh: <span className="font-medium text-foreground">{order.user?.name || '-'}</span></span>
</div>
</div>
<div className="w-full md:w-80 space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">{order.subtotal_formatted}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Potongan / Diskon</span>
<span className="font-medium text-red-500">- {order.discount_formatted}</span>
</div>
<Separator />
<div className="flex justify-between items-center pt-1">
<span className="text-lg font-bold">Total Tagihan</span>
<span className="text-2xl font-black text-primary">{order.total_formatted}</span>
</div>
<div className="flex justify-between items-center rounded-lg bg-primary/5 px-3 py-2 border border-primary/10 mt-4">
<span className="text-xs font-bold uppercase text-primary/70">Sudah Dibayar</span>
<span className="text-sm font-bold text-primary">{order.payment_formatted}</span>
</div>
</div>
</div>
<div className="pt-10 text-center text-sm text-muted-foreground print:pt-20">
<p className="font-medium">Semoga hari Anda menyenangkan!</p>
<p className="text-[10px] mt-1 opacity-50">Generated by {setting?.name || 'VN Grup'} System</p>
</div>
</CardContent>
</Card>
</div>
);
}
OrderShow.layout = (page: any) => (
<div className="min-h-screen bg-muted/20 dark:bg-muted/5">{page}</div>
);

View File

@ -24,6 +24,7 @@
Route::get('orders', [OrderController::class, 'index'])->name('order.index')->middleware('can:View:Order');
Route::get('order/create', [OrderController::class, 'create'])->name('order.create')->middleware('can:Create:Order');
Route::post('order/store', [OrderController::class, 'store'])->name('order.store')->middleware('can:Create:Order');
Route::get('order/show/{order}', [OrderController::class, 'show'])->name('order.show')->middleware('can:View:Order');
Route::get('order/edit/{order}', [OrderController::class, 'edit'])->name('order.edit')->middleware('can:Edit:Order');
Route::patch('order/update/{order}', [OrderController::class, 'update'])->name('order.update')->middleware('can:Edit:Order');
Route::delete('order/destroy/{order}', [OrderController::class, 'destroy'])->name('order.destroy')->middleware('can:Delete:Order');

View File

@ -4,6 +4,7 @@
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PriceType;
use App\Models\GeneralSetting;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
@ -45,6 +46,13 @@
'DeleteAny:Order',
]);
actingAs($user);
GeneralSetting::create([
'name' => 'VN Grup',
'phone' => '08123456789',
'address' => 'Jl. Test No. 1',
'description' => 'Test Description',
]);
});
it('can access order index page', function () {
@ -74,6 +82,42 @@
);
});
it('can access order show page', function () {
$order = Order::factory()
->has(OrderItem::factory()->count(2), 'items')
->create();
get(route('order.show', $order))
->assertOk()
->assertInertia(fn ($page) => $page
->component('admin/manage/order/show')
->has('order', fn ($page) => $page
->where('id', $order->id)
->has('items', 2)
->has('items.0.product')
->has('user')
->etc()
)
->has('setting')
);
});
it('can access order edit page', function () {
$order = Order::factory()->create();
get(route('order.edit', $order))
->assertOk()
->assertInertia(fn ($page) => $page
->component('admin/manage/order/edit')
->has('order')
->has('products')
->has('orderStatus')
->has('orderChannels')
->has('paymentMethods')
->has('priceTypes')
);
});
it('can manage cart items', function () {
$product = Product::factory()->create(['stock' => 10]);
@ -275,6 +319,12 @@
actingAs(createUnauthorizedUser());
});
it('cannot view an order without permission', function () {
$order = Order::factory()->create();
get(route('order.show', $order))
->assertStatus(403);
});
it('cannot store an order without permission', function () {
postJson(route('order.store'), ['customer_name' => 'Unauthorized'])
->assertStatus(403);