feat: add tiktok_order_id and shopee_order_id fields to order management, including validation, display, and search functionality

This commit is contained in:
Yoga Pangestu 2026-06-17 23:18:06 +07:00
parent b176854bc3
commit c86af4ab91
8 changed files with 69 additions and 8 deletions

View File

@ -31,6 +31,8 @@ public function rules(): array
'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')->whereNull('deleted_at')], 'marketing_id' => ['nullable', 'integer', Rule::exists('users', 'id')->whereNull('deleted_at')],
'channel' => ['required', Rule::enum(OrderChannel::class)], 'channel' => ['required', Rule::enum(OrderChannel::class)],
'price_type' => ['required', Rule::enum(PriceType::class)], 'price_type' => ['required', Rule::enum(PriceType::class)],
'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'], 'discount' => ['nullable', 'integer', 'min:0'],
'notes' => ['nullable', 'string'], 'notes' => ['nullable', 'string'],
]; ];
@ -58,6 +60,8 @@ public function attributes(): array
'marketing_id' => 'marketing', 'marketing_id' => 'marketing',
'channel' => 'channel', 'channel' => 'channel',
'price_type' => 'tipe harga', 'price_type' => 'tipe harga',
'tiktok_order_id' => 'ID pesanan TikTok Shop',
'shopee_order_id' => 'ID pesanan Shopee',
'discount' => 'diskon', 'discount' => 'diskon',
'notes' => 'keterangan', 'notes' => 'keterangan',
'items' => 'produk', 'items' => 'produk',

View File

