feat: add created_at formatting and update order calculations in Order model and observer
This commit is contained in:
parent
2763f1fbcc
commit
7d4368f774
@ -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
|
||||
{
|
||||
@ -75,6 +75,13 @@ protected function totalFormatted(): Attribute
|
||||
);
|
||||
}
|
||||
|
||||
protected function createdAtFormatted(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn() => $this->created_at->translatedFormat('d F Y H:i'),
|
||||
);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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 = ({
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{row.original.order_number}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{format(
|
||||
new Date(row.original.created_at),
|
||||
'dd MMM yyyy HH:mm',
|
||||
{ locale: id },
|
||||
)}
|
||||
{row.original.created_at_formatted}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
@ -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}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
meta: { title: 'Pelanggan' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'cogs',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Modal" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{row.original.cogs_formatted}
|
||||
</span>
|
||||
),
|
||||
meta: { title: 'Modal' },
|
||||
},
|
||||
{
|
||||
accessorKey: 'items',
|
||||
header: ({ column }) => (
|
||||
@ -114,33 +122,55 @@ export const getColumns = ({
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Total" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
cell: ({ row }) => {
|
||||
const order = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-primary">
|
||||
{row.original.subtotal_formatted}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
Subtotal:
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
- {row.original.discount_formatted}
|
||||
<span className="text-sm font-medium">
|
||||
{order.subtotal_formatted}
|
||||
</span>
|
||||
<span className="font-bold text-primary">
|
||||
{row.original.total_formatted}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
Potongan:
|
||||
</span>
|
||||
<span className="text-sm text-red-500">
|
||||
- {order.discount_formatted}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-col border-t pt-1">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
Total Tagihan:
|
||||
</span>
|
||||
<span className="text-base font-bold text-primary">
|
||||
{order.total_formatted}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
Dibayar ({PAYMENT_METHOD[order.payment_method]}):
|
||||
</span>
|
||||
<span className="text-sm font-medium text-green-600">
|
||||
{order.payment_formatted}
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
className="h-4 py-0 text-[10px]"
|
||||
className="mt-1 h-4 w-fit py-0 text-[10px]"
|
||||
variant={
|
||||
row.original.order_status === 'delivered'
|
||||
? 'default'
|
||||
: row.original.order_status === 'pending'
|
||||
? 'secondary'
|
||||
: row.original.order_status === 'cancelled'
|
||||
? 'destructive'
|
||||
: 'outline'
|
||||
ORDER_STATUS[order.order_status]?.color ?? 'outline'
|
||||
}
|
||||
>
|
||||
{row.original.order_status}
|
||||
{ORDER_STATUS[order.order_status]?.label ??
|
||||
order.order_status}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
);
|
||||
},
|
||||
meta: { title: 'Total' },
|
||||
},
|
||||
{
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user