From 7d4368f774187cafcc1e4097338f0e7cb40d8d4c Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 30 Apr 2026 15:48:05 +0700 Subject: [PATCH] feat: add created_at formatting and update order calculations in Order model and observer --- app/Models/Order.php | 19 ++-- app/Observers/OrderObserver.php | 8 ++ .../admin/manage/order/partials/columns.tsx | 100 ++++++++++++------ resources/js/types/order.ts | 33 +++++- tests/Feature/Admin/Manage/OrderTest.php | 18 +++- 5 files changed, 131 insertions(+), 47 deletions(-) diff --git a/app/Models/Order.php b/app/Models/Order.php index fc1907b..4be6598 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -20,7 +20,7 @@ use Spatie\Activitylog\Traits\LogsActivity; #[Guarded(['id'])] -#[Appends(['cogs_formatted', 'subtotal_formatted', 'discount_formatted', 'payment_formatted', 'total_formatted'])] +#[Appends(['cogs_formatted', 'subtotal_formatted', 'discount_formatted', 'payment_formatted', 'total_formatted', 'created_at_formatted'])] #[ObservedBy([OrderObserver::class])] class Order extends Model { @@ -43,35 +43,42 @@ protected function casts(): array protected function cogsFormatted(): Attribute { return Attribute::make( - get: fn () => 'Rp '.number_format($this->cogs, 0, ',', '.'), + get: fn() => 'Rp ' . number_format($this->cogs, 0, ',', '.'), ); } protected function subtotalFormatted(): Attribute { return Attribute::make( - get: fn () => 'Rp '.number_format($this->subtotal, 0, ',', '.'), + get: fn() => 'Rp ' . number_format($this->subtotal, 0, ',', '.'), ); } protected function discountFormatted(): Attribute { return Attribute::make( - get: fn () => 'Rp '.number_format($this->discount, 0, ',', '.'), + get: fn() => 'Rp ' . number_format($this->discount, 0, ',', '.'), ); } protected function paymentFormatted(): Attribute { return Attribute::make( - get: fn () => 'Rp '.number_format($this->payment, 0, ',', '.'), + get: fn() => 'Rp ' . number_format($this->payment, 0, ',', '.'), ); } protected function totalFormatted(): Attribute { return Attribute::make( - get: fn () => 'Rp '.number_format($this->total, 0, ',', '.'), + get: fn() => 'Rp ' . number_format($this->total, 0, ',', '.'), + ); + } + + protected function createdAtFormatted(): Attribute + { + return Attribute::make( + get: fn() => $this->created_at->translatedFormat('d F Y H:i'), ); } diff --git a/app/Observers/OrderObserver.php b/app/Observers/OrderObserver.php index 5c9d7ec..ea3a1d4 100644 --- a/app/Observers/OrderObserver.php +++ b/app/Observers/OrderObserver.php @@ -27,6 +27,14 @@ public function creating(Order $order): void $order->total = $order->subtotal - $order->discount; } + /** + * Handle the Order "updating" event. + */ + public function updating(Order $order): void + { + $order->total = $order->subtotal - $order->discount; + } + /** * Handle the Order "deleting" event. */ diff --git a/resources/js/pages/admin/manage/order/partials/columns.tsx b/resources/js/pages/admin/manage/order/partials/columns.tsx index ad1d5b0..46aef4f 100644 --- a/resources/js/pages/admin/manage/order/partials/columns.tsx +++ b/resources/js/pages/admin/manage/order/partials/columns.tsx @@ -9,10 +9,9 @@ import { import { usePermission } from '@/hooks/use-permission'; import * as orderRoutes from '@/routes/order'; 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 { format } from 'date-fns'; -import { id } from 'date-fns/locale'; import { Pencil, Printer, Trash2 } from 'lucide-react'; interface ColumnProps { @@ -43,11 +42,7 @@ export const getColumns = ({
{row.original.order_number} - {format( - new Date(row.original.created_at), - 'dd MMM yyyy HH:mm', - { locale: id }, - )} + {row.original.created_at_formatted}
), @@ -65,12 +60,25 @@ export const getColumns = ({ variant="outline" className="h-4 w-fit px-1 py-0 text-[10px]" > - {row.original.order_channel} + {ORDER_CHANNEL[row.original.order_channel] || + row.original.order_channel} ), meta: { title: 'Pelanggan' }, }, + { + accessorKey: 'cogs', + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + {row.original.cogs_formatted} + + ), + meta: { title: 'Modal' }, + }, { accessorKey: 'items', header: ({ column }) => ( @@ -114,33 +122,55 @@ export const getColumns = ({ header: ({ column }) => ( ), - cell: ({ row }) => ( -
- - {row.original.subtotal_formatted} - - - - {row.original.discount_formatted} - - - {row.original.total_formatted} - - - {row.original.order_status} - -
- ), + cell: ({ row }) => { + const order = row.original; + + return ( +
+
+ + Subtotal: + + + {order.subtotal_formatted} + +
+
+ + Potongan: + + + - {order.discount_formatted} + +
+
+ + Total Tagihan: + + + {order.total_formatted} + +
+
+ + Dibayar ({PAYMENT_METHOD[order.payment_method]}): + + + {order.payment_formatted} + +
+ + {ORDER_STATUS[order.order_status]?.label ?? + order.order_status} + +
+ ); + }, meta: { title: 'Total' }, }, { diff --git a/resources/js/types/order.ts b/resources/js/types/order.ts index b7787f8..64bb16b 100644 --- a/resources/js/types/order.ts +++ b/resources/js/types/order.ts @@ -1,6 +1,32 @@ import type { Product } from "./product"; import type { User } from "./auth"; +export const ORDER_STATUS = { + pending: { label: 'Menunggu', color: 'secondary' }, + processing: { label: 'Proses', color: 'outline' }, + shipped: { label: 'Dikirim', color: 'outline' }, + delivered: { label: 'Selesai', color: 'default' }, + cancelled: { label: 'Gagal', color: 'destructive' }, +} as const; + +export const PAYMENT_METHOD = { + cash: 'Tunai', + transfer: 'Transfer', + e_wallet: 'E-Wallet', + qris: 'QRIS', +} as const; + +export const ORDER_CHANNEL = { + tiktok: 'Tiktok', + shopee: 'Shopee', + tokopedia: 'Tokopedia', + lazada: 'Lazada', + facebook: 'Facebook', + website: 'Website', + store: 'Toko', + other: 'Lainnya', +} as const; + export interface Order { id: number; order_number: string; @@ -15,13 +41,14 @@ export interface Order { payment_formatted: string; total: number; total_formatted: string; - payment_method: 'cash' | 'transfer' | 'e_wallet' | 'qris'; - order_status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'; - order_channel: 'tiktok' | 'shopee' | 'tokopedia' | 'lazada' | 'facebook' | 'website' | 'store' | 'other'; + payment_method: keyof typeof PAYMENT_METHOD; + order_status: keyof typeof ORDER_STATUS; + order_channel: keyof typeof ORDER_CHANNEL; items?: OrderItem[]; user?: User; created_at: string; updated_at: string; + created_at_formatted: string; } export interface OrderItem { diff --git a/tests/Feature/Admin/Manage/OrderTest.php b/tests/Feature/Admin/Manage/OrderTest.php index 8a43cc3..3c0c1e6 100644 --- a/tests/Feature/Admin/Manage/OrderTest.php +++ b/tests/Feature/Admin/Manage/OrderTest.php @@ -56,6 +56,9 @@ ->component('admin/manage/order/index') ->has('orders', fn ($page) => $page ->has('0.user') + ->has('0.cogs_formatted') + ->has('0.payment_formatted') + ->has('0.created_at_formatted') ->etc() ) ); @@ -109,9 +112,8 @@ it('can store a new order', function () { $product = Product::factory()->create(['stock' => 10]); - // Add a price for PURCHASE to avoid errors in COGS calculation if applicable - $product->prices()->create([ - 'price_type' => PriceType::PURCHASE->value, + // Update existing PURCHASE price from factory to ensure COGS calculation is predictable + $product->prices()->where('price_type', PriceType::PURCHASE->value)->update([ 'price' => 8000, ]); @@ -140,7 +142,14 @@ assertDatabaseHas('orders', [ 'customer_name' => 'John Doe', + 'subtotal' => 20000, + 'discount' => 2000, 'total' => 18000, + 'payment' => 18000, + 'payment_method' => PaymentMethod::CASH->value, + 'order_status' => OrderStatus::PENDING->value, + 'order_channel' => OrderChannel::STORE->value, + 'cogs' => 16000, // 8000 * 2 ]); // Check stock decrement @@ -215,6 +224,9 @@ assertDatabaseHas('orders', [ 'id' => $order->id, 'customer_name' => 'Updated Customer', + 'order_status' => OrderStatus::PROCESSING->value, + 'payment' => 15000, + 'total' => 15000, ]); // Check stock: product1 should be restored (10), product2 should be decremented (7)