@ -46,6 +46,8 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$search = $tableQuery['search']; $search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void { $query->where(function (Builder $query) use ($search): void {
$query->where('order_number', 'like', "%{$search}%") $query->where('order_number', 'like', "%{$search}%")
->orWhere('tiktok_order_id', 'like', "%{$search}%")
->orWhere('shopee_order_id', 'like', "%{$search}%")
->orWhere('notes', 'like', "%{$search}%") ->orWhere('notes', 'like', "%{$search}%")
->orWhereHas('customer', fn (Builder $query) => $query->where('name', 'like', "%{$search}%")) ->orWhereHas('customer', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"))
->orWhereHas('items.productVariant', function (Builder $query) use ($search): void { ->orWhereHas('items.productVariant', function (Builder $query) use ($search): void {
@ -324,6 +326,8 @@ public function create(array $validated, User $user): Order
'channel' => $channel, 'channel' => $channel,
'price_type' => $priceType, 'price_type' => $priceType,
'status' => OrderStatus::PENDING, 'status' => OrderStatus::PENDING,
'tiktok_order_id' => $validated['tiktok_order_id'] ?? null,
'shopee_order_id' => $validated['shopee_order_id'] ?? null,
'created_by_id' => $user->id, 'created_by_id' => $user->id,
'subtotal' => $subtotal, 'subtotal' => $subtotal,
'discount' => $discount, 'discount' => $discount,
@ -397,6 +401,8 @@ public function update(Order $order, array $validated): void
$order->marketing_id = $validated['marketing_id'] ?? null; $order->marketing_id = $validated['marketing_id'] ?? null;
$order->channel = $channel; $order->channel = $channel;
$order->price_type = $priceType; $order->price_type = $priceType;
$order->tiktok_order_id = $validated['tiktok_order_id'] ?? null;
$order->shopee_order_id = $validated['shopee_order_id'] ?? null;
$order->subtotal = $subtotal; $order->subtotal = $subtotal;
$order->discount = $discount; $order->discount = $discount;
$order->marketplace_settings_snapshot = $this->marketplaceService->buildOrderSnapshot( $order->marketplace_settings_snapshot = $this->marketplaceService->buildOrderSnapshot(

View File

@ -21,6 +21,8 @@ public function up(): void
$table->enum('channel', array_column(OrderChannel::cases(), 'value')); $table->enum('channel', array_column(OrderChannel::cases(), 'value'));
$table->enum('price_type', array_column(PriceType::cases(), 'value')); $table->enum('price_type', array_column(PriceType::cases(), 'value'));
$table->enum('status', array_column(OrderStatus::cases(), 'value'))->default(OrderStatus::PENDING->value); $table->enum('status', array_column(OrderStatus::cases(), 'value'))->default(OrderStatus::PENDING->value);
$table->string('tiktok_order_id', 100)->nullable();
$table->string('shopee_order_id', 100)->nullable();
$table->unsignedBigInteger('subtotal'); $table->unsignedBigInteger('subtotal');
$table->unsignedBigInteger('discount')->default(0); $table->unsignedBigInteger('discount')->default(0);
$table->json('marketplace_settings_snapshot')->nullable(); $table->json('marketplace_settings_snapshot')->nullable();

View File

@ -98,6 +98,12 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
<Badge variant="secondary"> <Badge variant="secondary">
{{ order.channel_label }} {{ order.channel_label }}
</Badge> </Badge>
<Badge v-if="order.tiktok_order_id" variant="outline">
ID Pesanan: {{ order.tiktok_order_id }}
</Badge>
<Badge v-if="order.shopee_order_id" variant="outline">
ID Pesanan: {{ order.shopee_order_id }}
</Badge>
</div> </div>
<div class="text-muted-foreground space-y-1 text-sm"> <div class="text-muted-foreground space-y-1 text-sm">
<p v-if="order.customer"> <p v-if="order.customer">
@ -108,13 +114,13 @@ function statusVariant(status: string): 'default' | 'secondary' | 'destructive'
</div> </div>
<div class="flex flex-wrap gap-x-4 gap-y-1 text-sm"> <div class="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<span>Tipe Harga <strong class="text-foreground">{{ order.price_type_label <span>Tipe Harga <strong class="text-foreground">{{ order.price_type_label
}}</strong></span> }}</strong></span>
<span>Subtotal <strong class="text-foreground">{{ order.subtotal_formatted <span>Subtotal <strong class="text-foreground">{{ order.subtotal_formatted
}}</strong></span> }}</strong></span>
<span>Diskon <strong class="text-foreground">{{ order.discount_formatted <span>Diskon <strong class="text-foreground">{{ order.discount_formatted
}}</strong></span> }}</strong></span>
<span>Total <strong class="text-primary">{{ order.total_amount_formatted <span>Total <strong class="text-primary">{{ order.total_amount_formatted
}}</strong></span> }}</strong></span>
</div> </div>
<p v-if="order.notes" class="text-muted-foreground text-sm"> <p v-if="order.notes" class="text-muted-foreground text-sm">
{{ order.notes }} {{ order.notes }}

View File

@ -3,7 +3,6 @@ import { useForm, usePage } from '@inertiajs/vue3';
import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue'; import { Minus, Plus, Save, Search, ShoppingCart, Trash2 } from '@lucide/vue';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import type { Auth } from '@/types/auth';
import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue'; import PosCatalogCard from '@/components/admin/manage/PosCatalogCard.vue';
import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue'; import PosCatalogVariantThumb from '@/components/admin/manage/PosCatalogVariantThumb.vue';
import { NumberInput } from '@/components/form/number-input'; import { NumberInput } from '@/components/form/number-input';
@ -34,10 +33,12 @@ import {
} from '@/components/ui/select'; } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { OrderChannel } from '@/constants/order-channel';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { getFirstCoverImage } from '@/lib/catalog-cover'; import { getFirstCoverImage } from '@/lib/catalog-cover';
import { formErrors } from '@/lib/form'; import { formErrors } from '@/lib/form';
import { formatRupiah, parseRupiah } from '@/lib/rupiah'; import { formatRupiah, parseRupiah } from '@/lib/rupiah';
import type { Auth } from '@/types/auth';
import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order'; import type { EnumOption, OrderCartItem, OrderCatalogItem, SelectOption } from '@/types/order';
import type { ProductPriceItem, ProductVariantItem } from '@/types/product'; import type { ProductPriceItem, ProductVariantItem } from '@/types/product';
@ -52,6 +53,8 @@ const props = defineProps<{
marketing_id: string; marketing_id: string;
channel: string; channel: string;
price_type: string; price_type: string;
tiktok_order_id: string;
shopee_order_id: string;
discount: string; discount: string;
notes: string; notes: string;
items: OrderCartItem[]; items: OrderCartItem[];
@ -83,6 +86,8 @@ const form = useForm({
marketing_id: defaultMarketingId.value || 'none', marketing_id: defaultMarketingId.value || 'none',
channel: 'store', channel: 'store',
price_type: 'ecer', price_type: 'ecer',
tiktok_order_id: '',
shopee_order_id: '',
discount: '', discount: '',
notes: '', notes: '',
}); });
@ -98,6 +103,8 @@ function populateForm() {
form.marketing_id = props.initialData.marketing_id || 'none'; form.marketing_id = props.initialData.marketing_id || 'none';
form.channel = props.initialData.channel; form.channel = props.initialData.channel;
form.price_type = props.initialData.price_type; form.price_type = props.initialData.price_type;
form.tiktok_order_id = props.initialData.tiktok_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;
cart.value = props.initialData.items.map((item) => ({ ...item })); cart.value = props.initialData.items.map((item) => ({ ...item }));
@ -347,6 +354,15 @@ function buildFormData(): FormData {
formData.append('channel', form.channel); formData.append('channel', form.channel);
formData.append('price_type', form.price_type); formData.append('price_type', form.price_type);
if (form.channel === 'tiktok' && form.tiktok_order_id) {
formData.append('tiktok_order_id', form.tiktok_order_id);
}
if (form.channel === 'shopee' && form.shopee_order_id) {
formData.append('shopee_order_id', form.shopee_order_id);
}
formData.append('discount', parseRupiah(form.discount)); formData.append('discount', parseRupiah(form.discount));
formData.append('notes', form.notes); formData.append('notes', form.notes);
@ -409,8 +425,7 @@ function submit() {
Belum ada varian Belum ada varian
</p> </p>
<div v-for="variant in product.variants" :key="variant.id" <div v-for="variant in product.variants" :key="variant.id"
class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200" class="flex items-center gap-2.5 px-3 py-2.5 transition-all duration-200" :class="[
:class="[
getVariantPrice(variant) ? 'cursor-pointer hover:bg-muted/30' : 'opacity-60', getVariantPrice(variant) ? 'cursor-pointer hover:bg-muted/30' : 'opacity-60',
getCartItem(variant.id) ? 'border-2 border-primary bg-primary/5 rounded-md mx-1 my-0.5' : '' getCartItem(variant.id) ? 'border-2 border-primary bg-primary/5 rounded-md mx-1 my-0.5' : ''
]" ]"
@ -502,10 +517,28 @@ function submit() {
<Field v-else> <Field v-else>
<FieldLabel>Tipe Harga</FieldLabel> <FieldLabel>Tipe Harga</FieldLabel>
<div class="flex h-9 items-center rounded-md border bg-muted/40 px-3 text-sm"> <div class="flex h-9 items-center rounded-md border bg-muted/40 px-3 text-sm">
{{ form.channel === 'shopee' ? 'Shopee' : 'TikTok' }} {{ form.channel === OrderChannel.SHOPEE ? 'Shopee' : 'TikTok' }}
</div> </div>
</Field> </Field>
<Field v-if="form.channel === 'tiktok'">
<FieldLabel for="tiktok_order_id" :required="form.channel === OrderChannel.TIKTOK">ID
Pesanan
TikTok Shop</FieldLabel>
<Input id="tiktok_order_id" v-model="form.tiktok_order_id"
placeholder="Masukkan ID pesanan TikTok Shop" />
<FieldError :errors="formErrors(form, 'tiktok_order_id')" />
</Field>
<Field v-if="form.channel === 'shopee'">
<FieldLabel for="shopee_order_id" :required="form.channel === OrderChannel.SHOPEE">ID
Pesanan
Shopee</FieldLabel>
<Input id="shopee_order_id" v-model="form.shopee_order_id"
placeholder="Masukkan ID pesanan Shopee" />
<FieldError :errors="formErrors(form, 'shopee_order_id')" />
</Field>
<Field> <Field>
<FieldLabel for="customer">Pelanggan</FieldLabel> <FieldLabel for="customer">Pelanggan</FieldLabel>
<Select v-model="form.customer_id"> <Select v-model="form.customer_id">

View File

@ -0,0 +1,4 @@
export const OrderChannel = {
TIKTOK: 'tiktok',
SHOPEE: 'shopee',
} as const;

View File

@ -21,6 +21,8 @@ const initialData = computed(() => ({
marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '', marketing_id: props.order.marketing_id ? String(props.order.marketing_id) : '',
channel: props.order.channel, channel: props.order.channel,
price_type: props.order.price_type, price_type: props.order.price_type,
tiktok_order_id: props.order.tiktok_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 ?? '',
items: props.order.items.map((item) => ({ items: props.order.items.map((item) => ({

View File

@ -45,6 +45,8 @@ export type OrderListItem = {
status_label: string; status_label: string;
is_editable: boolean; is_editable: boolean;
available_actions: OrderStatusAction[]; available_actions: OrderStatusAction[];
tiktok_order_id: string | null;
shopee_order_id: string | null;
subtotal_formatted: string; subtotal_formatted: string;
discount_formatted: string; discount_formatted: string;
total_amount_formatted: string; total_amount_formatted: string;
@ -80,6 +82,8 @@ export type OrderEditItem = {
channel: string; channel: string;
price_type: string; price_type: string;
status: string; status: string;
tiktok_order_id: string | null;
shopee_order_id: string | null;
discount: number; discount: number;
notes: string | null; notes: string | null;
items: Array<{ items: Array<{