From fe7a3e4813a74f0fd1f07ceb3f6220aafa60e7a3 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 18 Jun 2026 01:33:35 +0700 Subject: [PATCH] feat: add shipping cost field to order management, including validation, formatting, and UI updates --- app/Http/Requests/Admin/Manage/OrderRequest.php | 2 ++ app/Models/Order.php | 9 +++++++++ app/Services/Manage/OrderService.php | 8 ++++++-- .../2026_06_12_100001_create_orders_table.php | 1 + .../admin/manage/orders/OrderGroupedTable.vue | 2 ++ .../components/admin/manage/orders/OrderPosForm.vue | 12 +++++++++++- .../js/lib/thermal-printer/encode-order-receipt.ts | 7 ++++++- resources/js/pages/admin/manage/orders/Edit.vue | 1 + resources/js/types/order.ts | 3 +++ 9 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/Http/Requests/Admin/Manage/OrderRequest.php b/app/Http/Requests/Admin/Manage/OrderRequest.php index c6fd907..1fa824c 100644 --- a/app/Http/Requests/Admin/Manage/OrderRequest.php +++ b/app/Http/Requests/Admin/Manage/OrderRequest.php @@ -37,6 +37,7 @@ public function rules(): array 'tiktok_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::TIKTOK->value), 'string', 'max:100'], 'shopee_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::SHOPEE->value), 'string', 'max:100'], 'discount' => ['nullable', 'integer', 'min:0'], + 'shipping_cost' => ['nullable', 'integer', 'min:0'], 'notes' => ['nullable', 'string'], ]; @@ -68,6 +69,7 @@ public function attributes(): array 'tiktok_order_id' => 'ID pesanan TikTok Shop', 'shopee_order_id' => 'ID pesanan Shopee', 'discount' => 'diskon', + 'shipping_cost' => 'ongkos kirim', 'notes' => 'keterangan', 'items' => 'produk', 'items.*.product_variant_id' => 'varian produk', diff --git a/app/Models/Order.php b/app/Models/Order.php index 731cc1c..7399e3c 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -20,6 +20,7 @@ #[Appends([ 'subtotal_formatted', 'discount_formatted', + 'shipping_cost_formatted', 'total_amount_formatted', 'created_at_formatted', 'channel_label', @@ -43,6 +44,7 @@ protected function casts(): array 'status' => OrderStatus::class, 'subtotal' => 'integer', 'discount' => 'integer', + 'shipping_cost' => 'integer', 'marketplace_settings_snapshot' => 'array', 'total_amount' => 'integer', ]; @@ -94,6 +96,13 @@ public function discountFormatted(): Attribute ); } + public function shippingCostFormatted(): Attribute + { + return Attribute::make( + get: fn () => 'Rp '.number_format($this->shipping_cost, 0, ',', '.'), + ); + } + public function totalAmountFormatted(): Attribute { return Attribute::make( diff --git a/app/Services/Manage/OrderService.php b/app/Services/Manage/OrderService.php index 24f9771..df7d0b8 100644 --- a/app/Services/Manage/OrderService.php +++ b/app/Services/Manage/OrderService.php @@ -320,7 +320,8 @@ public function create(array $validated, User $user): Order $subtotal = $draftItems->sum('subtotal'); $discount = (int) ($validated['discount'] ?? 0); - $totalAmount = max($subtotal - $discount, 0); + $shippingCost = (int) ($validated['shipping_cost'] ?? 0); + $totalAmount = max($subtotal - $discount + $shippingCost, 0); $channel = OrderChannel::from($validated['channel']); $order = Order::create([ @@ -336,6 +337,7 @@ public function create(array $validated, User $user): Order 'created_by_id' => $user->id, 'subtotal' => $subtotal, 'discount' => $discount, + 'shipping_cost' => $shippingCost, 'marketplace_settings_snapshot' => $this->marketplaceService->buildOrderSnapshot( $channel, $totalAmount, @@ -412,7 +414,8 @@ public function update(Order $order, array $validated): void $lineItems = $this->buildLineItems($validated['items'], $priceType); $subtotal = array_sum(array_column($lineItems, 'subtotal')); $discount = (int) ($validated['discount'] ?? 0); - $totalAmount = max($subtotal - $discount, 0); + $shippingCost = (int) ($validated['shipping_cost'] ?? 0); + $totalAmount = max($subtotal - $discount + $shippingCost, 0); $channel = OrderChannel::from($validated['channel']); $order->customer_id = $validated['customer_id'] ?? null; @@ -425,6 +428,7 @@ public function update(Order $order, array $validated): void $order->shopee_order_id = $validated['shopee_order_id'] ?? null; $order->subtotal = $subtotal; $order->discount = $discount; + $order->shipping_cost = $shippingCost; $order->marketplace_settings_snapshot = $this->marketplaceService->buildOrderSnapshot( $channel, $totalAmount, diff --git a/database/migrations/2026_06_12_100001_create_orders_table.php b/database/migrations/2026_06_12_100001_create_orders_table.php index b865e3c..7552d06 100644 --- a/database/migrations/2026_06_12_100001_create_orders_table.php +++ b/database/migrations/2026_06_12_100001_create_orders_table.php @@ -28,6 +28,7 @@ public function up(): void $table->string('shopee_order_id', 100)->nullable(); $table->unsignedBigInteger('subtotal'); $table->unsignedBigInteger('discount')->default(0); + $table->unsignedBigInteger('shipping_cost')->default(0); $table->json('marketplace_settings_snapshot')->nullable(); $table->unsignedBigInteger('total_amount'); $table->text('notes')->nullable(); diff --git a/resources/js/components/admin/manage/orders/OrderGroupedTable.vue b/resources/js/components/admin/manage/orders/OrderGroupedTable.vue index 7752ed6..ddd8ec3 100644 --- a/resources/js/components/admin/manage/orders/OrderGroupedTable.vue +++ b/resources/js/components/admin/manage/orders/OrderGroupedTable.vue @@ -124,6 +124,8 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive' }} Diskon {{ order.discount_formatted }} + Ongkir {{ + order.shipping_cost_formatted }} Total {{ order.total_amount_formatted }} diff --git a/resources/js/components/admin/manage/orders/OrderPosForm.vue b/resources/js/components/admin/manage/orders/OrderPosForm.vue index 7899200..74e91d1 100644 --- a/resources/js/components/admin/manage/orders/OrderPosForm.vue +++ b/resources/js/components/admin/manage/orders/OrderPosForm.vue @@ -63,6 +63,7 @@ const props = defineProps<{ tiktok_order_id: string; shopee_order_id: string; discount: string; + shipping_cost: string; notes: string; items: OrderCartItem[]; }; @@ -112,6 +113,7 @@ const form = useForm({ tiktok_order_id: '', shopee_order_id: '', discount: '', + shipping_cost: '', notes: '', }); @@ -132,6 +134,7 @@ function populateForm() { form.tiktok_order_id = props.initialData.tiktok_order_id; form.shopee_order_id = props.initialData.shopee_order_id; form.discount = props.initialData.discount; + form.shipping_cost = String(props.initialData.shipping_cost); form.notes = props.initialData.notes; cart.value = props.initialData.items.map((item) => ({ ...item })); } @@ -228,7 +231,8 @@ const subtotal = computed(() => ); const discountAmount = computed(() => Number(parseRupiah(form.discount)) || 0); -const totalAmount = computed(() => Math.max(subtotal.value - discountAmount.value, 0)); +const shippingAmount = computed(() => Number(parseRupiah(form.shipping_cost)) || 0); +const totalAmount = computed(() => Math.max(subtotal.value - discountAmount.value + shippingAmount.value, 0)); function getVariantPrice(variant: ProductVariantItem): ProductPriceItem | undefined { return variant.prices.find((price) => price.type === form.price_type); @@ -401,6 +405,7 @@ function buildFormData(): FormData { } formData.append('discount', parseRupiah(form.discount)); + formData.append('shipping_cost', parseRupiah(form.shipping_cost)); formData.append('notes', form.notes); if (isCreateMode.value && printAfterSave.value) { @@ -732,6 +737,11 @@ function submit() { + + Ongkir + + +
Total Rp {{ formatRupiah(totalAmount) }} diff --git a/resources/js/lib/thermal-printer/encode-order-receipt.ts b/resources/js/lib/thermal-printer/encode-order-receipt.ts index 8fc68d1..e706485 100644 --- a/resources/js/lib/thermal-printer/encode-order-receipt.ts +++ b/resources/js/lib/thermal-printer/encode-order-receipt.ts @@ -97,9 +97,14 @@ export function encodeOrderReceipt( const summaryRows: string[][] = [ ['Subtotal', order.subtotal_formatted], ['Diskon', order.discount_formatted], - ['Total', order.total_amount_formatted], ]; + if (order.shipping_cost > 0) { + summaryRows.push(['Ongkir', order.shipping_cost_formatted]); + } + + summaryRows.push(['Total', order.total_amount_formatted]); + encoder.table( [ { width: labelColumnWidth, align: 'left' }, diff --git a/resources/js/pages/admin/manage/orders/Edit.vue b/resources/js/pages/admin/manage/orders/Edit.vue index 8bcad48..43033ce 100644 --- a/resources/js/pages/admin/manage/orders/Edit.vue +++ b/resources/js/pages/admin/manage/orders/Edit.vue @@ -29,6 +29,7 @@ const initialData = computed(() => ({ tiktok_order_id: props.order.tiktok_order_id ?? '', shopee_order_id: props.order.shopee_order_id ?? '', discount: String(props.order.discount), + shipping_cost: String(props.order.shipping_cost), notes: props.order.notes ?? '', items: props.order.items.map((item) => ({ product_variant_id: item.product_variant_id, diff --git a/resources/js/types/order.ts b/resources/js/types/order.ts index 7679f40..91c78cf 100644 --- a/resources/js/types/order.ts +++ b/resources/js/types/order.ts @@ -52,6 +52,8 @@ export type OrderListItem = { shopee_order_id: string | null; subtotal_formatted: string; discount_formatted: string; + shipping_cost: number; + shipping_cost_formatted: string; total_amount_formatted: string; notes: string | null; created_at_formatted: string; @@ -90,6 +92,7 @@ export type OrderEditItem = { tiktok_order_id: string | null; shopee_order_id: string | null; discount: number; + shipping_cost: number; notes: string | null; items: Array<{ product_variant_id: number;