feat: add date filters and summary for order management
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
Yoga Pangestu 2026-08-05 00:30:41 +07:00
parent 014682ab56
commit c2c5f12b5c
6 changed files with 102 additions and 4 deletions

View File

@ -33,13 +33,18 @@ public function index(Request $request): Response
$tableQuery['channel'] = $request->string('channel')->toString();
$tableQuery['status'] = $request->string('status')->toString();
$tableQuery['payment_type'] = $request->string('payment_type')->toString();
$tableQuery['date_from'] = $request->string('date_from')->toString();
$tableQuery['date_to'] = $request->string('date_to')->toString();
return Inertia::render('admin/manage/orders/Index', [
'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()),
'summary' => $this->orderService->summaryForIndex($tableQuery, $request->user()),
'filters' => $this->dataTableFilters($tableQuery, [
'channel' => $tableQuery['channel'],
'status' => $tableQuery['status'],
'payment_type' => $tableQuery['payment_type'],
'date_from' => $tableQuery['date_from'],
'date_to' => $tableQuery['date_to'],
]),
]);
}

View File

@ -87,7 +87,9 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
})
->when(($tableQuery['channel'] ?? '') !== '', fn (Builder $query) => $query->where('channel', $tableQuery['channel']))
->when(($tableQuery['status'] ?? '') !== '', fn (Builder $query) => $query->where('status', $tableQuery['status']))
->when(($tableQuery['payment_type'] ?? '') !== '', fn (Builder $query) => $query->where('payment_type', $tableQuery['payment_type']));
->when(($tableQuery['payment_type'] ?? '') !== '', fn (Builder $query) => $query->where('payment_type', $tableQuery['payment_type']))
->when(($tableQuery['date_from'] ?? '') !== '', fn (Builder $query) => $query->whereDate('created_at', '>=', $tableQuery['date_from']))
->when(($tableQuery['date_to'] ?? '') !== '', fn (Builder $query) => $query->whereDate('created_at', '<=', $tableQuery['date_to']));
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
@ -113,6 +115,49 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
});
}
public function summaryForIndex(array $tableQuery, User $user): array
{
$query = Order::query()
->when($user->hasAnyRole(['marketing-offline', 'marketing-online']), fn (Builder $query) => $query->where('marketing_id', $user->id))
->when($user->hasRole('cashier'), fn (Builder $query) => $query->where('created_by_id', $user->id))
->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
$search = $tableQuery['search'];
$query->where(function (Builder $query) use ($search): void {
$query->where('order_number', 'like', "%{$search}%")
->orWhere('tiktok_order_id', 'like', "%{$search}%")
->orWhere('shopee_order_id', 'like', "%{$search}%")
->orWhere('notes', 'like', "%{$search}%")
->orWhereHas('customer', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"))
->orWhereHas('items.productVariant', function (Builder $query) use ($search): void {
$query->where('name', 'like', "%{$search}%")
->orWhereHas('product', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"));
});
});
})
->when(($tableQuery['channel'] ?? '') !== '', fn (Builder $query) => $query->where('channel', $tableQuery['channel']))
->when(($tableQuery['status'] ?? '') !== '', fn (Builder $query) => $query->where('status', $tableQuery['status']))
->when(($tableQuery['payment_type'] ?? '') !== '', fn (Builder $query) => $query->where('payment_type', $tableQuery['payment_type']))
->when(($tableQuery['date_from'] ?? '') !== '', fn (Builder $query) => $query->whereDate('created_at', '>=', $tableQuery['date_from']))
->when(($tableQuery['date_to'] ?? '') !== '', fn (Builder $query) => $query->whereDate('created_at', '<=', $tableQuery['date_to']));
$result = $query->selectRaw('
COUNT(*) as total_orders,
COALESCE(SUM(total_amount), 0) as total_amount,
COALESCE(SUM(subtotal), 0) as total_subtotal,
COALESCE(SUM(discount), 0) as total_discount
')->first();
return [
'total_orders' => (int) $result->total_orders,
'total_amount' => (int) $result->total_amount,
'total_amount_formatted' => 'Rp ' . number_format($result->total_amount, 0, ',', '.'),
'total_subtotal' => (int) $result->total_subtotal,
'total_subtotal_formatted' => 'Rp ' . number_format($result->total_subtotal, 0, ',', '.'),
'total_discount' => (int) $result->total_discount,
'total_discount_formatted' => 'Rp ' . number_format($result->total_discount, 0, ',', '.'),
];
}
public function customerOptions(): array
{
return Customer::query()

View File

@ -4,6 +4,7 @@ import { useDebounceFn, useMediaQuery } from '@vueuse/core';
import { computed, ref, watch } from 'vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DatePicker } from '@/components/ui/date-picker';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
@ -102,7 +103,7 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
</PopoverTrigger>
<PopoverContent align="end" class="w-80 space-y-4 p-4" @pointer-down-outside="(event) => {
const target = event.target as HTMLElement;
if (target.closest('[data-slot=select-content]') || target.closest('[data-slot=select-trigger]')) {
if (target.closest('[data-slot=select-content]') || target.closest('[data-slot=select-trigger]') || target.closest('[data-slot=popover-content]')) {
event.preventDefault();
}
}">
@ -123,6 +124,11 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
:placeholder="filter.placeholder ?? `Filter ${filter.label.toLowerCase()}`"
@update:model-value="onFilterChange(filter.key, String($event ?? ''))" />
<DatePicker v-else-if="filter.type === 'date'"
:model-value="filterValue(filter.key)"
:placeholder="filter.placeholder ?? 'Pilih tanggal'"
@update:model-value="onFilterChange(filter.key, String($event ?? ''))" />
<Select v-else :model-value="filterValue(filter.key) || 'all'"
@update:model-value="onFilterChange(filter.key, String($event ?? ''))">
<SelectTrigger :id="`filter-${filter.key}`" class="w-full">

View File

@ -26,6 +26,15 @@ import OrderGroupedTable from './table/OrderGroupedTable.vue';
const props = defineProps<{
orders: PaginatedOrders;
summary: {
total_orders: number;
total_amount: number;
total_amount_formatted: string;
total_subtotal: number;
total_subtotal_formatted: string;
total_discount: number;
total_discount_formatted: string;
};
filters: {
search: string;
sort?: string;
@ -33,6 +42,8 @@ const props = defineProps<{
channel?: string;
status?: string;
payment_type?: string;
date_from?: string;
date_to?: string;
};
}>();
@ -44,7 +55,7 @@ const search = ref(props.filters.search ?? '');
const { query, setSearch, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
url: index.url(),
initial: { ...props.filters },
filterKeys: ['channel', 'status', 'payment_type'],
filterKeys: ['channel', 'status', 'payment_type', 'date_from', 'date_to'],
});
useDataTableQuerySync(() => props.filters, syncFromServer);
@ -82,12 +93,24 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
{ value: OrderPaymentType.MARKETPLACE, label: 'Marketplace' },
],
},
{
key: 'date_from',
label: 'Dari Tanggal',
type: 'date',
},
{
key: 'date_to',
label: 'Sampai Tanggal',
type: 'date',
},
]);
const filterValues = computed(() => ({
channel: query.value.channel ?? '',
status: query.value.status ?? '',
payment_type: query.value.payment_type ?? '',
date_from: query.value.date_from ?? '',
date_to: query.value.date_to ?? '',
}));
const tablePagination = computed(() => ({
@ -184,6 +207,7 @@ onMounted(async () => {
<OrderGroupedTable
v-model:search="search"
:orders="orders.data"
:summary="summary"
:first-item="firstItem"
:pagination="tablePagination"
:pagination-links="orders.links"

View File

@ -30,6 +30,15 @@ import DataTableActions from './data-table-actions.vue';
const props = defineProps<{
orders: OrderListItem[];
summary?: {
total_orders: number;
total_amount: number;
total_amount_formatted: string;
total_subtotal: number;
total_subtotal_formatted: string;
total_discount: number;
total_discount_formatted: string;
};
firstItem?: number;
pagination?: DataTablePagination;
paginationLinks?: DataTablePaginationLink[];
@ -57,6 +66,15 @@ function rowNumber(index: number): number {
<DataTableToolbar v-model:search="search" :filter-defs="filterDefs" :filter-values="filterValues"
@filter-change="(key, value) => emit('filter-change', key, value)" @filters-reset="emit('filters-reset')" />
<div v-if="summary && summary.total_orders > 0"
class="flex flex-wrap items-center gap-4 rounded-md border bg-muted/30 px-4 py-3 text-sm">
<span class="font-medium">Ringkasan:</span>
<span>{{ summary.total_orders }} pesanan</span>
<span>Subtotal <strong class="text-primary">{{ summary.total_subtotal_formatted }}</strong></span>
<span>Diskon <strong class="text-primary">{{ summary.total_discount_formatted }}</strong></span>
<span>Total <strong class="text-green-600">{{ summary.total_amount_formatted }}</strong></span>
</div>
<div v-if="orders.length" class="space-y-4">
<div v-for="(order, index) in orders" :key="order.id" class="overflow-hidden rounded-md border">
<div

View File

@ -11,7 +11,7 @@ export type DataTableFilterOption = {
export type DataTableFilterDef = {
key: string;
label: string;
type: 'text' | 'select';
type: 'text' | 'select' | 'date';
placeholder?: string;
options?: DataTableFilterOption[];
};