feat: add date filters and summary for order management
This commit is contained in:
parent
014682ab56
commit
c2c5f12b5c
@ -33,13 +33,18 @@ public function index(Request $request): Response
|
|||||||
$tableQuery['channel'] = $request->string('channel')->toString();
|
$tableQuery['channel'] = $request->string('channel')->toString();
|
||||||
$tableQuery['status'] = $request->string('status')->toString();
|
$tableQuery['status'] = $request->string('status')->toString();
|
||||||
$tableQuery['payment_type'] = $request->string('payment_type')->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', [
|
return Inertia::render('admin/manage/orders/Index', [
|
||||||
'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()),
|
'orders' => $this->orderService->paginateForIndex($tableQuery, $request->user()),
|
||||||
|
'summary' => $this->orderService->summaryForIndex($tableQuery, $request->user()),
|
||||||
'filters' => $this->dataTableFilters($tableQuery, [
|
'filters' => $this->dataTableFilters($tableQuery, [
|
||||||
'channel' => $tableQuery['channel'],
|
'channel' => $tableQuery['channel'],
|
||||||
'status' => $tableQuery['status'],
|
'status' => $tableQuery['status'],
|
||||||
'payment_type' => $tableQuery['payment_type'],
|
'payment_type' => $tableQuery['payment_type'],
|
||||||
|
'date_from' => $tableQuery['date_from'],
|
||||||
|
'date_to' => $tableQuery['date_to'],
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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['channel'] ?? '') !== '', fn (Builder $query) => $query->where('channel', $tableQuery['channel']))
|
||||||
->when(($tableQuery['status'] ?? '') !== '', fn (Builder $query) => $query->where('status', $tableQuery['status']))
|
->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']);
|
$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
|
public function customerOptions(): array
|
||||||
{
|
{
|
||||||
return Customer::query()
|
return Customer::query()
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useDebounceFn, useMediaQuery } from '@vueuse/core';
|
|||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import {
|
import {
|
||||||
@ -102,7 +103,7 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" class="w-80 space-y-4 p-4" @pointer-down-outside="(event) => {
|
<PopoverContent align="end" class="w-80 space-y-4 p-4" @pointer-down-outside="(event) => {
|
||||||
const target = event.target as HTMLElement;
|
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();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}">
|
}">
|
||||||
@ -123,6 +124,11 @@ const selectSideOffset = computed(() => (isMobile.value ? 4 : 12));
|
|||||||
:placeholder="filter.placeholder ?? `Filter ${filter.label.toLowerCase()}`"
|
:placeholder="filter.placeholder ?? `Filter ${filter.label.toLowerCase()}`"
|
||||||
@update:model-value="onFilterChange(filter.key, String($event ?? ''))" />
|
@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'"
|
<Select v-else :model-value="filterValue(filter.key) || 'all'"
|
||||||
@update:model-value="onFilterChange(filter.key, String($event ?? ''))">
|
@update:model-value="onFilterChange(filter.key, String($event ?? ''))">
|
||||||
<SelectTrigger :id="`filter-${filter.key}`" class="w-full">
|
<SelectTrigger :id="`filter-${filter.key}`" class="w-full">
|
||||||
|
|||||||
@ -26,6 +26,15 @@ import OrderGroupedTable from './table/OrderGroupedTable.vue';
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
orders: PaginatedOrders;
|
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: {
|
filters: {
|
||||||
search: string;
|
search: string;
|
||||||
sort?: string;
|
sort?: string;
|
||||||
@ -33,6 +42,8 @@ const props = defineProps<{
|
|||||||
channel?: string;
|
channel?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
payment_type?: 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({
|
const { query, setSearch, setFilter, resetFilters, syncFromServer } = useDataTableQuery({
|
||||||
url: index.url(),
|
url: index.url(),
|
||||||
initial: { ...props.filters },
|
initial: { ...props.filters },
|
||||||
filterKeys: ['channel', 'status', 'payment_type'],
|
filterKeys: ['channel', 'status', 'payment_type', 'date_from', 'date_to'],
|
||||||
});
|
});
|
||||||
|
|
||||||
useDataTableQuerySync(() => props.filters, syncFromServer);
|
useDataTableQuerySync(() => props.filters, syncFromServer);
|
||||||
@ -82,12 +93,24 @@ const filterDefs = computed<DataTableFilterDef[]>(() => [
|
|||||||
{ value: OrderPaymentType.MARKETPLACE, label: 'Marketplace' },
|
{ value: OrderPaymentType.MARKETPLACE, label: 'Marketplace' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'date_from',
|
||||||
|
label: 'Dari Tanggal',
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'date_to',
|
||||||
|
label: 'Sampai Tanggal',
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const filterValues = computed(() => ({
|
const filterValues = computed(() => ({
|
||||||
channel: query.value.channel ?? '',
|
channel: query.value.channel ?? '',
|
||||||
status: query.value.status ?? '',
|
status: query.value.status ?? '',
|
||||||
payment_type: query.value.payment_type ?? '',
|
payment_type: query.value.payment_type ?? '',
|
||||||
|
date_from: query.value.date_from ?? '',
|
||||||
|
date_to: query.value.date_to ?? '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const tablePagination = computed(() => ({
|
const tablePagination = computed(() => ({
|
||||||
@ -184,6 +207,7 @@ onMounted(async () => {
|
|||||||
<OrderGroupedTable
|
<OrderGroupedTable
|
||||||
v-model:search="search"
|
v-model:search="search"
|
||||||
:orders="orders.data"
|
:orders="orders.data"
|
||||||
|
:summary="summary"
|
||||||
:first-item="firstItem"
|
:first-item="firstItem"
|
||||||
:pagination="tablePagination"
|
:pagination="tablePagination"
|
||||||
:pagination-links="orders.links"
|
:pagination-links="orders.links"
|
||||||
|
|||||||
@ -30,6 +30,15 @@ import DataTableActions from './data-table-actions.vue';
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
orders: OrderListItem[];
|
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;
|
firstItem?: number;
|
||||||
pagination?: DataTablePagination;
|
pagination?: DataTablePagination;
|
||||||
paginationLinks?: DataTablePaginationLink[];
|
paginationLinks?: DataTablePaginationLink[];
|
||||||
@ -57,6 +66,15 @@ function rowNumber(index: number): number {
|
|||||||
<DataTableToolbar v-model:search="search" :filter-defs="filterDefs" :filter-values="filterValues"
|
<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')" />
|
@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-if="orders.length" class="space-y-4">
|
||||||
<div v-for="(order, index) in orders" :key="order.id" class="overflow-hidden rounded-md border">
|
<div v-for="(order, index) in orders" :key="order.id" class="overflow-hidden rounded-md border">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export type DataTableFilterOption = {
|
|||||||
export type DataTableFilterDef = {
|
export type DataTableFilterDef = {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: 'text' | 'select';
|
type: 'text' | 'select' | 'date';
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
options?: DataTableFilterOption[];
|
options?: DataTableFilterOption[];
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user