From c2c5f12b5cb77e0cd7701bf20a7c1d4a869d47c7 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 5 Aug 2026 00:30:41 +0700 Subject: [PATCH] feat: add date filters and summary for order management --- .../Admin/Manage/Order/OrderController.php | 5 ++ app/Services/Manage/OrderService.php | 47 ++++++++++++++++++- .../data-table/DataTableToolbar.vue | 8 +++- .../js/pages/admin/manage/orders/Index.vue | 26 +++++++++- .../manage/orders/table/OrderGroupedTable.vue | 18 +++++++ resources/js/types/data-table.ts | 2 +- 6 files changed, 102 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Admin/Manage/Order/OrderController.php b/app/Http/Controllers/Admin/Manage/Order/OrderController.php index b04b297..94e6ab0 100644 --- a/app/Http/Controllers/Admin/Manage/Order/OrderController.php +++ b/app/Http/Controllers/Admin/Manage/Order/OrderController.php @@ -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'], ]), ]); } diff --git a/app/Services/Manage/OrderService.php b/app/Services/Manage/OrderService.php index a280f5f..3579daf 100644 --- a/app/Services/Manage/OrderService.php +++ b/app/Services/Manage/OrderService.php @@ -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() diff --git a/resources/js/components/data-table/DataTableToolbar.vue b/resources/js/components/data-table/DataTableToolbar.vue index fb139c1..303252c 100644 --- a/resources/js/components/data-table/DataTableToolbar.vue +++ b/resources/js/components/data-table/DataTableToolbar.vue @@ -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)); @@ -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 ?? ''))" /> + +