feat: add order status management to OrderRequest and OrderService, update frontend forms for status handling

This commit is contained in:
Yoga Pangestu 2026-06-28 00:01:54 +07:00
parent 60c58a9709
commit fe1549e0a5
5 changed files with 44 additions and 1 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Requests\Admin\Manage; namespace App\Http\Requests\Admin\Manage;
use App\Enums\OrderChannel; use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType; use App\Enums\PaymentType;
use App\Enums\Permission; use App\Enums\Permission;
use App\Enums\PriceType; use App\Enums\PriceType;
@ -39,6 +40,7 @@ public function rules(): array
'shopee_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::SHOPEE->value), 'string', 'max:100'], 'shopee_order_id' => [Rule::requiredIf(fn () => $this->channel === OrderChannel::SHOPEE->value), 'string', 'max:100'],
'discount' => ['nullable', 'integer', 'min:0'], 'discount' => ['nullable', 'integer', 'min:0'],
'notes' => ['nullable', 'string'], 'notes' => ['nullable', 'string'],
'status' => ['nullable', Rule::enum(OrderStatus::class)],
]; ];
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) { if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
@ -71,6 +73,7 @@ public function attributes(): array
'shopee_order_id' => 'ID pesanan Shopee', 'shopee_order_id' => 'ID pesanan Shopee',
'discount' => 'diskon', 'discount' => 'diskon',
'notes' => 'keterangan', 'notes' => 'keterangan',
'status' => 'status pesanan',
'items' => 'produk', 'items' => 'produk',
'items.*.product_variant_id' => 'varian produk', 'items.*.product_variant_id' => 'varian produk',
'items.*.quantity' => 'jumlah', 'items.*.quantity' => 'jumlah',
@ -85,6 +88,12 @@ public function withValidator(Validator $validator): void
$validator->errors()->add('customer_id', 'Pelanggan wajib diisi jika marketing dipilih.'); $validator->errors()->add('customer_id', 'Pelanggan wajib diisi jika marketing dipilih.');
} }
if ($this->filled('status') && $this->status === OrderStatus::COMPLETED->value) {
if (! $this->user()?->can(Permission::ORDERS_COMPLETE->value)) {
$validator->errors()->add('status', 'Anda tidak memiliki izin untuk menandai pesanan sebagai selesai.');
}
}
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) { if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
/** @var Order $order */ /** @var Order $order */
$order = $this->route('order'); $order = $this->route('order');

View File

@ -421,6 +421,10 @@ public function create(array $validated, User $user): Order
$totalAmount = max($subtotal - $discount, 0); $totalAmount = max($subtotal - $discount, 0);
$channel = OrderChannel::from($validated['channel']); $channel = OrderChannel::from($validated['channel']);
$status = isset($validated['status'])
? OrderStatus::from($validated['status'])
: OrderStatus::PENDING;
$order = Order::create([ $order = Order::create([
'customer_id' => $validated['customer_id'] ?? null, 'customer_id' => $validated['customer_id'] ?? null,
'marketing_id' => $validated['marketing_id'] ?? null, 'marketing_id' => $validated['marketing_id'] ?? null,
@ -428,7 +432,7 @@ public function create(array $validated, User $user): Order
'price_type' => $priceType, 'price_type' => $priceType,
'payment_type' => PaymentType::from($validated['payment_type']), 'payment_type' => PaymentType::from($validated['payment_type']),
'is_affiliate' => $validated['is_affiliate'] ?? false, 'is_affiliate' => $validated['is_affiliate'] ?? false,
'status' => OrderStatus::PENDING, 'status' => $status,
'tiktok_order_id' => $validated['tiktok_order_id'] ?? null, 'tiktok_order_id' => $validated['tiktok_order_id'] ?? null,
'shopee_order_id' => $validated['shopee_order_id'] ?? null, 'shopee_order_id' => $validated['shopee_order_id'] ?? null,
'created_by_id' => $user->id, 'created_by_id' => $user->id,
@ -547,6 +551,15 @@ public function update(Order $order, array $validated): void
); );
$order->total_amount = $totalAmount; $order->total_amount = $totalAmount;
$order->notes = $validated['notes'] ?? null; $order->notes = $validated['notes'] ?? null;
if (isset($validated['status'])) {
$newStatus = OrderStatus::from($validated['status']);
if ($order->status->canTransitionTo($newStatus) || $newStatus === $order->status) {
$order->status = $newStatus;
}
}
$order->save(); $order->save();
foreach ($lineItems as $itemData) { foreach ($lineItems as $itemData) {

View File

@ -1,4 +1,5 @@
export const OrderStatus = { export const OrderStatus = {
PENDING: 'pending',
PROCESSING: 'processing', PROCESSING: 'processing',
COMPLETED: 'completed', COMPLETED: 'completed',
CANCELLED: 'cancelled', CANCELLED: 'cancelled',

View File

@ -37,6 +37,7 @@ const initialData = computed(() => ({
shopee_order_id: props.order.shopee_order_id ?? '', shopee_order_id: props.order.shopee_order_id ?? '',
discount: String(props.order.discount), discount: String(props.order.discount),
notes: props.order.notes ?? '', notes: props.order.notes ?? '',
status: props.order.status,
items: props.order.items.map((item) => ({ items: props.order.items.map((item) => ({
product_variant_id: item.product_variant_id, product_variant_id: item.product_variant_id,
product_name: item.product_name, product_name: item.product_name,

View File

@ -42,6 +42,7 @@ import { Textarea } from '@/components/ui/textarea';
import { useCan } from '@/composables/useCan'; import { useCan } from '@/composables/useCan';
import { OrderChannel } from '@/constants/order-channel'; import { OrderChannel } from '@/constants/order-channel';
import { OrderPaymentType } from '@/constants/order-payment-type'; import { OrderPaymentType } from '@/constants/order-payment-type';
import { OrderStatus } from '@/constants/order-status';
import { PaperSize } from '@/constants/paper-size'; import { PaperSize } from '@/constants/paper-size';
import { StockQuality } from '@/constants/stock-quality'; import { StockQuality } from '@/constants/stock-quality';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
@ -75,6 +76,7 @@ const props = defineProps<{
shopee_order_id: string; shopee_order_id: string;
discount: string; discount: string;
notes: string; notes: string;
status: string;
items: OrderCartItem[]; items: OrderCartItem[];
}; };
draftItems?: OrderCartItem[]; draftItems?: OrderCartItem[];
@ -126,6 +128,7 @@ const form = useForm({
shopee_order_id: '', shopee_order_id: '',
discount: '', discount: '',
notes: '', notes: '',
status: OrderStatus.PENDING,
}); });
const isStoreChannel = computed(() => form.channel === 'store'); const isStoreChannel = computed(() => form.channel === 'store');
@ -146,6 +149,7 @@ function populateForm() {
form.shopee_order_id = props.initialData.shopee_order_id; form.shopee_order_id = props.initialData.shopee_order_id;
form.discount = props.initialData.discount; form.discount = props.initialData.discount;
form.notes = props.initialData.notes; form.notes = props.initialData.notes;
form.status = props.initialData.status || OrderStatus.PENDING;
cart.value = props.initialData.items.map((item) => ({ cart.value = props.initialData.items.map((item) => ({
...item, ...item,
stock_quality: item.stock_quality ?? StockQuality.GOOD, stock_quality: item.stock_quality ?? StockQuality.GOOD,
@ -457,6 +461,7 @@ function buildFormData(): FormData {
formData.append('discount', parseRupiah(form.discount)); formData.append('discount', parseRupiah(form.discount));
formData.append('notes', form.notes); formData.append('notes', form.notes);
formData.append('status', form.status);
if (isCreateMode.value && printAfterSave.value) { if (isCreateMode.value && printAfterSave.value) {
formData.append('print', '1'); formData.append('print', '1');
@ -823,6 +828,20 @@ function submit() {
</div> </div>
</div> </div>
<Field v-if="can('orders.complete')">
<div class="flex items-center gap-3">
<Switch id="status-completed" :model-value="form.status === OrderStatus.COMPLETED"
@update:model-value="form.status = $event ? OrderStatus.COMPLETED : OrderStatus.PENDING" />
<Label for="status-completed" class="cursor-pointer text-sm">
Pesanan selesai
</Label>
</div>
<p class="text-xs text-muted-foreground">
Aktifkan jika pesanan sudah selesai diproses.
</p>
<FieldError :errors="formErrors(form, 'status')" />
</Field>
<Field> <Field>
<FieldLabel for="notes">Keterangan</FieldLabel> <FieldLabel for="notes">Keterangan</FieldLabel>
<Textarea id="notes" v-model="form.notes" placeholder="Contoh: Pesanan walk-in" <Textarea id="notes" v-model="form.notes" placeholder="Contoh: Pesanan walk-in"