feat: add order detail page and link to order management table
This commit is contained in:
parent
43d399f07a
commit
85e04dbf6a
@ -8,6 +8,7 @@
|
|||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\Manage\Order\OrderRequest;
|
use App\Http\Requests\Admin\Manage\Order\OrderRequest;
|
||||||
|
use App\Models\GeneralSetting;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
use App\Models\Product;
|
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
|
public function update(OrderRequest $request, Order $order): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import type { Order } from '@/types/order';
|
|||||||
import { ORDER_CHANNEL, ORDER_STATUS, PAYMENT_METHOD } from '@/types/order';
|
import { ORDER_CHANNEL, ORDER_STATUS, PAYMENT_METHOD } from '@/types/order';
|
||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
import { Pencil, Printer, Trash2 } from 'lucide-react';
|
import { FileText, Pencil, Printer, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
interface ColumnProps {
|
interface ColumnProps {
|
||||||
onDelete: (order: Order) => void;
|
onDelete: (order: Order) => void;
|
||||||
@ -182,6 +182,22 @@ export const getColumns = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
194
resources/js/pages/admin/manage/order/show.tsx
Normal file
194
resources/js/pages/admin/manage/order/show.tsx
Normal 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>
|
||||||
|
);
|
||||||
@ -24,6 +24,7 @@
|
|||||||
Route::get('orders', [OrderController::class, 'index'])->name('order.index')->middleware('can:View:Order');
|
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::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::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::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::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');
|
Route::delete('order/destroy/{order}', [OrderController::class, 'destroy'])->name('order.destroy')->middleware('can:Delete:Order');
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
use App\Enums\OrderStatus;
|
use App\Enums\OrderStatus;
|
||||||
use App\Enums\PaymentMethod;
|
use App\Enums\PaymentMethod;
|
||||||
use App\Enums\PriceType;
|
use App\Enums\PriceType;
|
||||||
|
use App\Models\GeneralSetting;
|
||||||
use App\Models\Order;
|
use App\Models\Order;
|
||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
use App\Models\Product;
|
use App\Models\Product;
|
||||||
@ -45,6 +46,13 @@
|
|||||||
'DeleteAny:Order',
|
'DeleteAny:Order',
|
||||||
]);
|
]);
|
||||||
actingAs($user);
|
actingAs($user);
|
||||||
|
|
||||||
|
GeneralSetting::create([
|
||||||
|
'name' => 'VN Grup',
|
||||||
|
'phone' => '08123456789',
|
||||||
|
'address' => 'Jl. Test No. 1',
|
||||||
|
'description' => 'Test Description',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can access order index page', function () {
|
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 () {
|
it('can manage cart items', function () {
|
||||||
$product = Product::factory()->create(['stock' => 10]);
|
$product = Product::factory()->create(['stock' => 10]);
|
||||||
|
|
||||||
@ -275,6 +319,12 @@
|
|||||||
actingAs(createUnauthorizedUser());
|
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 () {
|
it('cannot store an order without permission', function () {
|
||||||
postJson(route('order.store'), ['customer_name' => 'Unauthorized'])
|
postJson(route('order.store'), ['customer_name' => 'Unauthorized'])
|
||||||
->assertStatus(403);
|
->assertStatus(403);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user